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:
parent
1cac74aa35
commit
a911eaf816
4 changed files with 77 additions and 33 deletions
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
[
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
7
tools/create_sitting_approvals.sh
Executable file
7
tools/create_sitting_approvals.sh
Executable 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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue