Skip to content
This repository was archived by the owner on Oct 24, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 26 additions & 39 deletions scripts/live_loop_proof.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 ""
)
Expand Down Expand Up @@ -377,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")
Expand Down Expand Up @@ -499,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")

Expand Down Expand Up @@ -569,39 +574,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
Comment thread
coderabbitai[bot] marked this conversation as resolved.

try:
run(args.api_url.rstrip("/"), args.output_dir)
Expand Down
66 changes: 45 additions & 21 deletions tests/test_quickstart_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,37 +300,30 @@ 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,
)


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()
Expand All @@ -348,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"))
Expand Down