fix(credential): a subscription paid for the run, and one print line decided it
ANTHROPIC_API_KEY is now the ONLY accepted credential. Through v0.1.0 the preflight cleared on CLAUDE_CODE_OAUTH_TOKEN, and run_s10 went further: an unset key printed "note: relying on the CLI's own credentials" and carried on. That note was not a warning, it was a decision - made silently, on the operator's behalf, about who pays. Both paths are gone; a run with no key refuses with exit 2 before anything is opened. Red first, both halves: _check_credentials refuses an OAuth-only env, and the run entrance is driven as a real subprocess with a deliberately missing bundle, so the credential refusal must win the race against the bundle error. Detach it and the process reaches navigate_bundle instead - a different exit code, no refusal line, the fallback back in the output. The positive control (key set) gets past the gate and fails on the bundle, so the gate is a gate and not a wall. 997 -> 1002, offline, no key in env. The SDK exception is now stated where a reader meets it, not implied: this framework runs on the Claude Agent SDK, which starts the Claude Code CLI it bundles as a subprocess. That is the SDK's intended use WITH an API key, and it is a deliberate, stated exception to the owner's rule that his own code never starts Claude Code. Rewriting to direct HTTP calls was weighed and declined - measuring what the Agent SDK offers is the point of D7. The repo is closed as a worked example. Two prose claims were corrected rather than left standing: run_s10.py is no longer byte-frozen (it carries exactly one change, and runs/s10/ is still the v0.1.0 run), and its two round() call sites moved 110->118, 130->138. The credential paragraph is prose under an existing heading, not a new section: test_readme_anchors_loadbearing.py pins 14 heading ids MEASURED on the published page and forbids re-deriving them. This order forbids push, so a new heading could not have been honestly re-measured. Version 0.1.1: pyproject.toml, uv.lock self-entry, CHANGELOG - 3 of 3. No version badge in README, no constant in src. v0.1.0 stands as released. Order 20260920T131502Z-7496226791-from-.claude. The older D7 mirroring order 20260913T053840Z-9473220509 is retired unexecuted: po closes at v1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
7b934275d8
commit
f92b04bf62
14 changed files with 210 additions and 37 deletions
28
CHANGELOG.md
28
CHANGELOG.md
|
|
@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.1.1] - 2026-09-20
|
||||
|
||||
### Removed
|
||||
|
||||
- **A Claude Code subscription is no longer a way to pay for a run.** Through `0.1.0` the
|
||||
preflight accepted `CLAUDE_CODE_OAUTH_TOKEN` — the bundled CLI's own login token — as a
|
||||
credential, and `run_s10` went further: with no `ANTHROPIC_API_KEY` set it printed a
|
||||
`note:` and carried on, letting the CLI resolve whatever credentials it found. Both paths
|
||||
are gone. `ANTHROPIC_API_KEY` is now the only accepted credential; an OAuth token alone is
|
||||
a structured refusal, and a run entrance with no key exits non-zero before it opens
|
||||
anything. `0.1.0` stands as released — this is stated, not rewritten.
|
||||
|
||||
### Added
|
||||
|
||||
- **The SDK exception is stated where a reader meets it.** README and CONTRIBUTING now say
|
||||
plainly that this framework runs on the Claude Agent SDK, that the SDK starts the Claude
|
||||
Code CLI it bundles as a subprocess, that this is the SDK's intended use *with an API
|
||||
key*, and that the repo is closed as a worked example. Rewriting to direct HTTP API calls
|
||||
was considered and deliberately not done: measuring what the Agent SDK offers is the point
|
||||
of the D7 sibling.
|
||||
- Load-bearing proof for both refusals (`tests/test_api_key_only_loadbearing.py`): the run
|
||||
entrance is driven as a real subprocess with a deliberately missing bundle, so the
|
||||
credential refusal must win the race against the bundle error — detach it and the process
|
||||
reaches `navigate_bundle` instead. The positive control (a key set) gets past the gate and
|
||||
fails on the bundle, so the gate is a gate and not a wall. 997 → 1002 tests, still offline
|
||||
and still with no key in the environment.
|
||||
|
||||
|
||||
### Changed
|
||||
|
||||
- **The SDK pin now has a proof behind it, not only a permission.** `claude-agent-sdk`
|
||||
|
|
|
|||
|
|
@ -72,6 +72,15 @@ Pakkehåndtering: `uv`. Pydantic for kontrakts-validering.
|
|||
- **Kostnadsdisiplin (D6):** alt utvikles offline/deterministisk; den ENE ekte API-kjøringen
|
||||
i programmet er S10 (mikro-bundle, billigste egnede modell — per model_map Haiku 4.5 —
|
||||
harde tak, kost loggføres).
|
||||
- **Kun egen API-nøkkel (v0.1.1):** `ANTHROPIC_API_KEY` er ENESTE godtatte legitimasjon.
|
||||
Et abonnements-token (`CLAUDE_CODE_OAUTH_TOKEN`) AVVISES av preflight og av kjøre-
|
||||
inngangen, som ender i exit ≠ 0 — aldri et stille tilbakefall på CLI-ens innlogging.
|
||||
- **Uttalt unntak (ikke skjult):** repoet er bygget på Claude Agent SDK, som selv starter
|
||||
den medfølgende Claude Code-CLI-en som underprosess. Det er Anthropics tiltenkte bruk MED
|
||||
API-nøkkel, og står som et UTTALT unntak fra eierens regel om at egen kode ikke starter
|
||||
Claude Code. Omskriving til rene API-kall er vurdert og valgt bort — poenget med D7 er å
|
||||
måle hva Agent SDK-en tilbyr. **Repoet er AVSLUTTET som eksempel:** ingen videre utvikling
|
||||
er planlagt.
|
||||
- **STATE.md er local-only** (gitignored — repoet skal publiseres; STATE når aldri et
|
||||
offentlig speil).
|
||||
|
||||
|
|
|
|||
|
|
@ -58,8 +58,10 @@ uv run ruff format --check .
|
|||
uv run mypy src
|
||||
```
|
||||
|
||||
Tests run offline without an API key; live runs use the Claude API — keep `ANTHROPIC_API_KEY` in
|
||||
your environment, never commit it.
|
||||
Tests run offline without an API key. A live run requires your own `ANTHROPIC_API_KEY` — keep it
|
||||
in your environment, never commit it. It is the only accepted credential: a consumer Claude Code
|
||||
subscription is not supported and must not be used to pay for a run, so a `CLAUDE_CODE_OAUTH_TOKEN`
|
||||
is refused by the preflight and by the run entrance.
|
||||
|
||||
## Questions?
|
||||
|
||||
|
|
|
|||
29
README.md
29
README.md
|
|
@ -54,6 +54,23 @@ credentials are needed. Nothing runs the suite automatically: this forge has no
|
|||
so that one command from a clean clone is the verification, and there is no CI badge above
|
||||
pretending otherwise.
|
||||
|
||||
**Credentials — your own API key, and nothing else.** A live run requires
|
||||
`ANTHROPIC_API_KEY`, an API key you hold yourself. It is the **only** credential this
|
||||
framework accepts. A consumer Claude Code subscription is not a way to pay for a run here:
|
||||
a `CLAUDE_CODE_OAUTH_TOKEN` is refused by the preflight, and a run entrance with no key
|
||||
set refuses with a non-zero exit instead of falling back to whatever login the bundled CLI
|
||||
happens to find. Through `v0.1.0` that fallback existed; `v0.1.1` removed it.
|
||||
|
||||
**A stated exception, not a hidden one.** This framework is built on the Claude Agent SDK,
|
||||
and the SDK starts the Claude Code CLI it bundles as a subprocess — that is how the SDK is
|
||||
designed to work, and it is Anthropic's intended use *with an API key*. The owner of this
|
||||
repo otherwise holds a rule that his own code never starts Claude Code; this repo is a
|
||||
deliberate, stated exception to it, which is why the credential rule above is enforced in
|
||||
code rather than left to discipline. Rewriting the framework to call the HTTP API directly
|
||||
was considered and deliberately not done — the point of this sibling implementation (D7) is
|
||||
to measure what the Agent SDK offers. The repo is **closed as a worked example**: no further
|
||||
development is planned.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- **Not a portfolio reallocator.** It finds savings *inside* each project; it never moves
|
||||
|
|
@ -213,9 +230,10 @@ description, never from its code)
|
|||
cost less). `uv run python -m portfolio_optimiser_claude.costsim`.
|
||||
- `preflight.py` — the SDK/API preflight (**offline** — the boundary the operator crosses
|
||||
*before* any spend): everything that can be validated without a model call is checked here,
|
||||
so a broken config stops cheaply instead of on the first billed call. Four checks — a
|
||||
credential is present and not a placeholder form (`ANTHROPIC_API_KEY`, or the bundled CLI's
|
||||
own `CLAUDE_CODE_OAUTH_TOKEN`; the value itself is *never* validated online), the model_map
|
||||
so a broken config stops cheaply instead of on the first billed call. Four checks — your own
|
||||
`ANTHROPIC_API_KEY` is set and is not a placeholder form (it is the **only** accepted
|
||||
credential: a Claude Code subscription token is refused, never a fallback; the value
|
||||
itself is *never* validated online), the model_map
|
||||
profile exists and every id it resolves to is real, `claude_agent_sdk` imports and its
|
||||
bundled CLI is present on disk, and the §8 stop/budget contract is set. It never calls the
|
||||
API — a green preflight implies no more than that (§1). Each deficiency is a structured,
|
||||
|
|
@ -449,8 +467,9 @@ claims more live behaviour than that one documented run.
|
|||
## Stack
|
||||
|
||||
Python ≥3.10 · [`claude-agent-sdk`](https://pypi.org/project/claude-agent-sdk/) ≥0.2
|
||||
(bundles the Claude Code CLI; an API key is needed only at actual `query()` time) ·
|
||||
`uv` · Pydantic for contract validation.
|
||||
(bundles the Claude Code CLI, which it starts as a subprocess; the offline suite needs no
|
||||
credential, and a live run requires your own `ANTHROPIC_API_KEY` — a subscription token is
|
||||
refused) · `uv` · Pydantic for contract validation.
|
||||
|
||||
## Development
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "portfolio-optimiser-claude"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
description = "Sibling implementation of the portfolio-optimiser method on the Claude Agent SDK (D7)"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
|
|
|||
|
|
@ -10,10 +10,12 @@ broken config stops here, cheaply, instead of on the first billed call.
|
|||
|
||||
Four offline checks (for a given backend profile, ``anthropic`` today):
|
||||
|
||||
* **credential** — a credential is available (``ANTHROPIC_API_KEY``, or the
|
||||
bundled CLI's own ``CLAUDE_CODE_OAUTH_TOKEN``) and is not a placeholder form.
|
||||
The value itself is NEVER checked online — a bad key surfaces on the first
|
||||
call, never here (§1 honesty: the preflight claims no more than it proves).
|
||||
* **credential** — an own API key (``ANTHROPIC_API_KEY``) is set and is not a
|
||||
placeholder form. It is the ONLY accepted credential: a consumer Claude Code
|
||||
subscription is never a way to pay for a run here, so the bundled CLI's own
|
||||
login token is refused. The value itself is NEVER checked online — a bad key
|
||||
surfaces on the first call, never here (§1 honesty: the preflight claims no
|
||||
more than it proves).
|
||||
* **model_map** — the requested profile exists, and every id it resolves to
|
||||
(THROUGH ``resolve_model``, so the ``default`` fall-through is covered) is a
|
||||
real id, not a placeholder left in the config.
|
||||
|
|
@ -107,15 +109,15 @@ def _looks_like_placeholder(value: str) -> bool:
|
|||
|
||||
|
||||
def _check_credentials(env: Mapping[str, str]) -> list[Refusal]:
|
||||
"""A usable credential is present (§10) — the VALUE is never checked online.
|
||||
"""An own API key is present (§10) — the VALUE is never checked online.
|
||||
|
||||
``ANTHROPIC_API_KEY`` is the named contract; the bundled CLI's own
|
||||
``CLAUDE_CODE_OAUTH_TOKEN`` also satisfies it (run_s10 relies on the CLI's
|
||||
credentials when no key is exported — refusing that would be a false alarm).
|
||||
A credential set to a placeholder form is refused; NEITHER set is refused.
|
||||
``ANTHROPIC_API_KEY`` is the ONLY accepted credential. A consumer Claude Code
|
||||
subscription is not one: the bundled CLI's ``CLAUDE_CODE_OAUTH_TOKEN`` was
|
||||
accepted through v0.1.0 and is refused from v0.1.1 on, so no run here can be
|
||||
paid for by an operator's personal subscription. A key set to a placeholder
|
||||
form is refused; an unset key is refused.
|
||||
"""
|
||||
key = env.get("ANTHROPIC_API_KEY", "").strip()
|
||||
oauth = env.get("CLAUDE_CODE_OAUTH_TOKEN", "").strip()
|
||||
if key:
|
||||
if _looks_like_placeholder(key):
|
||||
return [
|
||||
|
|
@ -126,14 +128,13 @@ def _check_credentials(env: Mapping[str, str]) -> list[Refusal]:
|
|||
)
|
||||
]
|
||||
return []
|
||||
if oauth and not _looks_like_placeholder(oauth):
|
||||
return [] # the bundled CLI authenticates via its OAuth token
|
||||
return [
|
||||
Refusal(
|
||||
"credential",
|
||||
"no API credential: export ANTHROPIC_API_KEY (or authenticate the bundled "
|
||||
"CLI, which sets CLAUDE_CODE_OAUTH_TOKEN). The value is never checked online "
|
||||
"— only that a credential is present.",
|
||||
"no API credential: export ANTHROPIC_API_KEY. It is the only accepted "
|
||||
"credential — a Claude Code subscription token (CLAUDE_CODE_OAUTH_TOKEN) is "
|
||||
"NOT accepted and must not be used to pay for a run. The value is never "
|
||||
"checked online — only that the key is set.",
|
||||
)
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
"""The shippable run entrance (C2.0): merge inbox → seed → fold → run (§3, §5).
|
||||
|
||||
Where ``run_s10.py`` is the byte-frozen fasit of the programme's ONE live run
|
||||
(never imported by the suite), this module is the generic, deliverable
|
||||
Where ``run_s10.py`` is the fasit of the programme's ONE live run (never
|
||||
imported by the suite; byte-frozen through v0.1.0, and since v0.1.1 carrying
|
||||
exactly one change — the credential refusal), this module is the generic, deliverable
|
||||
entrance the README's inbox claim points at. The composition
|
||||
(``compose_run_context``) is pure config/file logic and offline-testable: it
|
||||
ingests the inbox READ-only (role split §3 Step 7), seeds from the bundle, and
|
||||
|
|
|
|||
|
|
@ -53,10 +53,18 @@ def main(argv: list[str] | None = None) -> int:
|
|||
parser.add_argument("--top-k", type=int, default=3)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if not os.environ.get("ANTHROPIC_API_KEY"):
|
||||
# The bundled CLI resolves its own credentials (Claude Code login) when
|
||||
# no key is exported; an unauthenticated run fails on the FIRST call.
|
||||
print("note: ANTHROPIC_API_KEY not set — relying on the CLI's own credentials.")
|
||||
if not os.environ.get("ANTHROPIC_API_KEY", "").strip():
|
||||
# REFUSAL, never a fallback: an own API key is the only credential this
|
||||
# run accepts. Through v0.1.0 an unset key merely printed a note and the
|
||||
# run carried on, letting the bundled CLI resolve its own login — which
|
||||
# would have charged a consumer subscription. The gate fires FIRST, so
|
||||
# nothing is opened, built or spent before it.
|
||||
print(
|
||||
"REFUSED: ANTHROPIC_API_KEY is not set. It is the only accepted credential; "
|
||||
"a Claude Code subscription token (CLAUDE_CODE_OAUTH_TOKEN) is NOT accepted "
|
||||
"and must not be used to pay for a run. No model call was made."
|
||||
)
|
||||
return 2
|
||||
|
||||
# §10: ALL startup contracts schema-validated BEFORE any model client exists.
|
||||
contracts = load_contracts(
|
||||
|
|
|
|||
81
tests/test_api_key_only_loadbearing.py
Normal file
81
tests/test_api_key_only_loadbearing.py
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
"""The ONLY accepted credential is an own API key — LOAD-BEARING (§11, §1).
|
||||
|
||||
The seam this file keeps alive: no run path in this framework may be paid for by
|
||||
a consumer Claude Code subscription. Until v0.1.1 ``run_s10`` printed a ``note:``
|
||||
when ``ANTHROPIC_API_KEY`` was unset and carried on, letting the bundled CLI
|
||||
resolve its own login credentials. That silent fallback is the defect; the run
|
||||
must REFUSE instead.
|
||||
|
||||
Why a subprocess and not an import: ``run_s10`` is the fasit run entrance and is
|
||||
never imported by this suite. Driving it as a module proves the real entrance,
|
||||
not a model of it — and the probe is network-free by construction, because the
|
||||
refusal is asserted to fire BEFORE the bundle is even opened.
|
||||
|
||||
Detach proof (RED when the refusal is removed): with no key and a bundle path
|
||||
that does not exist, the refusal must win the race against the bundle error. Drop
|
||||
the refusal and the process reaches ``navigate_bundle`` instead — a different exit
|
||||
code, no refusal line, and the ``note:`` fallback back in the output.
|
||||
|
||||
Positive control: the SAME invocation WITH a key set gets past the credential gate
|
||||
and fails on the missing bundle instead — so the gate is a gate, not a wall.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
MISSING_BUNDLE = "/nonexistent-bundle-this-path-must-not-exist"
|
||||
# Shape only; never validated online and never leaves the process — the run is
|
||||
# asserted to stop on the missing bundle, long before any client is built.
|
||||
_FAKE_KEY = "sk-ant-api03-drill-not-a-real-key"
|
||||
|
||||
|
||||
def _run_s10(env_overrides: dict[str, str]) -> subprocess.CompletedProcess[str]:
|
||||
env = dict(os.environ)
|
||||
env.pop("ANTHROPIC_API_KEY", None)
|
||||
env.pop("CLAUDE_CODE_OAUTH_TOKEN", None)
|
||||
env.update(env_overrides)
|
||||
env["PYTHONPATH"] = str(REPO_ROOT / "src")
|
||||
return subprocess.run(
|
||||
[sys.executable, "-m", "portfolio_optimiser_claude.run_s10", "--bundle", MISSING_BUNDLE],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
cwd=REPO_ROOT,
|
||||
env=env,
|
||||
timeout=120,
|
||||
)
|
||||
|
||||
|
||||
class TestMissingApiKeyIsRefused:
|
||||
def test_no_key_refuses_before_anything_else_runs(self) -> None:
|
||||
proc = _run_s10({})
|
||||
combined = proc.stdout + proc.stderr
|
||||
assert proc.returncode == 2, combined
|
||||
assert "ANTHROPIC_API_KEY" in combined
|
||||
assert "index.md" not in combined # the bundle was never opened
|
||||
|
||||
def test_no_key_never_falls_back_to_the_cli_login(self) -> None:
|
||||
combined_out = _run_s10({})
|
||||
combined = combined_out.stdout + combined_out.stderr
|
||||
assert "note:" not in combined
|
||||
assert "CLI's own credentials" not in combined
|
||||
|
||||
def test_a_subscription_token_alone_is_not_enough(self) -> None:
|
||||
proc = _run_s10({"CLAUDE_CODE_OAUTH_TOKEN": "sk-ant-oat01-real"})
|
||||
combined = proc.stdout + proc.stderr
|
||||
assert proc.returncode == 2, combined
|
||||
assert "ANTHROPIC_API_KEY" in combined
|
||||
|
||||
|
||||
class TestPositiveControl:
|
||||
def test_a_key_clears_the_gate_and_the_run_proceeds_to_the_bundle(self) -> None:
|
||||
# Proves the refusal is conditional on the key, not unconditional: with a
|
||||
# key the process gets PAST the gate and fails on the missing bundle.
|
||||
proc = _run_s10({"ANTHROPIC_API_KEY": _FAKE_KEY})
|
||||
combined = proc.stdout + proc.stderr
|
||||
assert proc.returncode != 2, combined
|
||||
assert "index.md" in combined
|
||||
|
|
@ -16,7 +16,8 @@ present here.
|
|||
|
||||
What the harness then showed is sharper than "pinned only at the edge". Replacing the WHOLE
|
||||
rounding expression with the constant ``999.0`` left all 984 tests green at three of the four
|
||||
sites (``run.py``:159, ``run_s10.py``:110 and :130) — those branches are never executed with a
|
||||
sites (``run.py``:159, ``run_s10.py``:118 and :138 — the two moved by +8 when v0.1.1
|
||||
added the credential refusal above them) — those branches are never executed with a
|
||||
cost at all, so their green under a detach was never evidence about the rounding (økt 39,
|
||||
Verifiseringsloven ansikt 4 applied to the measuring apparatus). Only ``costsim.py`` was
|
||||
covered. The default branch WAS pinned (``getattr(..., None)`` -> ``0.0`` is red in
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ def _counting_factory() -> tuple[ClientFactory, list[ScriptedClient]]:
|
|||
|
||||
|
||||
def _clear_credentials(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# ANTHROPIC_API_KEY is the only accepted credential; the subscription token
|
||||
# is cleared as env hygiene, so an ambient one cannot colour any result.
|
||||
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
|
||||
monkeypatch.delenv("CLAUDE_CODE_OAUTH_TOKEN", raising=False)
|
||||
|
||||
|
|
|
|||
|
|
@ -64,10 +64,25 @@ class TestCredentialContract:
|
|||
# the boundary; a bad key surfaces on the FIRST call, never here (§1).
|
||||
assert _check_credentials({"ANTHROPIC_API_KEY": "obviously-not-a-real-key-42"}) == []
|
||||
|
||||
def test_the_bundled_cli_oauth_token_satisfies_the_credential(self) -> None:
|
||||
# run_s10 relies on the bundled CLI's own credentials when no key is
|
||||
# exported; refusing that would be a false alarm (§1 honesty).
|
||||
assert _check_credentials({"CLAUDE_CODE_OAUTH_TOKEN": "sk-ant-oat01-real"}) == []
|
||||
def test_the_bundled_cli_oauth_token_is_refused(self) -> None:
|
||||
# A consumer-subscription token is NOT a credential this framework
|
||||
# accepts. Until v0.1.1 it cleared the check, so a run could be paid for
|
||||
# by the operator's Claude Code login instead of an own API key. The
|
||||
# only accepted credential is ANTHROPIC_API_KEY (§1: the refusal says so).
|
||||
refusals = _check_credentials({"CLAUDE_CODE_OAUTH_TOKEN": "sk-ant-oat01-real"})
|
||||
assert any(r.check == "credential" for r in refusals)
|
||||
assert any("ANTHROPIC_API_KEY" in r.detail for r in refusals)
|
||||
|
||||
def test_an_oauth_token_does_not_weaken_a_missing_key(self) -> None:
|
||||
# The two are not interchangeable: setting BOTH is fine (the key decides),
|
||||
# but the token alone never substitutes for the key.
|
||||
assert _check_credentials({"ANTHROPIC_API_KEY": _REAL_KEY}) == []
|
||||
assert (
|
||||
_check_credentials(
|
||||
{"ANTHROPIC_API_KEY": _REAL_KEY, "CLAUDE_CODE_OAUTH_TOKEN": "sk-ant-oat01-real"}
|
||||
)
|
||||
== []
|
||||
)
|
||||
|
||||
|
||||
class TestPlaceholderModelIdRefused:
|
||||
|
|
@ -195,6 +210,8 @@ class TestCli:
|
|||
self, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
monkeypatch.setenv("ANTHROPIC_API_KEY", _REAL_KEY)
|
||||
# Cleared as env hygiene only — it is not a credential here (see
|
||||
# TestCredentialContract); an ambient token must not colour the result.
|
||||
monkeypatch.delenv("CLAUDE_CODE_OAUTH_TOKEN", raising=False)
|
||||
rc = main(["--profile", "anthropic"])
|
||||
out = capsys.readouterr().out
|
||||
|
|
@ -206,7 +223,9 @@ class TestCli:
|
|||
self, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
|
||||
monkeypatch.delenv("CLAUDE_CODE_OAUTH_TOKEN", raising=False)
|
||||
# A subscription token SET is the interesting case: through v0.1.0 it
|
||||
# cleared the CLI too, so this exit was 0. It must not rescue the run.
|
||||
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", "sk-ant-oat01-real")
|
||||
rc = main(["--profile", "anthropic"])
|
||||
out = capsys.readouterr().out
|
||||
assert rc == 1
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@
|
|||
The seam this file keeps alive: the SHIPPABLE entrance (``run.py``) composes
|
||||
the §5 sequence (merge inbox → seed → fold) and drives the same orchestration
|
||||
the fasit run used — so the README's inbox claim is true of a deliverable
|
||||
path. ``run_s10.py`` stays byte-frozen and is still never imported here.
|
||||
path. ``run_s10.py`` is still never imported here; it was byte-frozen through v0.1.0
|
||||
and carries exactly one change since — the credential refusal, proved by
|
||||
``test_api_key_only_loadbearing.py`` against the real entrance as a subprocess.
|
||||
|
||||
Detach proofs: drop the merge call from the composition → the inbox verdict
|
||||
never reaches the composed context → red. Drop the budget-stop persistence
|
||||
|
|
|
|||
2
uv.lock
generated
2
uv.lock
generated
|
|
@ -590,7 +590,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "portfolio-optimiser-claude"
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "claude-agent-sdk" },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue