Skip to content

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_GRANTSCOMMIT_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_id is modeled but not yet enforced beyond target_mission_id matching.