feat(promote): enforce the no-self-escalation ceiling at the gate — #5-ceiling#5
feat(promote): enforce the no-self-escalation ceiling at the gate — #5-ceiling#5gesh75 wants to merge 1 commit into
Conversation
…ceiling] Wires core/risk into the sealed evidence path: every bundle now carries change.authority (sealed under integrity.sha256), and gate rule G5 re-derives the ceiling decision at promote time, so an agent can never self-escalate past its bound. - core/risk: load_max_authorized() (env AEGIS_MAX_AUTHORIZED_TIER, default HOTL; BLOCK rejected; invalid raises = fail-closed, loud) + authority_record() (the sealed object). - evidence/bundler.py: build_bundle(authority=); bundle_version 1.1->1.2; change.authority sealed; _failclosed_authority fallback (a direct call without authority -> BLOCK). - evidence/schema: bundle_version 1.2; change.authority required (severity, required, max_authorized, allowed, effective, change_class). - core/orchestrator/pipeline.py: computes authority (unify_severity + classify_change + load_max_authorized) and seals it. - core/promote/gate.py: rule G5 — reads the SEALED required tier (tamper-proof via G1), re-derives authorize(required, ceiling); a BLOCK-tier (AS/RD/RT / critical) change or one above the ceiling is never promotable. Fail-closed if no authority record. - tests/ceiling_test.py: 8 tests (sealed v1.2, tamper-detected, AS->BLOCK, G5 blocks/allows/ fail-closed, ceiling env+fail-closed, 0-violation promote property). Note: max_authorized is env-configured here; binding it to a hardware-PIV signature so the ceiling itself cannot be edited is the CROSS-3 follow-up — no crypto is claimed. Green: ceiling 8/8; stress PASS (3000, 1.2 schema + INV-3); promote PASS (G5); authority 14/14; wedge 10/10; twin/contract PASS; PR-1 egress 29/29.
Reviewer's GuideImplements enforced no-self-escalation by sealing an authority record into bundles (bundle_version 1.2) and adding gate rule G5 that recomputes authorization against an environment-driven autonomy ceiling, with fail-closed behavior and comprehensive tests. Sequence diagram for enforced no-self-escalation ceiling from preflight to gatesequenceDiagram
participant OrchestratorPipeline as run_preflight
participant RiskAuthority as authority_module
participant EvidenceBundler as build_bundle
participant PromoteGate as evaluate
OrchestratorPipeline->>RiskAuthority: authority_record(severity, change_class, load_max_authorized())
activate RiskAuthority
RiskAuthority->>RiskAuthority: load_max_authorized()
RiskAuthority-->>OrchestratorPipeline: authority dict
deactivate RiskAuthority
OrchestratorPipeline->>EvidenceBundler: build_bundle(..., authority)
activate EvidenceBundler
EvidenceBundler-->>OrchestratorPipeline: bundle (bundle_version 1_2, change.authority sealed)
deactivate EvidenceBundler
OrchestratorPipeline-->>PromoteGate: bundle
activate PromoteGate
PromoteGate->>PromoteGate: evaluate(bundle,...)
PromoteGate->>PromoteGate: authority = bundle.change.authority
alt authority missing or invalid
PromoteGate-->>OrchestratorPipeline: GateDecision(False, fail_closed)
else authority present
PromoteGate->>RiskAuthority: load_max_authorized()
RiskAuthority-->>PromoteGate: ceiling Tier
PromoteGate->>RiskAuthority: authorize(required, ceiling)
RiskAuthority-->>PromoteGate: AuthorityDecision(allowed)
alt allowed is False
PromoteGate-->>OrchestratorPipeline: GateDecision(False, no_self_escalation)
else allowed is True
PromoteGate-->>OrchestratorPipeline: GateDecision(True, approved)
end
end
deactivate PromoteGate
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 2 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="core/promote/gate.py" line_range="52-56" />
<code_context>
+ # here so the gate enforces its OWN ceiling at promote time, not a recorded boolean.
+ # A change needing more autonomy than the ceiling allows -- or any hard-BLOCK change
+ # (AS/RD/RT / critical) -- is never promotable.
+ authority = bundle.get("change", {}).get("authority")
+ if not authority:
+ return GateDecision(False, "no authority record — cannot confirm the autonomy bound "
+ "(fail-closed)")
+ try:
+ required = Tier[str(authority.get("required", "")).upper()]
+ except KeyError:
</code_context>
<issue_to_address>
**issue (bug_risk):** Defensively handle non-dict `authority` values to avoid runtime AttributeError.
If `bundle['change']['authority']` exists but is not a mapping (e.g., a string or list), `authority.get(...)` will raise `AttributeError` and crash the gate instead of failing closed. Please guard with `isinstance(authority, Mapping)` (or `dict`) and, on failure, return a fail-closed `GateDecision`, consistent with the missing/`KeyError` handling.
</issue_to_address>
### Comment 2
<location path="evidence/bundler.py" line_range="144-153" />
<code_context>
}
+
+
+def _failclosed_authority(severity: str) -> dict:
+ """Fallback when no authority was computed (a direct build_bundle call). Fail CLOSED:
+ an unknown authority is BLOCK, so it can never be promoted. run_preflight always
+ supplies a real record (core.risk.authority_record)."""
+ return {
+ "severity": severity,
+ "required": "block",
+ "max_authorized": "auto",
+ "allowed": False,
+ "effective": "block",
+ "change_class": {"touches_asn": False, "touches_rd_rt": False,
+ "touches_spine": False, "touches_underlay": False},
+ }
</code_context>
<issue_to_address>
**suggestion:** Align the fail-closed authority ceiling with `load_max_authorized` or make the mismatch explicit.
This fallback uses `max_authorized: "auto"`, but the actual promote-time ceiling comes from `load_max_authorized`, which defaults to `HOTL`. Because the gate ignores the stored `max_authorized`, behavior is correct but the bundle can mislead auditors by implying a lower ceiling than actually applied. Consider either matching the default here (`"hotl"`) or using a sentinel like `"unknown"` and updating the schema/consumers so it’s clearly a synthetic fail-closed record.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| authority = bundle.get("change", {}).get("authority") | ||
| if not authority: | ||
| return GateDecision(False, "no authority record — cannot confirm the autonomy bound " | ||
| "(fail-closed)") | ||
| try: |
There was a problem hiding this comment.
issue (bug_risk): Defensively handle non-dict authority values to avoid runtime AttributeError.
If bundle['change']['authority'] exists but is not a mapping (e.g., a string or list), authority.get(...) will raise AttributeError and crash the gate instead of failing closed. Please guard with isinstance(authority, Mapping) (or dict) and, on failure, return a fail-closed GateDecision, consistent with the missing/KeyError handling.
| def _failclosed_authority(severity: str) -> dict: | ||
| """Fallback when no authority was computed (a direct build_bundle call). Fail CLOSED: | ||
| an unknown authority is BLOCK, so it can never be promoted. run_preflight always | ||
| supplies a real record (core.risk.authority_record).""" | ||
| return { | ||
| "severity": severity, | ||
| "required": "block", | ||
| "max_authorized": "auto", | ||
| "allowed": False, | ||
| "effective": "block", |
There was a problem hiding this comment.
suggestion: Align the fail-closed authority ceiling with load_max_authorized or make the mismatch explicit.
This fallback uses max_authorized: "auto", but the actual promote-time ceiling comes from load_max_authorized, which defaults to HOTL. Because the gate ignores the stored max_authorized, behavior is correct but the bundle can mislead auditors by implying a lower ceiling than actually applied. Consider either matching the default here ("hotl") or using a sentinel like "unknown" and updating the schema/consumers so it’s clearly a synthetic fail-closed record.
…authority gate, ceiling, CROSS-3 seal (#9) Squashed release of the 8-PR stack (#2..#8): the offline, hardware-rootable bounded-autonomy receipt and everything under it. AUTO disabled by default; the only un-built piece is the hardware-PIV signer (docs/PIV_HARDWARE_SIGNER_PLAN.md) — software Ed25519 runs the whole demo. - core/llm: single shared LLM egress + adapter (local-first fallback chain, retry/cooldown, 4-event hook bus; raw httpx, no anthropic SDK; air-gap fail-closed). [#2] - evidence: model-identity wedge — change.model_identity sealed into the bundle hash; honest UNKNOWN / unattested states, never a faked attribution. [#3] - core/risk: deterministic AUTO/HITL/HOTL/BLOCK authority model (severity != authority; AS/RD/RT -> BLOCK; spine/underlay -> >= HOTL) + the no-self-escalation ceiling. [#4] - core/promote: gate rule G5 enforces the ceiling from the sealed required tier; change.authority sealed (bundle_version 1.1->1.2). [#5] - core/seal: CROSS-3 detached, offline-verifiable bounded-autonomy receipt (Ed25519; gates G0..G6; refuses to seal an unbounded change). [#6] - serve: /api/preflight/run emits the seal; GET /api/seal/pubkey + POST /api/seal/verify; examiner PDF shows the seal. [#7] - docs: PIV hardware-signer plan — the final, hardware-gated CROSS-3 pass. [#8] Property-tested safety invariants: no-self-escalation 0-violations, every-seal-is-bounded, tamper detection, offline-keyless verify, air-gap fail-closed.
|
Superseded by the squashed release #9, now merged to main. This PR's changes are in main; closing for tidiness (branch kept for history). |
#5-ceiling — wire the authority model into the gate (stacked on #5-core)
Makes the no-self-escalation invariant enforced, not just available: every bundle seals a
change.authorityrecord, and gate rule G5 re-derives the ceiling decision at promote time.What's here
change.authoritysealed (bundle_version 1.1→1.2) —severity / required / max_authorized / allowed / effective / change_class, covered byintegrity.sha256(tamper-detected).load_max_authorized()— envAEGIS_MAX_AUTHORIZED_TIER(default HOTL);BLOCKrejected, invalid raises (fail-closed, loud). Binding it to a hardware-PIV signature so the ceiling itself can't be edited is the CROSS-3 follow-up — no crypto claimed.requiredtier (tamper-proof via G1) and re-derivesauthorize(required, ceiling). A BLOCK-tier change (AS/RD/RT / critical) or one above the ceiling is never promotable; fail-closed if no authority record.Tests —
tests/ceiling_test.py, 8/8sealed v1.2 + schema-valid · authority under the seal (tamper → hash breaks) · AS change forces BLOCK · G5 blocks a twin-clean AS change that otherwise passes G1–G4 · G5 allows within ceiling · G5 fail-closed on missing authority · ceiling env-override + fail-closed on
BLOCK/invalid · 0-violation property (over 300 promotions, nothing exceeds the ceiling; BLOCK-tier AS changes are denied).Regression: stress PASS (3000 — validates the 1.2 schema + determinism INV-3), promote PASS (G5 active), authority 14/14, wedge 10/10, twin/contract PASS, PR-1 egress 29/29.
Next → the Jun-11 HERO
CROSS-3 SEAL fuses the #4 model-identity wedge + this signed authority ceiling into the offline, hardware-sealed bounded-autonomy receipt — and is where the PIV crypto (the critic's D6–D7 spike) binds
max_authorizedto a YubiKey so the ceiling can't be raised by editing the environment.Summary by Sourcery
Enforce the no-self-escalation authority ceiling by sealing an authority record into promotion bundles and checking it at the gate.
New Features:
Enhancements:
Tests: