Skip to content

Obligation gate (Gate III): justification-required abort_run#553

Merged
xmap merged 2 commits into
mainfrom
worktree-obligation-gate
Jul 7, 2026
Merged

Obligation gate (Gate III): justification-required abort_run#553
xmap merged 2 commits into
mainfrom
worktree-obligation-gate

Conversation

@xmap

@xmap xmap commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Summary

Makes the dormant justification primitive a live gate: AbortRun now requires a non-empty, bounded justification at admission (fail-closed, HTTP 422). The abort_run decider calls require_justification first (admission is the outer precondition), before any state/reason/status check. Kind-blind: the primitive reads only the command name + text, never actor kind.

Two commits: (1) the dormant primitive (already built), (2) the wiring.

Why this gate matters to the paper

It's the "Four Gates, One Log" generativity punchline. The mechanism is conceded prior art (XACML pre-obligations / UCON); the value is the cheapness, so its marginal-cost row is the strongest in the set: 1 fold + 1 decider arm, 0 new aggregates, 0 subscribers, 0 projections. The attestation-failure meter + auto-hold subscriber (the richer half) is deferred (documented) — it would re-demonstrate shared-hold discharge already shown by the kill-switch and consequence gate.

Design notes

  • Target = abort_run: aborting a running experiment destroys in-progress data — the archetypal "account for yourself." It already carries a post-hoc reason, so justification (admission precondition) sits cleanly beside it: two distinct deontic objects. Same command was rejected as the consequence-gate target (never hold an emergency for a co-sign) but is right for obligation (synchronous justify-in-the-same-call). Same command, different gates, different deontic weights.
  • Kind-blindness demonstrated across human AND machine: the gate forced the 4 autonomous abort callers (EdgeConductor ×2, run-phase conductor, RunSupervisor) to justify too — and they do, with failure-derived text. The machine accounts for itself exactly as an operator does.
  • 422 handler distinct from the BC's 400 (domain-invalid value) and 409 (transition guard), matching the 422-for-unprocessable convention in Access/Federation/Operation.

Gate review (3 agents)

0 P0, 2 P1 + P2s, all addressed: MCP-surface refusal test; route 422 doc + ErrorResponse model; a coupling test pinning AbortRun stays declared (else the gate silently goes inert); stale-docstring fixes; endpoint validation tests isolated from the gate; intra-BC 400-vs-422 rationale.

Test plan

  • New gate unit tests (refuse/admit/blank/over-length/gate-precedes-state/total-across-statuses/kind-blind/coupling) + primitive allowlist + endpoint & MCP 422 refusal
  • The gate correctly broke 6 pre-existing tests on unjustified aborts (4 source system-callers + 2 setup) — all fixed at their source
  • pyright + ruff + tach clean, openapi drift clean, 43,888 unit + architecture + contract tests green
  • CI green

🤖 Generated with Claude Code

xmap and others added 2 commits July 7, 2026 13:00
Introduce a shared, BC-agnostic obligation-gate primitive: a fail-closed
"justification required at admission" check for a declared class of commands.
It is the deontic dual of the Authorize port ("may you?" vs "have you accounted
for yourself?"), and the first of two new governance gates for the "Four Gates,
One Log" line.

Why dormant: the declared-command set (COMMANDS_REQUIRING_JUSTIFICATION) ships
empty, so require_justification can never raise for a real command and no
existing behavior changes. Wiring a specific command's decider to the gate is a
separate per-command commit; this lands only the mechanism, mirroring CORA's
mechanism-first pattern.

Design: a bare validated-string decider primitive (the reason-text bucket), not
a value object with an aggregate; JUSTIFICATION_MAX_LENGTH is its own constant,
not an alias of REASON_MAX_LENGTH. Kind-blind by construction: the surface takes
no actor-kind argument, so a human and an agent are held to the identical
precondition.

Gate review: correctness/convention review (clean, safe to ship dormant) +
R3 naming review. Both findings addressed in this commit: renamed the error
class to JustificationRequiredError (family-noun primacy, matches the module's
"command" vocabulary), and tightened the optional-branch test to pin trim +
over-length-allowed together.

Co-Authored-By: Claude <noreply@anthropic.com>
Make the dormant justification primitive a live gate. AbortRun joins
COMMANDS_REQUIRING_JUSTIFICATION, so the abort_run decider calls
require_justification FIRST (admission is the outer precondition): an abort
without a non-empty, bounded justification is refused with
JustificationRequiredError (HTTP 422), fail-closed, before any state / reason /
status check. Kind-blind: the primitive reads only the command name + text, never
actor kind, so a human and an agent (and the autonomous conductors) are held to
the identical precondition.

This is the paper's generativity punchline (Four Gates, One Log). The obligation
gate's MECHANISM is conceded prior art (XACML pre-obligations / UCON, per the
novelty dossier); its value is the cheapness, so the marginal-cost row is the
strongest data point in the set: 1 fold + 1 decider arm, 0 new aggregates, 0
subscribers, 0 new projections. The attestation-failure meter + auto-hold
subscriber (the richer half) is DEFERRED (documented), because it would
re-demonstrate shared-hold discharge already shown by the kill-switch and
consequence gate.

Target = abort_run: the archetypal "account for yourself before a consequential
act" (aborting a running experiment destroys in-progress data). It already carries
a post-hoc `reason` field, so adding `justification` as an admission precondition
cleanly separates the two deontic objects (reason describes the abort on the
RunAborted event; justification accounts for taking it, and is validated but not
persisted in v1). Note abort_run was REJECTED as the consequence-gate target (you
never hold an emergency for a co-sign) but is right for obligation (a synchronous
justify-in-the-same-call is fine for an emergency): same command, different gates,
different deontic weights.

Kind-blindness demonstrated in shipped code across human AND machine: putting the
gate on abort_run forced the four autonomous abort callers to justify too, and
they do (failure-derived text): EdgeConductor (compute failed / cancelled
mid-flight), the run-phase conductor, and the RunSupervisor. The machine accounts
for itself exactly as an operator does.

Wiring: justification field on the AbortRun command + REST route model + MCP tool
(openapi regenerated); a 422 handler in run routes (distinct from the BC's 400 for
domain-invalid values and 409 for transition guards, matching the
422-for-unprocessable convention in Access/Federation/Operation).

Gate review (3 agents: architecture / test-coverage / cross-BC): 0 P0, 2 P1 + P2s,
all addressed. P1s: an MCP-surface refusal test (the gate fires through MCP too),
and the route's 422 responses= doc + ErrorResponse model. P2s: a coupling test
pinning AbortRun stays in the declared class (else the gate silently goes inert);
stale "ships EMPTY" docstrings corrected; endpoint reason/uuid-validation tests
given a justification so they isolate their intent from the gate; the intra-BC
400-vs-422 rationale documented; redundant MCP Field default dropped. Watch
(deferred): a fitness test binding the two-place command-name coupling lands at
the rule-of-three (2nd opt-in).

Verified: pyright + ruff + tach clean, openapi drift clean, 43888 unit +
architecture + contract tests green (incl. the 6 that the gate correctly broke on
unjustified aborts, now fixed at their source/setup callers).

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

github-actions Bot commented Jul 7, 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
  _run_supervisor.py
  apps/api/src/cora/run
  routes.py
  apps/api/src/cora/run/features/abort_run
  command.py
  decider.py
  route.py
  tool.py
  apps/api/src/cora/shared
  justification.py
Project Total  

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

@xmap xmap merged commit 9dd5b58 into main Jul 7, 2026
16 checks passed
@xmap xmap deleted the worktree-obligation-gate branch July 7, 2026 13:49
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