Implement SECRETS-WP-0008 unblocked layer-model obligations
Some checks are pending
CI Smoke / host-smoke (push) Waiting to run
CI Smoke / container-smoke (push) Waiting to run

Load pep-stance.yaml as the live unreachable-engine gate and record named
stance fields on privileged evidence. Classify evidence, queue load-bearing
records in a local outbox, and add heartbeat/drain commands that never sit
on a mutation path. Publish proposed SSH-CA and secret-use evidence
contracts without adding an OpenBao SSH-CA write.

T02 (access-engine decision records) and T06 (no standing credential) stay
wait on external endpoints.

Assistant: grok
Assistant-Session: 01a04cea-cb33-7c63-bad7-c1b0f9f0076b
This commit is contained in:
tegwick 2026-08-29 12:52:55 +02:00
parent 57f6c4fa65
commit 3cd9955ac9
16 changed files with 1041 additions and 77 deletions

View file

@ -14,6 +14,7 @@ Command surface (FR7):
revoke <catalog-id>
lifecycle suspend|deactivate|destroy <catalog-id>
audit <catalog-id> [--json]
evidence heartbeat|drain|classify
Every privileged action is decision-gated and writes non-secret evidence.
`plan` and `apply --dry-run` never mutate OpenBao.
@ -21,10 +22,8 @@ Every privileged action is decision-gated and writes non-secret evidence.
from __future__ import annotations
import argparse
import os
import sys
from pathlib import Path
from urllib.parse import urlparse
from secrets_engine import __version__
from secrets_engine.apply import apply_plan
@ -33,6 +32,7 @@ from secrets_engine.config import Config, repo_root
from secrets_engine.decisions import require_approved, resolve_decision
from secrets_engine.errors import DecisionError, SecretsEngineError
from secrets_engine.evidence import EvidenceWriter, PrivilegedActionEvidence
from secrets_engine.pep_stance import apply_unreachable_engine_stance, with_decision
from secrets_engine.openbao import OpenBaoClient
from secrets_engine.plan import build_plan
from secrets_engine.provision import provision_from_file, provision_generated
@ -85,28 +85,23 @@ def _privileged_evidence(
)
def _unsafe_local_demo_enabled(cfg: Config) -> bool:
"""Return true only for an explicit, offline, loopback-only demo."""
host = (urlparse(cfg.bao_addr).hostname or "").lower()
return (
os.environ.get("SECRETS_ENGINE_UNSAFE_DEMO") == "1"
and not cfg.hub_url
and host in {"127.0.0.1", "localhost", "::1"}
)
def _require_lane_approval(
cfg: Config,
entry,
action: str = "",
evidence: PrivilegedActionEvidence | None = None,
):
"""Apply published PEP stance, then resolve lane approval.
def _require_lane_approval(cfg: Config, entry, action: str = ""):
"""Resolve approval for a live action, failing production closed.
The durable State Hub action-authorization endpoint is not available yet.
Production therefore cannot rely on a coarse lane decision. The one narrow
exception is an explicit offline demo against a loopback OpenBao instance.
Production ``fail_closed`` is read from ``pep-stance.yaml``. The durable
access-engine decision record is not served yet, so that row refuses live
production work. The three-factor unsafe-demo exception is not a stance
row. Build/test ``fail_open`` still requires the existing lane-approval
check a tracked gap until SECRETS-WP-0008-T02.
"""
if entry.stage == "prod" and not _unsafe_local_demo_enabled(cfg):
raise DecisionError(
f"production action '{action or 'unknown'}' requires a durable "
"State Hub action authorization; live production remains disabled"
)
stance = apply_unreachable_engine_stance(cfg, entry, action or "unknown")
if evidence is not None:
evidence.mark_stance(stance)
if not entry.approval_required():
return None
decision = resolve_decision(
@ -115,6 +110,8 @@ def _require_lane_approval(cfg: Config, entry, action: str = ""):
decision_ref=entry.approval.get("decision_ref", entry.id),
)
require_approved(entry, decision)
if evidence is not None:
evidence.mark_stance(with_decision(stance, decision))
return decision
@ -229,7 +226,7 @@ def cmd_apply(cfg: Config, args) -> int:
return 0
with _privileged_evidence(cfg, entry, "apply") as evidence:
decision = _require_lane_approval(cfg, entry, "apply")
decision = _require_lane_approval(cfg, entry, "apply", evidence)
evidence.mark_approved(decision)
plan = build_plan(
entry, args.stage, decision_id=decision.id if decision else ""
@ -258,7 +255,7 @@ def cmd_provision(cfg: Config, args) -> int:
raise ProvisioningError(
f"lane '{entry.id}' is stage '{entry.stage}', not '{args.stage}'"
)
decision = _require_lane_approval(cfg, entry, "provision")
decision = _require_lane_approval(cfg, entry, "provision", evidence)
evidence.mark_approved(decision)
client = OpenBaoClient.resolve(
cfg.bao_addr, bootstrap_token_file=args.bootstrap_token_file
@ -291,7 +288,7 @@ def cmd_verify(cfg: Config, args) -> int:
"negative_requested": negative,
},
) as evidence:
decision = _require_lane_approval(cfg, entry, "verify")
decision = _require_lane_approval(cfg, entry, "verify", evidence)
evidence.mark_approved(decision)
client = OpenBaoClient.resolve(
cfg.bao_addr, bootstrap_token_file=args.bootstrap_token_file
@ -367,7 +364,7 @@ def cmd_handoff(cfg: Config, args) -> int:
raise ProvisioningError(
f"lane '{entry.id}' is {entry.kind}; handoff needs auth-capability"
)
decision = _require_lane_approval(cfg, entry, "handoff")
decision = _require_lane_approval(cfg, entry, "handoff", evidence)
evidence.mark_approved(decision)
client = OpenBaoClient.resolve(
cfg.bao_addr, bootstrap_token_file=args.bootstrap_token_file
@ -419,7 +416,7 @@ def cmd_exec(cfg: Config, args) -> int:
},
) as evidence:
# require approval + readiness before running.
decision = _require_lane_approval(cfg, entry, "exec")
decision = _require_lane_approval(cfg, entry, "exec", evidence)
evidence.mark_approved(decision)
if not args.command:
from secrets_engine.errors import DeliveryError
@ -498,7 +495,7 @@ def cmd_revoke(cfg: Config, args) -> int:
with _privileged_evidence(
cfg, entry, "revoke", detail={"operation": plan.operation}
) as evidence:
decision = _require_lane_approval(cfg, entry, "deactivate")
decision = _require_lane_approval(cfg, entry, "deactivate", evidence)
evidence.mark_approved(decision)
client = OpenBaoClient.resolve(
cfg.bao_addr, bootstrap_token_file=args.bootstrap_token_file
@ -546,7 +543,7 @@ def cmd_lifecycle(cfg: Config, args) -> int:
"live destroy is disabled until an exact-action destruction "
"approval contract is available; use --dry-run to inspect targets"
)
decision = _require_lane_approval(cfg, entry, args.operation)
decision = _require_lane_approval(cfg, entry, args.operation, evidence)
evidence.mark_approved(decision)
client = OpenBaoClient.resolve(
cfg.bao_addr, bootstrap_token_file=args.bootstrap_token_file
@ -579,6 +576,65 @@ def cmd_audit(cfg: Config, args) -> int:
return 0
def cmd_evidence_heartbeat(cfg: Config, args) -> int:
"""Write a positive load-bearing heartbeat. Not a permission check."""
import json
from secrets_engine.evidence import write_heartbeat
record = write_heartbeat(_writer(cfg), stage=args.stage)
if args.json:
print(json.dumps(record, indent=2, sort_keys=True))
else:
print(
f"heartbeat {record['result']} queued={record.get('outbox_queued')} "
f"completeness_claimed={record.get('completeness_claimed')}"
)
return 0
def cmd_evidence_drain(cfg: Config, args) -> int:
"""Best-effort drain of the local load-bearing outbox. Never a gate."""
import json
from secrets_engine.evidence import drain_outbox
result = drain_outbox(_writer(cfg), audit_core_url=args.audit_core_url)
if args.json:
print(json.dumps(result, indent=2, sort_keys=True))
else:
print(
f"outbox drain queued={result['queued']} "
f"delivered={result['delivered']} failed={result['failed']} "
f"skipped={result['skipped']}"
)
return 0
def cmd_evidence_classify(cfg: Config, args) -> int:
import json
from secrets_engine.evidence_class import classify
classified = classify(args.action, args.stage)
payload = {
"action": classified.action,
"stage": classified.stage,
"kind": classified.kind,
"rule_id": classified.rule_id,
"queued_locally": classified.queued_locally,
"completeness_claimed": classified.completeness_claimed,
}
if args.json:
print(json.dumps(payload, indent=2, sort_keys=True))
else:
print(
f"{classified.action}/{classified.stage}: {classified.kind} "
f"rule={classified.rule_id} queued={classified.queued_locally}"
)
return 0
# -- parser ----------------------------------------------------------------
@ -700,6 +756,32 @@ def build_parser() -> argparse.ArgumentParser:
au.add_argument("--json", action="store_true")
au.set_defaults(func=cmd_audit)
ev = sub.add_parser("evidence", help="load-bearing evidence heartbeat and outbox")
evsub = ev.add_subparsers(dest="subcmd", required=True)
hb = evsub.add_parser(
"heartbeat",
help="emit a positive nothing-to-report claim (not a permission check)",
)
hb.add_argument("--stage", default="prod", choices=("build", "test", "prod"))
hb.add_argument("--json", action="store_true")
hb.set_defaults(func=cmd_evidence_heartbeat)
dr = evsub.add_parser(
"drain",
help="best-effort drain of the local outbox; never blocks a mutation",
)
dr.add_argument(
"--audit-core-url",
default="",
help="optional audit-core base URL; empty skips delivery and keeps files",
)
dr.add_argument("--json", action="store_true")
dr.set_defaults(func=cmd_evidence_drain)
cl = evsub.add_parser("classify", help="show the §9.6 class for an action/stage")
cl.add_argument("action")
cl.add_argument("--stage", required=True, choices=("build", "test", "prod"))
cl.add_argument("--json", action="store_true")
cl.set_defaults(func=cmd_evidence_classify)
return p