Obligation gate (Gate III): justification-required abort_run#553
Merged
Conversation
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>
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.
Summary
Makes the dormant
justificationprimitive a live gate:AbortRunnow requires a non-empty, bounded justification at admission (fail-closed, HTTP 422). Theabort_rundecider callsrequire_justificationfirst (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
reason, sojustification(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.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
🤖 Generated with Claude Code