Kill-switch: revoke a grant, hold the revoked principal's runs (K1-K3)#547
Open
xmap wants to merge 3 commits into
Open
Kill-switch: revoke a grant, hold the revoked principal's runs (K1-K3)#547xmap wants to merge 3 commits into
xmap wants to merge 3 commits into
Conversation
Recover the grant-revocation slice on the Policy aggregate (Trust BC): the kill-switch's first increment. RevokePolicyGrant drops one principal from a Policy's permitted_principal_ids allow-list and emits PolicyGrantRevoked, the trigger event a later subscriber (K3) reacts to by holding the revoked principal's in-flight runs. Rebuilt from the recovered design of a prior slice whose source was lost (only stale bytecode remained; not on main). Decider is set-membership removal, SILENTLY IDEMPOTENT (returns [] when the principal is already absent, no error, no 409 since Policy has no status FSM); PolicyNotFoundError when the Policy does not exist; InvalidPolicyGrantRevokeReasonError on a bad reason (validated before the membership no-op so a malformed command cannot bypass validation via an absent target). Kind-blind: revoking a human's grant and an agent's grant run the same code; no decider reads actor kind. The invoker is the envelope principal, threaded by the handler and stamped as PolicyGrantRevoked.revoked_by (unspoofable), distinct from the command's permitted_principal_id (the grant removed). The mid-run hold is deliberately a SEPARATE eventually-consistent subscriber (K3), kept out of this handler per the compensation-slice no-cascade lock. Authorization is command-level, not per-Policy (the intended kill-switch posture, matching define_policy). Naming (R3 gate-review): command carries the aggregate qualifier (RevokePolicyGrant) as a per-aggregate sub-concept, while the slice dir + MCP tool drop it (revoke_grant); grant added to the sub-concept domain-noun allowlist + documented in conventions.md. Field named permitted_principal_id (ties to the state field) to avoid overloading the ambient invoker principal_id. Full slice-coverage compliance (no deferral allowlist entries): decider example + property-based tests, handler unit test, REST endpoint + MCP tool contract tests; Policy events + evolver tests extended for PolicyGrantRevoked. Gate review: correctness (APPROVE, no P0/P1) + R3 naming (applied). Verified: pyright + ruff clean, 24 slice tests, 27922 architecture, 3800 trust+contract. Co-Authored-By: Claude <noreply@anthropic.com>
…ookup (K2) Fold the Run event log into proj_run_actor_involvement so the authority- revocation holder subscriber (K3) can resolve a revoked principal's in-flight runs and hold each. Answers "which in-flight Runs does principal P drive?". The starter of a Run is the RunStarted event's ENVELOPE principal_id (the Authorize-gated issuer), not a payload field, so the projection attributes on event.principal_id and stamps created_at from the envelope occurred_at (both deterministic, so ORDER BY created_at is stable across rebuilds). A RunStarted with no envelope principal (pre-hook / backfilled) is skipped: no starter to attribute. Lifecycle events fold status by run_id via one status map (RunHeld -> Held, RunResumed -> Running, and the four terminals); the lookup filters to in-flight (status IN Running, Held). Terminal rows are retained, not deleted, so the resolver stays a pure fold and past involvement is auditable. Involvement scope is STARTER-ONLY for v1, structured extensible: the involvement_role column carries 'starter' and reserves 'supervisor' for a later additive fold (the HoldRun / ResumeRun envelope principals) that widens "drives" to "started or supervises" with no schema change. Kind-blind: attribution and the fold never read actor kind; a human's runs and an agent's runs resolve through the same code. The port is consumer-shaped (runs_driven_by), lives in cora.infrastructure.ports for neutral cross-BC access, and the Run BC ships PostgresRunActorInvolvementLookup; test envs default to NoInvolvementLookup (returns []), mirroring the ClearanceLookup / SupplyLookup test-default pattern. No AuthzResult widening, no cascade: this is a pure read-model + consumer port, the hold logic lands in K3. Gate review (4 agents: architecture / test-coverage / cross-BC / event-fold specialist): 0 P0, 3 P1, all addressed. P1s: port re-exported from ports/__init__.py (was imported from the submodule, breaking the neutral-access convention); idempotent replay-from-zero test added (guards ON CONFLICT DO NOTHING + set-to-constant UPDATEs); lifecycle-event-without-RunStarted test added (harmless zero-row no-op). P2s folded in: read created_at from the envelope occurred_at not payload (a missing payload key would have stalled the fold, blinding the kill-switch); collapsed RunResumed into the single status map (dropped the duplicate SQL); aligned the supervisor-widening note across port / projection / migration; created_at-primary ordering test + SQL-identity assertions. Watch: unconditional lifecycle UPDATE-by-run_id could revive a terminal row under a malformed stream; left as-is to match RunSummaryProjection precedent (the aggregate FSM prevents it), replay-safety verified holds since per-stream event order is total under the worker's (transaction_id, position). Verified: pyright + ruff clean, tach validated, 24 K2 tests (unit + integration against real Postgres), 28619 architecture tests green. Co-Authored-By: Claude <noreply@anthropic.com>
…(K3)
Complete the kill-switch (K1 trigger -> K2 resolver -> K3 holder). The
AuthorityRevocationHolder is a deterministic Agent-BC subscriber: on a committed
PolicyGrantRevoked it asks the K2 involvement lookup for the revoked principal's
in-flight Runs and holds each Running one, recording one
Decision(context=AuthorityRevocationHold, choice in {Held, HoldDeferred}) per run
parented to the revocation event. This is CORA's first fully-autonomous
enforcement action: an operator revoking trust now immediately contains the runs
that principal can no longer be trusted to drive.
Three user-locked design decisions: (1) ON BY DEFAULT (registered
unconditionally, no llm/setting gate: a kill-switch that must be turned on is not
a kill-switch; the hold is reversible via resume_run, so default-on matches the
safety intent); (2) own pinned agent + a Decision record per hold (full
provenance, AuthorityRevocationHolder in the b111 id block); (3) Running-only.
The hold is a Pattern C cross-BC write, not a call to the hold_run slice:
cora.agent may depend on cora.run.aggregates but NOT cora.run.features (the tach
BC boundary; the RunSupervisor loop is exempt only because it lives in cora.api).
So the subscriber loads the Run, guards status == Running in-process (mirroring
hold_run's own decider), authorizes HoldRun as its own principal via the Authorize
port, constructs RunHeld from cora.run.aggregates, and appends with optimistic
concurrency, exactly the pattern CautionPromoter uses for CautionRegistered.
Fail-safe by construction: every non-hold path (missing run, already-Held or
terminal, Authorize Deny, lost concurrency race) folds to HoldDeferred and is
recorded, never raised. Per-run failures are isolated in the fan-out loop so one
faulted run cannot abandon its siblings (silent partial enforcement is a
kill-switch's worst mode). Kind-blind: a revoked human's runs and a revoked
agent's runs are held by the same code; the revoked principal is only a lookup
key and never touches attribution (the hold + Decision are stamped with the
holder's own id, unspoofable). Operator stand-down uses the same
load_actor().active surface as every sibling agent. Idempotent on re-delivery:
deterministic per-(revocation, run) Decision ids + the Running-only guard +
ConcurrencyError-as-noop.
Gate review (4 agents: architecture / test-coverage / cross-BC / security+
authority specialist): 0 P0, 4 P1, all addressed. Security verdict: the
autonomous-authority grant is safely bounded (authz-gated, non-spoofable,
fail-safe against over-holding, reversible; residual risk is under-enforcement,
not over-reach). P1s: (a) per-run loop isolation so a fault holding one run does
not drop siblings [R4+R1, flagged twice]; (b) stand-down via load_actor().active
to match the sibling operator-deactivation surface [R3]; (c) registration test
pinning the unconditional on-by-default contract [R2]. P2s folded in: seed +
state.py prose corrected to Pattern C (no hold_run / RunCannotHoldError), the
ki11->b111 mnemonic reconciled, the deterministic-id namespace moved to the
sibling b1110002 suffix, and tests added for mixed-disposition fan-out, the
lost-race branch, decision-id distinctness across revocations, and
malformed-payload swallow. Watch (deferred, noted for the next slice): the broad
apply() swallow advances the shared bookmark on a transient fault, so a dropped
run is not redelivered; the holder is the 4th Reaction, which fires the
"wedged-bookmark operator escape-hatch" widening trigger. Also: under the
bootstrap AllowAllAuthorize, holds are ungated until TrustAuthorize is wired.
Verified: pyright + ruff clean, tach validated, 15 holder-subscriber unit tests +
6 registration + 4 vocab + 3 Postgres end-to-end (revoke holds the run + records
the Decision; another principal's run left Running; already-held -> HoldDeferred),
28876 unit + architecture tests green.
Co-Authored-By: Claude <noreply@anthropic.com>
Coverage reportClick to see where and how coverage changed
The report is truncated to 25 files out of 29. To see the full report, please visit the workflow summary page. 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.
Summary
Builds the kill-switch end to end: revoking a principal's policy grant now automatically holds the in-flight Runs that principal can no longer be trusted to drive. Three stacked slices:
4b6b30da83)revoke_granton the Policy aggregate emitsPolicyGrantRevoked, the trigger event.cddfb36c04) an actor-involvement resolver folds the Run event log intoproj_run_actor_involvement, answering "which in-flight Runs does principal P drive?" (starter-only for v1, envelope-principal attribution, in-flight = Running/Held).19183683d4) theAuthorityRevocationHoldersubscriber reacts toPolicyGrantRevoked, holds each Running run, and records oneDecision(context=AuthorityRevocationHold)per run.CORA's first fully-autonomous enforcement action. Three user-locked design choices for K3: on by default (a kill-switch that must be turned on is not a kill-switch; holds are reversible), own pinned agent + a Decision per hold, Running-only.
Design notes
RunHeld), not a call to thehold_runslice:cora.agentmay depend oncora.run.aggregatesbut notcora.run.features(tach boundary). Same shape CautionPromoter uses for Caution.HoldDeferred, never raised; per-run failures are isolated so one faulted run cannot abandon its siblings.Gate reviews
Deferred (noted, not blocking)
apply()swallow advances the shared bookmark on a transient fault, so a dropped run is not redelivered. The holder is the 4th Reaction, which fires the previously-deferred "operator escape-hatch / wedged-bookmark" framework-widening trigger.AllowAllAuthorize, holds are ungated untilTrustAuthorizeis wired.Test plan
🤖 Generated with Claude Code