From a911eaf81659e2ba2a3b8b8c9acc6d4f4daae361 Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 15 Sep 2026 22:31:42 +0200 Subject: [PATCH] Record sitting-create failure class and allow retry. Auth returned clean at 20:27Z but the child left a preflight receipt with no error. Named failures, refuse public bao.coulomb.social, and overwrite a failed receipt. Wrapper is executable for attended-exec. Assistant: grok Assistant-Session: 01a09dc1-b21e-77e1-919e-fcad2f82b267 --- docs/batches/2026-09-14/OPERATOR.md | 18 +++---- tests/test_create_sitting_approvals.py | 11 +++- tools/create_sitting_approvals.py | 74 ++++++++++++++++++-------- tools/create_sitting_approvals.sh | 7 +++ 4 files changed, 77 insertions(+), 33 deletions(-) create mode 100755 tools/create_sitting_approvals.sh diff --git a/docs/batches/2026-09-14/OPERATOR.md b/docs/batches/2026-09-14/OPERATOR.md index 5c09295..cca80ee 100644 --- a/docs/batches/2026-09-14/OPERATOR.md +++ b/docs/batches/2026-09-14/OPERATOR.md @@ -64,19 +64,17 @@ Open in this order. One question each. No approve-all. 2. Sitting requester is **live** (CCR-2026-0026/0027, exchange proof 2026-09-15). Create seven objects from an **attended** reader session (skips `c01`): - Installed `warden` may not know this catalog id; use the source catalog. - OpenBao is the operator tunnel (`127.0.0.1:18200`), not public - `bao.coulomb.social`. + Installed `warden` may not know this catalog id. Use the attended wrapper + so `BAO_ADDR` is `http://127.0.0.1:18200` (public `bao.coulomb.social` is + retracted). 2026-09-15 20:27Z: login succeeded; the create child wrote a + failed `preflight` receipt with no failure class. Retry overwrites that + failed receipt and now records a named `failure`. ```sh kubectl -n approval-engine port-forward svc/approval-engine 18281:8080 - export WARDEN_ROUTING_CATALOG=/home/worsch/ops-warden/registry/routing/catalog.yaml - export BAO_ADDR=http://127.0.0.1:18200 - export INFD_APPROVAL_ORIGIN=http://127.0.0.1:18281 - warden access informed-decision-sitting-requester-login --exec -- \ - /home/worsch/informed-decision/.venv/bin/python -B \ - /home/worsch/informed-decision/tools/create_sitting_approvals.py \ - --approval-origin http://127.0.0.1:18281 + python3 /home/worsch/railiance-platform/scripts/openbao-attended-exec.py \ + --lane informed-decision-sitting-requester-login -- \ + /home/worsch/informed-decision/tools/create_sitting_approvals.sh ``` Dry-run (this shell): `uv run python tools/create_sitting_approvals.py --dry-run`. diff --git a/tests/test_create_sitting_approvals.py b/tests/test_create_sitting_approvals.py index c3285bf..98b8de2 100644 --- a/tests/test_create_sitting_approvals.py +++ b/tests/test_create_sitting_approvals.py @@ -22,10 +22,19 @@ def test_dry_run_lists_seven_and_skips_c01(): def test_require_attended_refuses_this_shell(): - with pytest.raises(ValueError, match="attended_reader_required"): + with pytest.raises(create.Refused, match="attended_reader_required"): create.require_attended() +def test_failed_receipt_does_not_block_retry(tmp_path, monkeypatch): + receipt = tmp_path / "r.json" + monkeypatch.setattr(create, "RECEIPT", receipt) + receipt.write_text('{"status": "failed", "phase": "preflight"}\n') + assert create.existing_receipt_blocks() is False + receipt.write_text('{"status": "created", "phase": "seven_unapproved_requests_created"}\n') + assert create.existing_receipt_blocks() is True + + @pytest.mark.parametrize( "origin,ok", [ diff --git a/tools/create_sitting_approvals.py b/tools/create_sitting_approvals.py index 50984d1..677d620 100644 --- a/tools/create_sitting_approvals.py +++ b/tools/create_sitting_approvals.py @@ -46,12 +46,32 @@ def http(url, *, body=None, headers=None): raise ValueError("response_too_large") if not content: return status, {} - return status, json.loads(content) + try: + return status, json.loads(content) + except json.JSONDecodeError as exc: + raise ValueError("non_json_http_response") from exc + + +class Refused(ValueError): + pass def require_attended() -> None: if Path.home().parent.name != ".warden-attended-login" or os.getenv("BAO_TOKEN") or os.getenv("VAULT_TOKEN"): - raise ValueError("attended_reader_required") + raise Refused("attended_reader_required") + addr = os.getenv("BAO_ADDR") or os.getenv("VAULT_ADDR") or "" + if "bao.coulomb.social" in addr: + raise Refused("public_bao_retracted") + + +def existing_receipt_blocks() -> bool: + if not RECEIPT.exists(): + return False + try: + existing = json.loads(RECEIPT.read_text()) + except Exception: + return True + return existing.get("status") == "created" def postable_intents(data: dict) -> list[dict]: @@ -104,12 +124,12 @@ def main() -> int: require_attended() origin = args.approval_origin or os.environ.get("INFD_APPROVAL_ORIGIN") if not _approval_origin_ok(origin): - raise ValueError("approval_origin_required") + raise Refused("approval_origin_required") + if existing_receipt_blocks(): + raise Refused("receipt_already_exists") except ValueError as exc: print(exc, file=sys.stderr) return 2 - if RECEIPT.exists(): - raise ValueError("receipt_already_exists") data = json.loads(INTENTS.read_text()) rows = postable_intents(data) receipt = { @@ -123,9 +143,13 @@ def main() -> int: } try: _create(rows, origin.rstrip("/"), receipt) + except Exception as error: + receipt["failure"] = str(error) if isinstance(error, Refused) else "contained_operation_failed" + print(receipt["failure"], file=sys.stderr) + return 2 finally: RECEIPT.write_text(json.dumps(receipt, indent=2) + "\n") - print(json.dumps({"status": receipt["status"], "phase": receipt["phase"], "count": len(receipt["requests"])})) + print(json.dumps({"status": receipt["status"], "phase": receipt["phase"], "count": len(receipt["requests"]), "failure": receipt.get("failure")})) return 0 if receipt["status"] == "created" else 2 @@ -136,8 +160,11 @@ def _create(rows, origin, receipt): def bao(*args): p = subprocess.run(["bao", *args], capture_output=True, text=True, timeout=20) if p.returncode: - raise ValueError("metadata_failed") - return json.loads(p.stdout) + raise Refused("metadata_failed") + try: + return json.loads(p.stdout) + except json.JSONDecodeError as exc: + raise Refused("metadata_failed") from exc def capabilities(result): if isinstance(result, dict): @@ -151,23 +178,26 @@ def _create(rows, origin, receipt): lookup = bao("token", "lookup", "-format=json")["data"] policies = set(lookup.get("policies", [])) | set(lookup.get("identity_policies", [])) if POLICY not in policies or policies - {POLICY, "default"} or not lookup.get("entity_id") or not 0 < int(lookup.get("ttl") or 0) <= 900: - raise ValueError("reader_identity_failed") + raise Refused("reader_identity_failed") if capabilities(bao("token", "capabilities", "-format=json", KV)) != ["read"]: - raise ValueError("reader_scope_failed") + raise Refused("reader_scope_failed") if not is_deny(bao("token", "capabilities", "-format=json", SIBLING)): - raise ValueError("reader_scope_failed") + raise Refused("reader_scope_failed") if not is_deny(bao("token", "capabilities", "-format=json", PARENT)): - raise ValueError("reader_scope_failed") + raise Refused("reader_scope_failed") helper = Path.home() / ".vault-token" - info = helper.lstat() + try: + info = helper.lstat() + except FileNotFoundError as exc: + raise Refused("private_helper_required") from exc if not stat.S_ISREG(info.st_mode) or stat.S_IMODE(info.st_mode) != 0o600 or info.st_uid != os.getuid(): - raise ValueError("private_helper_required") + raise Refused("private_helper_required") payload = bao("read", "-format=json", KV) body = payload.get("data", payload) inner = body.get("data", body) secret = inner.get("CLIENT_SECRET") if isinstance(inner, dict) else None if (body.get("metadata") or {}).get("version") != 1 or not secret: - raise ValueError("requester_delivery_failed") + raise Refused("requester_delivery_failed") def exchange(scope, credential=secret): auth = __import__("base64").b64encode((CLIENT_ID + ":" + credential).encode()).decode() @@ -179,15 +209,15 @@ def _create(rows, origin, receipt): status, tokens = exchange("approval:create") if status != 200 or "access_token" not in tokens: - raise ValueError("requester_exchange_failed") + raise Refused("requester_exchange_failed") token = tokens["access_token"] status, jwks = http(ISSUER + "/jwks") if status != 200: - raise ValueError("jwks_failed") + raise Refused("jwks_failed") header = jwt.get_unverified_header(token) keys = [key for key in jwks["keys"] if key["kid"] == header.get("kid")] if header.get("alg") != "RS256" or len(keys) != 1: - raise ValueError("signing_key_failed") + raise Refused("signing_key_failed") claims = jwt.decode( token, jwt.PyJWK.from_dict(keys[0]).key, @@ -206,12 +236,12 @@ def _create(rows, origin, receipt): "scope": "approval:create", } if any(claims.get(k) != v for k, v in expected.items()) or roles != ["informed-decision-sitting-requester"]: - raise ValueError("requester_claims_failed") + raise Refused("requester_claims_failed") if int(claims["exp"]) - int(claims["iat"]) != 900: - raise ValueError("requester_claims_failed") + raise Refused("requester_claims_failed") for scope in ("approval:approve", "approval:consume", "approval:read"): if exchange(scope)[0] != 400: - raise ValueError("excess_scope_not_refused") + raise Refused("excess_scope_not_refused") del secret receipt["phase"] = "requester_verified" now = datetime.now(timezone.utc) @@ -238,7 +268,7 @@ def _create(rows, origin, receipt): or binding.get("human_control") is not True or not isinstance(binding.get("digest"), str) ): - raise ValueError("request_creation_requires_reconciliation") + raise Refused("request_creation_requires_reconciliation") receipt["requests"].append({"memo_id": row["memo_id"], "approval": result}) receipt.update(status="created", phase="seven_unapproved_requests_created") diff --git a/tools/create_sitting_approvals.sh b/tools/create_sitting_approvals.sh new file mode 100755 index 0000000..39fdddc --- /dev/null +++ b/tools/create_sitting_approvals.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Attended child for openbao-attended-exec / warden access. Metadata only. +set -euo pipefail +export INFD_APPROVAL_ORIGIN="${INFD_APPROVAL_ORIGIN:-http://127.0.0.1:18281}" +exec /home/worsch/informed-decision/.venv/bin/python -B \ + /home/worsch/informed-decision/tools/create_sitting_approvals.py \ + --approval-origin "$INFD_APPROVAL_ORIGIN"