test(sdk): the pin was a permission, so give the premises a proof

The guard checked whether the installed SDK satisfied the pin. Nobody had
ever checked whether anyone had READ it. Those are different questions, and
the gap between them was a whole version range: pinned >=0.2.111,<0.3,
premises source-verified through 0.2.110, installed 0.2.120. Every build in
between was admissible and unexamined — `uv sync --upgrade` would have kept
806 tests green on an SDK no one had opened. Written red first: a guard
handed 0.2.140 returned it without complaint.

_VERIFIED_THROUGH is the ratchet. It records the newest build actually read
at source, and a newer one fails naming the five premises to re-check. The
pin is untouched and was never the defect — measurement dissolved the
premise that it needed lifting. It was not too narrow but too wide, and a
wider permission is not repaired by widening it further.

The premises themselves were prose the failure message recited. Nothing
tested them, so one that stopped being true would have surfaced on the one
live paid run (S10, D6). They are now a table introspected against the
installed package, with the printed prose derived from that same table so a
checked attribute cannot go unreported or a reported one unchecked. The
premise introspection structurally cannot see — that query() yields an
AssistantMessage then a closing ResultMessage — is named apart, and is the
honest reason the human reading still has to happen.

Value-proved, not merely named: disabling the ratchet reds 1 test, stubbing
the inventory to "no gaps" reds 3, re-hardcoding the prose reds 1, and
lowering _VERIFIED_THROUGH below the installed build reds the real
installed-version test rather than only a monkeypatched one.

0.2.139 read at source (0.2.120 -> 0.2.139, latest on PyPI today; STATE said
0.2.134, measured 08-09 and stale). The public query.py is byte-identical,
every premise field keeps its type and default, and the parser changes are
additive. One needed a look: 0.2.139 added a skills path defaulting
setting_sources to ["user", "project"], which would have undone the S10
isolation fix — it fires only on None, so the explicit [] is out of reach.
Prose carrying stale version claims moved with the reading, never ahead of
it: each was re-verified at 0.2.139 before being restated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dKDjVG7qrBh9NkAAxutqN
This commit is contained in:
Kjell Tore Guttormsen 2026-08-18 16:57:57 +02:00
commit 90a41774fc
8 changed files with 271 additions and 38 deletions

View file

@ -21,7 +21,9 @@ Four offline checks (for a given backend profile, ``anthropic`` today):
refusal naming ``uv sync``, never an ImportError), the run-path symbols exist,
and the bundled Claude Code CLI is present on disk located offline via the
SDK package's own files, mirroring the SDK's ``_find_cli`` order (bundled
first, then a ``claude`` on PATH). Verified against installed 0.2.120.
first, then a ``claude`` on PATH). Verified against installed 0.2.139 by
reading its ``_find_cli`` (bundled at line 250, PATH at 256) the order is
unchanged from 0.2.120.
* **termination** the §8 stop contract constructs with positive round/token
caps and the per-call USD cap is positive (the budget belt is SET).

View file

@ -9,13 +9,18 @@ turn, a first-class USD cap (``ClaudeAgentOptions.max_budget_usd``) ON TOP of
the §8 token/round meter that the loop already charges, and NO filesystem
settings (``setting_sources=[]``).
Verified against claude-agent-sdk 0.2.110 at source level and release notes
through 0.2.120 pinned ``>=0.2.111,<0.3`` with a version guard
(``tests/test_sdk_version_guard.py``) that forces re-verification of these
premises before any widening: ``query()`` yields ``AssistantMessage`` (text
blocks + real model id) and a closing ``ResultMessage`` (provider-reported
``usage`` + ``total_cost_usd``). A reply without usage is passed through as
``None`` so the meter fails CLOSED (§8) this client never invents a count.
Verified against claude-agent-sdk 0.2.139 AT SOURCE LEVEL (2026-08-18; the
public ``query.py`` is byte-identical to 0.2.120 and every field below is
present with an unchanged type and default). Pinned ``>=0.2.111,<0.3``, but the
pin is only a permission ``tests/test_sdk_version_guard.py`` carries the
proof: it checks each attribute below against the INSTALLED package and goes
red on any build newer than the last one actually read. The premises:
``query()`` yields ``AssistantMessage`` (text blocks + real model id) and a
closing ``ResultMessage`` (provider-reported ``usage`` + ``total_cost_usd``).
Unknown message types are ignored by the loop below, so an SDK that adds one
(0.2.139 added ``ConversationResetMessage``) does not disturb this client.
A reply without usage is passed through as ``None`` so the meter fails CLOSED
(§8) this client never invents a count.
"""
from __future__ import annotations
@ -47,14 +52,16 @@ _USAGE_TOKEN_FIELDS = (
def build_call_options(model_id: str, *, max_budget_usd: float) -> ClaudeAgentOptions:
"""One bounded, ISOLATED completion call (§8 + S10 post-mortem).
``setting_sources=[]`` is the SDK's documented isolation mode (verified
against 0.2.110): the spawned CLI loads NO filesystem settings no
``setting_sources=[]`` is the SDK's documented isolation mode (re-read at
0.2.139): the spawned CLI loads NO filesystem settings no
session hooks, no CLAUDE.md, no operator instructions. The default
(``None``) loads ALL sources: in the S10 live run that injected the
operator's config into every call (~10-15k uncached tokens each) and
mandated a confirmation preamble that made pure-JSON replies impossible.
``system_prompt=None`` serializes to an EMPTY system prompt, not the
Claude Code preset.
Claude Code preset. 0.2.139 added a skills path that can DEFAULT
``setting_sources`` to ``["user", "project"]`` it fires only on ``None``,
so the explicit ``[]`` here is not reachable by it (read at source).
"""
return ClaudeAgentOptions(
model=model_id,