Skip to content

Let a deployment name its one real Conduit, so the verdict log can populate - #759

Merged
xmap merged 1 commit into
mainfrom
conduit-verdict-log
Aug 31, 2026
Merged

xmap merged 1 commit into
mainfrom
conduit-verdict-log

Conversation

@xmap

@xmap xmap commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Closes the gap the last PR (#757) documented but didn't fix: CORA's authorization decisions were not in the record CORA claims is the artifact.

The gap

TrustAuthorize._emit_verdict writes one Verdict row per decision, but only when the target Conduit exists and has an open verdict logbook. On 2-BM: zero Conduit streams exist, and all ~180 handler call sites pass conduit_id=NIL_SENTINEL_ID, so every write is silently skipped. warn_if_verdict_log_dormant exists solely to say this out loud at boot.

This was a deliberate, named deferral. Watch item 6 of project_authorization_envelope_design.md states the fix precisely: "conduit injection + a seeded verdict logbook, NOT just flipping trust_policy_id", trigger "a compliance/ops need for one queryable cross-layer log." The JSR paper's D5 exhibit is that need.

The choice: resolve, don't sweep

Two ways to close this. Thread conduit_id through the ~180 call sites the way surface_id was swept — a real parameter change for a value that's identical everywhere until multi-zone topology exists, which project_conduit_injection_design.md's WI10 defers on purpose. Or resolve the deployment's one configured Conduit once, in the adapter.

The second is not a shortcut around honesty; it's what the sentinel is for. NIL_SENTINEL_ID is documented as meaning "unspecified," never "none" — so resolving it to a configured default is a default for the unspecified case, not a fabrication. A caller that passes a real, non-nil conduit_id of its own is never overridden. The precedent is exact: get_surface_id is itself a static return that ignores the request.

What ships

A forward-only migration seeds SYSTEM_LOCAL_ZONE_ID / SYSTEM_LOCAL_CONDUIT_ID with an open verdict logbook, named deployment-neutrally ("System Local," not "2-BM") since it ships to every deployment. LogbookSchema matches _TRAVERSALS_SCHEMA byte-for-byte, so a hand-seeded Conduit's logbook declaration is indistinguishable from one opened through the API.

Settings.trust_conduit_id resolves the unspecified case in TrustAuthorize._effective_conduit_id, called exactly once per authorize() call so the same resolved value feeds both the Policy evaluation and the Verdict write:

authorize(conduit_id=nil) ──► _effective_conduit_id ──► ONE resolved value
                                                              │
                                          ┌───────────────────┴───────────────────┐
                                          ▼                                       ▼
                                  decide_authorization                    _emit_verdict
                                  (what was evaluated)                 (what gets written)

The row can never describe a conduit the command wasn't actually evaluated against.

Three boot guards, because this codebase keeps re-learning that asking for a control and silently not getting one is the failure that matters:

  • trust_conduit_id set with no trust_policy_idAllowAllAuthorize is never constructed with a conduit_id, so the setting would be read by nothing.
  • trust_conduit_id set but the Conduit is missing or has no open verdict logbook → looks wired, populates nothing.
  • trust_conduit_id and trust_policy_id both set but the policy governs a different conduit → Policy.evaluate checks conduit before surface or principal, so this would deny every command, not narrow what's permitted. This is what makes leaving 2-BM's nil-bound policies unmigrated safe rather than an oversight.

warn_if_verdict_log_dormant now checks the effective conduit instead of hardcoding nil, so a correctly-adopting deployment gets no false alarm and a misconfigured one still gets a true one.

What's deliberately not here

  • The ~180-call-site sweep (deferred to WI10, same as the design doc).
  • A V3 bootstrap policy (guard 3 makes it unnecessary).
  • 2-BM's own migration to the real conduit — separate, deliberate step, so the shadow period already accumulating there isn't interrupted by another restart. The runbook records the capability exists and 2-BM hasn't adopted it yet.
  • The in-process SurfaceKind work (agent-raised commands are still ungovernable) — next PR.

Verification

Unit + architecture: 49,100 passed. Contract: 3,436 passed. Full integration tier: 1,364 passed, including a new test file that applies the migration against a real Postgres testcontainer (not just unit-level folding) and confirms the Zone/Conduit fold correctly with an open logbook, plus a case driving TrustAuthorize end-to-end through the configured conduit with the caller passing nil. Both boot guards and the conduit-resolution logic are mutation-verified in both directions. Default is unset for every new setting, so every existing deployment and ~2400 AllowAll-based tests are unaffected.

🤖 Generated with Claude Code

…pulate

CORA's claim is that the record is the artifact. Its authorization decisions
were not in that record: no Conduit stream existed anywhere, every one of the
~180 handler call sites passed the nil sentinel as conduit_id, and
TrustAuthorize._emit_verdict silently skips whenever the target Conduit
doesn't exist or has no open verdict logbook. Watch item 6 of
project_authorization_envelope_design.md named the fix precisely -- conduit
injection plus a seeded verdict logbook, not just flipping trust_policy_id --
with the trigger "a compliance/ops need for one queryable cross-layer log".
The JSR paper's D5 exhibit is that need.

The honest fix and the cheap fix turned out to be the same fix. Sweeping
conduit_id through ~180 call sites the way surface_id was swept would add a
real parameter with only one value until multi-zone topology exists, which
project_conduit_injection_design.md's WI10 defers on purpose. Resolving an
UNSPECIFIED (nil-sentinel) conduit_id to the deployment's one configured
Conduit is not a shortcut around that: NIL_SENTINEL_ID is already documented
as meaning "unspecified", never "none", so filling it in with a configured
default is exactly what the sentinel is for. A caller that passes a real,
non-nil conduit_id of its own is never overridden.

A forward-only migration seeds SYSTEM_LOCAL_ZONE_ID and SYSTEM_LOCAL_CONDUIT_ID
with an open verdict logbook, named deployment-neutrally since this migration
ships everywhere, not just 2-BM. Settings.trust_conduit_id resolves the
unspecified case in TrustAuthorize._effective_conduit_id, called once per
authorize() call so the SAME resolved value feeds both the Policy evaluation
and the Verdict write -- the row that gets written can never describe a
conduit the command was not actually evaluated against.

Three misconfigurations get a boot guard rather than a silent gap, because
this area keeps re-learning that asking for a control and not getting one is
the failure that matters:

  - trust_conduit_id set with no trust_policy_id: AllowAllAuthorize is never
    constructed with a conduit_id, so the setting would be read by nothing.
  - trust_conduit_id set but the Conduit is missing or has no open verdict
    logbook: looks wired, populates nothing.
  - trust_conduit_id and trust_policy_id both set but the policy governs a
    different conduit: Policy.evaluate checks conduit before surface or
    principal, so this would deny EVERY command, not narrow what the policy
    permits.

warn_if_verdict_log_dormant now checks the EFFECTIVE conduit rather than
always asking about nil, so a deployment that adopts this correctly gets no
false alarm, and one that adopts it incorrectly still gets a true one.

Default is unset for all three settings/guards, so every existing deployment
and roughly 2400 AllowAll-based tests are unaffected. 2-BM's own migration to
the real Conduit is deliberately not part of this change -- a separate step,
so the shadow period already accumulating there isn't interrupted by another
restart.

Verified against a real Postgres testcontainer with the migration applied
fresh (not just unit-level mocks): the Zone and Conduit fold correctly, the
verdict logbook is open, and an authorize() call from a caller passing nil
lands a Verdict row under the configured conduit. Both new boot guards and the
conduit-resolution logic are mutation-verified in both directions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  apps/api/src/cora/api
  main.py
  apps/api/src/cora/infrastructure
  config.py
  routing.py
  schema_version.py
  apps/api/src/cora/trust
  __init__.py
  _bootstrap.py
  authorize.py
  build_authorize.py
Project Total  

This report was generated by python-coverage-comment-action

@xmap
xmap merged commit fc42545 into main Aug 31, 2026
19 checks passed
@xmap
xmap deleted the conduit-verdict-log branch August 31, 2026 18:23
xmap added a commit that referenced this pull request Sep 1, 2026
… one (#761)

Every in-process call (RunWitness, the capture readers, the inference
recorder, promote_seeded_fleet) now carries SYSTEM_IN_PROCESS_SURFACE_ID
per #760, but the deployment's one configured Policy is bound to the HTTP
door, so every one of those calls strict-denies at the surface check.
Reading 2-BM's shadow log confirmed it: 99.998% of a 152,000-event window
was exactly this mismatch, drowning out anything a real front-door
near-miss would show.

Gives the in-process door its own second, optional rulebook instead of
widening Policy itself (Policy.surface_id stays a scalar; that general
multi-surface case is deferred until MCP traffic exists to validate it
against). TrustAuthorize resolves which configured policy governs a call
from its surface_id, mirroring the trust_conduit_id shape from #759:
Settings.trust_in_process_policy_id, default None, existing deployments
and tests unaffected. Two new boot guards close the same "looks wired,
governs nothing" gap the conduit knob already guards against: a backdoor
policy set without a front one, and a backdoor policy that doesn't
actually govern the in-process surface (or, once trust_conduit_id is set,
the right conduit).

promote_seeded_fleet now always promotes through AllowAllAuthorize rather
than the caller's kernel.authz, matching pilot_seed.py's own kernel
construction -- it is a rare, explicitly-operator-run bulk recovery
outside every request surface, and should not need enrollment in
whichever policy ends up governing the in-process door to run at all.

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