From 9a0927aaf2bdbcbc406b1261e116ac1017ee2676 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Aug 2026 01:29:13 +0000 Subject: [PATCH 1/3] refactor(demo): make the live-loop proof loopback-only Remove --allow-remote. The proof self-provisions a key and sends it on every request, and self-provision exists only on dev-like postures (production-like deployments refuse it), so there was no supported remote target: --allow-remote could only reach a remote instance that had self-provision enabled, a configuration with no real user. Rather than carry and document a flag for a speculative case, refuse any non-loopback host before a credential is minted. This also removes the cleartext-credential foot-gun entirely (no non-loopback path exists to send a key over http), shrinking the surface to exactly what the golden path uses. The URL sanitizer stays: a loopback URL can still carry userinfo, and the transcript ships to a partner. Tests: the guard test now asserts a non-loopback host (http or https) is refused with exit 2 and writes no bundle; the unreachable-loopback and sanitizer tests are unchanged. Full quickstart-path suite green (5 passed). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01MpjPj59M4n6boNmbs7CpGR --- scripts/live_loop_proof.py | 54 +++++++++++------------------------ tests/test_quickstart_path.py | 26 +++++------------ 2 files changed, 24 insertions(+), 56 deletions(-) diff --git a/scripts/live_loop_proof.py b/scripts/live_loop_proof.py index 67cf8a6a..f79ee1fe 100644 --- a/scripts/live_loop_proof.py +++ b/scripts/live_loop_proof.py @@ -29,10 +29,9 @@ Self-provision only exists on dev-like postures — production-like deployments (staging, preprod, preview, ...) refuse it — so this script is -loopback-only unless ``--allow-remote`` is passed, and then only against a -remote instance that has self-provision enabled. Non-loopback targets must -use ``https://``: the run mints an API key and sends it on every request, -which must never cross the network in the clear. +loopback-only by design. The run mints an API key and sends it on every +request; refusing any non-loopback target keeps that credential on the local +host and off the network entirely. """ from __future__ import annotations @@ -204,9 +203,8 @@ def authenticate(self) -> dict[str, str]: json={"agent_id": f"live-loop-proof-{self.run_id}"}, ) hint = ( - " -- a production-like target (staging/preprod/preview) refuses " - "self-provision by design; point --allow-remote at a remote " - "instance with ENABLE_DEV_KEY_SELF_PROVISION=true" + " -- self-provision is refused on production-like targets by " + "design; run this against a local `make quickstart` server" if minted.status_code == 403 else "" ) @@ -569,39 +567,21 @@ def main(argv: list[str] | None = None) -> int: default=DEFAULT_OUTPUT_DIR, help=f"Where to write the handoff bundle (default: {DEFAULT_OUTPUT_DIR}).", ) - parser.add_argument( - "--allow-remote", - action="store_true", - help=( - "Permit a non-loopback --api-url (which must then use https://). " - "Only for a remote instance with self-provision enabled -- " - "production-like hosts (staging, preprod, preview, ...) refuse " - "self-provision by design, so this mode cannot authenticate there." - ), - ) args = parser.parse_args(argv) - parsed = urlparse(args.api_url) - host = parsed.hostname or "" + # Loopback only, by design: the run self-provisions a key and sends it on + # every request, and self-provision exists only on dev-like postures. There + # is no supported remote target, so any non-loopback host is refused before + # a credential is minted. + host = urlparse(args.api_url).hostname or "" if host not in LOOPBACK_HOSTS: - if not args.allow_remote: - print( - f"refusing non-loopback target {args.api_url!r} without " - "--allow-remote", - file=sys.stderr, - ) - return 2 - # The run mints an API key and sends it in X-API-Key on every request; - # a non-loopback hop must be encrypted or that credential (and every - # governed call) crosses the network in the clear. - if parsed.scheme != "https": - print( - f"refusing cleartext {parsed.scheme or 'http'}:// for " - f"non-loopback target {args.api_url!r}; use https:// so the " - "minted key is not sent over the wire unencrypted", - file=sys.stderr, - ) - return 2 + print( + f"refusing non-loopback target {args.api_url!r}: the live-loop " + "proof is loopback-only; run it against a local `make quickstart` " + "server", + file=sys.stderr, + ) + return 2 try: run(args.api_url.rstrip("/"), args.output_dir) diff --git a/tests/test_quickstart_path.py b/tests/test_quickstart_path.py index c75775a9..0280e248 100644 --- a/tests/test_quickstart_path.py +++ b/tests/test_quickstart_path.py @@ -311,26 +311,14 @@ def _run_proof_script(*args: str, timeout: int = 30) -> subprocess.CompletedProc ) -def test_live_loop_proof_refuses_unsafe_remote_targets(tmp_path): - """The guard must fail closed before minting or sending any credential. - - Both rejections happen before any network call, so no server is needed. - """ +def test_live_loop_proof_refuses_non_loopback_target(tmp_path): + """The proof is loopback-only: any non-loopback host is refused before a + credential is minted, over http or https alike. No server is needed.""" out = tmp_path / "out" - # Non-loopback without opting in. - without_optin = _run_proof_script( - "--api-url", "https://example.com", "--output-dir", str(out) - ) - assert without_optin.returncode == 2 - assert "without --allow-remote" in without_optin.stderr - - # Non-loopback over cleartext, even with --allow-remote: the minted key - # would cross the network unencrypted, so it must be refused. - cleartext = _run_proof_script( - "--api-url", "http://example.com", "--allow-remote", "--output-dir", str(out) - ) - assert cleartext.returncode == 2 - assert "cleartext" in cleartext.stderr + for url in ("https://example.com", "http://example.com"): + result = _run_proof_script("--api-url", url, "--output-dir", str(out)) + assert result.returncode == 2, result.stdout + result.stderr + assert "loopback-only" in result.stderr # Fail closed means no handoff directory, not a half-written bundle. assert not out.exists() From 0ec4d9d48e9d28183d6fa025feb027f0419f5e08 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Aug 2026 01:38:14 +0000 Subject: [PATCH 2/3] fix(demo): keep the minted key off ambient proxies (trust_env=False) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Loopback-only stops a remote *target*, but httpx still honors ambient HTTP_PROXY/HTTPS_PROXY/ALL_PROXY by default (trust_env=True), which could route the X-API-Key-bearing requests off the loopback host through a proxy. Disable env trust on both the main client and the trust-keys fetch so the whole loopback-only guarantee holds regardless of the caller's proxy environment. Regression test: with HTTP(S)_PROXY pointed at a dead port and NO_PROXY cleared, the run still completes (exit 0, bundle written) — which is only possible if proxy inheritance is off. Re-enabling trust_env would route the requests through the dead proxy and fail the test. Verified: quickstart-path suite 6 passed; ruff and check_doc_references clean. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01MpjPj59M4n6boNmbs7CpGR --- scripts/live_loop_proof.py | 11 ++++++++-- tests/test_quickstart_path.py | 40 +++++++++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/scripts/live_loop_proof.py b/scripts/live_loop_proof.py index f79ee1fe..33667d8c 100644 --- a/scripts/live_loop_proof.py +++ b/scripts/live_loop_proof.py @@ -375,7 +375,9 @@ def trust_keys(self) -> Path: # base_url with a trailing slash, and Starlette does not collapse # "//.well-known/..." to the single-slash route. keys_url = self.client.base_url.join(".well-known/trust-keys.json") - keys = httpx.get(str(keys_url), timeout=30) + # trust_env=False for the same reason as the main client: keep every + # request on the loopback host, never through an ambient proxy. + keys = httpx.get(str(keys_url), timeout=30, trust_env=False) require(keys.status_code == 200, "trust-keys.json not served") path = self.output_dir / "trust-keys.json" path.write_text(keys.text, encoding="utf-8") @@ -497,7 +499,12 @@ def run(api_url: str, output_dir: Path) -> None: # partner as a single coherent bundle. for name in ARTIFACTS: (output_dir / name).unlink(missing_ok=True) - with httpx.Client(base_url=api_url, timeout=30) as client: + # trust_env=False: the run mints an API key and sends it in X-API-Key on + # every request. httpx honors ambient HTTP_PROXY/HTTPS_PROXY/ALL_PROXY by + # default, which could route that credential off the loopback host through + # a proxy. Disabling env trust keeps the whole loopback-only guarantee + # airtight regardless of the caller's proxy environment. + with httpx.Client(base_url=api_url, timeout=30, trust_env=False) as client: proof = ProofRun(client, output_dir) print(f"live loop proof {proof.run_id} against {api_url}\n") diff --git a/tests/test_quickstart_path.py b/tests/test_quickstart_path.py index 0280e248..ca33197d 100644 --- a/tests/test_quickstart_path.py +++ b/tests/test_quickstart_path.py @@ -300,11 +300,16 @@ def test_documented_quickstart_path(quickstart_server, tmp_path): client.close() -def _run_proof_script(*args: str, timeout: int = 30) -> subprocess.CompletedProcess: +def _run_proof_script( + *args: str, timeout: int = 30, extra_env: dict | None = None +) -> subprocess.CompletedProcess: + env = _prepend_pythonpath(REPO_ROOT) + if extra_env: + env.update(extra_env) return subprocess.run( [sys.executable, str(REPO_ROOT / "scripts" / "live_loop_proof.py"), *args], cwd=REPO_ROOT, - env=_prepend_pythonpath(REPO_ROOT), + env=env, capture_output=True, text=True, timeout=timeout, @@ -336,6 +341,37 @@ def test_live_loop_proof_reports_unreachable_server(tmp_path): assert "make quickstart" in result.stderr +def test_live_loop_proof_ignores_ambient_proxy(quickstart_server, tmp_path): + """The minted key must stay on loopback even under a proxy environment. + + With HTTP(S)_PROXY pointed at a dead port and NO_PROXY cleared, the run + can only succeed if it does NOT honor those proxy vars — i.e. the client + is built with trust_env=False. If proxy inheritance were left on, httpx + would route the X-API-Key requests through the dead proxy and the run + would fail. This is the regression guard for that credential-hygiene + property. + """ + dead_proxy = f"http://127.0.0.1:{_free_port()}" + proxy_env = { + var: dead_proxy + for var in ("HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", + "http_proxy", "https_proxy", "all_proxy") + } + # Clear any ambient loopback exclusion so the proxy vars would otherwise bite. + proxy_env["NO_PROXY"] = "" + proxy_env["no_proxy"] = "" + + output_dir = tmp_path / "handoff" + result = _run_proof_script( + "--api-url", quickstart_server, + "--output-dir", str(output_dir), + timeout=180, + extra_env=proxy_env, + ) + assert result.returncode == 0, result.stdout + result.stderr + assert (output_dir / "receipt-bundle.json").exists() + + def test_sanitize_url_for_record_strips_credentials(): """The transcript ships to partners; a secret in --api-url must not.""" sys.path.insert(0, str(REPO_ROOT / "scripts")) From 88b909332ea5ce43b7912eb774be536d6dfe3fce Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Aug 2026 04:38:19 +0000 Subject: [PATCH 3/3] ci: trigger a fresh run to test Actions recovery (no-op) Empty commit to re-trigger CI on #296. The prior runs failed at Actions startup during a repository-wide platform/quota outage (main and unrelated PRs all red with no job logs); this probes whether the platform has recovered. No code change. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01MpjPj59M4n6boNmbs7CpGR