Skip to content

feat(promote): enforce the no-self-escalation ceiling at the gate — #5-ceiling#5

Closed
gesh75 wants to merge 1 commit into
feat/llm-tier-authorityfrom
feat/llm-ceiling
Closed

feat(promote): enforce the no-self-escalation ceiling at the gate — #5-ceiling#5
gesh75 wants to merge 1 commit into
feat/llm-tier-authorityfrom
feat/llm-ceiling

Conversation

@gesh75

@gesh75 gesh75 commented Jun 1, 2026

Copy link
Copy Markdown
Owner

#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.authority record, and gate rule G5 re-derives the ceiling decision at promote time.

Base = feat/llm-tier-authority (#5-core). Merge the stack in order.

What's here

  • change.authority sealed (bundle_version 1.1→1.2) — severity / required / max_authorized / allowed / effective / change_class, covered by integrity.sha256 (tamper-detected).
  • load_max_authorized() — env AEGIS_MAX_AUTHORIZED_TIER (default HOTL); BLOCK rejected, 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.
  • Gate rule G5 — reads the sealed required tier (tamper-proof via G1) and re-derives authorize(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/8

sealed 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_authorized to 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:

  • Seal a change.authority record into each evidence bundle, including severity, required tier, ceiling, decision, and change classification.
  • Introduce environment-driven loading of the deployment autonomy ceiling tier with strict validation and fail-closed behavior at BLOCK or invalid values.
  • Add gate rule logic that re-derives and enforces the authority ceiling at promote time based on the sealed required tier, blocking changes above the ceiling or with missing/invalid authority metadata.

Enhancements:

  • Bump the evidence bundle schema version from 1.1 to 1.2 to cover the new authority payload and keep model identity under the existing integrity seal.

Tests:

  • Add ceiling-focused test coverage validating sealed authority structure and tamper detection, gate rule G5 behavior, ceiling loader semantics, and a property test ensuring no promoted bundles exceed the configured ceiling.
  • Update existing wedge tests to expect the new bundle version while preserving prior verification behavior.

…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.
@sourcery-ai

sourcery-ai Bot commented Jun 1, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements 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 gate

sequenceDiagram
    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
Loading

File-Level Changes

Change Details Files
Introduce environment-driven autonomy ceiling loader with fail-closed validation.
  • Add _DEFAULT_MAX_AUTHORIZED constant defaulting to HOTL.
  • Implement load_max_authorized() that reads AEGIS_MAX_AUTHORIZED_TIER, normalizes it to a Tier, rejects BLOCK, and raises ValueError on invalid values.
  • Re-export load_max_authorized from core.risk for wider use.
core/risk/authority.py
core/risk/__init__.py
tests/ceiling_test.py
Seal a structured authority record into evidence bundles and handle missing authority fail-closed.
  • Add authority parameter to build_bundle and store it under change.authority.
  • Bump bundle_version from 1.1 to 1.2 and update dependent test to assert v1.2.
  • Introduce _failclosed_authority() to synthesize a BLOCK/denied authority record when none is supplied, ensuring un-attributed bundles cannot be promoted.
evidence/bundler.py
tests/wedge_test.py
tests/ceiling_test.py
Compute and seal authority records during preflight based on severity, change classification, and ceiling.
  • Expose authority_record() from core.risk and implement it to compute required authority, call authorize(), and return a lowercase-encoded record including change_class flags.
  • In run_preflight, compute authority using unify_severity, classify_change, and load_max_authorized(), then pass it into build_bundle.
  • Ensure change_class structure in authority_record matches the schema asserted in tests.
core/risk/authority.py
core/orchestrator/pipeline.py
core/risk/__init__.py
tests/ceiling_test.py
Add gate rule G5 to enforce no-self-escalation at promote time using sealed authority and runtime ceiling.
  • Update gate.evaluate() to require a change.authority record and fail closed if missing.
  • Parse the sealed required tier from the bundle, validate it against Tier enum, and fail closed on unknown values.
  • Call authorize(required, load_max_authorized()) and deny promotion when the decision is not allowed, returning an explanatory reason string.
  • Keep existing G1–G4 behavior and final success path unchanged when G5 passes.
core/promote/gate.py
tests/ceiling_test.py
Add focused tests for authority sealing, gate ceiling enforcement, and ceiling loader behavior.
  • Create tests/ceiling_test.py covering bundle authority presence and sealing, tamper detection via integrity hash, authority classification for ASN changes, G5 behavior (block vs within ceiling, missing authority), and a 0-violation property over randomized promotions.
  • Add tests for load_max_authorized default behavior, env override, and fail-closed semantics for BLOCK/invalid values.
  • Wire schema loading and jsonschema-based validation (self-skipping if jsonschema is unavailable).
tests/ceiling_test.py
evidence/schema/evidence_bundle.schema.json

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread core/promote/gate.py
Comment on lines +52 to +56
authority = bundle.get("change", {}).get("authority")
if not authority:
return GateDecision(False, "no authority record — cannot confirm the autonomy bound "
"(fail-closed)")
try:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread evidence/bundler.py
Comment on lines +144 to +153
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",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

gesh75 added a commit that referenced this pull request Jun 1, 2026
…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.
@gesh75

gesh75 commented Jun 1, 2026

Copy link
Copy Markdown
Owner Author

Superseded by the squashed release #9, now merged to main. This PR's changes are in main; closing for tidiness (branch kept for history).

@gesh75 gesh75 closed this Jun 1, 2026
@gesh75
gesh75 deleted the feat/llm-ceiling branch June 1, 2026 18:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant