Transkript-analyse av den stoppede live-kjøringen (10 kall, 162 250 tokens, $0.331506): konfig-lekkasjen (setting_sources=None laster ALLE filsystem- settings) injiserte operatørens Claude-konfig i hvert kall — ~10-15k uncachede tokens, en påtvunget bekreftelses-preamble som gjorde ren-JSON-svar umulige, og en checker kapret av lekkede instrukser (debatt konvergerte aldri). - persist_stop_artifacts: stopp-event verbatim + usage/kost persisteres ALLTID ved BudgetExceeded (delt usage-shape med fullført-run-stien) - build_call_options: setting_sources=[] (SDK isolation mode, verifisert mot installert 0.2.110-kilde), system_prompt=None → tom system-prompt; detach- bevis via monkeypatchet query - _generation_prompt: krever ONLY the raw JSON object (fence-innpakning ga 4 fullpris parse-retries) 187/187 uten nøkkel · ruff + mypy --strict rene · tre detach-bevis RØDE → grønn Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QdSfQdND84oeq2mbjueLTS
77 lines
3.3 KiB
Python
77 lines
3.3 KiB
Python
"""Prompt-isolation proof for the SDK call options (§11, S10 post-mortem).
|
|
|
|
The S10 live run leaked the operator's Claude Code configuration into every
|
|
spawned CLI session: ``ClaudeAgentOptions.setting_sources`` defaults to
|
|
``None``, which loads ALL filesystem settings (verified against SDK 0.2.110)
|
|
— session-start hooks injected STATE.md into the model's context, every reply
|
|
opened with a mandated confirmation line (so a reply was NEVER pure JSON),
|
|
and each call paid ~10-15k uncached context tokens. ``[]`` is the SDK's
|
|
documented isolation mode: no filesystem settings, no hooks, no CLAUDE.md.
|
|
|
|
Importing the client here is offline-safe: constructing options touches no
|
|
network and needs no API key; ``query()`` is replaced with a recording fake.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, AsyncIterator
|
|
|
|
import pytest
|
|
|
|
from portfolio_optimiser_claude import sdk_client
|
|
from portfolio_optimiser_claude.contracts import ModelMapContract
|
|
from portfolio_optimiser_claude.sdk_client import SdkModelClient, build_call_options
|
|
|
|
|
|
class TestBuildCallOptions:
|
|
def test_all_filesystem_settings_are_disabled(self) -> None:
|
|
# LOAD-BEARING (§11): [] is SDK isolation mode. The default (None)
|
|
# loads user+project+local settings — hooks and CLAUDE.md leak into
|
|
# the model's context, exactly the observed S10 failure.
|
|
options = build_call_options("model-x", max_budget_usd=0.25)
|
|
assert options.setting_sources == []
|
|
|
|
def test_the_system_prompt_is_empty(self) -> None:
|
|
# None serializes to --system-prompt "" (verified against 0.2.110):
|
|
# no Claude Code preset, no appended operator instructions.
|
|
options = build_call_options("model-x", max_budget_usd=0.25)
|
|
assert options.system_prompt is None
|
|
|
|
def test_the_call_stays_bounded_single_turn_no_tools(self) -> None:
|
|
options = build_call_options("model-x", max_budget_usd=0.25)
|
|
assert options.max_turns == 1
|
|
assert options.tools == []
|
|
assert options.max_budget_usd == 0.25
|
|
assert options.model == "model-x"
|
|
|
|
|
|
class TestCompleteThreadsIsolatedOptions:
|
|
def test_complete_passes_the_isolated_options_to_query(
|
|
self, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
# Detach-proof: if complete() ever builds its options inline again
|
|
# (dropping the isolation), this goes RED — grønn-men-død guard.
|
|
captured: dict[str, Any] = {}
|
|
|
|
def fake_query(*, prompt: str, options: Any) -> AsyncIterator[Any]:
|
|
captured["prompt"] = prompt
|
|
captured["options"] = options
|
|
|
|
async def _empty() -> AsyncIterator[Any]:
|
|
return
|
|
yield
|
|
|
|
return _empty()
|
|
|
|
monkeypatch.setattr(sdk_client, "query", fake_query)
|
|
client = SdkModelClient(
|
|
ModelMapContract(profiles={"anthropic": {"default": "model-default"}}),
|
|
max_budget_usd_per_call=0.10,
|
|
)
|
|
reply = client.complete("the prompt", role="proposer")
|
|
assert captured["prompt"] == "the prompt"
|
|
assert captured["options"].setting_sources == []
|
|
assert captured["options"].max_budget_usd == 0.10
|
|
assert captured["options"].model == "model-default"
|
|
# No usage surfaced by the fake → the reply fails CLOSED (§8).
|
|
assert reply.usage_tokens is None
|