informed-decision/tools/prepare_budget_batch.py
tegwick c8c60ebc21 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
2026-09-21 18:35:49 +02:00

188 lines
7.4 KiB
Python

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