"""Create compact-sitting approval objects (INFD-WP-0002-T03). Attended reader only. Does not bind, consume, or print CLIENT_SECRET. Skips infd-20260914-c01 until its create_client is decided. """ from __future__ import annotations import argparse import json import os import stat import sys from datetime import datetime, timedelta, timezone from pathlib import Path from urllib.error import HTTPError from urllib.parse import urlencode from urllib.request import HTTPRedirectHandler, ProxyHandler, Request, build_opener ROOT = Path(__file__).resolve().parents[1] INTENTS = ROOT / "docs" / "batches" / "2026-09-14" / "approval-create-intents.json" RECEIPT = ROOT / "docs" / "evidence" / "2026-09-15-sitting-approval-creates.json" POLICY = "workload-kv-read-informed-decision-sitting-requester-client" KV = "platform/data/workloads/informed-decision/sitting-requester" SIBLING = "platform/data/workloads/secrets-engine/approval-requester" PARENT = "platform/metadata/workloads/informed-decision" ISSUER = "https://kc.coulomb.social" CLIENT_ID = "informed-decision-sitting-requester" class NoRedirect(HTTPRedirectHandler): def redirect_request(self, *args, **kwargs): return None def http(url, *, body=None, headers=None): req = Request(url, data=body, headers=headers or {}) try: with build_opener(ProxyHandler({}), NoRedirect()).open(req, timeout=20) as response: content = response.read(1048577) status = response.status except HTTPError as error: status = error.code content = error.read(1048577) error.close() if len(content) > 1048576: raise ValueError("response_too_large") if not content: return status, {} 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 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]: rows = [row for row in data.get("intents") or [] if row.get("create_client") == CLIENT_ID] if len(rows) != 7: raise ValueError("expected seven postable sitting intents") for row in rows: binding = row["binding"] if binding.get("actor") != "informed-decision": raise ValueError("binding.actor must be informed-decision") if set(binding) != {"action", "actor", "principal", "purpose", "target"}: raise ValueError("binding must be the five native fields") return rows def _approval_origin_ok(origin: str | None) -> bool: if not origin: return False if origin.startswith("http://127.0.0.1:") and origin.count("/") == 2: return True return origin in { "http://approval-engine.approval-engine.svc.cluster.local:8080", "http://approval-engine.approval-engine.svc:8080", } def dry_run() -> dict: data = json.loads(INTENTS.read_text()) rows = postable_intents(data) return { "kind": "informed-decision-sitting-create-dry-run", "posted": False, "memo_ids": [row["memo_id"] for row in rows], "skipped": [row["memo_id"] for row in data["intents"] if row.get("create_client") != CLIENT_ID], "requester_client": CLIENT_ID, "human_control": True, "pdp_path": False, } def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--dry-run", action="store_true", help="list postable memos; no credentials, no POST") parser.add_argument("--approval-origin", help="approval-engine origin; or INFD_APPROVAL_ORIGIN") args = parser.parse_args() if args.dry_run: print(json.dumps(dry_run(), indent=2, ensure_ascii=False)) return 0 try: require_attended() origin = args.approval_origin or os.environ.get("INFD_APPROVAL_ORIGIN") if not _approval_origin_ok(origin): 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 data = json.loads(INTENTS.read_text()) rows = postable_intents(data) receipt = { "observed_at": datetime.now(timezone.utc).isoformat(), "status": "failed", "phase": "preflight", "requests": [], "credential_values_emitted": False, "human_entries_created": False, "skipped": [row["memo_id"] for row in data["intents"] if row.get("create_client") != CLIENT_ID], } 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"]), "failure": receipt.get("failure")})) return 0 if receipt["status"] == "created" else 2 def _create(rows, origin, receipt): import jwt import subprocess def bao(*args): p = subprocess.run(["bao", *args], capture_output=True, text=True, timeout=20) if p.returncode: 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): result = result.get("data", result).get("capabilities", result) return result if isinstance(result, list) else [] def is_deny(result): caps = set(capabilities(result)) return caps <= {"deny"} or not caps.intersection({"read", "create", "update", "delete", "list", "patch", "sudo"}) 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 Refused("reader_identity_failed") if capabilities(bao("token", "capabilities", "-format=json", KV)) != ["read"]: raise Refused("reader_scope_failed") if not is_deny(bao("token", "capabilities", "-format=json", SIBLING)): raise Refused("reader_scope_failed") if not is_deny(bao("token", "capabilities", "-format=json", PARENT)): raise Refused("reader_scope_failed") helper = Path.home() / ".vault-token" 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 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 Refused("requester_delivery_failed") def exchange(scope, credential=secret): auth = __import__("base64").b64encode((CLIENT_ID + ":" + credential).encode()).decode() return http( ISSUER + "/token", body=urlencode({"grant_type": "client_credentials", "scope": scope}).encode(), headers={"Authorization": "Basic " + auth, "Content-Type": "application/x-www-form-urlencoded"}, ) status, tokens = exchange("approval:create") if status != 200 or "access_token" not in tokens: raise Refused("requester_exchange_failed") token = tokens["access_token"] status, jwks = http(ISSUER + "/jwks") if status != 200: 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 Refused("signing_key_failed") claims = jwt.decode( token, jwt.PyJWK.from_dict(keys[0]).key, algorithms=["RS256"], issuer=ISSUER, audience="approval-engine", options={"strict_aud": True, "require": ["sub", "iat", "exp", "iss", "aud"]}, ) roles = claims.get("roles") if isinstance(roles, str): roles = [roles] expected = { "sub": "informed-decision", "tenant": "tenant:platform", "principal_type": "service", "scope": "approval:create", } if any(claims.get(k) != v for k, v in expected.items()) or roles != ["informed-decision-sitting-requester"]: raise Refused("requester_claims_failed") if int(claims["exp"]) - int(claims["iat"]) != 900: raise Refused("requester_claims_failed") for scope in ("approval:approve", "approval:consume", "approval:read"): if exchange(scope)[0] != 400: raise Refused("excess_scope_not_refused") del secret receipt["phase"] = "requester_verified" now = datetime.now(timezone.utc) for row in rows: body = { "binding": row["binding"], "validity": {"not_before": now.isoformat(), "expires_at": (now + timedelta(hours=24)).isoformat()}, "required_count": 1, "human_control": True, "pdp_path": False, } receipt["phase"] = "create_attempt:" + row["memo_id"] RECEIPT.write_text(json.dumps(receipt, indent=2) + "\n") status, result = http( origin + "/v1/approvals", body=json.dumps(body).encode(), headers={"Authorization": "Bearer " + token, "Content-Type": "application/json"}, ) binding = (result or {}).get("binding") or {} if ( status != 201 or result.get("status") != "requested" or result.get("entries") or binding.get("human_control") is not True or not isinstance(binding.get("digest"), str) ): 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") if __name__ == "__main__": raise SystemExit(main())