soul.demarkus.io:6309/index/decisions.mdsoul.demarkus.io:6309/adr/0045-write-gates-run-in-the-store.md draft reader metaADR 0045: Write gates run in the store, in both tiers
Status: accepted (2026-10-19). Plan: Same gates in both tiers, step 5 of Memory and knowledge layering. Extends ADR 0040 (mechanics in the server, the hook as the pre-write mirror) to the content and policy gates. Part of the decisions hub.
Context
- The knowledge server judges every write inside the commit through
server/internal/writepolicy: secrets refused, personal data per operator config, the world's policy document, malformed policy and template candidates. The soul server handed its store to the runtime unwrapped: no content check, no policy, on any client. The doctor found three container DSNs in this soul that the knowledge system would have refused. - A server-side warn had no channel to the writer: the knowledge server logged it and the response said
created. Warn-level policy misses vanished the same way. - The plugin hook scans pre-write for coding agents, but a hook is one harness; CLI, TUI, library and MCP writes from other hosts reach the store directly.
Decision
- Both tiers wrap their store with
writepolicy.Enforce. The knowledge server keeps its floor, required policy and operator content config; the soul server runsRequire: false(a publishedpolicy.mdgoverns, absent is the empty policy at warn), secrets at warn for one release then block, personal data at warn with the existing override key, no work domains, no seeded policy. - What a gate lets through with a note reaches the writer as the reserved response key
warningon a landed write: rule and line, never the value; policy misses under warn strictness too. The key is server owned, response only, never stored. Clients render it and keep it out of derived state such as the graph observation. - The hook mirrors the scan pre-write with the same
contentcheckoptions the soul server runs: a secret blocks with the fix, personal data warns. No strictness knob for secrets, no new tool, parameter, env var or config key.marktoolsscans nothing; the server is the one home. - A new server-side refusal warns for one release before it refuses, with a release note.
Consequences
- One implementation, two tiers, every client: the gap the hook could not close is closed in the store, and the hook stays the fast path with the fix in front of the agent.
- The knowledge server's personal-data warns and warn-level policy misses become visible to the writer for the first time.
- An untagged soul write now hears
missing-tagsfrom the server, as it did from the hook. A soul that wants block strictness publishes apolicy.md. - During the warn release a secret can land on a soul from a client without the hook; the stored remedy applies (rotate, then remove).
- Hook verdicts for a secret are
block, notask: anaskhas no answerer in a headless run and only a deny is documented to return its reason to the model.
Alternatives considered
- Scan in
marktoolsonly. Rejected: covers the two MCP surfaces, not the CLI, TUI, library or an older client; a second home for a rule the server already owns. - Hook only, as today for style. Rejected: one harness; the three findings came through the MCP tools of this harness with the hook installed and no content rule in it.
- Server refuses from the first release. Rejected: the delivery rule warns for one release so false positives surface before a refusal, and the warning channel had to exist first.
- A strictness knob for secrets. Rejected: a secret is never a preference; the placeholder rule is the way to write an example password.
- Seed a
policy.mdon every soul. Rejected: a write to people's stores for a document the empty policy already stands in for.