2026-08-29 12:52:55 +02:00
|
|
|
"""Published PEP unreachable-engine stance (security-layer-model v0.7 §6.4).
|
|
|
|
|
|
|
|
|
|
Runtime reads ``pep-stance.yaml``. ``SHIPPED_STANCE`` is the pin that makes
|
|
|
|
|
drift between the published map and this module fail the conformance test.
|
|
|
|
|
"""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import os
|
|
|
|
|
from dataclasses import dataclass, replace
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from typing import Any
|
|
|
|
|
from urllib.parse import urlparse
|
|
|
|
|
|
|
|
|
|
import yaml
|
|
|
|
|
|
|
|
|
|
from secrets_engine.config import repo_root
|
|
|
|
|
from secrets_engine.errors import DecisionError
|
|
|
|
|
|
|
|
|
|
SHIPPED_STANCE = {
|
|
|
|
|
"build": "fail_open",
|
|
|
|
|
"test": "fail_open",
|
|
|
|
|
"prod": "fail_closed",
|
|
|
|
|
"unknown": "fail_closed",
|
|
|
|
|
}
|
|
|
|
|
REQUIRED_STAGES = tuple(SHIPPED_STANCE)
|
|
|
|
|
VALID_MODES = frozenset({"fail_open", "fail_closed"})
|
|
|
|
|
LOOPBACK_HOSTS = frozenset({"127.0.0.1", "localhost", "::1"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class StanceApplication:
|
|
|
|
|
"""Named residue applied when access-engine is unreachable."""
|
|
|
|
|
|
|
|
|
|
stage: str
|
|
|
|
|
failure_mode: str
|
|
|
|
|
action: str
|
|
|
|
|
demo_exception: bool = False
|
|
|
|
|
decision_id: str = ""
|
feat: complete and prove the authorization chain end to end
Implements step 2 (access-engine POST /v1/check) and wires the whole
GH-DEC-2026-003 sequence together, then proves it against a live throwaway
OpenBao rather than only unit-level fakes.
- decision_check.check_decision performs the PDP call; an unreachable or
non-200 PDP raises, since silence is never permission.
- approval_consume.authorize_action coordinates steps 1 and 2 and returns an
AuthorizedAction. Both steps build the same CheckRequest via a shared
_expected_request, since two descriptions of the action cannot produce
corresponding digests.
- apply_unreachable_engine_stance takes authorized=. The published map
defines fail_closed as no side effect WITHOUT a durable decision record,
so holding a validated one means the residue does not apply. Not a bypass:
both steps must have succeeded and CAS consume still precedes OpenBao.
Unconfigured still returns None and fails closed.
The end-to-end test caught one more instance of the cross-vocabulary bug: a
leftover comparison of the claim's binding.action against ours. The claim
says secrets.kv.destroy where we say destroy, so it would have failed against
every real claim. Removed; the tie is pdp_digest.
Integration coverage asserts PIP-then-PDP ordering, that consume is the last
step before the backend, and that an unreachable PDP, denied decision,
invalid claim, missing pdp_digest, consume conflict and action mismatch each
stop before OpenBao. 284 tests pass; production still fails closed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M65ovP3eiiPHubibvWs9mD
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 393550@bnt-lap001
Assistant-Session: 4bb359f9-1f12-4410-9e76-079cf23c82e4
2026-09-06 14:56:02 +02:00
|
|
|
authorized: bool = False
|
2026-08-29 12:52:55 +02:00
|
|
|
|
|
|
|
|
def as_evidence(self) -> dict[str, object]:
|
|
|
|
|
payload: dict[str, object] = {
|
|
|
|
|
"stance_stage": self.stage,
|
|
|
|
|
"stance_failure_mode": self.failure_mode,
|
|
|
|
|
"stance_demo_exception": self.demo_exception,
|
feat: complete and prove the authorization chain end to end
Implements step 2 (access-engine POST /v1/check) and wires the whole
GH-DEC-2026-003 sequence together, then proves it against a live throwaway
OpenBao rather than only unit-level fakes.
- decision_check.check_decision performs the PDP call; an unreachable or
non-200 PDP raises, since silence is never permission.
- approval_consume.authorize_action coordinates steps 1 and 2 and returns an
AuthorizedAction. Both steps build the same CheckRequest via a shared
_expected_request, since two descriptions of the action cannot produce
corresponding digests.
- apply_unreachable_engine_stance takes authorized=. The published map
defines fail_closed as no side effect WITHOUT a durable decision record,
so holding a validated one means the residue does not apply. Not a bypass:
both steps must have succeeded and CAS consume still precedes OpenBao.
Unconfigured still returns None and fails closed.
The end-to-end test caught one more instance of the cross-vocabulary bug: a
leftover comparison of the claim's binding.action against ours. The claim
says secrets.kv.destroy where we say destroy, so it would have failed against
every real claim. Removed; the tie is pdp_digest.
Integration coverage asserts PIP-then-PDP ordering, that consume is the last
step before the backend, and that an unreachable PDP, denied decision,
invalid claim, missing pdp_digest, consume conflict and action mismatch each
stop before OpenBao. 284 tests pass; production still fails closed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M65ovP3eiiPHubibvWs9mD
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 393550@bnt-lap001
Assistant-Session: 4bb359f9-1f12-4410-9e76-079cf23c82e4
2026-09-06 14:56:02 +02:00
|
|
|
"stance_authorized": self.authorized,
|
2026-08-29 12:52:55 +02:00
|
|
|
}
|
|
|
|
|
if self.decision_id:
|
|
|
|
|
payload["stance_decision_id"] = self.decision_id
|
|
|
|
|
return payload
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class PepStanceMap:
|
|
|
|
|
stance: dict[str, str]
|
|
|
|
|
path: Path
|
|
|
|
|
|
|
|
|
|
def for_stage(self, stage: str) -> tuple[str, str]:
|
|
|
|
|
key = stage if stage in self.stance else "unknown"
|
|
|
|
|
mode = self.stance.get(key, "")
|
|
|
|
|
if mode not in VALID_MODES:
|
|
|
|
|
raise DecisionError(
|
|
|
|
|
f"pep-stance.yaml has no usable mode for stage {stage!r}"
|
|
|
|
|
)
|
|
|
|
|
return key, mode
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def pep_stance_path() -> Path:
|
|
|
|
|
override = os.environ.get("SECRETS_ENGINE_PEP_STANCE", "")
|
|
|
|
|
if override:
|
|
|
|
|
return Path(override)
|
|
|
|
|
return repo_root() / "pep-stance.yaml"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def load_pep_stance(path: Path | None = None) -> PepStanceMap:
|
|
|
|
|
target = path or pep_stance_path()
|
|
|
|
|
try:
|
|
|
|
|
data = yaml.safe_load(target.read_text(encoding="utf-8")) or {}
|
|
|
|
|
except (OSError, yaml.YAMLError) as exc:
|
|
|
|
|
raise DecisionError(f"unable to load PEP stance map {target}: {exc}") from exc
|
|
|
|
|
raw = data.get("stance")
|
|
|
|
|
if not isinstance(raw, dict):
|
|
|
|
|
raise DecisionError(f"{target} is missing a stance map")
|
|
|
|
|
stance = {str(key): str(value) for key, value in raw.items()}
|
|
|
|
|
missing = [stage for stage in REQUIRED_STAGES if stage not in stance]
|
|
|
|
|
if missing:
|
|
|
|
|
raise DecisionError(
|
|
|
|
|
f"{target} is not total; missing stages {missing}"
|
|
|
|
|
)
|
|
|
|
|
unknown_modes = {
|
|
|
|
|
f"{stage}={mode}"
|
|
|
|
|
for stage, mode in stance.items()
|
|
|
|
|
if mode not in VALID_MODES
|
|
|
|
|
}
|
|
|
|
|
if unknown_modes:
|
|
|
|
|
raise DecisionError(f"{target} has invalid modes: {sorted(unknown_modes)}")
|
|
|
|
|
return PepStanceMap(stance=stance, path=target)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def demo_exception_enabled(cfg: Any) -> bool:
|
|
|
|
|
"""Three-factor throwaway exception; not a stance row."""
|
|
|
|
|
host = (urlparse(getattr(cfg, "bao_addr", "")).hostname or "").lower()
|
|
|
|
|
return (
|
|
|
|
|
os.environ.get("SECRETS_ENGINE_UNSAFE_DEMO") == "1"
|
|
|
|
|
and not getattr(cfg, "hub_url", "")
|
|
|
|
|
and host in LOOPBACK_HOSTS
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def apply_unreachable_engine_stance(
|
|
|
|
|
cfg: Any,
|
|
|
|
|
entry: Any,
|
|
|
|
|
action: str,
|
|
|
|
|
*,
|
|
|
|
|
stance_map: PepStanceMap | None = None,
|
feat: complete and prove the authorization chain end to end
Implements step 2 (access-engine POST /v1/check) and wires the whole
GH-DEC-2026-003 sequence together, then proves it against a live throwaway
OpenBao rather than only unit-level fakes.
- decision_check.check_decision performs the PDP call; an unreachable or
non-200 PDP raises, since silence is never permission.
- approval_consume.authorize_action coordinates steps 1 and 2 and returns an
AuthorizedAction. Both steps build the same CheckRequest via a shared
_expected_request, since two descriptions of the action cannot produce
corresponding digests.
- apply_unreachable_engine_stance takes authorized=. The published map
defines fail_closed as no side effect WITHOUT a durable decision record,
so holding a validated one means the residue does not apply. Not a bypass:
both steps must have succeeded and CAS consume still precedes OpenBao.
Unconfigured still returns None and fails closed.
The end-to-end test caught one more instance of the cross-vocabulary bug: a
leftover comparison of the claim's binding.action against ours. The claim
says secrets.kv.destroy where we say destroy, so it would have failed against
every real claim. Removed; the tie is pdp_digest.
Integration coverage asserts PIP-then-PDP ordering, that consume is the last
step before the backend, and that an unreachable PDP, denied decision,
invalid claim, missing pdp_digest, consume conflict and action mismatch each
stop before OpenBao. 284 tests pass; production still fails closed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M65ovP3eiiPHubibvWs9mD
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 393550@bnt-lap001
Assistant-Session: 4bb359f9-1f12-4410-9e76-079cf23c82e4
2026-09-06 14:56:02 +02:00
|
|
|
authorized: bool = False,
|
2026-08-29 12:52:55 +02:00
|
|
|
) -> StanceApplication:
|
|
|
|
|
"""Apply the published unreachable-engine residue for a live action.
|
|
|
|
|
|
feat: complete and prove the authorization chain end to end
Implements step 2 (access-engine POST /v1/check) and wires the whole
GH-DEC-2026-003 sequence together, then proves it against a live throwaway
OpenBao rather than only unit-level fakes.
- decision_check.check_decision performs the PDP call; an unreachable or
non-200 PDP raises, since silence is never permission.
- approval_consume.authorize_action coordinates steps 1 and 2 and returns an
AuthorizedAction. Both steps build the same CheckRequest via a shared
_expected_request, since two descriptions of the action cannot produce
corresponding digests.
- apply_unreachable_engine_stance takes authorized=. The published map
defines fail_closed as no side effect WITHOUT a durable decision record,
so holding a validated one means the residue does not apply. Not a bypass:
both steps must have succeeded and CAS consume still precedes OpenBao.
Unconfigured still returns None and fails closed.
The end-to-end test caught one more instance of the cross-vocabulary bug: a
leftover comparison of the claim's binding.action against ours. The claim
says secrets.kv.destroy where we say destroy, so it would have failed against
every real claim. Removed; the tie is pdp_digest.
Integration coverage asserts PIP-then-PDP ordering, that consume is the last
step before the backend, and that an unreachable PDP, denied decision,
invalid claim, missing pdp_digest, consume conflict and action mismatch each
stop before OpenBao. 284 tests pass; production still fails closed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M65ovP3eiiPHubibvWs9mD
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 393550@bnt-lap001
Assistant-Session: 4bb359f9-1f12-4410-9e76-079cf23c82e4
2026-09-06 14:56:02 +02:00
|
|
|
``fail_closed`` is the *unreachable-engine* residue, not a blanket ban: the
|
|
|
|
|
published map defines it as no protected side effect without a durable
|
|
|
|
|
access-engine decision record. ``authorized=True`` means the caller already
|
|
|
|
|
obtained and validated that record for this exact action, so the engine was
|
|
|
|
|
reachable and the residue does not apply. It is never a bypass -- the caller
|
|
|
|
|
must have completed steps 1 and 2, and GH-DEC-2026-003 still requires a
|
|
|
|
|
successful CAS consume before any OpenBao call.
|
|
|
|
|
|
|
|
|
|
Without such a record, ``fail_closed`` raises ``DecisionError`` carrying
|
|
|
|
|
named stance fields. ``fail_open`` is the documented residue for build/test.
|
2026-08-29 12:52:55 +02:00
|
|
|
"""
|
|
|
|
|
loaded = stance_map or load_pep_stance()
|
|
|
|
|
stage, mode = loaded.for_stage(getattr(entry, "stage", "unknown"))
|
|
|
|
|
demo = demo_exception_enabled(cfg)
|
|
|
|
|
applied = StanceApplication(
|
|
|
|
|
stage=stage,
|
|
|
|
|
failure_mode=mode,
|
|
|
|
|
action=action or "unknown",
|
|
|
|
|
demo_exception=bool(demo and mode == "fail_closed"),
|
feat: complete and prove the authorization chain end to end
Implements step 2 (access-engine POST /v1/check) and wires the whole
GH-DEC-2026-003 sequence together, then proves it against a live throwaway
OpenBao rather than only unit-level fakes.
- decision_check.check_decision performs the PDP call; an unreachable or
non-200 PDP raises, since silence is never permission.
- approval_consume.authorize_action coordinates steps 1 and 2 and returns an
AuthorizedAction. Both steps build the same CheckRequest via a shared
_expected_request, since two descriptions of the action cannot produce
corresponding digests.
- apply_unreachable_engine_stance takes authorized=. The published map
defines fail_closed as no side effect WITHOUT a durable decision record,
so holding a validated one means the residue does not apply. Not a bypass:
both steps must have succeeded and CAS consume still precedes OpenBao.
Unconfigured still returns None and fails closed.
The end-to-end test caught one more instance of the cross-vocabulary bug: a
leftover comparison of the claim's binding.action against ours. The claim
says secrets.kv.destroy where we say destroy, so it would have failed against
every real claim. Removed; the tie is pdp_digest.
Integration coverage asserts PIP-then-PDP ordering, that consume is the last
step before the backend, and that an unreachable PDP, denied decision,
invalid claim, missing pdp_digest, consume conflict and action mismatch each
stop before OpenBao. 284 tests pass; production still fails closed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M65ovP3eiiPHubibvWs9mD
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 393550@bnt-lap001
Assistant-Session: 4bb359f9-1f12-4410-9e76-079cf23c82e4
2026-09-06 14:56:02 +02:00
|
|
|
authorized=bool(authorized),
|
2026-08-29 12:52:55 +02:00
|
|
|
)
|
feat: complete and prove the authorization chain end to end
Implements step 2 (access-engine POST /v1/check) and wires the whole
GH-DEC-2026-003 sequence together, then proves it against a live throwaway
OpenBao rather than only unit-level fakes.
- decision_check.check_decision performs the PDP call; an unreachable or
non-200 PDP raises, since silence is never permission.
- approval_consume.authorize_action coordinates steps 1 and 2 and returns an
AuthorizedAction. Both steps build the same CheckRequest via a shared
_expected_request, since two descriptions of the action cannot produce
corresponding digests.
- apply_unreachable_engine_stance takes authorized=. The published map
defines fail_closed as no side effect WITHOUT a durable decision record,
so holding a validated one means the residue does not apply. Not a bypass:
both steps must have succeeded and CAS consume still precedes OpenBao.
Unconfigured still returns None and fails closed.
The end-to-end test caught one more instance of the cross-vocabulary bug: a
leftover comparison of the claim's binding.action against ours. The claim
says secrets.kv.destroy where we say destroy, so it would have failed against
every real claim. Removed; the tie is pdp_digest.
Integration coverage asserts PIP-then-PDP ordering, that consume is the last
step before the backend, and that an unreachable PDP, denied decision,
invalid claim, missing pdp_digest, consume conflict and action mismatch each
stop before OpenBao. 284 tests pass; production still fails closed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M65ovP3eiiPHubibvWs9mD
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 393550@bnt-lap001
Assistant-Session: 4bb359f9-1f12-4410-9e76-079cf23c82e4
2026-09-06 14:56:02 +02:00
|
|
|
if mode == "fail_closed" and not demo and not authorized:
|
2026-08-29 12:52:55 +02:00
|
|
|
raise DecisionError(
|
|
|
|
|
f"production action '{applied.action}' requires a durable "
|
|
|
|
|
"access-engine decision record; live production remains disabled",
|
|
|
|
|
stance=applied.as_evidence(),
|
|
|
|
|
)
|
|
|
|
|
return applied
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def with_decision(stance: StanceApplication, decision: Any) -> StanceApplication:
|
|
|
|
|
decision_id = str(getattr(decision, "id", "") or "")
|
|
|
|
|
return replace(stance, decision_id=decision_id)
|