PyCharter — A Toolkit FOR AI Agents (AG)¶
Status: in progress (read tier shipped 2026-09-13; guarded write AG-8/9/10 implemented 2026-09-13) · Created: 2026-09-13 ·
Audience: maintainers ·
Absorbs: agent-native-contracts.md
(AN-1…AN-8 = the foundation layer) ·
Depends on: the usability Phase-1 trust items (COR-1, COR-2, DX-1, TEST-1; all shipped — the usability roadmap has been folded into BACKLOG.md) ·
Honours: cross-package-decoupling.md,
ARCHITECTURE.md (the two laws).
Scope decisions (locked with maintainer, 2026-09-13)¶
- Consumers: both dev-time coding agents (Claude Code, Cursor) and runtime data agents — served over one MCP-first surface.
- Autonomy: read + guarded write — grounding/validation always available; mutations are policy-gated, fail-closed by default, and require explicit approval (human-in-the-loop).
- Surface (v1): focused core — contract authoring (sketch/diff/lint), validation grounding, and concept context. Pipelines, streaming, quality, and vocabulary come in a later wave.
- Bundling: raw tools + adapters only — no new artifact kind.
kind: Toolsetandkind: Agentare rejected (see §12). PyCharter exposes individual, governed tools; the caller composes them.
1. Vision & positioning¶
PyCharter is the toolkit an AI agent reaches for when it must be correct about your data. The same versioned contract that validates an API payload, a Kafka record, and an ETL batch becomes the agent's ground truth: the schema it is handed, the validator that checks its output, the vocabulary that tells it what a field means, and the governed set of actions it is allowed to take — all over MCP, with zero pycharter install on the agent side.
This extends the core value proposition ("the contract is what actually runs") to the agent surface without changing it. PyCharter does not become an agent, define an agent, or run an agent loop. It provides three things an agent needs and nothing more:
- Eyes — just-in-time, governance-stripped schemas and governed concept context (an agent is told only what it needs, policy-filtered).
- Conscience — graduated validation returned as model-consumable feedback, deterministic field mapping, PII/restricted redaction.
- Hands (guarded) — a focused set of actions (sketch/diff/lint/publish a contract) that are policy-gated, approval-required, and audit-logged.
The agent's model, prompt, and loop stay entirely with the caller. That boundary is what keeps PyCharter a generalizable toolkit rather than an agent framework.
2. Design invariants (the guardrails)¶
The "toolkit for agents" idea is PyCharter's biggest bloat / lock-in risk. Every AG item is checked against these; several become CI gates.
| # | Invariant | Enforced by |
|---|---|---|
| I-1 | No agent in src/. No persona, prompt, loop, or vertical "bot." Tools are registered capabilities over existing services. |
CI check: src/pycharter/agent/ is an engine (registry/resolver/MCP wiring), never a domain agent. |
| I-2 | Stateless, LLM-free core. No core module (runtime_validator, contract_*, pipeline_*) imports a provider SDK or calls a model. |
Import-lint test: no anthropic/openai/provider import outside agent/, semantic/ai/, contrib/. |
| I-3 | Provider-agnostic, no bundled LLM. All optional model use goes through AIProvider; [ai] stays optional; NoOpProvider keeps defaults working. |
semantic/ai/provider.py; tests run under NoOpProvider. |
| I-4 | No orchestration framework. PyCharter emits schemas/feedback and exposes tools; the loop is caller-owned or a thin contrib/ adapter. |
AN-8 non-goals; adapters are import-guarded leaf modules. |
| I-5 | Extraction stays in pyintake. No document/RAG ingestion duplicated here. |
cross-package-decoupling.md. |
| I-6 | Runtime determinism. No per-record embedding/semantic lookup on any hot path; agent-facing resolution (aliases/mappings) is frozen at publish time (CB rule). | AN-4 freezing; reproducibility tests. |
| I-7 | Governance is mandatory. Every tool call is policy-filtered (role/workspace/PII/restricted) and audit-logged, reusing the context-pack policy layer. | Reuse mcp/server.py policy filter + audit/lineage surface. |
| I-8 | Writes fail-closed. Mutating tools are disabled unless explicitly enabled in config, and even then require an approval step (§5). | Config default allow_write=false; two-phase commit. |
3. Architecture placement — MCP-first shell, no core changes¶
┌──────────────────────────────────────────────────────────────┐
│ AGENT / FRAMEWORK (Claude Code, Cursor, Claude Agent SDK, │
│ Pydantic AI, a script) ── owns the model, prompt, and loop │
└──────────────┬───────────────────────────────┬───────────────┘
MCP │ (zero-install, primary) │ contrib adapter (pip)
┌──────────────▼───────────────────────────────▼───────────────┐
│ AGENT SHELL (new: src/pycharter/agent/, thin) │
│ tool registry · read/guarded-write tiers · approval + audit │
│ · feedback renderer · provider boundary · policy filter │
└──────────────┬───────────────────────────────────────────────┘
grounding (AN-1/5) guardrails (AN-2/3/4) guarded actions
┌──────────────▼───────────────────────────────────────────────┐
│ STATELESS CORE (unchanged, LLM-free) │
│ Validator · contracts · concepts/vocabulary · diff · sketch │
│ · quality · DLQ · stores · audit │
└───────────────────────────────────────────────────────────────┘
The agent shell is peer to the HTTP and worker shells. It composes and exposes existing core capabilities as governed tools; it adds no capability to the core.
4. The tool catalog (the product)¶
The deliverable is a small, sharp, governed set of MCP tools (plus adapter equivalents), focused on contracts + concepts, tiered by autonomy. Both audiences are served by one catalog.
4.1 Read / advise tier — always available (policy-filtered)¶
| Tool | Does | Serves | Built on |
|---|---|---|---|
list_contracts |
Namespace-scoped contract discovery | both | store + policy (AN-5) |
fetch_contract_schema |
JIT pruned, governance-stripped schema, optional token budget | both | to_llm_schema (AN-1) |
validate_payload |
Validate candidate output → is_valid + structured feedback |
runtime | Validator + to_agent_feedback (AN-2) |
suggest_field_mapping |
Rejected (AG-6, §12). Replaced by a did-you-mean hint on undeclared-field diagnostics in validate_record feedback |
runtime | ValidationResult.to_agent_feedback() |
fetch_concept_pack |
Governed concept/relationship context for fields | both | existing MCP concept tools |
sketch_contract_preview |
Infer a candidate contract from sample rows — returns a draft, does not persist | coding | contract sketch |
diff_contract_candidate |
Classify a proposed change vs a stored version (breaking/safe) | coding | contract diff |
lint_contract |
Static checks on a candidate contract | coding | contract lint |
list_capabilities / describe_capability |
Enumerate registered building blocks | coding | existing capability tools |
Read tools never mutate the store and never surface PII/restricted raw values (redaction per policy).
4.2 Guarded-write tier — off by default, approval-required (§5)¶
| Tool | Does | Guard |
|---|---|---|
propose_contract_version |
Stage a new/updated contract → returns a diff + plan + approval token; does not commit | write-enabled + role + workspace |
commit_contract_version |
Commit a previously-proposed change by token (immutable new version) | approval token + audit event |
bind_field_to_concept |
Propose a field↔concept binding → suggestion-queue or approval | governed flywheel |
run_pipeline / streaming actuation are deliberately deferred past v1
(focused-core scope) and, when added, use the same guard.
4.3 Dual-audience mapping¶
- Coding agent (dev-time):
list_contracts→fetch_contract_schema→sketch_contract_preview/diff_contract_candidate/lint_contract→propose_/commit_contract_version. "Help me evolve this contract safely." - Runtime data agent:
fetch_contract_schema→ (emit) →validate_payload(feedback with did-you-mean hints) → repair loop → quarantine. "Make my output conform, or tell me exactly why it doesn't."
5. Guarded-write design (the safety core of decision #2)¶
Mutations are the highest-risk surface, so they get a first-class, config-driven safety model — not an afterthought.
- Fail-closed (I-8). Write tools are absent from the catalog unless
allow_write=true. A read-only deployment is the default and needs no configuration. - Two-phase, human-in-the-loop. A write is always
propose_*→ (human approves) →commit_*(token).propose_*returns a rendered diff + impact classification (breaking/safe) + a short-lived token; it commits nothing. This maps cleanly onto how MCP hosts (Claude Code) already surface actions for approval, and onto a UI approval panel. - Policy-scoped. The requester's role/workspace must permit the write; PII/restricted fields are honoured; the same audited policy filter as the read tier applies.
- Immutable + audited. A commit creates a new immutable contract version (existing immutable-versions semantics) and emits an audit event with the requester, token, and diff — so every agent-made change is reviewable and reversible by publishing a prior version.
- Escalation. On rejected/expired approval, nothing is written; the tool returns a clean, model-readable reason (no traceback — ties to DX-1).
This is what makes "let an agent help author contracts" safe enough to enable in a real org.
6. The friendliest configuration-driven surface (no new artifact kind)¶
Decision #4 rules out a Toolset artifact, but "config-driven" still
governs the tool surface — via plain config, not a versioned domain
artifact. A single, small, optional tool-exposure policy controls who
gets which tools at what autonomy:
# agent-tools.yaml — plain config, passed with --config (as implemented, AG-8)
agent:
# --- autonomy (fail-closed) ---
allow_write: false # I-8: writes off unless set
require_approval: true # writes are always two-phase (§5); false is rejected
# --- identity + governance (reuses the existing requester policy layer) ---
principal: claude-code # recorded as proposer / approver
role: reviewer # bound role; --role overrides
workspace: <concept-scheme id> # bound workspace; --workspace overrides
approval:
ttl_hours: 24 # S2
require_distinct_approver: false # S3
propose_role: reviewer
commit_role: reviewer
# --- scope (omit a list for "all tools of that tier") ---
tools:
read: [list_contracts, fetch_contract_schema, validate_record]
write: [propose_contract_version, commit_contract_version] # ignored if allow_write:false
Implementation notes (AG-8). The draft sketch above had enabled,
transport, roles, and redact keys; they were left out rather than
shipped as no-ops, because unknown keys are rejected at load and a key that
silently does nothing is a safety hazard in a policy file. PII redaction is
already enforced by the read tools, and identity is a single bound role, as
it is for the HTTP layer. The [agent] block in pycharter.cfg was also
left out, so there is no new implicit config lookup; --config is explicit.
The authoritative schema lives in
docs/reference/agent-tools-policy.md.
Properties that keep it friendly and disciplined:
- Zero-config default is safe and useful: with no file, the MCP server
serves the full read tier, policy-filtered, writes off.
- One knob to enable authoring: allow_write: true (still approval-gated).
- It is config, not a new artifact kind — versionable in the app repo,
no engine schema, no Toolset abstraction to learn.
7. Foundation layer — adopt AN-1…AN-8¶
These primitives are the tools' internals and ship first (useful even before the catalog is packaged):
- AN-1
to_llm_schema/to_tool_schema(provider)— JIT schemas. - AN-2
ValidationResult.to_agent_feedback()— correction text. - AN-3
repair_loop(...)— bounded, shell-layer, DLQ escalation. AN-4Rejected (AG-6, §12).SemanticMapper— deterministic, publish-frozen alias mapping.- AN-5 MCP
fetch_contract_schema/validate_payload/list_contracts. - AN-6 contrib adapters (Claude Agent SDK, Pydantic AI).
- AN-7 "PyCharter for AI agents" docs +
llms.txt. - AN-8 non-goals (== invariants).
AG adds on top: the read/guarded-write tiering + approval model (§5), the config-driven exposure policy (§6), the coding-agent tools (sketch/diff/lint preview, §4.1), and the dual-audience packaging (§8).
8. Friendly developer/agent experience¶
- Zero-install path (primary): point any MCP host at
pycharter agent serve --mcp; the agent immediately has the read tier. One line in the host's MCP config; nothing topip installagent-side. - CLI:
pycharter agent serve [--mcp] [--config agent-tools.yaml]— run the governed tool surface.pycharter agent tools— list exposed tools with tier, contracts, and policy (the "what can the agent do here?" answer).pycharter agent try --record r.json— dry-run the runtime loop (fetch schema → validate → feedback → repair → quarantine) offline withNoOpProvider; plug a provider to see real repair. Mirrors thepycharter init"works in 30s" win.pycharter schemaextended to describe agent tool I/O (closes DX-4).- contrib adapters: turn a contract into a native tool/response-format for Claude Agent SDK / Pydantic AI (AN-6) — for teams that prefer SDK over MCP.
- Errors as UX: the global CLI/tool error handler (DX-1) turns bad config / unknown tool / rejected write into clean, model-readable messages — never tracebacks.
- UI (later): an approval panel for guarded writes and a "Try it" panel reusing the AN-2 renderer, built on the existing editor + SWR (UX-1), not a new stack.
9. Generalizability & anti-bloat plan¶
- Thin new shell.
src/pycharter/agent/target < 1.5k LOC (registry, tiering, approval, MCP wiring). Status 2026-09-14: ~1.76k LOC after AG-17 (proposal lifecycle ~840, conformance ~320); every module is under the 400-line cap. Revisit when ARCH-1 lands. No new artifact kind (decision #4) means no parser/store/migration surface added. - Consolidate before building. AG is gated on
ARCH-1 (now tracked in
BACKLOG.md) (collapse the sprawlingsemantic/ai/kb/overlapping subpackages) so the provider boundary and grounding logic have one home. - One provider boundary. Unify
semantic/ai/*behind the singleAIProviderprotocol, re-homed underagent/providers/;[ai]stays the only SDK-pulling extra. - The capability registry is the sole extension point. New tools register
through the existing registry that backs
pycharter capabilities; no parallel plugin system. - CI invariant checks (I-1, I-2, I-8) run in
scripts/ci.sh.
10. Prerequisites (from the usability review, all shipped)¶
An agent toolkit is only as trustworthy as the ground it stands on:
- ⛔ COR-1 enforce additionalProperties (an agent told a schema is
closed must have that be true).
- ⛔ COR-2 unify artifact format to v2 (agents fetch/emit the format the
engine accepts).
- ⛔ DX-1 global error handler (tool errors are model-readable, not
tracebacks).
- ⛔ TEST-1 doc-example CI gate (every agent recipe stays runnable).
11. Workstreams, IDs & phased roadmap¶
IDs AG-N; foundation items reference AN-N. Effort S/M/L/XL.
Phase 0 — Prerequisites¶
- ⛔ COR-1, COR-2, DX-1, TEST-1.
Phase 1 — Grounding & feedback primitives (runtime agents)¶
- AG-1 [SHIPPED — runtime_validator/llm_schema.py] (AN-1)
to_llm_schema/to_tool_schema— S/M - AG-2 [SHIPPED — ValidationResult.to_agent_feedback()] (AN-2)
to_agent_feedback()— S - AG-3 [SHIPPED — MCP fetch_contract_schema/list_contracts + validate_record feedback] (AN-5) MCP read tools:
fetch_contract_schema,validate_payload,list_contracts(policy-filtered) — M - AG-4 [SHIPPED — pycharter.agent.repair_loop/arepair_loop] (AN-3)
repair_loop+ DLQ escalation — M
Phase 2 — Coding-agent read tools + deterministic mapping¶
- AG-5 [SHIPPED — MCP sketch_contract/diff_contract_candidate/lint_contract] MCP read tools:
sketch_contract_preview,diff_contract_candidate,lint_contract(draft-only, no persist) — M - AG-6 [REJECTED 2026-09-13 — see §12; replaced by a did-you-mean hint in validation feedback (
runtime_validator/_suggestions.py)] (AN-4)SemanticMapper+suggest_field_mappingtool, publish-frozen alias map — L - AG-7 [SHIPPED —
agent_toolcapability kind withtier; registry is the tiering source of truth; drift test pins it to the MCP catalog] Register agent-facing operations in the capability registry;pycharter agent tools— S/M
Phase 3 — Guarded write (autonomy, decision #2)¶
- AG-8 [SHIPPED — agent/policy.py + mcp/_exposure.py;
--configon serve/tools; I-8 CI invariant] Read/guarded-write tiering + config-driven exposure policy (§6), fail-closed default — M - AG-9 [SHIPPED — agent/proposals.py; drafts migration
draft_proposals_20260913(SQLite+Postgres round-trip tested); MCP propose/commit;pycharter agent proposals; contribcatalog_tools] Two-phasepropose_/commit_contract_versionwith diff+token, immutable version, audit event (§5) — L - AG-10 [SHIPPED — mcp/_binding_tool.py; files a pending
bind_fieldsuggestion accepted via BindingLifecycle]bind_field_to_conceptvia approval/suggestion queue — M
Phase 4 — Packaging, DX, positioning¶
- AG-11 [SHIPPED — pycharter agent serve/tools/try] CLI
agent serve/tools/try;schemafor tool I/O (DX-4) — M - AG-12 [SHIPPED — pycharter.contrib.agents (Anthropic + Pydantic AI)] contrib adapters: Claude Agent SDK, Pydantic AI (AN-6) — M
- AG-13 [SHIPPED — guides/pycharter-for-ai-agents.md + llms.txt + README] (AN-7) "PyCharter for AI agents" guide, cookbook (runnable vs
ecommerce seed), README section,
llms.txt— M - AG-14 [SHIPPED — tests/unit/test_agent_invariants.py (I-1/I-2)] Provider consolidation under
agent/providers/+ I-1/I-2/I-8 CI invariant checks (coordinate with ARCH-1) — M - AG-15 [SHIPPED — Contracts → Agents → Proposals approval panel + Contracts → Test → Try it;
/api/v1/agent/*routes (approver = authenticated user); first SWR adoption (UX-1 pattern: typed hooks inlib/agent/hooks.ts); new components kept small (UX-2)] UI approval panel + "Try it" panel (existing editor + SWR) — L
Phase 5 — Later scope expansion (explicitly out of v1)¶
- AG-16 [SHIPPED (read tier) — list/fetch pipeline + stream configs (credentials masked), get_quality_score, list_drift_events, get_dlq_summary (counts only), list_concept_schemes (workspace-scoped). Maintainer decision 2026-09-14: read tools only; actuation and config-version writes stay out] Extend the catalog to pipelines/streaming/quality/vocabulary tools, each under the same read/guarded-write model — L/XL
- AG-17 [SHIPPED — Prometheus
pycharter_agent_validations_total/_repairs_total/_repair_attemptsviaAgentConformanceObserver(repair_loop(observer=, contract=),pycharter agent serve --metrics-port); per-contract quarantine + quality summary as MCPget_agent_conformance,GET /api/v1/agent/conformance, and Contracts → Agents → Conformance. Maintainer decision 2026-09-14: existing signals plus new counters, no new storage. Deviation:agent/is now ~1.76k LOC, over the §9 < 1.5k target (develop was 1.37k before AG-17)] Conformance monitoring: reframe quarantine + quality + drift as "agent-output conformance" over the existing worker/Prometheus — S/M
Dependency spine: Phase 0 → AG-½ → AG-3 → AG-5 → AG-8 → AG-9 → AG-11/12/13. AG-4 parallel AG-⅗. AG-14 runs with ARCH-1.
12. Rejected alternatives (recorded decisions)¶
kind: Toolsetartifact — rejected (decision #4). A versioned bundle artifact adds parser/store/migration/UI surface and a new abstraction to learn, for value a plain exposure-policy config (§6) delivers. Revisit only if real usage shows a need to version and share tool bundles across orgs.kind: Agentmanifest (persona/prompt) — rejected. Defining an agent is the caller's job; putting personas in PyCharter would breach I-1/I-4 and the "toolkit, not framework" boundary.- Full-surface tool catalog on day one — deferred (decision #3) to keep v1 sharp and hardenable; expansion is AG-16.
- Autonomous writes without approval — rejected (decision #2); all mutations are two-phase and fail-closed (§5).
- Field aliases /
SemanticMapper(AG-6, AN-4) — rejected by the maintainer (2026-09-13). Contract-level alternate names (x-aliases, vocabulary-derived synonyms, a rename-on-read map) would give one field two names, hide upstream naming drift, and add a contract-format keyword, all to solve a problem the toolkit already solves another way. Agents get exact field names up front (fetch_contract_schema/to_tool_schema); closed contracts reject undeclared fields;to_agent_feedback()+repair_loopmake the producer correct itself, and the feedback now carries a did-you-mean hint (undeclared field 'user_email'; did you mean 'email_address'?) computed on the fly from declared names — never stored, never applied. Sources that cannot change use the explicit pipelinerenametransform.
13. Risks & mitigations¶
| Risk | Mitigation |
|---|---|
| Toolkit drifts into an agent framework | I-1/I-4; no persona/loop in src/; CI invariant. |
| LLM/vendor lock-in in the core | I-2/I-3 import-lint CI; NoOpProvider default; [ai] optional. |
| An agent writes a bad/breaking contract | §5 two-phase + diff + breaking-classification + human approval + immutable versions + audit; fail-closed default. |
| Tool exposes PII / cross-workspace data | I-7 policy filter + redaction on every tool, reusing the audited context path. |
semantic/ sprawl worsens |
AG gated on ARCH-1; new agent/ shell capped and single-homed (§9). |
| Scope creep beyond focused core | Decision #3; expansion isolated to AG-16. |
| Provider tool-schema formats churn | Golden-file tests per provider; wrappers are one-file leaf code. |
14. Success metrics / acceptance¶
Friendliness (config-driven):
- With no config, an MCP host gets the full read tier, policy-filtered, in
one line — no install, no artifact to author.
- allow_write: true is the single switch that (approval-gated) lets a coding
agent help author contracts.
- pycharter agent try --record r.json shows a full runtime loop offline in
< 60s, no model, no traceback.
Generalizability (hard constraint):
- No new artifact kind; no agent/persona/loop in src/; no provider import in
the stateless core — all CI-enforced (I-1/I-2/I-8).
- The same tool catalog serves ecommerce, healthcare, or trading contracts
with zero engine change.
Value-prop reinforcement: - Every agent action (read or write) is contract-typed, policy-filtered, and audit-logged; guarded writes produce reviewable, reversible immutable versions.
Engineering health:
- Net new core LOC ≈ 0; agent/ shell < 1.5k LOC; semantic/ LOC down
after ARCH-1; ./scripts/ci.sh + docs build green.
15. Decisions & open questions¶
- MCP write ergonomics — DECIDED: two tools (
propose_contract_version/commit_contract_version). Reads clearly in an MCP host's approval UI; the proposal call is side-effect-free, the commit call is the one the host gates. - Adapter home — DECIDED:
contrib/(import-guarded, matchingcontrib/fastapi/airflow/dagster). Not a downstream package. - Approval mechanism & proposal backing — DECIDED: store-backed
(option C), reusing the existing
draftstable. Out-of-band / different-person approval is in scope (S1=yes), so the proposal must be durable and shared across processes and time. A proposal = a draft in aproposedstate; the token = the draft id;commit= a promotion to an immutable contract version. Governance defaults: S2 24h TTL, one-shot commit, GC of expired proposals; S3 segregation of duties supported (distinctproposer/approverroles), default config collapses them so the single-user laptop case stays frictionless; S4 stale-diff rejection — if the base version changed between propose and commit, commit is refused and a re-propose is required. See §16. Drives AG-9.
16. Guarded-write proposal backing (DECIDED)¶
Decision: option C — store-backed, reusing the drafts table, with
out-of-band approval in scope and the S1–S4 resolutions recorded at the end
of this section. The rest of this section is retained as the rationale.
What the token must do. propose_contract_version(...) stages a change
without committing and returns {diff, breaking|safe, token}. commit_
contract_version(token) persists an immutable new version after a human has
approved. So the token references staged state: proposed bytes + base
version + requester + computed diff/classification + status + expiry.
The decision is really about deployment topology:
| Topology | Description | Needs |
|---|---|---|
| T1 | Single-process stdio MCP on a dev laptop (Claude Code/Cursor) | approval = the MCP host's own per-call gate; any backing works |
| T2 | Multi-worker HTTP MCP/API for an org | commit may hit a different worker than propose → shared state |
| T3 | Async approval: agent proposes over MCP; a human approves later in the PyCharter UI; then commit | durable, queryable, cross-process "pending proposals" |
Three candidate backings:
- A. In-memory (uuid → dict in the MCP process). Trivial; covers T1 only. Breaks under T2 (wrong worker) and T3 (no cross-process/UI visibility); lost on restart. A false economy given T2/T3 are on the roadmap (HTTP transport, UI approval panel AG-15).
- B. Signed stateless (token = HMAC/JWT-signed serialization of the
proposed contract). No proposal store;
commitverifies + persists. Great for T2. But: large tokens (contracts are big), no record that a proposal is pending, hard single-use/revocation without reintroducing state, and can't do T3 (nothing to list/approve in the UI, nowhere to record the approval decision). - C. Store-backed proposal (recommended) — reuse the existing
draftsmachinery. A proposal is a draft in aproposedstate; the token is the draft id;commitis a promotion to an immutable contract version. Covers T1+T2+T3; durable audit; enables the UI panel; and adds almost no surface becausedb/models/draft.pyalready stores raw+parsed payload, owner/team/visibility, and promotion provenance (promoted_to_kind/ref/ at), and the store already has in-memory + SQLAlchemy (SQLite/Postgres) backends. The suggestion-queue / review-events subsystem (SuggestionManager, accept/dismiss) is the existing precedent for the approval workflow.
Why C reuses, not adds: a "proposed contract version" ≈ a draft +
status: proposed + expires_at + approved_by/at. Promotion already means
"draft → real artifact, deterministically." We'd add a small status/expiry to
the draft flow rather than a bespoke token table, and route approval through
the review-events pattern.
Sub-decisions — RESOLVED:
- S1 — Where does approval happen? → Out-of-band in scope. Approval may
happen in-session (MCP host gates commit) or later in the UI by a
different person. This mandates option C.
- S2 — Expiry & single-use. → 24h TTL; one-shot commit (token consumed on
success); periodic GC of expired proposed drafts.
- S3 — Who may approve? → Segregation of duties supported. Config can
require a distinct approver role for commit_*; the default collapses
proposer==approver so the single-user laptop flow needs no setup.
- S4 — Conflict handling. → Stale-diff rejection. If the base version
changed between propose and commit, commit refuses and forces a
re-propose, so the diff a human approved is guaranteed to still apply.
AG-9 implementation shape (from this decision)¶
- Add to the draft flow:
status(draft|proposed|committed|expired|rejected),expires_at,approved_by,approved_at,base_version(the version the diff was computed against). propose_contract_version→ validate + lint the candidate, compute the diff/classification vsbase_version, persist aproposeddraft, return{draft_id (token), diff, breaking|safe, expires_at}. Side-effect-free beyond writing the pending draft; emits acontract.proposedaudit event.- Approval surfaces (both write
approved_by/at, policy-checked per S3): the MCP host gatingcommit_*(in-session), or the UI approval panel (AG-15, out-of-band). commit_contract_version(token)→ re-check status/TTL (S2) and base_version freshness (S4); on pass, promote the draft to a new immutable contract version (reuse existing promotion + immutable-versions), mark the draftcommitted, emit acontract.committedaudit event.- Reuse the
SuggestionManager/ review-events pattern for the approval workflow; the drafts store's existing in-memory + SQLAlchemy backends give T1/T2/T3 for free.