diff --git a/docs/batches/2026-09-14/OPERATOR.md b/docs/batches/2026-09-14/OPERATOR.md index a6f54c8..0478c3d 100644 --- a/docs/batches/2026-09-14/OPERATOR.md +++ b/docs/batches/2026-09-14/OPERATOR.md @@ -10,27 +10,33 @@ 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. +for `SECRETS-WP-0010-T03-{apply,verify,exec}`. That is T08 history, not this +batch. `infd-20260914-c01` must not reuse those approval ids. -Those three memos are **not** this sitting. `infd-20260914-c01` asks a different -question (admit native delivery) and must not reuse those approval ids. +## Live accept reopened (2026-09-14 22:16 UTC) -## What is closed right now (2026-09-14 16:56 UTC) +audit-core `/readyz` was hanging; the Service had no ready endpoints; this +origin refused accept (`/readyz` 503). audit-core `b0e6792` rolled out. Now: -Live accept is refused: +- Origin `/healthz` 200, `/readyz` **200 ready**, `/auth/start` 303 to KeyCape +- Review pod reaches `audit-core.audit-core.svc:8080` healthz/readyz 200 +- Preflight: `live_accept=open`, still not `ready_to_sit` -- 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-accept-reopened.json`. -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. +## What still blocks this sitting + +1. **Flex Auth** still admits only the three T03 memo ids + (`FLEX-WP-0027`, `examples/informed-decision-t03`). Compact ids would be + denied. Request shape: `policy-request.md`. Do not expand the T03 mandate + in place. +2. **No `approval:create` requester** whose `binding.actor` matches these + acts. Do **not** reuse `secrets-engine-requester` for WSL2, clock + ownership, mason plan, or warden seal. +3. Drafts still have `approval_id: null` and principal + `pending-human-session`. Live review refuses `missing_act_binding` / + `wrong_recipient`. +4. A human must bind. An agent must not. ## Eight unsigned memos (this sitting) @@ -47,28 +53,18 @@ Open in this order. One question each. No approve-all. | 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 requester, receipts, and policy admission -## 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 +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. +3. `uv run python tools/attach_compact_bindings.py --principal --receipt ` + writes `bound/` copies. Unsigned drafts stay unsigned. +4. Flex Auth admits a **new** package pinning those eight `memo:` ids to the + native approval id/digest, same identity bar as T03, deny everything else. +5. Load packet bytes then `save_memo` from `bound/` into the review store. +6. Sign in. Open `/review?memo_id=infd-20260914-c01` through `…-d04` in order. + Acknowledge the required highlight. Bind that memo. Repeat. +7. 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 e2a8962..403973e 100644 --- a/docs/batches/2026-09-14/README.md +++ b/docs/batches/2026-09-14/README.md @@ -22,6 +22,7 @@ Preflight: `uv run python tools/sitting_bind_preflight.py --origin https://decis 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. +Those memos are not this sitting. Live accept reopened 2026-09-14 22:16 UTC +(`/readyz` 200). This sitting stays `draft-unsigned` until native receipts, +a live KeyCape subject, a new Flex Auth package, and a human bind. +Attach (does not bind): `uv run python tools/attach_compact_bindings.py`. diff --git a/docs/batches/2026-09-14/policy-request.md b/docs/batches/2026-09-14/policy-request.md new file mode 100644 index 0000000..dc169e3 --- /dev/null +++ b/docs/batches/2026-09-14/policy-request.md @@ -0,0 +1,37 @@ +# Flex Auth policy request — compact sitting + +Not admitted. Not a local allow. Not an expansion of the T03 three-record +mandate (`FLEX-WP-0027`, `examples/informed-decision-t03`). + +`net-kingdom-admins` may review these eight Decision Memos **only after** +the operator admits a new package that pins exact `memo_id`, `approval_id` +and native `binding.digest`. Until those approval objects exist, this file +is a request shape, not a compilable package. + +## Intended allow (same identity bar as T03) + +- caller: `system:serviceaccount:informed-decision:review` via TokenReview +- subject: verified human, `tenant:platform`, group `net-kingdom-admins`, + KeyCape AAL2 MFA facts as in the T03 package +- actions: `read`, `acknowledge`, `accept`, `return`, `discuss`, `decline` +- deny every other resource id +- no consume, no approval create, no presentation claim as policy input + +## Exact resource ids (approval ids still unknown) + +| resource.id | Blocking record | +| --- | --- | +| `memo:infd-20260914-c01` | SECRETS-WP-0010 native delivery | +| `memo:infd-20260914-c02` | RPF-WP-0035-T02 | +| `memo:infd-20260914-c03` | NK-WP-0032-T03 | +| `memo:infd-20260914-c04` | WARDEN-WP-0027-T02 | +| `memo:infd-20260914-d01` | CUST-WP-0038-T08 | +| `memo:infd-20260914-d02` | HFACT-WP-0001-T03 | +| `memo:infd-20260914-d03` | MASON-WP-0005 plan | +| `memo:infd-20260914-d04` | RCLK-WP-0002-T01 | + +Do not copy T03 approval ids into this table. Do not serve this list as +policy until a created-receipt supplies `approval_id` and `binding.digest` +for every row. + +Owner: flex-auth. This repository drafts; it does not evaluate authorization. diff --git a/docs/batches/2026-09-14/sitting.json b/docs/batches/2026-09-14/sitting.json index c48103d..57f8e49 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 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." + "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." } diff --git a/docs/evidence/2026-09-14-infd-0002-t03-accept-reopened.json b/docs/evidence/2026-09-14-infd-0002-t03-accept-reopened.json new file mode 100644 index 0000000..8963b33 --- /dev/null +++ b/docs/evidence/2026-09-14-infd-0002-t03-accept-reopened.json @@ -0,0 +1,51 @@ +{ + "kind": "infd-wp-0002-t03-bind-path-probe", + "observed_at": "2026-09-14T22:16:07Z", + "workplan": "INFD-WP-0002", + "task": "INFD-WP-0002-T03", + "scope": "Re-probe after audit-core /readyz fix rollout. No human login, no presentation, no disposition, no approval create, no secret read.", + "precedes": "docs/evidence/2026-09-14-infd-0002-t03-bind-path-probe.json", + "origin": { + "url": "https://decisions.coulomb.social/", + "healthz": {"http": 200, "status": "ok"}, + "readyz": {"http": 200, "status": "ready"}, + "auth_start": {"http": 303, "location_host": "kc.coulomb.social"} + }, + "cluster": { + "audit-core": { + "pod": "audit-core-6f6545dcf5-xklmd", + "ready": true, + "age": "73m at probe", + "classic_endpoints": "10.42.0.4:8080", + "from_informed_decision_pod": { + "healthz": 200, + "readyz": 200 + } + }, + "informed-decision": { + "pod": "informed-decision-b7f6fc6c9-kd4bl", + "ready": true + }, + "approval-engine": { + "pod": "approval-engine-0", + "ready": true, + "in_cluster_healthz": 200 + } + }, + "preflight": { + "live_accept": "open", + "ready_to_sit": false, + "gates": ["missing_act_binding", "principal_not_live_subject"], + "agent_disposition": "forbidden" + }, + "remaining_external_gates": [ + "flex-auth T03 package still admits only memo:SECRETS-WP-0010-T03-{apply,verify,exec}", + "no approval:create requester whose actor matches these eight acts; secrets-engine-requester must not be reused for unrelated acts", + "unsigned drafts still have approval_id null and principal pending-human-session", + "human bind still required; agent may not accept" + ], + "audit_core_ack": { + "message_id": "676f123d-ea3d-47f0-bf86-568635c4b3e5", + "fix": "audit-core b0e6792 ADHOC-2026-09-14-T01; live rollout observed ready" + } +} diff --git a/docs/specs/CompactSignoffBatches.md b/docs/specs/CompactSignoffBatches.md index 4024db5..2a42212 100644 --- a/docs/specs/CompactSignoffBatches.md +++ b/docs/specs/CompactSignoffBatches.md @@ -52,9 +52,12 @@ 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`. +(`SECRETS-WP-0010-T03-*`) on this origin. New accepts were closed while +`GET /readyz` returned 503 because audit-core had no ready endpoints. + +2026-09-14 22:16 UTC: live accept reopened (`/readyz` 200) after the +audit-core `/readyz` fix rolled out. Compact memos still have +`approval_id: null`. Flex Auth still admits only the three T03 records. +Operator packet: `docs/batches/2026-09-14/OPERATOR.md`. Preflight: +`tools/sitting_bind_preflight.py`. Attach (no bind): +`tools/attach_compact_bindings.py`. diff --git a/tests/test_attach_compact_bindings.py b/tests/test_attach_compact_bindings.py new file mode 100644 index 0000000..ada03ca --- /dev/null +++ b/tests/test_attach_compact_bindings.py @@ -0,0 +1,97 @@ +import json +from pathlib import Path + +import pytest + +from informed_decision.records import memo_from + +import importlib.util + +spec = importlib.util.spec_from_file_location( + "attach_compact_bindings", + Path(__file__).resolve().parents[1] / "tools" / "attach_compact_bindings.py", +) +attach_mod = importlib.util.module_from_spec(spec) +spec.loader.exec_module(attach_mod) + + +def _receipt(memo_ids, *, human_control=True, entries=None, status="requested"): + requests = [] + for i, memo_id in enumerate(memo_ids, start=1): + digest = "sha256:" + f"{i:064x}" + requests.append( + { + "memo_id": memo_id, + "approval": { + "id": f"approval-{i:02d}", + "status": status, + "entries": entries or [], + "binding": {"digest": digest, "human_control": human_control}, + }, + } + ) + return {"status": "created", "requests": requests} + + +def _memo_ids(root): + ids = [] + for name in ("credentials", "decisions"): + index = json.loads((root / name / "index.json").read_text()) + ids.extend(row["memo_id"] for row in index["ordinal"]) + return ids + + +def test_attach_writes_bound_copies_and_leaves_drafts_unsigned(tmp_path): + src = Path(__file__).resolve().parents[1] / "docs" / "batches" / "2026-09-14" + root = tmp_path / "sitting" + for name in ("credentials", "decisions"): + (root / name).mkdir(parents=True) + for item in (src / name).iterdir(): + (root / name / item.name).write_bytes(item.read_bytes()) + (root / "sitting.json").write_bytes((src / "sitting.json").read_bytes()) + ids = _memo_ids(root) + index = attach_mod.attach("human-subject-1", _receipt(ids), root) + assert index["principal"] == "human-subject-1" + assert len(index["memos"]) == 8 + draft = memo_from(json.loads((root / "credentials" / "infd-20260914-c01.memo.json").read_text())) + assert draft.approval_id is None + assert draft.binding.principal.id == "pending-human-session" + bound = memo_from(json.loads((root / "bound" / "infd-20260914-c01.memo.json").read_text())) + assert bound.approval_id == "approval-01" + assert bound.approval_binding_digest.startswith("sha256:") + assert bound.binding.principal.id == "human-subject-1" + assert bound.question == draft.question + + +@pytest.mark.parametrize( + "fault", + ["pending-human-session", "has space", "", "not-created", "wrong-count", "has-entry", "no-control"], +) +def test_attach_refuses_unsafe_inputs(tmp_path, fault): + src = Path(__file__).resolve().parents[1] / "docs" / "batches" / "2026-09-14" + root = tmp_path / "sitting" + for name in ("credentials", "decisions"): + (root / name).mkdir(parents=True) + for item in (src / name).iterdir(): + (root / name / item.name).write_bytes(item.read_bytes()) + (root / "sitting.json").write_bytes((src / "sitting.json").read_bytes()) + ids = _memo_ids(root) + principal = "human-subject-1" + receipt = _receipt(ids) + if fault == "pending-human-session": + principal = "pending-human-session" + elif fault == "has space": + principal = "human subject" + elif fault == "": + principal = "" + elif fault == "not-created": + receipt["status"] = "draft" + elif fault == "wrong-count": + receipt["requests"] = receipt["requests"][:7] + elif fault == "has-entry": + receipt = _receipt(ids, entries=[{"subject_id": "someone"}]) + elif fault == "no-control": + receipt = _receipt(ids, human_control=False) + with pytest.raises(ValueError): + attach_mod.attach(principal, receipt, root) + assert not (root / "bound").exists() diff --git a/tools/attach_compact_bindings.py b/tools/attach_compact_bindings.py new file mode 100644 index 0000000..33a74fa --- /dev/null +++ b/tools/attach_compact_bindings.py @@ -0,0 +1,118 @@ +"""Attach native approval ids to compact-sitting drafts (INFD-WP-0002-T03). + +Requires an exact live KeyCape subject and eight unapproved human-control +receipts. Writes bound memo copies; does not present, bind, create approvals, +or load the live store. +""" +from __future__ import annotations + +import argparse +import json +import re +from dataclasses import replace +from pathlib import Path + +from informed_decision.memo import Principal +from informed_decision.records import dumps, memo_from + +ROOT = Path(__file__).resolve().parents[1] / "docs" / "batches" / "2026-09-14" +DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$") +APPROVAL_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$") + + +def _principal(value: str) -> Principal: + if not value or any(c.isspace() for c in value) or len(value) > 256: + raise ValueError("exact human subject required") + if value == "pending-human-session": + raise ValueError("live key-cape subject required") + return Principal(id=value, kind="person", display_name=value, role="reviewer") + + +def _approval(row: dict, memo_id: str) -> dict: + if row.get("memo_id") != memo_id: + raise ValueError(f"receipt memo_id mismatch for {memo_id}") + approval = row.get("approval") or {} + binding = approval.get("binding") or {} + digest = binding.get("digest") + if approval.get("status") != "requested" or approval.get("entries"): + raise ValueError(f"unapproved human-control request required for {memo_id}") + if binding.get("human_control") is not True: + raise ValueError(f"human_control required for {memo_id}") + if not isinstance(approval.get("id"), str) or not APPROVAL_ID.fullmatch(approval["id"]): + raise ValueError(f"native approval id required for {memo_id}") + if not isinstance(digest, str) or not DIGEST.fullmatch(digest): + raise ValueError(f"native binding digest required for {memo_id}") + return approval + + +def attach(principal: str, receipt: dict, root: Path = ROOT) -> dict: + actor = _principal(principal) + if receipt.get("status") != "created": + raise ValueError("created native receipt required") + sitting = json.loads((root / "sitting.json").read_text()) + expected = [] + for name in ("credentials", "decisions"): + index = json.loads((root / name / "index.json").read_text()) + for row in index["ordinal"]: + expected.append((name, row["memo_id"], row["memo"], row["packet"])) + requests = receipt.get("requests") or [] + if len(requests) != len(expected): + raise ValueError("receipt must cover every sitting memo once") + by_id = {row.get("memo_id"): row for row in requests} + if set(by_id) != {memo_id for _, memo_id, _, _ in expected}: + raise ValueError("receipt memo set must match the sitting") + prepared = [] + for name, memo_id, memo_name, packet_name in expected: + approval = _approval(by_id[memo_id], memo_id) + memo = memo_from(json.loads((root / name / memo_name).read_text())) + if memo.approval_id is not None or memo.binding.principal.id != "pending-human-session": + raise ValueError(f"unsigned draft required for {memo_id}") + bound = replace( + memo, + binding=replace(memo.binding, principal=actor), + approval_id=approval["id"], + approval_binding_digest=approval["binding"]["digest"], + ) + prepared.append((name, memo_id, memo_name, packet_name, bound)) + bound_root = root / "bound" + bound_root.mkdir(parents=True, exist_ok=True) + written = [] + for name, memo_id, memo_name, packet_name, bound in prepared: + path = bound_root / memo_name + path.write_text(dumps(bound) + "\n", encoding="utf-8") + packet_src = root / name / packet_name + packet_dst = bound_root / packet_name + packet_dst.write_bytes(packet_src.read_bytes()) + written.append( + { + "memo_id": memo_id, + "approval_id": bound.approval_id, + "memo": path.name, + "packet": packet_dst.name, + } + ) + index = { + "kind": "informed-decision-bound-sitting", + "sitting_id": sitting["id"], + "principal": actor.id, + "submitted": False, + "agent_disposition_forbidden": True, + "memos": written, + } + (bound_root / "index.json").write_text(json.dumps(index, indent=2, ensure_ascii=False) + "\n") + return index + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--principal", required=True, help="exact live KeyCape subject") + parser.add_argument("--receipt", type=Path, required=True, help="native created-approval receipt") + args = parser.parse_args() + receipt = json.loads(args.receipt.read_text()) + index = attach(args.principal, receipt) + print(json.dumps({"status": "bound_copies_written", "count": len(index["memos"])}, indent=2)) + print("No presentations, dispositions, or approval entries created.") + + +if __name__ == "__main__": + main() diff --git a/workplans/INFD-WP-0002-compact-signoff-batches.md b/workplans/INFD-WP-0002-compact-signoff-batches.md index a507742..8876219 100644 --- a/workplans/INFD-WP-0002-compact-signoff-batches.md +++ b/workplans/INFD-WP-0002-compact-signoff-batches.md @@ -11,7 +11,7 @@ flavor: planning depends_on: - INFD-WP-0001 created: "2026-09-14" -updated: "2026-09-14" +updated: "2026-09-15" related: - INFD-WP-0001 - STATE-WP-0092 @@ -131,6 +131,19 @@ refused talking to the audit ClusterIP. Operator packet `tools/sitting_bind_preflight.py`. No agent disposition. Task stays `wait`. +2026-09-14 22:16 UTC — **live accept reopened; sitting still not +admitted.** audit-core `b0e6792` is Ready; origin `/readyz` 200; +preflight `live_accept=open`. Remaining gates: (1) Flex Auth T03 +package still allows only `memo:SECRETS-WP-0010-T03-*` — +`docs/batches/2026-09-14/policy-request.md` is a request, not an +admission; (2) no `approval:create` requester for these eight acts; +(3) drafts still `approval_id: null` / `pending-human-session`; +(4) human bind. Attach tool `tools/attach_compact_bindings.py` writes +bound copies from a created receipt and live subject; it does not +create approvals or dispositions. Evidence: +`docs/evidence/2026-09-14-infd-0002-t03-accept-reopened.json`. +Task stays `wait`. + ## Feed outcomes back to State Hub without hub-authoring ```task