ADR-0022: STPA traceability completion (2026-06-14)¶
Status¶
Accepted
Date¶
2026-06-14
Authors¶
praxis maintainers (governance traceability completion; resolves ADR-0015 finding BL-089)
Context¶
ADR-0015 (finding 089) recorded a governance-traceability gap rather than a code
defect: eight Unsafe Control Actions appeared in no SEC "Prevents" column in
docs/stpa/07-security-constraints.md, two planned actuation adapters had UCA rows
but no implementation and no "planned" marking, and the set_mode escalation
concern (UCA-23) had no proving test. The STPA tables are load-bearing here
(ADR-0009): a UCA with no covering SEC constraint is a visible gap, and since
ADR-0021 the SEC-to-module-to-test-to-invariant traceability is machine-checked in
CI (scripts/validate_compliance.py, BL-031). The UCA-to-SEC coverage itself is
not yet machine-checked, so it had drifted as adapters and tools were added.
The eight uncovered UCAs split three ways:
- The DRY_RUN-then-approve actuation UCAs (UCA-4, UCA-5 for
act_ansible; UCA-6, UCA-7 foract_opentofu; UCA-10 foract_talos) are already enforced. They were simply not listed under their covering constraints (SEC-6 human-gated convergence and baseline currency; SEC-2 minted approval and pipeline order; SEC-5 talosctl node-aware T3 one-target rule). - The
set_modeescalation UCA (UCA-23) concerns the server-wide mode ceiling (open/guarded/readonly), the third clause of invariant 2. There is no runtimeset_modetool: the mode is bound once at startup (config.mode,PRAXIS_MODE) andexecution/policy.py::Policy.checkapplies it uniformly to every tool, before the tier and approval gates. The escalation surface is "raise the ceiling silently" or "override it per tool", and both are foreclosed by construction, but no test asserted it. - The planned-adapter UCAs (UCA-12, UCA-13 for
act_redfish; UCA-14 foract_cloud) belong to adapters that do not exist in this version (only aCLOUDhost_type and a redfish deny/classify pattern exist). Their UCA rows pre-date the code.
Decision¶
-
Complete the UCA-to-SEC coverage in
07-security-constraints.mdso every UCA-1..28 appears in a SEC "Prevents" column: UCA-4/UCA-6/UCA-10 added to SEC-2; UCA-4..UCA-7 to SEC-6; UCA-10 to SEC-5; UCA-23 to SEC-3. No new SEC constraint is minted and no module citation changes, so the ADR-0021 validator stays green (the SEC set still equals the STPA set, R3). -
Cover UCA-23 under SEC-3 (tiered authority, invariant 2) rather than a new constraint: the mode ceiling is part of the same tiered-authority enforcement in
policy.py, which already carries theSEC-3back-citation token. SEC-3's statement is extended (in the STPA table and the compliance catalog) to name the mode ceiling explicitly, and a back-citation comment is added at the mode-gate site inPolicy.check. A new proving test,tests/execution/test_policy.py::test_mode_ceiling_cannot_be_escalated_per_tool, asserts that no tool name, command, or declaredbase_tierlifts a call past the ceiling, that a mode refusal is a hard refusal (deniedandrequires_approvalboth False, so no minted nonce can satisfy it), and that the ceiling is server-wide (no per-tool exemption). The test is registered in SEC-3's catalogproving_testsso the CI validator enforces its existence (R9). -
Pre-stage the planned-adapter UCAs rather than delete the rows: mark the
act_redfishandact_cloudrows[planned]in05-ucas.md, and flag UCA-12, UCA-13, UCA-14(planned ...)where they appear in the SEC "Prevents" columns (SEC-2 for the approval and pipeline-order coverage, SEC-7 for the cloud SSRF egress check). The flags clear when the adapters are implemented and route through the audited path. Keeping the UCA registry ahead of the code means a new adapter inherits its constraint instead of shipping uncovered.
This change adds no runtime behavior: the enforcement it documents already existed.
The one code change is the SEC-3 back-citation comment in policy.py.
Consequences¶
Positive: the STPA derivation regains end-to-end coverage (every UCA maps to a covering SEC constraint with a proving test), closing the last governance finding from the ADR-0015 review; the mode ceiling now has an explicit anti-escalation test; planned adapters carry their constraints before their code, so the UCA registry leads implementation rather than trailing it.
Negative: the UCA-to-SEC coverage is documented and test-anchored but not yet machine-checked the way the SEC-to-module-to-test links are; a future UCA could be added without a covering SEC and only a human reviewer would catch it. Extending the validator with a UCA-coverage rule is the natural follow-up and is noted as a revisit trigger.
Neutral: ADR-0015's finding table is left as the immutable snapshot of that review
(its sibling findings, though resolved, also remain marked "open" there); the living
status moves in docs/backlog.md (BL-089 resolved) per the established convention.
Alternatives considered and rejected¶
- Mint a new SEC constraint for the mode ceiling (UCA-23). Rejected: the mode ceiling is the third clause of invariant 2, already enforced in the SEC-1/SEC-3 policy module; a parallel constraint would split one analysis and force a catalog, schema, and back-citation churn for no new enforcement. Extending SEC-3 keeps the invariant and its derivation as one.
- Delete the planned-adapter UCA rows until the adapters land. Rejected: the UCA
table is the registry that a new state-changing tool must extend; removing the
rows would let
act_redfish/act_cloudship without a pre-existing constraint, the exact gap this ADR closes. Flagging them[planned]keeps the registry honest and ahead of the code. - Add a runtime
set_modetool to make UCA-23 a live control action. Rejected: out of scope and a new escalation surface; the mode is a startup ceiling by design (ADR-0004), and the safer property is that there is no runtime raise at all.
Revisit triggers¶
- A new state-changing tool or actuation adapter is added (the
[planned]adapters land, or another is introduced): its UCA rows and SEC coverage must be added in the same change, and the relevant(planned)flags cleared. - The compliance validator (BL-031) is extended to machine-check UCA-to-SEC coverage, at which point this prose mapping becomes a CI-enforced rule.
- A later review finds a UCA-to-SEC mapping here unsound (append an audit note; never rewrite a resolved row).