NK-WP-0056-T10: journey-only executor mode; fail-closed proof on the T08 stack
cli.py --journey-only: read-only, no admission; journey-read role only (CCR-2026-0059), no Kubernetes/directory client, no approval or audit call; exit 0 pass / 1 fail closed / 4 refused. Shared operations.journey_from_kv with the admission check. Synthetic proof: healthy pass, broken svc-authelia bind fails closed at authelia_first_factor, restored pass, journey token cannot read the bind; all_met, no values. 45 executor tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 5502@bnt-lap001 Assistant-Session: 68b3b02f-5d0f-4e71-8421-796f62e50196
This commit is contained in:
parent
ef6284c371
commit
abed4360d4
8 changed files with 327 additions and 18 deletions
|
|
@ -16,6 +16,13 @@ The config (JSON, non-secret) names endpoints, token files and the pilot's targe
|
|||
|
||||
Exit codes: 0 completed, 3 reverted, 4 refused before any effect, 5 stopped / unknown / revert failed / budget,
|
||||
6 consumed but not in custody. The result printed is value-free.
|
||||
|
||||
python3 bounded-executor/cli.py --journey-only --config FILE [--result FILE]
|
||||
|
||||
Journey-only (NK-WP-0056-T10): read-only, no admission. Logs in with the journey role only (journey.k8s_role, the
|
||||
openbao-audience token), runs the synthetic journey once and prints its value-free receipt. No write role, no
|
||||
Kubernetes or directory client, no approval or audit call. Needs only openbao{addr, jwt_file, ca_file?} and
|
||||
journey{kv, k8s_role, config}. Exit codes: 0 pass, 1 journey failed (fail closed), 4 refused before the journey.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -82,6 +89,30 @@ def build(cfg: dict, logins: list):
|
|||
return config, deps
|
||||
|
||||
|
||||
def journey_only(cfg: dict, logins: list) -> dict:
|
||||
o, j = cfg["openbao"], cfg["journey"]
|
||||
client = OPS.OpenBao(o["addr"], k8s_role=j["k8s_role"], jwt_file=o["jwt_file"], ca_file=o.get("ca_file"))
|
||||
logins.append(client)
|
||||
receipt = OPS.journey_from_kv(client, j["kv"], j["config"])
|
||||
return {"mode": "journey-only", "outcome": "pass" if receipt["result"] == "pass" else "fail",
|
||||
"effects": False, "journey": receipt}
|
||||
|
||||
|
||||
def run_journey_only(cfg: dict) -> tuple[dict, int]:
|
||||
logins: list = []
|
||||
try:
|
||||
result = journey_only(cfg, logins)
|
||||
code = 0 if result["outcome"] == "pass" else 1
|
||||
except Exception as error: # nothing was changed: the journey only reads and signs in
|
||||
result = {"mode": "journey-only", "outcome": "refused", "reason": "setup:" + type(error).__name__,
|
||||
"effects": False}
|
||||
code = 4
|
||||
finally:
|
||||
for client in logins:
|
||||
client.revoke_self()
|
||||
return result, code
|
||||
|
||||
|
||||
def exit_code(result: dict) -> int:
|
||||
if result.get("reason") in ("receipt_not_in_custody", "audit_unavailable") and result["outcome"] != "refused":
|
||||
return 6
|
||||
|
|
@ -90,12 +121,21 @@ def exit_code(result: dict) -> int:
|
|||
|
||||
def main(argv=None) -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0])
|
||||
parser.add_argument("--admission", required=True)
|
||||
parser.add_argument("--approval-id", required=True)
|
||||
parser.add_argument("--journey-only", action="store_true", help="read-only journey run, no admission (T10)")
|
||||
parser.add_argument("--admission")
|
||||
parser.add_argument("--approval-id")
|
||||
parser.add_argument("--config", required=True)
|
||||
parser.add_argument("--result", help="also write the result here")
|
||||
args = parser.parse_args(argv)
|
||||
if args.journey_only == bool(args.admission or args.approval_id):
|
||||
parser.error("give either --journey-only, or --admission with --approval-id")
|
||||
if not args.journey_only and not (args.admission and args.approval_id):
|
||||
parser.error("--admission and --approval-id go together")
|
||||
cfg = json.loads(pathlib.Path(args.config).read_text())
|
||||
if args.journey_only:
|
||||
result, code = run_journey_only(cfg)
|
||||
_emit(result, args.result)
|
||||
return code
|
||||
admission = X.bea.load(pathlib.Path(args.admission))
|
||||
logins: list = []
|
||||
try:
|
||||
|
|
@ -106,12 +146,16 @@ def main(argv=None) -> int:
|
|||
finally:
|
||||
for client in logins:
|
||||
client.revoke_self()
|
||||
text = json.dumps(result, indent=2, sort_keys=True)
|
||||
if args.result:
|
||||
pathlib.Path(args.result).write_text(text + "\n")
|
||||
print(text)
|
||||
_emit(result, args.result)
|
||||
return exit_code(result)
|
||||
|
||||
|
||||
def _emit(result: dict, path: str | None) -> None:
|
||||
text = json.dumps(result, indent=2, sort_keys=True)
|
||||
if path:
|
||||
pathlib.Path(path).write_text(text + "\n")
|
||||
print(text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue