From 6a386dd787369f9d4daae5e5a00b136276f87ac5 Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 14 Sep 2026 18:59:49 +0200 Subject: [PATCH] Prepare INFD-WP-0002-T03 sitting without a fake bind. T08 historically bound three SECRETS-WP-0010-T03 memos on this origin. Live accept is closed: /readyz 503 because audit-core has no ready endpoints. Compact drafts still lack approval_id. Operator packet and preflight record the gates; T03 stays wait. Assistant: grok Assistant-Session: 01a09dc1-b21e-77e1-919e-fcad2f82b267 --- docs/batches/2026-09-14/OPERATOR.md | 74 +++++++++ docs/batches/2026-09-14/README.md | 8 +- docs/batches/2026-09-14/sitting.json | 4 +- ...6-09-14-infd-0002-t03-bind-path-probe.json | 83 ++++++++++ docs/specs/CompactSignoffBatches.md | 8 + tests/test_sitting_bind_preflight.py | 78 ++++++++++ tools/sitting_bind_preflight.py | 146 ++++++++++++++++++ .../INFD-WP-0002-compact-signoff-batches.md | 15 ++ 8 files changed, 413 insertions(+), 3 deletions(-) create mode 100644 docs/batches/2026-09-14/OPERATOR.md create mode 100644 docs/evidence/2026-09-14-infd-0002-t03-bind-path-probe.json create mode 100644 tests/test_sitting_bind_preflight.py create mode 100644 tools/sitting_bind_preflight.py diff --git a/docs/batches/2026-09-14/OPERATOR.md b/docs/batches/2026-09-14/OPERATOR.md new file mode 100644 index 0000000..a6f54c8 --- /dev/null +++ b/docs/batches/2026-09-14/OPERATOR.md @@ -0,0 +1,74 @@ +# Compact sitting — operator packet (INFD-WP-0002-T03) + +Review group: `net-kingdom-admins`. +Surface: https://decisions.coulomb.social/ +Contract: `docs/specs/CompactSignoffBatches.md`. + +An agent may draft, order, and record evidence. It may not `accept` / `decline` / `seal`. +Do not mark INFD-WP-0002 finished on a partial sitting. + +## What is already proven + +On 2026-09-14 around 01:06 UTC this origin took **three real human accepts** +for `SECRETS-WP-0010-T03-{apply,verify,exec}`: presentations, required-highlight +acknowledgments, `accept` dispositions, and confirmed approval-engine +submissions are in the review store. That discharges “can a human bind through +this surface?” for T08 historically. + +Those three memos are **not** this sitting. `infd-20260914-c01` asks a different +question (admit native delivery) and must not reuse those approval ids. + +## What is closed right now (2026-09-14 16:56 UTC) + +Live accept is refused: + +- Origin `/healthz` 200; `/auth/start` 303 to KeyCape (`informed-decision-approver`, PKCE S256). +- Origin `/readyz` **503** `approval_path_not_connected`. +- `audit-core` pod Ready=false; `/readyz` hangs; Service has no ready endpoints; + the review pod gets connection refused talking to `audit-core.audit-core.svc:8080`. +- Accept requires recent audit delivery health. Until that Service has a ready + endpoint, a new sitting cannot submit entries. + +Evidence: `docs/evidence/2026-09-14-infd-0002-t03-bind-path-probe.json`. +Audit-core owns the hang; this packet does not change that deployment. + +## Eight unsigned memos (this sitting) + +Open in this order. One question each. No approve-all. + +| n | memo_id | Blocking record | +| --: | --- | --- | +| 1 | `infd-20260914-c01` | SECRETS-WP-0010 native delivery | +| 2 | `infd-20260914-c02` | RPF-WP-0035-T02 | +| 3 | `infd-20260914-c03` | NK-WP-0032-T03 | +| 4 | `infd-20260914-c04` | WARDEN-WP-0027-T02 | +| 5 | `infd-20260914-d01` | CUST-WP-0038-T08 | +| 6 | `infd-20260914-d02` | HFACT-WP-0001-T03 | +| 7 | `infd-20260914-d03` | MASON-WP-0005 plan | +| 8 | `infd-20260914-d04` | RCLK-WP-0002-T01 | + +Drafts still have `approval_id: null` and principal `pending-human-session`. +The live controller refuses those with `missing_act_binding` / `wrong_recipient`. +They are not in the live store. + +## After audit-core is Ready + +1. Confirm `GET https://decisions.coulomb.social/readyz` returns 200, or run + `uv run python tools/sitting_bind_preflight.py --origin https://decisions.coulomb.social`. +2. For **each** memo, the owning requester creates one `approval-engine` object + with `human_control=true`, required_count 1, no entries. Do **not** reuse + `secrets-engine-requester` for unrelated acts (WSL2 retirement, clock + ownership, mason plan, …). +3. Substitute the operator’s live KeyCape `sub` for `pending-human-session`. + Carry the native `approval_id` and `binding.digest` on the memo. Do not + invent a digest. +4. Load packet bytes then `save_memo` into the review store (named-memo form, + no inbox). +5. Sign in at the origin. Open `/review?memo_id=infd-20260914-c01` through + `…-d04` in order. Acknowledge the required highlight. Bind that memo. Repeat. +6. Unfinished memos stay in the batch. File-level owning-repo updates are T04 + after a human bind, via `fix-consistency`, never `POST /workplans/`. + +A recorded desktop sitting (same order, same highlights, `view_hash` from +`presentation.render` only) is allowed if the Stage 1 UI still cannot take the +group. It is still a human bind. An agent must not complete the form. diff --git a/docs/batches/2026-09-14/README.md b/docs/batches/2026-09-14/README.md index bd8dc94..e2a8962 100644 --- a/docs/batches/2026-09-14/README.md +++ b/docs/batches/2026-09-14/README.md @@ -18,4 +18,10 @@ Two batches, eight memos, one compact sitting: Contract: `docs/specs/CompactSignoffBatches.md`. Regenerate: `uv run python tools/prepare_compact_batches.py`. -Live bind waits on `INFD-WP-0001-T08`. +Preflight: `uv run python tools/sitting_bind_preflight.py --origin https://decisions.coulomb.social`. +Operator packet: `OPERATOR.md`. + +T08 already took three human accepts on this origin for `SECRETS-WP-0010-T03-*`. +Those memos are not this sitting. Live accept is currently closed +(`/readyz` 503) because audit-core has no ready Service endpoints. +This sitting stays `draft-unsigned` until a human binds through the surface. diff --git a/docs/batches/2026-09-14/sitting.json b/docs/batches/2026-09-14/sitting.json index 2236ed4..c48103d 100644 --- a/docs/batches/2026-09-14/sitting.json +++ b/docs/batches/2026-09-14/sitting.json @@ -8,6 +8,6 @@ "infd-batch-2026-09-14-decisions" ], "memo_count": 8, - "bind_path": "INFD-WP-0001-T08 (not yet complete)", - "note": "Unsigned drafts. Do not submit until CompactSignoffBatches.md is accepted. Live bind substitutes pending-human-session with the authenticated key-cape subject." + "bind_path": "INFD-WP-0001-T08 historically proven; live accept closed 2026-09-14T16:56:55Z", + "note": "Unsigned drafts. Do not submit until a human sitting is opened under T03. Live bind substitutes pending-human-session with the authenticated key-cape subject. Probe: docs/evidence/2026-09-14-infd-0002-t03-bind-path-probe.json. Operator packet: OPERATOR.md." } diff --git a/docs/evidence/2026-09-14-infd-0002-t03-bind-path-probe.json b/docs/evidence/2026-09-14-infd-0002-t03-bind-path-probe.json new file mode 100644 index 0000000..a609f71 --- /dev/null +++ b/docs/evidence/2026-09-14-infd-0002-t03-bind-path-probe.json @@ -0,0 +1,83 @@ +{ + "kind": "infd-wp-0002-t03-bind-path-probe", + "observed_at": "2026-09-14T16:56:55Z", + "workplan": "INFD-WP-0002", + "task": "INFD-WP-0002-T03", + "scope": "Live bind-path diagnosis for the compact sitting. No human login, no presentation, no disposition, no approval create, no secret read.", + "origin": { + "url": "https://decisions.coulomb.social/", + "tls_verify": 0, + "healthz": {"http": 200, "body": {"status": "ok"}}, + "readyz": { + "http": 503, + "body": {"status": "incomplete", "reason": "approval_path_not_connected"} + }, + "auth_start": { + "http": 303, + "location_host": "kc.coulomb.social", + "client_id": "informed-decision-approver", + "redirect_uri": "https://decisions.coulomb.social/auth/callback", + "scopes": ["openid", "approval:read", "approval:approve"], + "pkce": "S256" + }, + "unsigned_home": "Sign in with KeyCape only; memo form is session-gated" + }, + "cluster": { + "informed-decision": { + "namespace": "informed-decision", + "pod": "informed-decision-b7f6fc6c9-kd4bl", + "ready": true, + "start_time": "2026-09-14T01:30:01Z", + "image": "forgejo.coulomb.social/coulomb/informed-decision@sha256:8f55bcecf37a8d65f96e073510b1ffb4636c0a91d75e1ee7d582ad4bce8b953a" + }, + "approval-engine": { + "namespace": "approval-engine", + "pod": "approval-engine-0", + "ready": true, + "in_cluster_healthz": {"http": 200, "body": {"status": "ok"}} + }, + "audit-core": { + "namespace": "audit-core", + "pod": "audit-core-7cb66768cf-pq6pw", + "ready": false, + "started_at": "2026-09-11T08:27:26Z", + "restarts": 0, + "local_healthz": {"http": 200}, + "local_readyz": "timeout (>8s); kubelet probe timeoutSeconds=2 on /readyz", + "service_cluster_ip": "10.43.74.236", + "classic_endpoints": "no ready addresses; one notReadyAddresses entry", + "from_informed_decision_pod": "connection refused to audit-core.audit-core.svc:8080" + } + }, + "review_store_counts": { + "schema_version": 2, + "sqlite_mtime_in_pod": "2026-09-14T01:06:00Z approximately", + "memos": 3, + "presentations": 3, + "acknowledgments": 3, + "dispositions": {"accept": 3}, + "submissions": {"confirmed": 3}, + "outbox": {"delivered": 15}, + "memo_ids": [ + "SECRETS-WP-0010-T03-apply", + "SECRETS-WP-0010-T03-exec", + "SECRETS-WP-0010-T03-verify" + ], + "note": "Counts and identifiers only. No packet bytes, principals, view_hash values, or approval entry bodies were copied out of the store." + }, + "compact_sitting": { + "id": "infd-sitting-2026-09-14", + "status": "draft-unsigned", + "memo_count": 8, + "approval_ids": "all null", + "principal": "pending-human-session", + "in_live_store": false + }, + "conclusion": { + "t08_historical_bind": "Three real human accept dispositions with confirmed engine submissions already exist on this surface for the SECRETS-WP-0010-T03 named memos. That sitting is not the INFD-WP-0002 compact batch.", + "t03_live_accept": "closed", + "reason": "informed-decision /readyz is 503 because audit-core is not Ready, its Service has no ready endpoints, and accept is refused while pump.ready() is false.", + "agent_disposition": "forbidden", + "infd_wp_0002_t03": "wait" + } +} diff --git a/docs/specs/CompactSignoffBatches.md b/docs/specs/CompactSignoffBatches.md index 257c583..4024db5 100644 --- a/docs/specs/CompactSignoffBatches.md +++ b/docs/specs/CompactSignoffBatches.md @@ -50,3 +50,11 @@ a human bind (T04), via repo files + `fix-consistency`, never `docs/batches/2026-09-14/` — four credential/custody memos and four decision/assent memos. Indexes name the blocking workplan/task. Do not submit until a human sitting is opened under T03. + +2026-09-14 16:56 UTC: Stage 1 historically bound three other memos +(`SECRETS-WP-0010-T03-*`) on this origin. New accepts are closed while +`GET /readyz` returns 503 (`approval_path_not_connected`) because +audit-core is not Ready and its Service has no ready endpoints. Compact +memos still have `approval_id: null`. Operator packet: +`docs/batches/2026-09-14/OPERATOR.md`. Preflight: +`tools/sitting_bind_preflight.py`. diff --git a/tests/test_sitting_bind_preflight.py b/tests/test_sitting_bind_preflight.py new file mode 100644 index 0000000..743471a --- /dev/null +++ b/tests/test_sitting_bind_preflight.py @@ -0,0 +1,78 @@ +import importlib.util +from io import BytesIO +from pathlib import Path + +spec = importlib.util.spec_from_file_location( + "sitting_bind_preflight", + Path(__file__).resolve().parents[1] / "tools" / "sitting_bind_preflight.py", +) +preflight = importlib.util.module_from_spec(spec) +spec.loader.exec_module(preflight) +evaluate, load_sitting, probe_origin = preflight.evaluate, preflight.load_sitting, preflight.probe_origin + + +class _Resp: + def __init__(self, status, body=b"", headers=None): + self.status = status + self.headers = headers or {} + self._body = BytesIO(body) + + def getcode(self): + return self.status + + def read(self, n=-1): + return self._body.read(n) + + def __enter__(self): + return self + + def __exit__(self, *exc): + return False + + +def test_unsigned_sitting_is_not_ready_to_sit(): + report = evaluate(load_sitting()) + assert report["ready_to_sit"] is False + assert report["agent_disposition"] == "forbidden" + assert report["memo_count"] == 8 + assert "missing_act_binding" in report["gates"] + assert "principal_not_live_subject" in report["gates"] + assert len(report["missing_act_binding"]) == 8 + assert report["live_accept"] is None + + +def test_origin_readyz_503_is_live_accept_closed(): + bodies = { + "/healthz": _Resp(200, b'{"status":"ok"}'), + "/readyz": _Resp(503, b'{"status":"incomplete","reason":"approval_path_not_connected"}'), + "/auth/start": _Resp(303, b"", {"Location": "https://kc.coulomb.social/authorize?client_id=informed-decision-approver"}), + } + + def opener(req, timeout=10): + return bodies[req.full_url.split("https://decisions.coulomb.social", 1)[1]] + + probe = probe_origin("https://decisions.coulomb.social", opener=opener) + report = evaluate(load_sitting(), probe) + assert report["live_accept"] == "closed" + assert "live_accept_closed" in report["gates"] + assert report["ready_to_sit"] is False + assert probe["auth_start"]["http"] == 303 + assert probe["auth_start"]["location_host"] == "kc.coulomb.social" + assert "body" not in probe["auth_start"] + + +def test_origin_ready_still_blocked_on_unsigned_memos(): + bodies = { + "/healthz": _Resp(200, b'{"status":"ok"}'), + "/readyz": _Resp(200, b'{"status":"ready"}'), + "/auth/start": _Resp(303, b"", {"Location": "https://kc.example/authorize"}), + } + + def opener(req, timeout=10): + return bodies[req.full_url.split("https://origin.example", 1)[1]] + + report = evaluate(load_sitting(), probe_origin("https://origin.example", opener=opener)) + assert report["live_accept"] == "open" + assert "live_accept_closed" not in report["gates"] + assert report["ready_to_sit"] is False + assert "missing_act_binding" in report["gates"] diff --git a/tools/sitting_bind_preflight.py b/tools/sitting_bind_preflight.py new file mode 100644 index 0000000..30a928e --- /dev/null +++ b/tools/sitting_bind_preflight.py @@ -0,0 +1,146 @@ +"""INFD-WP-0002-T03 sitting bind preflight. + +Reports whether the compact sitting can be submitted to the live Stage 1 +surface. Does not present, acknowledge, bind, or create approvals. +""" +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from urllib.error import HTTPError, URLError +from urllib.request import HTTPRedirectHandler, Request, build_opener, urlopen + +from informed_decision.records import memo_from + +ROOT = Path(__file__).resolve().parents[1] / "docs" / "batches" / "2026-09-14" + + +def load_sitting(root: Path = ROOT) -> dict: + sitting = json.loads((root / "sitting.json").read_text()) + memos = [] + for name in ("credentials", "decisions"): + index = json.loads((root / name / "index.json").read_text()) + for row in index["ordinal"]: + memo = memo_from(json.loads((root / name / row["memo"]).read_text())) + memos.append( + { + "batch": name, + "memo_id": memo.id, + "approval_id": memo.approval_id, + "principal": memo.binding.principal.id, + "question": memo.question, + } + ) + return {"sitting": sitting, "memos": memos} + + +class _NoRedirect(HTTPRedirectHandler): + def http_error_302(self, req, fp, code, msg, headers): + raise HTTPError(req.full_url, code, msg, headers, fp) + + http_error_301 = http_error_303 = http_error_307 = http_error_308 = http_error_302 + + +def _default_opener(): + return build_opener(_NoRedirect()).open + + +def probe_origin(origin: str, opener=None) -> dict: + origin = origin.rstrip("/") + fetch = opener or _default_opener() + result = {"origin": origin, "healthz": None, "readyz": None, "auth_start": None} + + def get(path): + req = Request(origin + path, method="GET") + try: + with fetch(req, timeout=10) as response: + return _probe_row(response, getattr(response, "status", None) or response.getcode()) + except HTTPError as exc: + return _probe_row(exc, exc.code) + except (URLError, TimeoutError, OSError) as exc: + return {"http": None, "error": type(exc).__name__} + + result["healthz"] = get("/healthz") + result["readyz"] = get("/readyz") + result["auth_start"] = get("/auth/start") + return result + + +def _probe_row(response, status): + headers = getattr(response, "headers", None) + location = headers.get("Location") if headers else None + raw = response.read(4096) + try: + body = json.loads(raw.decode("utf-8")) if raw else None + except (UnicodeDecodeError, json.JSONDecodeError): + body = None + row = {"http": status} + if location: + row["location_host"] = location.split("/")[2] if "://" in location else location + if isinstance(body, dict): + row["status"] = body.get("status") + if "reason" in body: + row["reason"] = body.get("reason") + return row + + +def evaluate(bundle: dict, origin_probe: dict | None = None) -> dict: + sitting = bundle["sitting"] + memos = bundle["memos"] + missing_act = [row["memo_id"] for row in memos if not row["approval_id"]] + pending_principal = [row["memo_id"] for row in memos if row["principal"] == "pending-human-session"] + gates = [] + if sitting.get("status") != "draft-unsigned" or sitting.get("submitted") is True: + gates.append("sitting_already_submitted") + if len(memos) != 8: + gates.append("unexpected_memo_count") + if missing_act: + gates.append("missing_act_binding") + if pending_principal: + gates.append("principal_not_live_subject") + live_accept = None + if origin_probe is not None: + health = (origin_probe.get("healthz") or {}).get("http") + ready = (origin_probe.get("readyz") or {}).get("http") + start = (origin_probe.get("auth_start") or {}).get("http") + if health != 200: + gates.append("origin_healthz_not_ok") + if start not in (302, 303): + gates.append("auth_start_not_redirect") + if ready != 200: + gates.append("live_accept_closed") + live_accept = "closed" + else: + live_accept = "open" + ready_to_sit = not gates + return { + "kind": "informed-decision-sitting-preflight", + "sitting_id": sitting.get("id"), + "memo_count": len(memos), + "missing_act_binding": missing_act, + "principal_not_live_subject": pending_principal, + "live_accept": live_accept, + "gates": gates, + "ready_to_sit": ready_to_sit, + "agent_disposition": "forbidden", + "origin_probe": origin_probe, + } + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--origin", help="Probe this Stage 1 origin; omit for file checks only") + parser.add_argument("--receipt", type=Path, help="Write the JSON report") + args = parser.parse_args() + probe = probe_origin(args.origin) if args.origin else None + report = evaluate(load_sitting(), probe) + text = json.dumps(report, indent=2, ensure_ascii=False) + "\n" + if args.receipt: + args.receipt.write_text(text, encoding="utf-8") + print(text, end="") + return 0 if report["ready_to_sit"] else 2 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/workplans/INFD-WP-0002-compact-signoff-batches.md b/workplans/INFD-WP-0002-compact-signoff-batches.md index 899bf1a..a507742 100644 --- a/workplans/INFD-WP-0002-compact-signoff-batches.md +++ b/workplans/INFD-WP-0002-compact-signoff-batches.md @@ -116,6 +116,21 @@ by a human through this surface, reconstructable from stored presentation, and the blocking hub tasks are updated from those dispositions rather than from chat. +2026-09-14 — **T08 bind path historically proven; this sitting still +cannot run.** Probe +`docs/evidence/2026-09-14-infd-0002-t03-bind-path-probe.json`. +The live store already holds three `accept` dispositions with confirmed +engine submissions for `SECRETS-WP-0010-T03-{apply,verify,exec}` +(presentations + required acks). That is not this batch. Compact memos +remain `approval_id: null` / `pending-human-session` and are not in the +store. New accept is refused: origin `/readyz` 503 +`approval_path_not_connected` because `audit-core` is not Ready, its +Service has no ready endpoints, and the review pod gets connection +refused talking to the audit ClusterIP. Operator packet +`docs/batches/2026-09-14/OPERATOR.md`; preflight +`tools/sitting_bind_preflight.py`. No agent disposition. Task stays +`wait`. + ## Feed outcomes back to State Hub without hub-authoring ```task