D746 — A KB carries its own policy in settings.yaml, and it wins over the server¶
Amended by D906: the KB's
settings.yamlis the only home of a KB's policy (thekbs[]twins and the "ignored server value" report are gone),mcp_allowlistandallow_artifact_writeare removed, andsecretsis derived from the sops files undersecrets/.
Decision. The per-KB keys that describe how a KB behaves (the schema's kb scope: artifact and
template writes, write gate, auto-repair, doctor cadence, MCP allow-list, how changes land, and the
new secrets declaration) can be set in settings.yaml at the KB root, next to paths.yaml. The
KB is authoritative: effective value = the KB file if it sets the key, else the server's
kbs[]/env/flag value, else the default. A differing server value is ignored and reported, with no
local override. A KB without the file behaves exactly as before. No MCP tool writes the file.
Why. A KB is shared. Five people mounting it must get the same behaviour, and with the policy in
each server's kbs[] a mount could have artifacts enabled while another did not, and a discovered KB
could carry none at all. The file travels with the KB through git, reviewed like a Map contract.
Moving allow_artifact_write and mcp_allowlist into the KB does not weaken anything: anyone who
can push to the KB can already commit artifacts and mcp/*.json with git, so the server flag was
never the trust boundary; signature verification and the hash-bound local approval of MCP
descriptors (D115) stay per machine. The cost: an operator can no longer tighten a KB's policy from
the server side, and a bad commit changes every mount at once. github_api_url is the exception
among the forge keys and stays server-only: the server sends its own GitHub token to that URL, so a
KB able to set it would let anyone who can push to the KB send the token to a host of theirs.
Alternatives rejected.
- The server wins and the KB only suggests: the failure this removes, a shared KB behaving
differently per mount.
- A local override on top of the KB: it brings the divergence back through the side door and needs
a second precedence story. An operator who disagrees changes the KB or does not mount it.
- A hidden .cartographer/ file: paths.yaml already is the KB-root settings file, with the same
tolerant parsing and lint reporting; a new directory would need explaining.
- Moving machine-bound keys too (ssh_key, author_*, sops_age_key_file, tokens): each server
needs its own, and artifact_signing_seed is a private key. secrets: true|false lets the KB
declare the requirement while the key stays per machine.
Consequences. internal/kb cannot import config (config -> lint -> kb), so kb holds a table
of the accepted keys and their value checks, and TestKBSettingsKeysMatchSchema pins it to the
schema in both directions; checks that need lint or provisioning (auto_repair names,
mcp_allowlist entries) run in config.ResolveKBSettings and in the lint check. Policy fields of
kb.KB are read without a lock, so the refresh (KB.RefreshSettings, after a pull that moved HEAD
and before a write) assigns them in one place and runs under the git lock; mcp_allowlist and the
git workflow keys are read at mount and apply at the next start. The settings view gains origin
kb. A future per-KB key goes in the schema first and, if it describes the KB and not the machine,
gets scope kb, which makes it settable here.