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 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 272244@bnt-lap001
Assistant-Session: c8962fa7-b290-47df-865f-403ddb6c77e9
This commit is contained in:
tegwick 2026-09-21 18:35:49 +02:00
parent 1bce7c663a
commit c8c60ebc21
9 changed files with 366 additions and 3 deletions

View file

@ -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.

View file

@ -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"
}
}
}
]
}

View file

@ -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"
}
]
}

View file

@ -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}

View file

@ -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

View file

@ -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."
}

View file

@ -19,6 +19,7 @@ from urllib.request import HTTPRedirectHandler, ProxyHandler, Request, build_ope
ROOT = Path(__file__).resolve().parents[1] ROOT = Path(__file__).resolve().parents[1]
INTENTS = ROOT / "docs" / "batches" / "2026-09-14" / "approval-create-intents.json" INTENTS = ROOT / "docs" / "batches" / "2026-09-14" / "approval-create-intents.json"
RECEIPT = ROOT / "docs" / "evidence" / "2026-09-15-sitting-approval-creates.json" RECEIPT = ROOT / "docs" / "evidence" / "2026-09-15-sitting-approval-creates.json"
EXPECTED = 7
POLICY = "workload-kv-read-informed-decision-sitting-requester-client" POLICY = "workload-kv-read-informed-decision-sitting-requester-client"
KV = "platform/data/workloads/informed-decision/sitting-requester" KV = "platform/data/workloads/informed-decision/sitting-requester"
SIBLING = "platform/data/workloads/secrets-engine/approval-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]: def postable_intents(data: dict) -> list[dict]:
rows = [row for row in data.get("intents") or [] if row.get("create_client") == CLIENT_ID] rows = [row for row in data.get("intents") or [] if row.get("create_client") == CLIENT_ID]
if len(rows) != 7: if len(rows) != EXPECTED:
raise ValueError("expected seven postable sitting intents") raise ValueError(f"expected {EXPECTED} postable sitting intents")
for row in rows: for row in rows:
binding = row["binding"] binding = row["binding"]
if binding.get("actor") != "informed-decision": if binding.get("actor") != "informed-decision":
@ -116,7 +117,18 @@ def main() -> int:
parser = argparse.ArgumentParser(description=__doc__) parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--dry-run", action="store_true", help="list postable memos; no credentials, no POST") 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("--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() 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: if args.dry_run:
print(json.dumps(dry_run(), indent=2, ensure_ascii=False)) print(json.dumps(dry_run(), indent=2, ensure_ascii=False))
return 0 return 0

View file

@ -4,5 +4,5 @@ set -euo pipefail
export INFD_APPROVAL_ORIGIN="${INFD_APPROVAL_ORIGIN:-http://127.0.0.1:18281}" export INFD_APPROVAL_ORIGIN="${INFD_APPROVAL_ORIGIN:-http://127.0.0.1:18281}"
exec /home/worsch/informed-decision/.venv/bin/python -B \ exec /home/worsch/informed-decision/.venv/bin/python -B \
/home/worsch/informed-decision/tools/create_sitting_approvals.py \ /home/worsch/informed-decision/tools/create_sitting_approvals.py \
--approval-origin "$INFD_APPROVAL_ORIGIN" \ --approval-origin "$INFD_APPROVAL_ORIGIN" "$@" \
>/dev/null 2>&1 >/dev/null 2>&1

View file

@ -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()