Skip to content

Stop a hand-typed permitted_commands list from silently going stale - #750

Merged
xmap merged 1 commit into
mainfrom
policy-command-list-fitness-test
Aug 30, 2026
Merged

xmap merged 1 commit into
mainfrom
policy-command-list-fitness-test

Conversation

@xmap

@xmap xmap commented Aug 30, 2026

Copy link
Copy Markdown
Owner

Why

A Trust Policy.permitted_commands set is define-once: there's 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 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() generalizes test_visit_command_names.py's existing single-file AST walk (_wire_command_name_literals(), hardcoded to trust/wire.py) across all 18 BCs. command_name= is a required keyword-only parameter on both with_tracing and with_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_NAME constants 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-BM permitted_commands set, 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 if trust_policy_id is set and app_env != "test" unless require_authenticated_principal=true is also set. 2-BM runs APP_ENV=dev, which doesn't get the "test" exemption — nobody has mapped what depends on the implicit SYSTEM_PRINCIPAL_ID fallback today.
  • Verdict logging (the audit-trail payoff) is dormant in every real deployment regardless of policy: it rides on trust_policy_id being set, but every handler routes through the nil-sentinel Conduit, which has no open verdict logbook.
  • Policy is meant to be defined through the already-documented first-boot workflow (docs/stack/deployment.md: boot under the bootstrap policy, POST /policies as SYSTEM, repoint and restart) — not a raw-event seed ceremony.

None of that is touched here. No settings, no migration, no deployment.

Verification

  • New test file run directly: 3 passed.
  • Architecture tier, staged first (blind to untracked files otherwise): 34,419 passed.
  • Unit tier as a non-regression check: 14,598 passed, unchanged from baseline.
  • pyright, ruff: clean.

🤖 Generated with Claude Code

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>
@github-actions

Copy link
Copy Markdown

Coverage report

This PR does not seem to contain any modification to coverable code.

@xmap
xmap merged commit 74afded into main Aug 30, 2026
17 checks passed
@xmap
xmap deleted the policy-command-list-fitness-test branch August 30, 2026 16:45
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant