Repository navigation
Description
Design (repo design architect, pass 12, 2026-10-29): one policy evaluator in a new internal/policy package, evaluated inside resolveAndGuard after it gains the call's arguments, in a fixed order — hidden-tool check (existing) → unknown-write ledger check → argument rules → write mode → confirmDestructive floor. The three origin issues below are three separately-filed slices of one design; they share one seam, one write classification, and one evaluator (#1986's own body: "Design the two together… the evaluator is one function so the two can never disagree"). This design adopts them as its phases instead of filing parallel implementation issues.
Design doc: files/designs/code-mcpx-write-policy.md in the keep-the-four-repos-healthy goal workspace (problem, current-state audit against origin/main 73ba3a96, proposal, alternatives, risks, open questions).
Why now / grounding
- The only write gate today is
elicit.confirmDestructive(default off,internal/settings/consumer.go:65;internal/daemon/consumer.go:382) — a boolean aimed at one annotation. It never shows the arguments, becauseresolveAndGuard(internal/daemon/registry.go:868) andconfirmDestructiveare both argument-blind even though all three call paths (Callregistry.go:852,CallReportinginput_required.go:75,CallAskroutes_proto.go:302) holdargsin hand. - The validator argument rules were blocked on (ex-nix#254) now exists:
internal/schemaval, landed by mcpx: Phase 1 of #1481 — schemaval core: a real JSON Schema 2020-12 validator + dialect policy (no call-path changes) #1482 and wired in by mcpx: Phase 2 of #1481 — validate tool arguments at CallTool (enforce, -32602) + structuredContent against outputSchema (warn-only) #1483 (internal/mcpclient/schemacheck.go). Tool-pattern keys areconfig.ToolPatterns; the classification precedent isdestructiveHint(internal/daemon/consumer.go:439-444, spec defaults — inverted, it is the write classification). - No unknown-outcome recording exists anywhere in
pkgs/mcpx/internal(grep-verified): a write lost after dispatch leaves no trace and the next write proceeds.
Decisions recorded in the design doc
- Classification shared and annotation-derived (write iff
readOnlyHintis not true); annotation dishonesty stays owned by mcpx: smaller conformance gaps -- user identity, annotation trust, rate limits, cursors, URI encoding, prompt validation, restart, invalidation #1957 — argument rules are the annotation-independent control. - Modes are settings (
policy.write, daemon scope,askdefault per mcpx: a write policy -- ask, allow, deny -- with the exact call shown when asking #1979's acceptance; per-serverwritePolicyoverride); rules are per-server config (argumentRules, kept over a foldedpolicyblock — mcpx: argument rules -- allow a call by its arguments, per tool and per argument (JSON Schema; depends on #254) #1986 open question 1 resolved); the evaluator is the shared part. askrenders the exact call (tool + canonical arguments) through the existing broker confirmation;elicit.confirmDestructiveis subsumed as a floor (destructive asks even underallow— decision 0002's "no level skips a consent gate").- Rule refusals render natively per surface (daemon 400
HiddenTool-style naming argument path/value/constraint/rule origin; MCP pass-throughisError: trueresult) — mcpx: argument rules -- allow a call by its arguments, per tool and per argument (JSON Schema; depends on #254) #1986 open question 3 resolved by the hidden-tool precedent. Rules only tighten (allOfacross pool/server/layered files); bad schema or non-RE2patternis a load-time config error; dead rules warn at load and on catalog change. - Unknown-outcome ledger is a versioned state-dir file (server, tool, args SHA-256 — never the arguments — time, surface), written atomically, persisted across restarts, never auto-cleared; "ambiguous" is conservative (post-dispatch marker only once request bytes were written).
- The policy is an operator consent gate, not the autonomy dial (decision
0002,pkgs/mcpx/docs/decisions/0002-autonomy-dial.md:confirmDestructiveclassed "not the dial"); neither clamps the other, and Phase 1 documents the interaction.
Phased breakdown (each independently landable)
- Phase 1 — the gate: mcpx: a write policy -- ask, allow, deny -- with the exact call shown when asking #1979 (sub-issue). Plumb
argsintoresolveAndGuard;internal/policy+ classification;policy.write+ per-serverwritePolicy; exact-call ask;deny;confirmDestructivesubsumption; dial documentation. Leaves ledger/rules steps as no-op hooks. Acceptance: mcpx: a write policy -- ask, allow, deny -- with the exact call shown when asking #1979's own checklist. - Phase 2 — argument rules: mcpx: argument rules -- allow a call by its arguments, per tool and per argument (JSON Schema; depends on #254) #1986 (sub-issue, gated on Phase 1,
status/blocked).argumentRulesconfig +schemavalcompilation, evaluation before the mode step, per-surface refusals, dead-rule warnings. Acceptance: mcpx: argument rules -- allow a call by its arguments, per tool and per argument (JSON Schema; depends on #254) #1986's own checklist including its four worked examples. Triage first confirms mcpx: JSON Schema validation of tool inputs/outputs and elicitation schemas (7 conformance rows) #1951's in-review reconciliation does not reopen the validator question. - Phase 3 — unknown-outcome ledger: mcpx: block further writes after a write whose outcome is unknown #1977 (sub-issue, gated on Phase 1,
status/blocked). Post-dispatch error marker, state-dir ledger, guard step,mcpx writes/mcpx writes clear <server>+/v1clear route, restart-persistence tests. Acceptance: mcpx: block further writes after a write whose outcome is unknown #1977's own checklist. On landing, post evidence on tracker mcpx: adopt what is useful from codex-connectors-mcp #1984 for its mcpx: a write policy -- ask, allow, deny -- with the exact call shown when asking #1979/mcpx: block further writes after a write whose outcome is unknown #1977 checkboxes.
Not in this design (open questions in the doc)
Schema advertising of rules (allOf in mcpx types / tools/list); pinning/set (#1933's slice); redaction of secret-shaped values in ask renderings; per-principal policy (waits on #1968/#1972); ledger visibility in mcpx status.
Related: #1984 (tracker that lists #1979/#1977), #1481 (validation — dependency, not a slice), #1957 (annotation trust), #1634 (landed; its client recovery sits below this design's recording point).
{"kind":"design","by":"repo-design-architect-code","pass":12,"date":"2026-10-29","design_doc":"files/designs/code-mcpx-write-policy.md","phases":{"1":1979,"2":1986,"3":1977},"origins":{"1979":"dezren39/nix#340","1986":"dezren39/nix#355","1977":"dezren39/nix#339"}}
Activity
developing-today commentedon Oct 29, 2026
CODE-1025
mcpx: argument rules -- per-tool/per-argument JSON Schema call constr…
mcpx: argument rules — per-tool/per-argument JSON Schema call constra…
dezren39 commentedon Nov 3, 2026
Triage — 2026-11-03 (repo-health queue, worker
repo-worker-code)Verdict: the design is fully delivered; nothing in this epic remains to design or build. Closing with evidence.
All three phases named in this design landed on
main, verified atffad69add:mainpolicy.write, classification, args plumbed intoresolveAndGuard,deny, destructive floor)bd48deb1e)pkgs/mcpx/internal/policy/policy.go,pkgs/mcpx/internal/daemon/consumer.go,pkgs/mcpx/internal/settings/effect.go;write_policy_test.goargumentRules,schemavalcompilation, refusals, dead-rule warnings)cffd524fd)pkgs/mcpx/internal/config/argumentrules.go,pkgs/mcpx/internal/daemon/consumer.go,pkgs/mcpx/internal/daemon/argument_rules_test.gomcpx writes clear,/v1/writes/{server}/clear, restart persistence, guard step)a41cba35d)pkgs/mcpx/internal/daemon/write_ledger.go,pkgs/mcpx/internal/api/ops_writes.go,pkgs/mcpx/internal/daemon/registry.go(ledgerLookupFor),pkgs/mcpx/docs/parity.mdThe evaluation order this design fixed (hidden-tool → ledger → argument rules → mode →
confirmDestructivefloor) is the one the landed code implements; the ledger's own error text namesmcpx writes clear <server>.What this close does not do: it does not close the sub-issues. #1977 is still open (
status/needs-triage,queue/queued) even though its implementation merged in #4721; its closure is that issue's own queue task, and #1975 has the same shape. Tracker #1984 already records both as "implemented and merged but not yet closed" with the same SHAs, so no bookkeeping is lost here.Out-of-scope items this design listed (schema advertising of rules, pinning/
set, secret redaction in ask renderings, per-principal policy, ledger visibility inmcpx status) remain separately tracked and are not part of this epic.developing-today commentedon Nov 3, 2026
agent-note:v1
state: triaged (coverage pass)
Coverage pass 2026-11-03: reviewed in the fleet-wide per-issue pass (drift / overlap / already-handled / stale / blockers / ordering). Verdict: still live; queued in repo-health-code-pilot as triage at priority 40 (priority/normal). If this is already handled at current main, the triage closes it with evidence; if it needs splitting, the triage files the split.
Found via: coverage pass 2026-11-03