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>
81 lines
3.4 KiB
Python
81 lines
3.4 KiB
Python
"""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
|