Observed-usage settlement: tests, renewal support and docs (REINAH-WP-0008-T02)
Some checks failed
Governed runtime contract / contract (push) Failing after 11s

Completes c7dceee. owner renew accepts settlement and max_runs and writes
policies via as_dict, so unset terms stay out of the file and digest.
Tests cover metered request settlement, reported-cost floor, unfinished
holds, the 6efa9436 reconciliation shape, overrun freeze, and three
attempts within EUR 10 with the fourth refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 269692@bnt-lap001
Assistant-Session: 8077e68a-0985-4b41-898c-2dc602b0d6be
This commit is contained in:
tegwick 2026-10-14 01:54:51 +02:00
parent c7dceee8e1
commit c04d1c62dd
6 changed files with 205 additions and 3 deletions

View file

@ -47,6 +47,23 @@ No digest is typed by hand. Paths do not change, so the Secrets Engine
`exec_owner` catalog needs only the new file hashes, which are printed as
`catalog_file_pins`. Update those pins, then run `--rehearse`.
**Settlement and attempt limit (opt-in per envelope).** Two optional
envelope fields change how spend is charged:
- `settlement`: `full-hold` (default) charges every request and run at
least its whole hold. `observed-usage` charges a completed provider
request its gateway-metered cost (provider-reported tokens at the admitted
rates). A run is then charged the larger of its metered requests and the
cost the CLI reports, at completion and at owner reconciliation alike.
Requests whose stream never completed still count at their full hold, and
an overrun still freezes the envelope.
- `max_runs`: the number of runs the envelope admits.
Left unset, neither field is written or included in the policy digest, so
existing pins are unchanged. Observed-usage trusts provider-reported usage
until the owner's billing check, which is why it needs explicit owner
acceptance (decision `a93619a9`).
Use the standalone candidate interpreter as `python -I -B -m rein_aharness.cli`.
`-I` excludes editable PYTHONPATH/user-site fallbacks; `-B` prevents bytecode writes
from changing the complete artifact digest. The source and namespace rein commands

View file

@ -33,6 +33,7 @@ ENVELOPE_FIELDS = frozenset({
"envelope_id", "authority_ref", "valid_from", "expires_at",
"eur_per_usd", "per_run_eur", "daily_eur", "total_eur",
"max_budget_usd", "max_liability_usd",
"settlement", "max_runs",
})
_OWNER_KEYS = {"version", "authority_ref", "spend_policy_sha256", "messages_policy", "runtime"}
@ -151,7 +152,7 @@ def apply(renewal: dict[str, Any]) -> dict[str, Any]:
_write_private(backup, source.read_text())
os.rename(ledger_path, retired)
os.rename(staged, ledger_path)
_write_private(policy_path, json.dumps(renewal["new_policy"].__dict__, indent=2) + "\n")
_write_private(policy_path, json.dumps(renewal["new_policy"].as_dict(), indent=2) + "\n")
_write_private(owner_config, json.dumps(renewal["new_owner"], indent=2) + "\n")
return {
"applied": True,

View file

@ -160,7 +160,7 @@ def case(root, runtime, checksum, server, cap, args, bootstrap=False):
parent = SpendLedger(private / "spend.sqlite3", policy); parent.initialize()
meter = RequestLedger(parent); meter.initialize()
policy_path = private / "spend-policy.json"
policy_path.write_text(json.dumps(policy.__dict__)); policy_path.chmod(0o600)
policy_path.write_text(json.dumps(policy.as_dict())); policy_path.chmod(0o600)
admission_requests = []
class RecordingPolicy(MessagesPolicy):
def validate(self, data, betas):

View file

@ -116,3 +116,23 @@ def test_charged_reservation_renews_and_survives_in_retired_ledger(prepared, tmp
("charged", 10000000)]
retired.close()
assert SpendLedger(paths[2], SpendPolicy.load(paths[1])).status()["reservations"] == []
def test_renewal_to_observed_usage_with_run_limit(prepared, tmp_path):
path, config, _, _ = prepared
envelope = {**ENVELOPE, "settlement": "observed-usage", "max_runs": 3,
"total_eur": "10", "daily_eur": "10"}
paths = _paths(prepared, tmp_path, envelope)
apply(plan(*paths))
written = json.loads(paths[1].read_text())
assert written["settlement"] == "observed-usage" and written["max_runs"] == 3
policy = SpendPolicy.load(paths[1])
assert json.loads(paths[0].read_text())["spend_policy_sha256"] == policy.sha256
prepare(path, config)
def test_default_renewal_writes_no_optional_terms(prepared, tmp_path):
paths = _paths(prepared, tmp_path)
apply(plan(*paths))
written = json.loads(paths[1].read_text())
assert "settlement" not in written and "max_runs" not in written

View file

@ -0,0 +1,140 @@
"""Opt-in observed-usage settlement and run limits (REINAH-WP-0008, decision a93619a9)."""
from __future__ import annotations
import json
from dataclasses import replace
from datetime import UTC, datetime, timedelta
import pytest
from rein_aharness.request_admission import RequestLedger
from rein_aharness.spend_admission import (
SpendAdmissionError,
SpendLedger,
SpendPolicy,
digest,
)
from test_spend_admission import ledger as ledger, run, success
POLICY_SHA = "c" * 64
def _observed(ledger, **changes):
policy = replace(ledger.policy, settlement="observed-usage", **changes)
store = SpendLedger(ledger.path.with_name("observed.sqlite3"), policy)
store.initialize()
meter = RequestLedger(store)
meter.initialize()
return store, meter
def _route(meter, key):
return meter.bind_route(
run(key).id, POLICY_SHA, lease_id=f"lease-{key}",
expires_at=(datetime.now(UTC) + timedelta(seconds=60)).isoformat())
def _liability(store, key):
row = next(r for r in store.status()["reservations"] if r["run_id"] == run(key).id)
return row["state"], row["liability"]
def test_default_policy_digest_is_unchanged(ledger, tmp_path):
policy = ledger.policy
legacy = {k: v for k, v in policy.__dict__.items() if k not in ("settlement", "max_runs")}
assert policy.sha256 == digest(legacy)
assert replace(policy, settlement="observed-usage").sha256 != policy.sha256
assert replace(policy, max_runs=3).sha256 != policy.sha256
path = tmp_path / "legacy.json"
path.write_text(json.dumps(legacy)); path.chmod(0o600)
assert SpendPolicy.load(path).sha256 == policy.sha256
@pytest.mark.parametrize("changes", [{"settlement": "refund"}, {"max_runs": 0},
{"max_runs": "3"}, {"max_runs": True}])
def test_invalid_terms_refuse(ledger, changes):
with pytest.raises(SpendAdmissionError):
replace(ledger.policy, **changes)
def test_full_hold_default_still_charges_whole_hold(ledger):
meter = RequestLedger(ledger)
meter.initialize()
ledger.reserve(run())
token = _route(meter, "one")
meter.complete_request(meter.reserve_request(token, POLICY_SHA, 2_000_000), 10)
assert meter.status()[0]["liability_microusd"] == 2_000_000
def test_completed_requests_settle_to_metered_cost(ledger):
store, meter = _observed(ledger)
store.reserve(run())
token = _route(meter, "one")
# Four 2 USD holds in a 5 USD run: impossible under full hold.
for _ in range(4):
meter.complete_request(meter.reserve_request(token, POLICY_SHA, 2_000_000), 150_000)
assert [r["liability_microusd"] for r in meter.status()] == [150_000] * 4
assert store.observe(run().id, success(store, run(), 0.1))
assert _liability(store, "one") == ("charged", 600_000)
def test_reported_cost_wins_when_higher_than_metered(ledger):
store, meter = _observed(ledger)
store.reserve(run())
token = _route(meter, "one")
meter.complete_request(meter.reserve_request(token, POLICY_SHA, 2_000_000), 100_000)
assert store.observe(run().id, success(store, run(), 0.9))
assert _liability(store, "one") == ("charged", 900_000)
def test_zero_usage_still_records_a_positive_charge(ledger):
store, meter = _observed(ledger)
store.reserve(run())
token = _route(meter, "one")
meter.complete_request(meter.reserve_request(token, POLICY_SHA, 2_000_000), 0)
assert meter.status()[0]["liability_microusd"] == 1
def test_unfinished_request_keeps_full_hold_through_reconciliation(ledger):
store, meter = _observed(ledger)
store.reserve(run())
token = _route(meter, "one")
meter.complete_request(meter.reserve_request(token, POLICY_SHA, 2_000_000), 100_000)
meter.reserve_request(token, POLICY_SHA, 2_000_000) # stream never completed
assert not store.observe(run().id, success(store, run(), 0.05))
assert _liability(store, "one")[0] == "held"
store.reconcile(run().id, cost_usd="0.05", receipt="audit:provider-stopped-final")
assert _liability(store, "one") == ("charged", 2_100_000)
def test_failed_run_without_requests_reconciles_to_attested_cost(ledger):
# The run 6efa9436 shape: failure before any provider request.
store, meter = _observed(ledger)
store.reserve(run())
_route(meter, "one")
assert not store.observe(run().id, {"ok": False, "evidence": {}})
store.reconcile(run().id, cost_usd="0.05", receipt="owner-estimate:launcher-exit127")
assert _liability(store, "one") == ("charged", 50_000)
def test_overrun_still_freezes_the_envelope(ledger):
store, meter = _observed(ledger)
store.reserve(run())
token = _route(meter, "one")
receipt = meter.reserve_request(token, POLICY_SHA, 100_000)
with pytest.raises(SpendAdmissionError, match="breached"):
meter.complete_request(receipt, 200_000)
assert store.status()["breached"] is True
def test_three_attempts_within_eur_10_then_run_limit(ledger):
store, meter = _observed(ledger, total_eur="10", daily_eur="10", max_runs=3)
for key in ("one", "two", "three"):
store.reserve(run(key))
token = _route(meter, key)
meter.complete_request(meter.reserve_request(token, POLICY_SHA, 4_640_000), 400_000)
assert store.observe(run(key).id, success(store, run(key), 0.2))
assert sum(r["liability"] for r in store.status()["reservations"]) == 1_200_000
with pytest.raises(SpendAdmissionError, match="run limit"):
store.reserve(run("four"))

View file

@ -92,7 +92,7 @@ glas-harness 0, claude 0.
```task
id: REINAH-WP-0008-T02
status: todo
status: done
priority: high
state_hub_task_id: "6a413f5a-4ac2-5d5e-9b19-71b33445c0a9"
```
@ -113,6 +113,30 @@ reservation, or with a live route, refuses the release. A second, identical
reconcile changes nothing. The envelope is never marked breached by a
release.
### Widened to observed-usage settlement — 2026-10-14
On 2026-10-13 the owner asked for three attempts within EUR 10 and chose
option B: settle to actual usage instead of releasing only unused holds.
Under full-hold accounting each request holds USD 4.64 (1M context plus
64k output), so EUR 10 cannot fit three attempts.
Implemented as an opt-in envelope term (`settlement: observed-usage`) plus
`max_runs`; see `docs/owner-bootstrap.md`. Under it:
- a completed request is charged its gateway-metered cost;
- runs and reconciliations are charged
`max(metered requests, reported or attested cost)`;
- unfinished requests keep their full hold, and overruns still freeze the
envelope.
The run 6efa9436 shape (failure before any request) now reconciles to its
attested cost. Defaults and existing digests are unchanged. Tests:
`tests/test_spend_settlement.py`, plus renewal cases in
`tests/test_owner_renew.py`.
Merge-ready, but the rule still needs the owner's acceptance in decision
`a93619a9`.
## D. One owner command for renewal
```task