From c8c60ebc215b0ff55193d8e228f248851fe60b81 Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 21 Sep 2026 18:35:49 +0200 Subject: [PATCH] Draft the 2026-09-21 spend-envelope budget memo for SECRETS-WP-0009-T03 One unsigned memo, infd-20260921-b01, accepting the Glas Anthropic spend envelope. The approval-create tool now takes an explicit batch, receipt and expected count; defaults keep the 2026-09-14 sitting unchanged. Co-Authored-By: Claude Opus 5 Assistant: claude-code Assistant-Model: opus Assistant-Process: 272244@bnt-lap001 Assistant-Session: c8962fa7-b290-47df-865f-403ddb6c77e9 --- docs/batches/2026-09-21/OPERATOR.md | 47 +++++ .../2026-09-21/approval-create-intents.json | 28 +++ docs/batches/2026-09-21/budget/index.json | 24 +++ .../budget/infd-20260921-b01.memo.json | 1 + .../budget/infd-20260921-b01.packet.md | 51 +++++ docs/batches/2026-09-21/sitting.json | 12 ++ tools/create_sitting_approvals.py | 16 +- tools/create_sitting_approvals.sh | 2 +- tools/prepare_budget_batch.py | 188 ++++++++++++++++++ 9 files changed, 366 insertions(+), 3 deletions(-) create mode 100644 docs/batches/2026-09-21/OPERATOR.md create mode 100644 docs/batches/2026-09-21/approval-create-intents.json create mode 100644 docs/batches/2026-09-21/budget/index.json create mode 100644 docs/batches/2026-09-21/budget/infd-20260921-b01.memo.json create mode 100644 docs/batches/2026-09-21/budget/infd-20260921-b01.packet.md create mode 100644 docs/batches/2026-09-21/sitting.json create mode 100644 tools/prepare_budget_batch.py diff --git a/docs/batches/2026-09-21/OPERATOR.md b/docs/batches/2026-09-21/OPERATOR.md new file mode 100644 index 0000000..366f69b --- /dev/null +++ b/docs/batches/2026-09-21/OPERATOR.md @@ -0,0 +1,47 @@ +# Budget sitting — operator packet (SECRETS-WP-0009-T03) + +Review group: `net-kingdom-admins`. +Surface: https://decisions.coulomb.social/ +Contract: `docs/specs/CompactSignoffBatches.md`. +Generator: `tools/prepare_budget_batch.py` (re-running rewrites the same bytes). + +An agent may draft, order, and record evidence. It may not `accept` / `decline` / `seal`. + +## One unsigned memo + +| n | memo_id | Blocking record | +| --: | --- | --- | +| 1 | `infd-20260921-b01` | SECRETS-WP-0009-T03 spend envelope (`authority_ref` for HFACT-WP-0001 SpendPolicy) | + +Required acknowledgement: the EUR 100 monthly limit is recorded, not enforced. + +## Steps + +1. **Create the native approval (attended).** Same contained reader as the + 2026-09-14 sitting, with the batch selected explicitly: + + ```bash + tools/create_sitting_approvals.sh --sitting 2026-09-21 \ + --receipt 2026-09-21-budget-approval-create.json --expect 1 + ``` + + Check first without credentials: + `.venv/bin/python -B tools/create_sitting_approvals.py --dry-run --sitting 2026-09-21 --receipt 2026-09-21-budget-approval-create.json --expect 1`. + The receipt at `docs/evidence/2026-09-21-budget-approval-create.json` gives + `approval_id` and the engine-computed binding digest. + +2. **Flex Auth admission as a visible version change.** In + `flex-auth/examples/informed-decision-sitting/policy.md`: `version: v1` → `v2`, + add `"memo:infd-20260921-b01"` to `records` with the receipt's `approval_id`, + `binding_digest`, `label: "b01"`, the binding from + `approval-create-intents.json`, `memo_version: 1`; mirror it in `records.json`. + Keep all seven v1 records. Do not add the record before step 1: an empty + approval id must never be a policy value. Build the image in CI, bump + `values/informed-decision-sitting.yaml` digest, deploy. + +3. **Point Informed Decision at v2.** `deploy/sitting-admission.json`: + `version: v2`, new `package_digest`. Roll out the review service. + +4. **Load and sit.** Attach the live KeyCape subject, load the memo + (`tools/load_sitting_memos.py`), and the human accepts at the surface. + The accepted approval id is recorded as `authority_ref` in the SpendPolicy. diff --git a/docs/batches/2026-09-21/approval-create-intents.json b/docs/batches/2026-09-21/approval-create-intents.json new file mode 100644 index 0000000..93aa7c5 --- /dev/null +++ b/docs/batches/2026-09-21/approval-create-intents.json @@ -0,0 +1,28 @@ +{ + "kind": "informed-decision-approval-create-intents", + "sitting_id": "infd-sitting-2026-09-21", + "status": "not-posted", + "posted": false, + "requester_client": "informed-decision-sitting-requester", + "human_control": true, + "pdp_path": false, + "required_count": 1, + "note": "Intents only. Digests are computed by approval-engine at create; do not invent them here.", + "intents": [ + { + "memo_id": "infd-20260921-b01", + "create_client": "informed-decision-sitting-requester", + "binding": { + "action": "accept", + "actor": "informed-decision", + "principal": "helixforge-factory", + "purpose": "Accept the spend envelope for metered Glas runs on the Anthropic key", + "target": { + "id": "spend-envelope:hfact-glas-anthropic-2026-09", + "type": "spend-envelope", + "system": "rein-aharness" + } + } + } + ] +} diff --git a/docs/batches/2026-09-21/budget/index.json b/docs/batches/2026-09-21/budget/index.json new file mode 100644 index 0000000..0b83c09 --- /dev/null +++ b/docs/batches/2026-09-21/budget/index.json @@ -0,0 +1,24 @@ +{ + "kind": "informed-decision-batch", + "id": "infd-batch-2026-09-21-budget", + "review_group": "net-kingdom-admins", + "status": "draft-unsigned", + "submitted": false, + "one_question_per_memo": true, + "approve_all_forbidden": true, + "agent_disposition_forbidden": true, + "ordinal": [ + { + "n": 1, + "memo_id": "infd-20260921-b01", + "workplan": "SECRETS-WP-0009", + "task": "SECRETS-WP-0009-T03", + "hub_task_prefix": "f8069c8a", + "question": "Accept the spend envelope for metered Glas runs on the Anthropic key (EUR 5 per run, 20 per day, 500 total, until 2027-01-31)?", + "memo": "infd-20260921-b01.memo.json", + "packet": "infd-20260921-b01.packet.md", + "packet_hash": "sha256:a3d3f4e33d75579795c0e407971c0d472432569ef68a9408e9897587a58f0deb", + "required_highlight": "infd-20260921-b01-h1" + } + ] +} diff --git a/docs/batches/2026-09-21/budget/infd-20260921-b01.memo.json b/docs/batches/2026-09-21/budget/infd-20260921-b01.memo.json new file mode 100644 index 0000000..03cd6b5 --- /dev/null +++ b/docs/batches/2026-09-21/budget/infd-20260921-b01.memo.json @@ -0,0 +1 @@ +{"approval_binding_digest":null,"approval_id":null,"binding":{"justification":null,"principal":{"display_name":"Named reviewer in net-kingdom-admins","id":"pending-human-session","identifiers":[],"kind":"person","role":"reviewer"},"target":{"environment":"prod","id":"spend-envelope:hfact-glas-anthropic-2026-09","kind":"spend-envelope","label":"Glas metered runs on the Anthropic key (railiance01)","requires_new_bind":false},"terms":null},"binding_level":"organizational","brief":"Sets the spending authority that rein-aharness MessagesOwner enforces before each metered Glas run on railiance01. Accepting it does not deliver the key, run a model or create an OpenBao policy; those are three separate SECRETS-WP-0009-T03 approvals. The EUR 100 monthly limit is recorded here but not enforced by the software.","highlights":[{"id":"infd-20260921-b01-h1","item_id":"infd-20260921-b01-packet","locator":{"kind":"work_record","task":"SECRETS-WP-0009-T03","workplan":"SECRETS-WP-0009"},"note":"The EUR 100 per month limit is NOT enforced: SpendPolicy has no monthly ceiling. Only EUR 5 per run, EUR 20 per day and EUR 500 total are enforced. Every completed run is charged its full reservation (EUR 4.9938), not its actual cost.","required_ack":true,"severity":"critical"}],"id":"infd-20260921-b01","locale":"en","packet":[{"hash":"sha256:a3d3f4e33d75579795c0e407971c0d472432569ef68a9408e9897587a58f0deb","item_id":"infd-20260921-b01-packet","label":"infd-20260921-b01-packet"}],"question":"Accept the spend envelope for metered Glas runs on the Anthropic key (EUR 5 per run, 20 per day, 500 total, until 2027-01-31)?","requested_act":"approve","sealed":false,"step_kind":"approve","ui_release":"informed-decision@0.2.0","version":1} diff --git a/docs/batches/2026-09-21/budget/infd-20260921-b01.packet.md b/docs/batches/2026-09-21/budget/infd-20260921-b01.packet.md new file mode 100644 index 0000000..5d1a9e4 --- /dev/null +++ b/docs/batches/2026-09-21/budget/infd-20260921-b01.packet.md @@ -0,0 +1,51 @@ +# SECRETS-WP-0009-T03 — spend envelope for metered Glas runs + +Blocking workplan: SECRETS-WP-0009 +Blocking task: SECRETS-WP-0009-T03 (hub prefix f8069c8a) +Related: HFACT-WP-0001-T01/T04 (SpendPolicy and owner config), GLAS-WP-0012 + +## Question +Accept the spend envelope for metered Glas runs on the Anthropic key (EUR 5 per run, 20 per day, 500 total, until 2027-01-31)? + +## One act +Accept this envelope as the spend authority for rein-aharness MessagesOwner +`metered-once` runs on railiance01 using the Anthropic key of catalog lane +`glas-claude-agent-dev-anthropic`. The accepted approval id becomes the +SpendPolicy `authority_ref` for envelope `hfact-glas-anthropic-2026-09`. + +## Enforced values (rein-aharness SpendPolicy, decimal strings) + +| Field | Value | Note | +| --- | --- | --- | +| `per_run_eur` | 5 | Operator-stated | +| `daily_eur` | 20 | Operator-stated; timezone Europe/Berlin | +| `total_eur` | 500 | Operator-stated; over the whole envelope | +| `eur_per_usd` | 0.87 | Operator rate 1 EUR = 1.15 USD (1/1.15 = 0.8696), rounded up | +| `max_liability_usd` | 5.74 | Per run. Reserves ceil(5.74 x 0.87) = EUR 4.9938, inside EUR 5 | +| `max_budget_usd` | 5.00 | Per run. Claude CLI stop threshold, inside liability | +| `valid_from` | acceptance time | | +| `expires_at` | 2027-01-31T23:59:59+01:00 | Operator-stated | + +Resulting capacity: at most 4 runs per day and 100 runs in total. A completed run +is charged its full reservation, not its reported cost. A failed, cancelled or +unaccounted run keeps its reservation and blocks further runs until reconciled. +Cost above liability permanently marks the envelope breached. + +The operator's overall USD 600 budget and USD 800 liability caps are dominated +by `total_eur` 500 (= USD 575) and need no separate field. + +## Recorded, not enforced +- EUR 100 per calendar month. SpendPolicy has no monthly ceiling; a rein-aharness + follow-up adds `monthly_eur`. Until then the operator reviews monthly use. + +## Scope bound by the SpendPolicy, filled in before the private file is written +`worker_id`, `activity_definition_id`, `target_repo`, `project`, `profile_ref`, +`profile_sha256`, `descriptor_sha256`, `repository_grant_id`, `max_turns` come from +the admitted factory profile (HFACT-WP-0001-T01). They narrow this envelope; they +cannot widen the amounts above. + +## Must not +- Deliver, read or display the Anthropic key +- Authorize the OpenBao apply, verify or exec actions (separate T03 approvals) +- Cover llm-connect or its DeepSeek default; that path is not this envelope +- Raise any amount or extend the expiry without a new memo diff --git a/docs/batches/2026-09-21/sitting.json b/docs/batches/2026-09-21/sitting.json new file mode 100644 index 0000000..c045892 --- /dev/null +++ b/docs/batches/2026-09-21/sitting.json @@ -0,0 +1,12 @@ +{ + "kind": "informed-decision-sitting", + "id": "infd-sitting-2026-09-21", + "review_group": "net-kingdom-admins", + "status": "draft-unsigned", + "batches": [ + "infd-batch-2026-09-21-budget" + ], + "memo_count": 1, + "bind_path": "create approval (attended), Flex Auth exact-record admission, load store, human accept", + "note": "Unsigned draft. Live bind substitutes pending-human-session with the authenticated key-cape subject." +} diff --git a/tools/create_sitting_approvals.py b/tools/create_sitting_approvals.py index 677d620..3165732 100644 --- a/tools/create_sitting_approvals.py +++ b/tools/create_sitting_approvals.py @@ -19,6 +19,7 @@ from urllib.request import HTTPRedirectHandler, ProxyHandler, Request, build_ope 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" +EXPECTED = 7 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" @@ -76,8 +77,8 @@ def existing_receipt_blocks() -> bool: 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") + if len(rows) != EXPECTED: + raise ValueError(f"expected {EXPECTED} postable sitting intents") for row in rows: binding = row["binding"] if binding.get("actor") != "informed-decision": @@ -116,7 +117,18 @@ 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") + parser.add_argument("--sitting", help="batch date directory under docs/batches, e.g. 2026-09-21") + parser.add_argument("--receipt", help="receipt file name under docs/evidence; required with --sitting") + parser.add_argument("--expect", type=int, help="exact number of postable intents; required with --sitting") args = parser.parse_args() + global INTENTS, RECEIPT, EXPECTED + if args.sitting: + if not args.receipt or not args.expect or "/" in args.sitting or "/" in args.receipt: + print("sitting_requires_receipt_and_expect", file=sys.stderr) + return 2 + INTENTS = ROOT / "docs" / "batches" / args.sitting / "approval-create-intents.json" + RECEIPT = ROOT / "docs" / "evidence" / args.receipt + EXPECTED = args.expect if args.dry_run: print(json.dumps(dry_run(), indent=2, ensure_ascii=False)) return 0 diff --git a/tools/create_sitting_approvals.sh b/tools/create_sitting_approvals.sh index e46d716..8da0984 100755 --- a/tools/create_sitting_approvals.sh +++ b/tools/create_sitting_approvals.sh @@ -4,5 +4,5 @@ set -euo pipefail export INFD_APPROVAL_ORIGIN="${INFD_APPROVAL_ORIGIN:-http://127.0.0.1:18281}" exec /home/worsch/informed-decision/.venv/bin/python -B \ /home/worsch/informed-decision/tools/create_sitting_approvals.py \ - --approval-origin "$INFD_APPROVAL_ORIGIN" \ + --approval-origin "$INFD_APPROVAL_ORIGIN" "$@" \ >/dev/null 2>&1 diff --git a/tools/prepare_budget_batch.py b/tools/prepare_budget_batch.py new file mode 100644 index 0000000..1281c33 --- /dev/null +++ b/tools/prepare_budget_batch.py @@ -0,0 +1,188 @@ +"""Draft the unsigned 2026-09-21 spend-envelope budget memo (SECRETS-WP-0009-T03). + +One question, one memo. No secret values. Does not submit, present, or bind. +The accepted approval id becomes the SpendPolicy ``authority_ref``. +""" +from __future__ import annotations + +import json +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from prepare_compact_batches import REVIEW_GROUP, _memo, _sha # noqa: E402 + +from informed_decision.memo import Scope # noqa: E402 +from informed_decision.records import dumps, memo_from # noqa: E402 + +ROOT = Path(__file__).resolve().parents[1] / "docs" / "batches" / "2026-09-21" +MEMO_ID = "infd-20260921-b01" +ENVELOPE_ID = "hfact-glas-anthropic-2026-09" +WORKPLAN = "SECRETS-WP-0009" +TASK = "SECRETS-WP-0009-T03" +HUB_TASK_PREFIX = "f8069c8a" + +QUESTION = ( + "Accept the spend envelope for metered Glas runs on the Anthropic key " + "(EUR 5 per run, 20 per day, 500 total, until 2027-01-31)?" +) +BRIEF = ( + "Sets the spending authority that rein-aharness MessagesOwner enforces before each " + "metered Glas run on railiance01. Accepting it does not deliver the key, run a model " + "or create an OpenBao policy; those are three separate SECRETS-WP-0009-T03 approvals. " + "The EUR 100 monthly limit is recorded here but not enforced by the software." +) +HIGHLIGHT = ( + "The EUR 100 per month limit is NOT enforced: SpendPolicy has no monthly ceiling. " + "Only EUR 5 per run, EUR 20 per day and EUR 500 total are enforced. Every completed " + "run is charged its full reservation (EUR 4.9938), not its actual cost." +) +TARGET = Scope( + kind="spend-envelope", + id=f"spend-envelope:{ENVELOPE_ID}", + label="Glas metered runs on the Anthropic key (railiance01)", + environment="prod", +) +BINDING = { + "action": "accept", + "actor": "informed-decision", + "principal": "helixforge-factory", + "purpose": "Accept the spend envelope for metered Glas runs on the Anthropic key", + "target": { + "id": f"spend-envelope:{ENVELOPE_ID}", + "type": "spend-envelope", + "system": "rein-aharness", + }, +} + +PACKET = f"""# {TASK} — spend envelope for metered Glas runs + +Blocking workplan: {WORKPLAN} +Blocking task: {TASK} (hub prefix {HUB_TASK_PREFIX}) +Related: HFACT-WP-0001-T01/T04 (SpendPolicy and owner config), GLAS-WP-0012 + +## Question +{QUESTION} + +## One act +Accept this envelope as the spend authority for rein-aharness MessagesOwner +`metered-once` runs on railiance01 using the Anthropic key of catalog lane +`glas-claude-agent-dev-anthropic`. The accepted approval id becomes the +SpendPolicy `authority_ref` for envelope `{ENVELOPE_ID}`. + +## Enforced values (rein-aharness SpendPolicy, decimal strings) + +| Field | Value | Note | +| --- | --- | --- | +| `per_run_eur` | 5 | Operator-stated | +| `daily_eur` | 20 | Operator-stated; timezone Europe/Berlin | +| `total_eur` | 500 | Operator-stated; over the whole envelope | +| `eur_per_usd` | 0.87 | Operator rate 1 EUR = 1.15 USD (1/1.15 = 0.8696), rounded up | +| `max_liability_usd` | 5.74 | Per run. Reserves ceil(5.74 x 0.87) = EUR 4.9938, inside EUR 5 | +| `max_budget_usd` | 5.00 | Per run. Claude CLI stop threshold, inside liability | +| `valid_from` | acceptance time | | +| `expires_at` | 2027-01-31T23:59:59+01:00 | Operator-stated | + +Resulting capacity: at most 4 runs per day and 100 runs in total. A completed run +is charged its full reservation, not its reported cost. A failed, cancelled or +unaccounted run keeps its reservation and blocks further runs until reconciled. +Cost above liability permanently marks the envelope breached. + +The operator's overall USD 600 budget and USD 800 liability caps are dominated +by `total_eur` 500 (= USD 575) and need no separate field. + +## Recorded, not enforced +- EUR 100 per calendar month. SpendPolicy has no monthly ceiling; a rein-aharness + follow-up adds `monthly_eur`. Until then the operator reviews monthly use. + +## Scope bound by the SpendPolicy, filled in before the private file is written +`worker_id`, `activity_definition_id`, `target_repo`, `project`, `profile_ref`, +`profile_sha256`, `descriptor_sha256`, `repository_grant_id`, `max_turns` come from +the admitted factory profile (HFACT-WP-0001-T01). They narrow this envelope; they +cannot widen the amounts above. + +## Must not +- Deliver, read or display the Anthropic key +- Authorize the OpenBao apply, verify or exec actions (separate T03 approvals) +- Cover llm-connect or its DeepSeek default; that path is not this envelope +- Raise any amount or extend the expiry without a new memo +""" + + +def main() -> None: + ROOT.mkdir(parents=True, exist_ok=True) + directory = ROOT / "budget" + directory.mkdir(exist_ok=True) + packet_path = directory / f"{MEMO_ID}.packet.md" + packet_path.write_text(PACKET, encoding="utf-8") + digest = _sha(PACKET) + memo = _memo( + memo_id=MEMO_ID, + question=QUESTION, + brief=BRIEF, + target=TARGET, + packet_id=MEMO_ID + "-packet", + packet_hash=digest, + highlight=HIGHLIGHT, + workplan=WORKPLAN, + task=TASK, + ) + if memo_from(json.loads(dumps(memo))).question != QUESTION: + raise RuntimeError("round-trip failed") + memo_path = directory / f"{MEMO_ID}.memo.json" + memo_path.write_text(dumps(memo) + "\n", encoding="utf-8") + index = { + "kind": "informed-decision-batch", + "id": "infd-batch-2026-09-21-budget", + "review_group": REVIEW_GROUP, + "status": "draft-unsigned", + "submitted": False, + "one_question_per_memo": True, + "approve_all_forbidden": True, + "agent_disposition_forbidden": True, + "ordinal": [ + { + "n": 1, + "memo_id": MEMO_ID, + "workplan": WORKPLAN, + "task": TASK, + "hub_task_prefix": HUB_TASK_PREFIX, + "question": QUESTION, + "memo": memo_path.name, + "packet": packet_path.name, + "packet_hash": digest, + "required_highlight": f"{MEMO_ID}-h1", + } + ], + } + (directory / "index.json").write_text(json.dumps(index, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + intents = { + "kind": "informed-decision-approval-create-intents", + "sitting_id": "infd-sitting-2026-09-21", + "status": "not-posted", + "posted": False, + "requester_client": "informed-decision-sitting-requester", + "human_control": True, + "pdp_path": False, + "required_count": 1, + "note": "Intents only. Digests are computed by approval-engine at create; do not invent them here.", + "intents": [{"memo_id": MEMO_ID, "create_client": "informed-decision-sitting-requester", "binding": BINDING}], + } + (ROOT / "approval-create-intents.json").write_text(json.dumps(intents, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + sitting = { + "kind": "informed-decision-sitting", + "id": "infd-sitting-2026-09-21", + "review_group": REVIEW_GROUP, + "status": "draft-unsigned", + "batches": [index["id"]], + "memo_count": 1, + "bind_path": "create approval (attended), Flex Auth exact-record admission, load store, human accept", + "note": "Unsigned draft. Live bind substitutes pending-human-session with the authenticated key-cape subject.", + } + (ROOT / "sitting.json").write_text(json.dumps(sitting, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + print(f"Wrote 1 unsigned memo under {ROOT}") + + +if __name__ == "__main__": + main()