soul.demarkus.io:6309/index/decisions.md

Graph: decisions

decisions0029-broker-issues-ag…/adr/0029-broker-issues-agent-hub-tokens.md — soul.demarkus.io:63090030-broker-generates…/adr/0030-broker-generates-its-cookie-key.md — soul.demarkus.io:63090034-federation-graph…/adr/0034-federation-graph-derived-in-process.md — soul.demarkus.io:63090036-bucket-store-com…/adr/0036-bucket-store-commit-log.md — soul.demarkus.io:63090038-write-identity-i…/adr/0038-write-identity-is-user-and-agent.md — soul.demarkus.io:63090039-drop-standalone-…/adr/0039-drop-standalone-federation-agent.md — soul.demarkus.io:63090040-memory-mechanics…/adr/0040-memory-mechanics-in-the-server.md — soul.demarkus.io:63090042-soul-entries-are…/adr/0042-soul-entries-are-agent-written.md — soul.demarkus.io:63090043-memory-model-in-…/adr/0043-memory-model-in-client-memory.md — soul.demarkus.io:63090044-mdoutline-split/adr/0044-mdoutline-split.md — soul.demarkus.io:63090045-write-gates-run-…/adr/0045-write-gates-run-in-the-store.md — soul.demarkus.io:63090046-near-duplicate-g…/adr/0046-near-duplicate-gate-refuses-with-an-override.md — soul.demarkus.io:6309conventions/conventions.md — soul.demarkus.io:6309index/index.md — soul.demarkus.io:63090001-0020/index/decisions/0001-0020.md — soul.demarkus.io:63092026-10-02/journal/2026-10-02.md — soul.demarkus.io:63090021-world-registry-n…/adr/0021-world-registry-names-and-one-tenant-door.md — soul.demarkus.io:63090022-gateway-dependen…/adr/0022-gateway-dependencies-not-the-server.md — soul.demarkus.io:63090023-broker-packages-…/adr/0023-broker-packages-behind-one-run.md — soul.demarkus.io:63090024-plugin-binary-pa…/adr/0024-plugin-binary-packages-by-concern-and-host-values.md — soul.demarkus.io:63090025-watch-verb/adr/0025-watch-verb.md — soul.demarkus.io:63090026-client-sdk-mit-s…/adr/0026-client-sdk-mit-server-never-imports.md — soul.demarkus.io:63090027-world-token-file…/adr/0027-world-token-files-optional-and-split.md — soul.demarkus.io:63090028-stores-own-a-dur…/adr/0028-stores-own-a-durable-change-sequence.md — soul.demarkus.io:63090029-broker-issues-ag…/adr/0029-broker-issues-agent-hub-tokens.md — soul.demarkus.io:63090030-broker-generates…/adr/0030-broker-generates-its-cookie-key.md — soul.demarkus.io:63090031-bucket-store-sea…/adr/0031-bucket-store-seals-change-blocks.md — soul.demarkus.io:63090032-broker-in-the-kn…/adr/0032-broker-in-the-knowledge-server-process.md — soul.demarkus.io:63090033-identity-bearers…/adr/0033-identity-bearers-on-the-mark-front-door.md — soul.demarkus.io:63090034-federation-graph…/adr/0034-federation-graph-derived-in-process.md — soul.demarkus.io:63090035-drop-content-add…/adr/0035-drop-content-addressed-fetch.md — soul.demarkus.io:63090036-bucket-store-com…/adr/0036-bucket-store-commit-log.md — soul.demarkus.io:63090037-contract-is-the-…/adr/0037-contract-is-the-protocol-two-doors.md — soul.demarkus.io:63090038-write-identity-i…/adr/0038-write-identity-is-user-and-agent.md — soul.demarkus.io:6309+11 more links
soul.demarkus.io:6309/adr/0045-write-gates-run-in-the-store.md draft reader meta

ADR 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 runs Require: false (a published policy.md governs, 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 warning on 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 contentcheck options 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. marktools scans 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-tags from the server, as it did from the hook. A soul that wants block strictness publishes a policy.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, not ask: an ask has no answerer in a headless run and only a deny is documented to return its reason to the model.

Alternatives considered

  • Scan in marktools only. 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.md on every soul. Rejected: a write to people's stores for a document the empty policy already stands in for.
trail
  1. soul.demarkus.io:6309 graph: decisions
  2. 0045-write-gates-run-in-the-store