From e6252bc0e474607c37ae9167e0ef8b8361ad6797 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 1 Nov 2026 00:39:17 +0100 Subject: [PATCH] NK-WP-0056-T21 done: request phase presents the approval through informed-decision admission_memo composes the present_existing envelope (admission YAML as packet, three required highlights, intent = binding without actor, key nk-bind-executor.); InformedDecision.submit posts it with the projected intake token and checks the receipt. Exit 7 when the memo is refused (approval stays usable). Envelope validated by informed-decision's own validate_submission with the present_existing profile. Service-identity declaration notes key-cape's prepared approval:read. 61 executor tests. Co-Authored-By: Claude Opus 5.5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 5502@bnt-lap001 Assistant-Session: 68b3b02f-5d0f-4e71-8421-796f62e50196 --- WORK-RECORDS.md | 2 +- bounded-executor/cli.py | 85 ++++++++++++++++++- bounded-executor/clients.py | 18 ++++ .../tests/test_approval_request.py | 66 ++++++++++++++ .../declarations/keycape.yaml | 2 +- workplans/NK-WP-0056-bounded-executor-path.md | 31 ++++++- 6 files changed, 200 insertions(+), 4 deletions(-) diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 5b9ceaa..10b538e 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -363,7 +363,7 @@ | task | NK-WP-0056-T18 | done | — | workplans/NK-WP-0056-bounded-executor-path.md | | task | NK-WP-0056-T19 | todo | — | workplans/NK-WP-0056-bounded-executor-path.md | | task | NK-WP-0056-T20 | done | — | workplans/NK-WP-0056-bounded-executor-path.md | -| task | NK-WP-0056-T21 | todo | — | workplans/NK-WP-0056-bounded-executor-path.md | +| task | NK-WP-0056-T21 | done | — | workplans/NK-WP-0056-bounded-executor-path.md | | intake | NK-IN-0001 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md | | intake | NK-IN-0002 | closed | blue | docs/intakes/activity-core-ops-sso-operators.md | | intake | NET-IN-0001 | open | — | intakes/intakes.md | diff --git a/bounded-executor/cli.py b/bounded-executor/cli.py index 6ba704a..8f87fb4 100644 --- a/bounded-executor/cli.py +++ b/bounded-executor/cli.py @@ -25,7 +25,9 @@ Request (NK-WP-0056-T18): file the approval for one admission as the executor it binding.actor == the token's sub). Binding: action bounded-execution, actor = executor_identity, principal = the first listed approver's subject, target {admission_id, admission_digest}, validity = the admission window, human_control on, required_count = number of listed approvers. Prints the approval id; consumes nothing. -Exit codes: 0 requested, 4 refused. +With informed_decision{url, token_file} configured (T21), the approval is then presented to the founder: one memo +through informed-decision's present_existing intake, carrying the admission YAML. Exit codes: 0 requested (and +presented, if configured), 4 refused, 7 requested but the memo was not accepted (resubmitting is idempotent). approval_engine.client{token_url, client_id, k8s_role, kv, host?} selects the executor's own KeyCape client (SecurityCaseType oidc-service-client, case nk-bind-executor-approval): the client secret is read once through its @@ -151,6 +153,76 @@ def request_binding(doc: dict, cfg: dict) -> tuple[dict, dict, int]: return binding, validity, len(approvers) +MEMO_CLASS = "bounded-execution" +REVIEW_GROUP = "net-kingdom-admins" + + +def admission_memo(doc: dict, admission_text: str, binding: dict, approval: dict) -> dict: + """The informed-decision intake envelope that presents this executor-filed approval (present_existing profile, + informed-decision docs/decision-services-alignment-answer.md S1). The packet carries the admission YAML itself; + required highlights point at the scope ceiling, the window and budget, and the revert. The intent restates the + approval's binding without the actor (informed-decision takes the actor from its registry).""" + spec = doc["spec"] + approval_id = approval["id"] + memo_id = (spec["id"].lower() + "-" + approval_id.split("-")[0])[:63] + item = memo_id + "-admission" + digest = "sha256:" + X.hashlib.sha256(admission_text.encode("utf-8")).hexdigest() + ceiling = "; ".join(f"{c['mode']} {c['scope']}" for c in spec["scope_ceiling"]) + budget = spec["budget"] + + def highlight(n, severity, section, note): + return {"id": f"{memo_id}-h{n}", "item_id": item, "severity": severity, "required_ack": True, + "locator": {"kind": "admission", "admission_id": spec["id"], "section": section}, "note": note} + + return { + "kind": "informed-decision.memo-intake", + "version": 1, + "idempotency_key": f"nk-bind-executor.{approval_id}", + "memo_class": MEMO_CLASS, + "review_group": REVIEW_GROUP, + "drafted_by": "net-kingdom bounded executor (service:nk-bind-executor)", + "approval_intent": {k: binding[k] for k in ("action", "principal", "purpose", "target")}, + "memo": { + "id": memo_id, + "version": 1, + "question": f"Admit {spec['id']}: {spec['title']}?"[:400], + "requested_act": "approve", + "binding_level": "organizational", + "brief": (f"Bounded Execution Admission {spec['id']} (risk {spec['risk_class']}). " + f"Executor {spec['executor']['identity']} may act once, inside " + f"{spec['window']['not_before']} .. {spec['window']['not_after']}, within: {ceiling}. " + f"Budget {budget['wall_time_seconds']} s, {budget['attempts']} attempt(s). " + f"Purpose: {spec['purpose']}")[:20000], + "binding": { + "justification": None, + "principal": {"display_name": f"Named reviewer in {REVIEW_GROUP}", "id": "pending-human-session", + "identifiers": [], "kind": "person", "role": "approver"}, + "target": {"environment": "prod", "id": spec["id"], "kind": "bounded-execution-admission", + "label": spec["title"][:200], "requires_new_bind": False}, + "terms": None, + }, + "step_kind": "approve", + "packet": [{"item_id": item, "label": f"Admission {spec['id']} (YAML)", "hash": digest}], + "highlights": [ + highlight(1, "critical", "scope_ceiling", + "Approving lets the executor act once, unattended, within exactly this scope ceiling: " + + ceiling), + highlight(2, "warning", "window_budget", + f"Window {spec['window']['not_before']} .. {spec['window']['not_after']}; budget " + f"{budget['wall_time_seconds']} s, {budget['attempts']} attempt(s); the executor stops " + "and reverts when a check fails or the budget is exhausted."), + highlight(3, "warning", "plan_revert", + "Each mutating step names its own checks and revert; read the plan before approving."), + ], + "locale": "en", + "approval_id": approval_id, + "approval_binding_digest": approval["binding"]["digest"], + "sealed": False, + }, + "packet": [{"item_id": item, "media_type": "text/plain", "content": admission_text}], + } + + def run_request(admission_path: str, cfg: dict) -> tuple[dict, int]: logins: list = [] try: @@ -168,6 +240,17 @@ def run_request(admission_path: str, cfg: dict) -> tuple[dict, int]: "admission_digest": binding["target"]["admission_digest"], "required_count": required, "effects": "approval requested"} code = 0 + if "informed_decision" in cfg: # present it to the founder (T21); the approval stays usable either way + idd = cfg["informed_decision"] + envelope = admission_memo(doc, pathlib.Path(admission_path).read_text(), binding, created) + try: + receipt = C.InformedDecision(idd["url"], idd["token_file"], idd.get("ca_file")).submit(envelope) + result.update(outcome="presented", memo_id=receipt["memo_id"], + effects="approval requested; memo submitted to informed-decision") + except Exception as error: # the approval exists; the memo can be resubmitted with the same key + result.update(outcome="requested_not_presented", memo_id=envelope["memo"]["id"], + reason="memo:" + type(error).__name__) + code = 7 except Exception as error: # nothing was consumed; at most an unapproved request exists result = {"mode": "request", "outcome": "refused", "reason": "request:" + type(error).__name__} code = 4 diff --git a/bounded-executor/clients.py b/bounded-executor/clients.py index 3185a4d..78b7e6e 100644 --- a/bounded-executor/clients.py +++ b/bounded-executor/clients.py @@ -142,6 +142,24 @@ class ApprovalEngine: scope="approval:consume")[1] +class InformedDecision: + """POST /v1/memos on informed-decision's private workload listener (informed-decision INFD-WP-0010, + present_existing). The bearer is the executor's projected ServiceAccount token, audience informed-decision-intake; + informed-decision checks it with TokenReview and joins it to service:nk-bind-executor in its registry.""" + + def __init__(self, base: str, token_file: str, ca_file: str | None = None): + self.http = _Http(base, token_file, ca_file) + + def submit(self, envelope: dict) -> dict: + status, receipt = self.http.call("POST", "/v1/memos", envelope) + memo = envelope["memo"] + if status not in (200, 201) or receipt.get("memo_id") != memo["id"] \ + or receipt.get("approval_id") != memo["approval_id"] \ + or receipt.get("approval_binding_digest") != memo["approval_binding_digest"]: + raise ClientError(f"memo receipt {status}") + return receipt + + class AuditCore: """POST /v1/events with Idempotency-Key = event id (audit-core docs/event-envelope.md).""" diff --git a/bounded-executor/tests/test_approval_request.py b/bounded-executor/tests/test_approval_request.py index d62470e..060073c 100644 --- a/bounded-executor/tests/test_approval_request.py +++ b/bounded-executor/tests/test_approval_request.py @@ -47,6 +47,8 @@ class World: self.create_status = 201 self.create_override = {} self.kv = {"CLIENT_SECRET": SECRET} + self.memos = [] + self.memo_status = 201 def serve(w: World): @@ -89,6 +91,14 @@ def serve(w: World): scope = urllib.parse.parse_qs(raw.decode())["scope"][0] return self._send(200, w.token_body or {"access_token": "at-" + scope.replace(":", "-"), "token_type": "Bearer", "expires_in": 900}) + if self.path == "/v1/memos": + env = json.loads(raw) + w.memos.append((dict(self.headers), env)) + if w.memo_status != 201: + return self._send(w.memo_status, {"error": "approval_not_requested"}) + m = env["memo"] + return self._send(201, {"memo_id": m["id"], "approval_id": m["approval_id"], + "approval_binding_digest": m["approval_binding_digest"]}) if self.path == "/v1/approvals": body = json.loads(raw) created = {"id": "apr-1", "status": "requested", @@ -185,6 +195,62 @@ class ScopesPerCall(Base): C.ApprovalEngine(self.addr, token_file="/x", token_provider=lambda s: "t") +INFD = pathlib.Path("/home/worsch/informed-decision") + + +class PresentToFounder(Base): + """T21: after creating the approval the executor presents it through informed-decision (present_existing).""" + + def run_cli(self, cfg): + (self.root / "cfg.json").write_text(json.dumps(cfg)) + (self.root / "intake-token").write_text("synthetic-intake-token\n") + with contextlib.redirect_stdout(io.StringIO()) as out: + code = CLI.main(["--request", "--admission", str(ADMISSION), "--config", str(self.root / "cfg.json")]) + return code, json.loads(out.getvalue()), out.getvalue() + + def cfg(self, **kw): + return {**super().cfg(), "informed_decision": {"url": self.addr, + "token_file": str(self.root / "intake-token")}} + + def test_files_then_presents_with_the_admission_as_packet(self): + code, result, text = self.run_cli(self.cfg()) + self.assertEqual((code, result["outcome"]), (0, "presented")) + headers, env = self.w.memos[0] + self.assertEqual(headers["Authorization"], "Bearer synthetic-intake-token") + self.assertEqual(env["memo"]["approval_id"], "apr-1") + self.assertEqual(env["memo"]["approval_binding_digest"], "sha256:" + "ab" * 32) + self.assertEqual(env["packet"][0]["content"], ADMISSION.read_text()) + body = json.loads(self.calls("/v1/approvals")[0][3]) + self.assertEqual(env["approval_intent"], {k: body["binding"][k] for k in ("action", "principal", "purpose", "target")}) + self.assertNotIn("actor", env["approval_intent"]) + self.assertEqual(result["memo_id"], env["memo"]["id"]) + self.assertNotIn(SECRET, text) + + def test_refused_memo_keeps_the_approval_and_exits_7(self): + self.w.memo_status = 409 + code, result, _ = self.run_cli(self.cfg()) + self.assertEqual((code, result["outcome"], result["approval_id"]), (7, "requested_not_presented", "apr-1")) + + def test_envelope_passes_informed_decisions_own_validator(self): + if not (INFD / "informed_decision" / "intake.py").exists(): + self.skipTest("informed-decision checkout absent") + sys.path.insert(0, str(INFD)) + try: + from informed_decision import intake + finally: + sys.path.remove(str(INFD)) + doc = X.bea.load(ADMISSION) + binding, _, _ = CLI.request_binding(doc, super().cfg()) + approval = {"id": "0f1e2d3c-4b5a-4968-8776-655443322110", "binding": {**binding, "digest": "sha256:" + "b" * 64}} + env = CLI.admission_memo(doc, ADMISSION.read_text(), binding, approval) + grant = intake.SubmitterGrant("nk-bind-executor", frozenset({"bounded-execution"}), + frozenset({"net-kingdom-admins"})) + profile = intake.PresentExisting("service:nk-bind-executor", frozenset({"bounded-execution"})) + validated = intake.validate_submission(env, submitter_id="nk-bind-executor", grant=grant, + present_existing=profile) + self.assertEqual(validated.memo.id, env["memo"]["id"]) + + class RequestPhase(Base): def run_cli(self, cfg, admission=ADMISSION): (self.root / "cfg.json").write_text(json.dumps(cfg)) diff --git a/tools/service-identity/declarations/keycape.yaml b/tools/service-identity/declarations/keycape.yaml index 9b74ee7..c363daa 100644 --- a/tools/service-identity/declarations/keycape.yaml +++ b/tools/service-identity/declarations/keycape.yaml @@ -65,7 +65,7 @@ identities: purpose: "Confidential OIDC client (client_credentials); token scopes limited to its registered scopes" grants: - {mode: USE, scope: keycape, activation: STANDING, environments: [PRODUCTION], note: "client credentials"} - - {mode: USE, scope: "approval-engine: approval:create", activation: STANDING, environments: [PRODUCTION], note: "key-cape a1ab8ff L122-135"} + - {mode: USE, scope: "approval-engine: approval:create", activation: STANDING, environments: [PRODUCTION], note: "key-cape a1ab8ff L122-135; approval:read prepared in key-cape 82b471f (founder decision 2026-11-01, informed-decision present_existing), live only after the attended registry change"} interactive_login: false second_factor: not_applicable custody: [delivered-secret] diff --git a/workplans/NK-WP-0056-bounded-executor-path.md b/workplans/NK-WP-0056-bounded-executor-path.md index d59c2a1..51f4d81 100644 --- a/workplans/NK-WP-0056-bounded-executor-path.md +++ b/workplans/NK-WP-0056-bounded-executor-path.md @@ -953,7 +953,7 @@ Next for the founder: queue memo r2 through the State Hub memo dispatcher and ac ```task id: NK-WP-0056-T21 -status: todo +status: done priority: high assignee: claude-code depends_on: [NK-WP-0056-T18] @@ -979,6 +979,35 @@ Outside net-kingdom: **Done when:** the request phase files the approval and the memo, tested against informed-decision's real intake validator. +**Done 2026-11-01.** After creating the approval, `cli.py --request` composes one present_existing memo: + +- the packet is the admission YAML itself (informed-decision's choice); +- three required highlights cover the scope ceiling, the window and budget, and the plan with its revert; +- the intent restates the binding without the actor; +- the idempotency key is `nk-bind-executor.`. + +It is submitted to informed-decision's private intake with the projected `informed-decision-intake` token. Exit +codes: 0 presented; 7 requested but the memo was refused (the approval stays usable, and resubmitting is idempotent). + +Tests: + +- the envelope passes informed-decision's own `validate_submission` with the present_existing profile; +- the receipt is checked against the memo; +- a refused memo exits 7. + +61 executor tests. + +rapp (T21 commit): the request Job's projected token, the exact config section, and egress to informed-decision:8081. + +The service-identity declaration notes key-cape's prepared `approval:read` for informed-decision's requester. + +Live (founder-run, after the owners' releases): + +- access-engine policy v14; +- key-cape `approval:read`; +- informed-decision's registry row and its ingress for the executor pod; +- the executor image pin. + ## Pilot: unattended bind rotation under one admission ```task