Stop a hand-typed permitted_commands list from silently going stale - #750
Merged
Merged
Conversation
A Trust Policy's permitted_commands set is define-once: there is no UpdatePolicy, only RevokePolicyGrant, which can only shrink permitted_principal_ids. Widening or fixing permitted_commands after the fact means defining a brand-new Policy, repointing Settings.trust_policy_id, and restarting. That makes any hand-typed command-name list feeding a real Policy the exact hand-copied-list bug class already documented elsewhere in this repo: a slice gets renamed or removed, the list still names the old string, and every caller of that command starts getting an unexplained 403 in production instead of a red build. No function anywhere enumerates "every command name the app exposes." The mechanically-enforced source of truth is the command_name= keyword literal passed to with_tracing/with_idempotency at every BC's wire.py composition site, a required keyword-only parameter on both, so nothing can be wired without supplying one. The nearest precedent, test_visit_command_names.py, already does this exact AST walk but hardcoded to trust/wire.py alone. This generalizes it across all 18 BCs (301 distinct command names today) and checks the one hand-typed list in the repo that is shaped like a future Policy's permitted_commands: _facility_fixture.py's _OPERATIONS_COMMANDS and _AGENT_COMMANDS, 2-BM's own representative operator/agent command lists. Verified before writing this: all 29 entries are real, live command names today, so the test lands green with nothing to fix. The System Bootstrap Policy seed is deliberately not in this registry: its own check already imports the _COMMAND_NAME constants directly rather than retyping strings, so it carries no drift risk to begin with. Scoped to the guard rail only. Research into actually turning on enforcement at 2-BM surfaced that it is a separate, higher-stakes project: main.py's boot gate requires require_authenticated_principal whenever trust_policy_id is set outside test, 2-BM runs APP_ENV=dev which does not exempt it, and Verdict logging is dormant everywhere today since no Conduit has an open verdict logbook. None of that is touched here. No production code, settings, or deployment changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
xmap
added a commit
that referenced
this pull request
Sep 1, 2026
…tus page a name (#764) Groundwork for moving CORA's background runtimes onto the in-process door. Neither half changes what the running application does. ## The grant table `cora.api.in_process_grants.IN_PROCESS_GRANTS` names, per principal, the exact commands that principal issues through the back door: 20 principals, 28 distinct commands, 44 grant pairs. The cross-product a pre-#762 rulebook would have granted is 560, so 92% of it was fiction. It lives in the composition root, not in `cora.trust`, because `tach.toml` restricts Trust to infrastructure, shared, and its own aggregates: a table there importing agent-id constants from `cora.agent` does not build. The composition root is also where it belongs, since which agents a deployment runs is wiring rather than domain. Inert by construction, which is the load-bearing property. Nothing in `src/` imports it; only the fitness test (which AST-parses rather than imports) and `tools/gen_policy_grants.py`, which emits the `POST /policies` body an operator pipes into the API. If the running app read this table and defined a Policy from it, code would be granting itself its own authority, and a merged commit would be the only thing between an edit here and live authority. A human still posts it; nothing arms itself. The fitness test extends #750's registry, so every command name here must appear in the real wire surface. That closes the gap #750 opened but could not fill: it guards hand-typed command lists in the repo, while the two rulebooks actually armed at 2-BM were typed into a curl body and live only as events in a database CI cannot see. Generating the body from a CI-checked table is what turns "these names were correct on the day I checked" into a property. ## StatusPublisher `_status_push.py` acts as `SYSTEM_PRINCIPAL_ID`, which is also the fallback identity an unauthenticated HTTP request receives. Safe, since the back door demands a surface no HTTP request can claim, but the verdict log would read "nobody in particular" about a thousand times an hour. It now has a seeded Agent of its own, following `seed_run_witness.py`'s shape. `_status_push.py` itself is untouched: it still issues every read as SYSTEM until the call-site sweep. This commit creates the identity that sweep will use. ## Notes The eight synthetic names (`CampaignWatcherTick` and siblings) turned out NOT to need the allow-list the brief anticipated. They are `command_name=` values on `to_new_event(...)` envelopes, audit labels on a Decision record, and never reach `authorize()`. Verified at every site before omitting them. Two principals in the table, RunDebriefer and CautionDrafter, are overridable by env var and are overridden at 2-BM to deployment-specific Agent ids. The table names the source-code constants; a deployment that overrides them must grant the ids it actually runs. Co-authored-by: xmap <16776958+xmap@users.noreply.github.com> Co-authored-by: Claude Sonnet 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A Trust
Policy.permitted_commandsset is define-once: there's noUpdatePolicy, onlyRevokePolicyGrant(which can only shrinkpermitted_principal_ids). Widening or fixingpermitted_commandsafter the fact means defining a brand-new Policy, repointingSettings.trust_policy_id, and restarting. That makes any hand-typed command-name list feeding a real Policy exactly this repo's own documented hand-copied-list bug class: a slice gets renamed or removed, the list still names the old string, and every caller of that command gets an unexplained 403 in production instead of a red build.What changed
One new file, no production code touched.
_all_wire_command_names()generalizestest_visit_command_names.py's existing single-file AST walk (_wire_command_name_literals(), hardcoded totrust/wire.py) across all 18 BCs.command_name=is a required keyword-only parameter on bothwith_tracingandwith_idempotency, so this is a mechanically-enforced, complete enumeration — 301 distinct command/query names today, confirmed by running it.That gets checked against the one hand-typed list in the repo shaped like a future Policy's
permitted_commands:_facility_fixture.py's_OPERATIONS_COMMANDS(26 strings) and_AGENT_COMMANDS(3 strings) — 2-BM's own representative operator/agent command lists. Verified before writing any code: all 29 entries are real, live command names today, so this test lands green with nothing to fix — it's forward protection, not a fix for existing drift.The System Bootstrap Policy seed is deliberately not added to the registry: its own check (
test_bootstrap_policy_seed_postgres.py) already imports_COMMAND_NAMEconstants directly rather than retyping strings, so it has no drift risk to begin with. Duplicating it here would just create a second hand-typed list with the exact problem this file exists to prevent.A registry (
_HARDCODED_COMMAND_LISTS) makes adding a future list — including the real 2-BMpermitted_commandsset, whenever that gets defined — a two-line addition.Scoped deliberately narrow
Research into actually turning on enforcement at 2-BM surfaced that it's a separate, higher-stakes project, not a natural extension of this PR:
main.py's boot gate refuses to start iftrust_policy_idis set andapp_env != "test"unlessrequire_authenticated_principal=trueis also set. 2-BM runsAPP_ENV=dev, which doesn't get the"test"exemption — nobody has mapped what depends on the implicitSYSTEM_PRINCIPAL_IDfallback today.trust_policy_idbeing set, but every handler routes through the nil-sentinel Conduit, which has no open verdict logbook.docs/stack/deployment.md: boot under the bootstrap policy,POST /policiesas SYSTEM, repoint and restart) — not a raw-event seed ceremony.None of that is touched here. No settings, no migration, no deployment.
Verification
🤖 Generated with Claude Code