Land attended sitting create now that CCR-2026-0026/0027 is live.

Platform verified create-only exchange; this shell cannot POST.
create_sitting_approvals.py requires attended reader, skips c01, and
refuses a non-loopback approval origin. Dry-run lists the seven memos.

Assistant: grok
Assistant-Session: 01a09dc1-b21e-77e1-919e-fcad2f82b267
This commit is contained in:
tegwick 2026-09-15 22:19:25 +02:00
parent 8c03eb85c0
commit c3742e27d2
8 changed files with 318 additions and 16 deletions

View file

@ -61,9 +61,18 @@ Open in this order. One question each. No approve-all.
1. `uv run python tools/sitting_bind_preflight.py --origin https://decisions.coulomb.social`
`live_accept` must stay `open`.
2. Owning requester creates eight `human_control=true` objects, required_count 1,
no entries. Carry the native id and `binding.digest`; do not invent a digest.
Platform custody (unallocated CCR): `docs/sitting-requester-custody-request.md`.
2. Sitting requester is **live** (CCR-2026-0026/0027, exchange proof 2026-09-15).
Create seven objects from an **attended** reader session (skips `c01`):
```sh
kubectl -n approval-engine port-forward svc/approval-engine 18281:8080
warden access informed-decision-sitting-requester-login --exec -- \
env INFD_APPROVAL_ORIGIN=http://127.0.0.1:18281 \
uv run python tools/create_sitting_approvals.py
```
Dry-run (this shell): `uv run python tools/create_sitting_approvals.py --dry-run`.
Do not invent a digest. `c01` stays off this client until decided.
3. `uv run python tools/attach_compact_bindings.py --principal <exact-keycape-sub> --receipt <created.json>`
writes `bound/` copies. Unsigned drafts stay unsigned.
4. Flex Auth admits a **new** package pinning those eight `memo:` ids to the

View file

@ -8,6 +8,6 @@
"infd-batch-2026-09-14-decisions"
],
"memo_count": 8,
"bind_path": "INFD-WP-0001-T08 historically proven; live accept open 2026-09-14T22:16:07Z; sitting not admitted",
"note": "Unsigned drafts. Live accept reopened after audit-core rollout. Remaining: native approval receipts, live KeyCape subject, new Flex Auth package (not the T03 three-record mandate), then a human sitting. Operator packet: OPERATOR.md."
"bind_path": "live accept open; sitting-requester live; attended create not yet run",
"note": "Unsigned drafts. CCR-2026-0026/0027 applied and create-only exchange proof verified 2026-09-15. Next: attended tools/create_sitting_approvals.py (skips c01), then Flex Auth package, then human sitting."
}

View file

@ -16,6 +16,8 @@
"informed-decision-sitting-requester"
],
"tokenLifetime": "15m",
"status": "requested",
"applied": false
"status": "applied",
"applied": true,
"ccrs": ["CCR-2026-0026", "CCR-2026-0027"],
"exchange_proof": "railiance-platform/docs/evidence/2026-09-15-sitting-requester-exchange.json"
}

View file

@ -4,9 +4,10 @@
**For:** `key-cape` (same registration shape as `secrets-engine-requester`)
**Copied to:** `approval-engine` (`docs/keycape-service-registrations.md`
“no requester identity has been settled for `approval:create`”)
**Status:** requested 2026-09-14. **Not registered. Not in OpenBao. No secret
is in this repository.** Custody request (no CCR id):
`docs/sitting-requester-custody-request.md`.
**Status:** **applied 2026-09-15** (CCR-2026-0026/0027, KeyCape row live,
create-only exchange proof verified). No secret is in this repository.
Sittings are still INFD-WP-0002; platform did not POST. Attended create:
`tools/create_sitting_approvals.py`.
This is the missing presenter for compact Decision Memo sittings whose
protected side effect is a **work-record update** (`INFD-WP-0002-T04`), not a

View file

@ -0,0 +1,40 @@
import importlib.util
from pathlib import Path
import pytest
spec = importlib.util.spec_from_file_location(
"create_sitting_approvals",
Path(__file__).resolve().parents[1] / "tools" / "create_sitting_approvals.py",
)
create = importlib.util.module_from_spec(spec)
spec.loader.exec_module(create)
def test_dry_run_lists_seven_and_skips_c01():
report = create.dry_run()
assert report["posted"] is False
assert report["skipped"] == ["infd-20260914-c01"]
assert len(report["memo_ids"]) == 7
assert "infd-20260914-c01" not in report["memo_ids"]
assert report["memo_ids"][0] == "infd-20260914-c02"
assert report["memo_ids"][-1] == "infd-20260914-d04"
def test_require_attended_refuses_this_shell():
with pytest.raises(ValueError, match="attended_reader_required"):
create.require_attended()
@pytest.mark.parametrize(
"origin,ok",
[
("http://127.0.0.1:18281", True),
("http://approval-engine.approval-engine.svc.cluster.local:8080", True),
("https://evil.example", False),
("http://10.43.103.108:8080", False),
(None, False),
],
)
def test_approval_origin_is_loopback_or_in_cluster_only(origin, ok):
assert create._approval_origin_ok(origin) is ok

View file

@ -9,7 +9,7 @@ REG = json.loads((ROOT / "docs" / "keycape-sitting-requester-registration.json")
INTENTS = json.loads((ROOT / "docs" / "batches" / "2026-09-14" / "approval-create-intents.json").read_text())
def test_sitting_requester_is_create_only_and_unapplied():
def test_sitting_requester_is_create_only_and_applied():
assert REG["clientId"] == "informed-decision-sitting-requester"
assert REG["audience"] == "approval-engine"
assert REG["allowedScopes"] == ["approval:create"]
@ -17,7 +17,8 @@ def test_sitting_requester_is_create_only_and_unapplied():
assert REG["clientType"] == "confidential"
assert REG["serviceSubject"] == "informed-decision"
assert REG["tenant"] == "tenant:platform"
assert REG["applied"] is False
assert REG["applied"] is True
assert REG["ccrs"] == ["CCR-2026-0026", "CCR-2026-0027"]
assert "redirect" not in json.dumps(REG).lower()
forbidden = {"approval:approve", "approval:consume", "approval:read", "openid"}
assert forbidden.isdisjoint(REG["allowedScopes"])

View file

@ -0,0 +1,247 @@
"""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, {}
return status, json.loads(content)
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")
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 ValueError("approval_origin_required")
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 = {
"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)
finally:
RECEIPT.write_text(json.dumps(receipt, indent=2) + "\n")
print(json.dumps({"status": receipt["status"], "phase": receipt["phase"], "count": len(receipt["requests"])}))
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 ValueError("metadata_failed")
return json.loads(p.stdout)
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 ValueError("reader_identity_failed")
if capabilities(bao("token", "capabilities", "-format=json", KV)) != ["read"]:
raise ValueError("reader_scope_failed")
if not is_deny(bao("token", "capabilities", "-format=json", SIBLING)):
raise ValueError("reader_scope_failed")
if not is_deny(bao("token", "capabilities", "-format=json", PARENT)):
raise ValueError("reader_scope_failed")
helper = Path.home() / ".vault-token"
info = helper.lstat()
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")
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")
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 ValueError("requester_exchange_failed")
token = tokens["access_token"]
status, jwks = http(ISSUER + "/jwks")
if status != 200:
raise ValueError("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")
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 ValueError("requester_claims_failed")
if int(claims["exp"]) - int(claims["iat"]) != 900:
raise ValueError("requester_claims_failed")
for scope in ("approval:approve", "approval:consume", "approval:read"):
if exchange(scope)[0] != 400:
raise ValueError("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 ValueError("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())

View file

@ -152,10 +152,12 @@ no redirect). Intents for the eight bindings are in
`docs/batches/2026-09-14/approval-create-intents.json` (`posted: false`;
`c01` create-client undecided). No secret, no POST, no bind.
2026-09-14 23:00 UTC — **custody requested from railiance-platform, no CCR
id allocated.** `docs/sitting-requester-custody-request.md` asks for a new
KV path `platform/workloads/informed-decision/sitting-requester` (verifier +
attended reader), not a widening of CCR-2026-0024/0025. Task stays `wait`.
2026-09-15 — **requester live; sittings still wait on attended create +
human bind.** RPF-WP-0042 finished: CCR-2026-0026/0027 applied, exchange
proof verified, no sitting POST. `tools/create_sitting_approvals.py`
requires attended reader and posts seven intents (`c01` skipped). Flex
Auth package still waits on those native ids. This shell is not an
attended session. Task stays `wait`.
## Feed outcomes back to State Hub without hub-authoring