Implement SECRETS-WP-0008 unblocked layer-model obligations
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:
parent
57f6c4fa65
commit
3cd9955ac9
16 changed files with 1041 additions and 77 deletions
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue