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
This commit is contained in:
tegwick 2026-09-15 22:31:42 +02:00
parent 1cac74aa35
commit a911eaf816
4 changed files with 77 additions and 33 deletions

View file

@ -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`.

View file

@ -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",
[

View file

@ -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")

View file

@ -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"