Assync Authorization Model¶
Default-Deny¶
Absent authorization means deny. authorization/policy.py::effective_capabilities
returns the empty set whenever the role or authorization is None, expired,
revoked, or of type ABORT — never raises for that case, since callers
frequently need to display "what CAN this run do" without crashing; use
require_capability() when denial should actually stop execution.
Effective Capabilities Are an Intersection, Never a Union¶
effective = (role.allowed − role.forbidden) ∩ (authorization.permitted − authorization.prohibited) ∩ run_capabilities?
No layer may widen what another layer allows. A role that somehow listed
commit_execute would still get nothing under an ANALYSIS_GO
authorization, because ANALYSIS_GO's own canonical grant never includes
it (core/enums.py::AUTHORIZATION_CAPABILITY_GRANTS) — verified directly
in tests/test_authorization.py::test_effective_capabilities_is_intersection_not_union.
Canonical Authorization Types¶
ANALYSIS_GO, IMPLEMENTATION_GO, REVIEW_GO, SECURITY_GO, QA_GO,
DECISION_GO, COMMIT_GO, PUSH_GO, MERGE_GO, RELEASE_GO, ABORT.
Each maps to a fixed, disjoint-by-design capability set in
AUTHORIZATION_CAPABILITY_GRANTS — COMMIT_GO never implies PUSH_GO,
PUSH_GO never implies MERGE_GO, MERGE_GO never implies RELEASE_GO.
ABORT grants nothing at all; it exists so an active authorization can be
explicitly revoked without deleting its record.
Authorization Records (authorization/models.py)¶
Each Authorization is immutable once created: type, issuing authority,
issued timestamp, scope, permitted/prohibited capabilities, target mission
(and optional target run), optional expiry, an evidence-or-decision
reference, and a status. authorization/service.py::create_authorization
is the only constructor — it always derives permitted_capabilities
from the canonical grant table for the requested type, so a caller can
narrow (via additional_prohibitions) but never widen it.
Escalation Requires a Human Decision¶
No code path in this MVP mints a new, more powerful Authorization on its
own. The only place that can is
decisions/service.py::resolve_decision(..., grant_authorization_type=...),
and it refuses to do so unless the decision resolves to APPROVED by a
named human actor — see decision-queue.md.
Expiry¶
Authorization.is_expired() / is_usable() are checked on every
effective_capabilities() call; an expired authorization behaves
identically to a missing one.
MVP Limitations¶
- There is no authorization renewal flow yet — an expired authorization must be re-issued from scratch (via a new human decision).
- Scoping authorization to a specific
target_run_idis modeled but not yet enforced beyondtarget_mission_idmatching.