Pin actor-kind blindness, and fix the audit sweep that contradicted it#566
Merged
Conversation
CORA's governance claim is that an agent and a human operator are the same kind of actor to the system: every gate decides over a bare principal and none asks whether that principal is a machine. That claim was asserted in prose and enforced by nothing, so a single careless branch could falsify it silently. An AST sweep over the git-tracked source finds exactly two branches on ActorKind, and both are deliberate: register_actor guards the kind of the Actor being minted (never the caller) to force agent genesis through define_agent's atomic write, and register_decision guards the kind of the Actor a Decision is attributed to (never the authenticated principal) so the unsigned operator route cannot become a signing-bypass door. Both are enumerated with their reason, and the allowlist is asserted in both directions so a deleted branch fails as a stale entry rather than rotting. The load-bearing pin is the Authorize signature. The policy decision point takes principal_id, command_name, conduit_id, surface_id, and Policy carries no kind field, so authorization is kind-blind structurally rather than by discipline. Widening that signature would make the claim false, so the parameter set is locked. The register_decision reason records the rule-of-three trigger: a third carve-out means the discriminator belongs in Policy data, not in machinery. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…have Each of these asserts a kind check that no code performs, which is worse than silence: a reader trusts them, and a reviewer who greps finds documentation describing an unimplemented human-only gate sitting next to CORA's kind-blindness claim. register_decision's decider said SIGNED_EVENT_TYPES is "discriminated at write time by actor.kind == AGENT". It is not. Every signing site gates on `signer is None or event_type not in SIGNED_EVENT_TYPES` and none reads Actor.kind, so the signing layer is more kind-blind than the prose claimed. The docstring now also records that the attribution guard rests on honest declaration, since decided_by is body-supplied and never reconciled against principal_id. SignedOffBy and CoDevelopedBy said a federation decider enforces `Actor.kind == human` and raises DcoChainMissingHumanSignoffError / CoDevelopedByForbidsAgentError. Neither error class exists anywhere in the tree, no decider reads the kind, and check_dco_chain verifies only that the chain carries a SignedOffBy entry, never that its actor is human. An agent actor_id passes today. The human-only rule stays as stated design intent from the Linux-kernel coding-assistants transplant, now marked unenforced, with a pointer to the fitness test where the reason for a third kind branch would have to be argued. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…egistry Two defects, both found by adversarially evaluating the rule against the fix it will have to permit. Rule 3 keyed on files importing SIGNED_EVENT_TYPES as a proxy for "signing site". It was wrong in both directions. The two subscriber sites take their Signer by constructor and never import the port, so they were pinned only by accident of also importing the registry; and signing.py DEFINES the registry rather than importing it, so the one file holding the verifier was exempt from the rule meant to guard it. Meanwhile an innocent read-side obligation predicate that imports the registry and signs nothing would have been caught. Keying on an actual `<x>.sign(...)` call fixes both: it pins seven sites where it previously pinned four, adding the calibration signing path and both adapters, and it frees the verify side. The allowlist's rule-of-three trigger was counting the wrong axis. Both entries read the kind of a command's SUBJECT (the Actor being minted, the Actor being attributed), and the trigger told a future reader to move the discriminator into Policy data. That is a category error: Policy governs what a CALLER may do, and the caller-kind count is zero and structurally pinned by Rule 2. A subject-kind read has no business in Policy. The module docstring now states the subject/caller distinction the whole file turns on, and the allowlist says plainly that a caller-kind branch is not allowlistable at all. Verified by mutation both ways: a kind read inside a signing site fails Rule 3, while the same read inside signing.py's verifier does not, since verification reading a subject's kind to decide whether an absent signature is an anomaly is evidence-checking rather than a gate. Rule 1 still enumerates it, which is the intended division of labour and is now spelled out. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
verify_stream took a `strict: bool` meaning "raise on any unsigned row whose type is in SIGNED_EVENT_TYPES". Event type is the wrong predicate. DecisionRegistered is in that set, but only rows a Signer-wired path produced carry a signature: an operator recording a human decision through register_decision legitimately emits an unsigned DecisionRegistered, and the flag raised on it. The function's own docstring said human rows are legitimately unsigned about thirty-five lines above the code that raised on them. Event type alone cannot express "should this row have been signed", because the answer depends on the Actor the row is attributed to. Teaching this module to load Actors and read their kind would put an authorship branch inside signing infrastructure, which is the one place it must not go. So the obligation moves out to the caller, who already has a store to read and a reason to care, as a `must_be_signed` predicate: absent, unsigned rows are skipped as before; supplied, it decides per row. One parameter instead of a boolean plus a registry lookup, and the state that produced the bug is now unrepresentable. No production caller exists, so this is a choice about what to build rather than an outage, and no predicate ships with it: writing one before an audit sweep needs it would invent a consumer. The docstring carries the exact shape a correct one takes. Two docstrings nearby claimed enforcement the code does not have. The SIGNED_EVENT_TYPES registry said the subscriber tier "checks actor_id" before signing; it does not, the split is achieved by which paths the composition root hands a Signer. And _agent_decision_signing said register_decision's decider "rejects AGENT principals"; it refuses rows ATTRIBUTED to an agent-kind Actor, never the calling principal, whose kind no decider sees. That distinction is the whole point of the sibling fitness test, so leaving it inverted here would have undone the lesson two files away. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
xmap
added a commit
that referenced
this pull request
Jul 16, 2026
`assert authz["median"] < 5.0` is red on main-merged PRs. It failed at 5.418 and 5.453 on PR #566, whose diff cannot touch authz at all, minutes after passing on main with the same test. Same code, different runner. The ceiling was never careless, and that is the interesting part: developer machine 0.253us what it was calibrated against old ceiling 5.0us ~20x the dev number; reads as generous shared CI runner ~5.44us ~21x SLOWER than dev for this in-memory call Written on a machine where it had 20x headroom, it lands a hair under the actual cost on a runner that is ~21x slower at a no-I/O `Policy.evaluate`. Nobody would guess that ratio, which is why the number survived review. Main cleared it by luck; the next PR did not. Assert the shape of the claim instead. Policy.evaluate is an in-memory decision, so it costs microseconds; a regression that matters means someone put a lookup on the decision path, and that costs milliseconds. Between runner noise and a real regression sit three orders of magnitude, so any ceiling in that gap catches everything this test exists to catch while ignoring which CPU GitHub assigned. 50us is ~200x the dev median, ~9x the CI median, and mirrors the headroom the replay floor already had (78k/s observed against a 10k/s floor) which is exactly why that assertion never flaked and this one did. The paper's numbers are unaffected: they come from a deliberate CORA_PERF_OUT run on the developer testcontainer, not from this guard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
xmap
added a commit
that referenced
this pull request
Jul 16, 2026
…567) `assert authz["median"] < 5.0` is red on main-merged PRs. It failed at 5.418 and 5.453 on PR #566, whose diff cannot touch authz at all, minutes after passing on main with the same test. Same code, different runner. The ceiling was never careless, and that is the interesting part: developer machine 0.253us what it was calibrated against old ceiling 5.0us ~20x the dev number; reads as generous shared CI runner ~5.44us ~21x SLOWER than dev for this in-memory call Written on a machine where it had 20x headroom, it lands a hair under the actual cost on a runner that is ~21x slower at a no-I/O `Policy.evaluate`. Nobody would guess that ratio, which is why the number survived review. Main cleared it by luck; the next PR did not. Assert the shape of the claim instead. Policy.evaluate is an in-memory decision, so it costs microseconds; a regression that matters means someone put a lookup on the decision path, and that costs milliseconds. Between runner noise and a real regression sit three orders of magnitude, so any ceiling in that gap catches everything this test exists to catch while ignoring which CPU GitHub assigned. 50us is ~200x the dev median, ~9x the CI median, and mirrors the headroom the replay floor already had (78k/s observed against a 10k/s floor) which is exactly why that assertion never flaked and this one did. The paper's numbers are unaffected: they come from a deliberate CORA_PERF_OUT run on the developer testcontainer, not from this guard. Co-authored-by: xmap <16776958+xmap@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Coverage reportClick to see where and how coverage changed
This report was generated by python-coverage-comment-action |
||||||||||||||||||||||||||||||||||||||||||||||||
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
CORA's governance claim is that an agent and a human operator are the same kind of actor to the system. That claim is load-bearing for two in-flight papers, and it was enforced by nothing.
Making it enforced turned up three docstrings asserting checks that do not exist, and one real defect that contradicted the claim outright.
What an AST sweep actually found
Exactly two branches on
ActorKindin the whole source tree. Both read the kind of a command's subject, never the kind of the caller:register_actorguards the kind of the Actor being minted, forcing agent genesis throughdefine_agent's cross-BC atomic write so theAgent.id == Actor.idlock holds.register_decisionguards the kind of the Actor a Decision is attributed to, so the unsigned operator route offers no unsigned path for rows the Signer-wired path would have signed.The load-bearing pin is the
Authorizesignature:principal_id,command_name,conduit_id,surface_id.Policyhas no kind field. The policy decision point cannot branch on kind even in principle, so authority is kind-blind structurally rather than by discipline.Commits
Authorizeparameter set pinned. Nothing that produces a signature may readActorKind.DcoChainMissingHumanSignoffErrorandCoDevelopedByForbidsAgentErrordo not exist anywhere in the tree;check_dco_chainverifies only that aSignedOffByentry is present, never that its actor is human, so an agent actor_id passes today.Signerby constructor and never import the port, whilesigning.pydefines the registry and so escaped the rule meant to guard it. Keying on an actual.sign(...)call pins 7 sites where it pinned 4.verify_stream'sstrict: boolraised on any unsigned row whose type is inSIGNED_EVENT_TYPES, but human-recordedDecisionRegisteredrows are legitimately unsigned. Replaced with a caller-suppliedmust_be_signedpredicate.The framing this settles
Three axes, not one:
Signer, never by a branch inside signing code."Sign everything" was evaluated and rejected: a human at a beamline holds no key, so the server would sign, which attests only that the server wrote the row down. That is what the envelope
principal_idalready attests.Verification
Every rule mutation-tested rather than trusted green. Injected
ActorKindcomparison, bare-string compare, andmatchprobes into the PDP: all caught. WidenedAuthorizewith aprincipal_kindparameter: caught. Regressedverify_streamto type-only logic: fails exactly the two regression tests and nothing else.Architecture tier 29533 passed, 588 skipped. Infrastructure tier 449 passed.
Known, deliberately not addressed
decided_byis body-supplied and not reconciled againstprincipal_id. This is designed (the route documents any Actor, "including Deactivated (historical fact still holds)"), and the envelope records the submitter. The residual is transparency:proj_decision_summarycarriesdecided_bybut not the recording principal.must_be_signedpredicate ships. There are zero audit callers, and writing one now would invent a consumer.InMemorySignerignoresactor_id, one ephemeral key), but_enforce_production_signing_posturerefuses to boot production wired to it.🤖 Generated with Claude Code