Skip to content

Authority revocation + mid-run kill-switch (hold a revoked actor's in-flight runs)#537

Closed
xmap wants to merge 5 commits into
mainfrom
worktree-authority-revocation-killswitch
Closed

Authority revocation + mid-run kill-switch (hold a revoked actor's in-flight runs)#537
xmap wants to merge 5 commits into
mainfrom
worktree-authority-revocation-killswitch

Conversation

@xmap

@xmap xmap commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Summary

First-class authority revocation wired to safe mid-run compensation: when a principal's grant is revoked, every in-flight run that principal is behind (started or supervises) is held into a defined, reversible state. Works identically for a human operator and an autonomous agent, because it operates on a bare principal_id end to end.

Four independently-reviewed slices, each committed on its own:

  • Slice 1 (931ac14e87) - Trust revoke_grant primitive. New PolicyGrantRevoked event removes one principal from a Policy's permitted_principal_ids. The 6th compensation slice, 2nd set-membership variant (after revoke_tool_from_agent): silently idempotent, required reason, single-stream append, no cascade. evaluate() unchanged (the shrunk set already denies).
  • Slice 2 (6bdfd8ad90) - proj_run_actor_involvement projection + lookup. CORA's first cross-BC projection: folds Run lifecycle + RunSupervision Decision events into an actor -> in-flight-runs index (starter = RunStarted envelope principal UNION supervisor = RunSupervision decider). Backs the kill-switch's "which runs is this actor behind?" lookup.
  • Slice 3 (49098c9d2e) - the kill-switch. AuthorityRevocationHolderSubscriber, a deterministic Run-BC Reaction: on PolicyGrantRevoked it holds the revoked principal's in-flight runs (records DecisionRegistered(context=AuthorityRevocation, choice=Hold), then HoldRun, reusing the RunSupervisor hold path). Acts as SYSTEM_PRINCIPAL_ID (plain infrastructure, not a seeded Agent). Off by default via authority_revocation_holder_enabled.
  • Slice 4 (025d1971e3) - non-bypass ordering fitness test. Pins that authorize precedes the first state-writing append across every handler with a direct authorize+append pair.

Compensation is HOLD (reversible), never abort: a human can take over or resume the parked run.

Design + review

  • Stage-0 design lock: project_authority_revocation_design (auto-memory).
  • Each BC/schema slice went through the gate-review panel (naming R1-R6, correctness/concurrency, conventions/design-lock, and for Slice 3 a cross-BC-reaction-safety specialist). Slice 3's review caught a P0 (a Reaction needs a projection_bookmarks seed row or it silently never fires) that green tests missed; fixed with a seed migration + a new test_reaction_bookmark_seeded fitness test (which also documents the same latent gap in the 3 pre-existing agent reactions) + a worker-driven regression test. P1 (loud authz-deny on the safety path) and a Tach cross-BC edge were also folded in.

Deferred (follow-up PR)

  • Slice 5: per-actor withdraw_actor fan-out (revoke a principal from every policy at once) + a policy-principals projection. Always scoped as later.
  • Pre-existing latent gap: the 3 Agent LLM reactions also lack bookmark-seed migrations (allowlisted + documented); fixing them is an Agent-BC change.

Test plan

  • Unit: revoke_grant decider + PBT; policy events/evolver; involvement projection; holder subscriber; AuthorityRevocation vocab.
  • Contract: revoke_grant REST endpoint + MCP tool.
  • Integration (real Postgres): revoke_grant handler; involvement projection + lookup; E4 scenario (operator starts -> agent supervises -> agent grant revoked -> run Held), including a worker-driven path that exercises the bookmark.
  • Architecture: full suite green (naming, non-bypass ordering, reaction-bookmark-seeded, subscriber completeness, migration + cross-BC-projection fitness).
  • pyright + ruff + tach clean.

🤖 Generated with Claude Code

xmap and others added 4 commits July 2, 2026 20:03
First-class authority revocation: remove one principal from a Policy's
permitted_principal_ids allow-list. This is the primitive the mid-run
kill-switch (later slices) triggers on to hold a revoked actor's
in-flight runs.

The 6th compensation slice and 2nd set-membership variant (after
revoke_tool_from_agent): silently idempotent (no event when the
principal is already absent), single-stream append (no cross-BC
cascade), required reason via the shared REASON_MAX_LENGTH. evaluate()
is unchanged: the shrunk set already denies the removed principal.

Symmetric by construction: operates on a bare principal_id, so a human
grant and an autonomous agent grant are revoked by the identical path.

Slice 1 of the authority-revocation + compensation design
(project_authority_revocation_design). Gate-reviewed (naming R1-R6,
correctness/security, conventions, design-lock consistency): pass.

Co-Authored-By: Claude <noreply@anthropic.com>
Answers "which in-flight runs is an actor behind?" for the
authority-revocation kill-switch: the compensation subscriber (next
slice) looks up a revoked actor's in-flight runs here and holds them.

CORA's first cross-BC projection. One projection folds two streams,
dispatching on event_type (the framework matches on event_type only,
no stream-type scoping):
  - RunStarted     -> a 'starter' row, actor = the event envelope
                      principal_id (RunStarted carries no actor in payload)
  - DecisionRegistered (context=RunSupervision) -> a 'supervisor' row,
                      actor = decided_by, run = inputs.run_id
  - Run lifecycle  -> status for all rows of the run; terminal statuses
                      fall out of the in-flight partial index

The lookup (RunActorInvolvementLookup + Postgres adapter) reads distinct
Running|Held run_ids for an actor, collapsing the starter/supervisor
double-row. BC-local like RunChannelLookup; not Kernel-wired yet (its
consumer is the next slice). Symmetric: keys on a bare actor UUID, so a
human starter and an agent supervisor are folded by the same code.

Naming (naming-r3): involvement not authority (authz overload),
involvement_kind not role (shadows the Role aggregate). The supervisor
INSERT is guarded on the starter row existing, so a supervision Decision
for a starter-less run creates no phantom in-flight row.

Slice 2 of the authority-revocation design. Gate-reviewed (naming,
correctness, conventions/design-lock, migration-safety): pass, two P2
fixes folded in (starter-guard + supervisor-SQL unit assertion).

Co-Authored-By: Claude <noreply@anthropic.com>
… runs)

The mid-run compensation centerpiece: on a Trust PolicyGrantRevoked, hold
every in-flight run the revoked principal is behind, so a withdrawn actor
(human or agent) cannot leave work running unattended. Holding is
reversible (Held, not aborted): the run lands in a defined state a human
can take over or resume.

AuthorityRevocationHolderSubscriber is a deterministic Run-BC Reaction:
  - reacts to PolicyGrantRevoked; looks up the revoked principal's
    in-flight runs via proj_run_actor_involvement (Slice 2);
  - per run, records DecisionRegistered(context=AuthorityRevocation,
    choice=Hold) at a deterministic uuid5 id, then issues HoldRun,
    reusing the RunSupervisor hold path;
  - acts as SYSTEM_PRINCIPAL_ID, NOT a seeded Agent: the kill-switch is
    plain infrastructure, not a decider, the strongest form of the "no
    special machinery" thesis (a human's grant is held by the identical
    path). Off by default via settings.authority_revocation_holder_enabled.

Adds DECISION_CONTEXT_AUTHORITY_REVOCATION, run/_subscribers.py +
composition-root wiring, a subscriber-completeness fitness test, the tach
edge cora.run -> cora.decision.aggregates (compose DecisionRegistered,
mirroring the Agent reactions), and the E4 integration scenario, the
T-ASE paper's centerpiece.

Gate-reviewed (naming, correctness/concurrency, conventions/design-lock,
cross-BC reaction safety); crash-window confirmed closed; fixes folded in:
  - P0: a Reaction needs a projection_bookmarks seed row or its advance
    loop wedges (MissingBookmarkError) and silently never fires. Adds the
    seed migration + test_reaction_bookmark_seeded fitness test (also
    documenting the same latent gap in the 3 pre-existing agent LLM
    reactions) + a worker-driven regression test.
  - P1: a failed kill-switch hold (SYSTEM lacking HoldRun under
    TrustAuthorize) now logs at ERROR with remediation, not a quiet warn.
  - P2: namespace rename for grep symmetry + a comment on why the
    ConcurrencyError-continue is safe.

Slice 3 of the authority-revocation design.

Co-Authored-By: Claude <noreply@anthropic.com>
… effect)

Companion to test_handler_authorizes (which proves every feature handler
CALLS + ENFORCES authorize). This adds the stronger ORDERING property
that call-existence does not: in a handler that authorizes and appends
directly in bind(), the authorize() call must precede the first
state-writing event_store.append / append_streams. An authorize that ran
after the append would have already let the effect land.

This is the codified non-bypass invariant (the T-ASE paper's E2: "no
facility-state effect without a prior authorization"). A positional AST
check (first authorize lineno < first append lineno) over every handler
with a direct authorize+append pair, plus a drift-catcher asserting >=10
such pairs exist so the check can't silently pass on an empty set.
Reuses test_handler_authorizes's discovery helpers.

Slice 4 of the authority-revocation design (tests only).

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 3, 2026

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/decision/aggregates/decision
  state.py
  apps/api/src/cora/run
  __init__.py
  _projections.py
  _subscribers.py 25-27
  apps/api/src/cora/run/adapters
  __init__.py
  postgres_run_actor_involvement_lookup.py
  apps/api/src/cora/run/ports
  __init__.py
  run_actor_involvement_lookup.py
  apps/api/src/cora/run/projections
  __init__.py
  actor_involvement.py 142
  apps/api/src/cora/run/subscribers
  __init__.py
  authority_revocation_holder.py 247-255
  apps/api/src/cora/trust
  routes.py
  tools.py
  apps/api/src/cora/trust/aggregates/policy
  __init__.py
  events.py
  evolver.py
  state.py
  apps/api/src/cora/trust/features/revoke_grant
  __init__.py
  command.py
  decider.py
  handler.py
  route.py
  tool.py
Project Total  

The report is truncated to 25 files out of 26. To see the full report, please visit the workflow summary page.

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

…metry tests

Evaluation evidence for the T-ASE paper, both against real Postgres:

- perf harness: authorization-decision latency (~0.3us median), replay
  throughput (~110k events/s), and kill-switch propagation across K=1,5,20
  concurrent supervised runs (2.7-29ms; per-run cost falls as the batch
  amortizes; all K held). Doubles as a regression test with loose sanity
  bounds, and emits a JSON summary for the paper's evaluation table.
- multi-scenario tests under a real TrustAuthorize gate: escalation denied
  symmetrically across actor kind, human-and-agent same-grant same-allow, and
  replay recovers the correct distinct actor (operator / agent / system holder)
  for each effect from the one log.

These demonstrate the actor-symmetry principle at the deny boundary and via
replay attribution, beyond the single happy-path kill-switch scenario. No
production code changes.

Co-Authored-By: Claude <noreply@anthropic.com>
@xmap xmap closed this Jul 4, 2026
@xmap xmap deleted the worktree-authority-revocation-killswitch branch July 4, 2026 05:14
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