Skip to content

Serve HTTPS on loopback so Safari can reach the runtime - #1

Merged
thinkinginmath merged 1 commit into
mainfrom
feat/https-listener
Sep 19, 2026
Merged

thinkinginmath merged 1 commit into
mainfrom
feat/https-listener

Conversation

@thinkinginmath

Copy link
Copy Markdown
Collaborator

Safari blocks HTTPS pages from calling http://127.0.0.1 (mixed content). Chrome and Firefox allow it. This adds an HTTPS listener on loopback with a narrow, locally generated certificate.

Measured on macOS, 2026-09-19. The test page ran from a public HTTPS origin (the SciMigo CDN) and paired, ran checks, streamed output and interrupted.

Browser http://127.0.0.1:9477 https://127.0.0.1:9478, trusted cert
Chrome 152 ✓ after the Local Network Access prompt –
Firefox 155 ✓ –
Safari 26.6 ✗ TypeError: Load failed ✓

What's in it

  • agent-runtime tls setup | status | remove
    • Creates <runtime dir>/tls/localhost.pem and its key (key 0600, dir 0700).
    • The certificate is RSA 2048 with SAN 127.0.0.1, ::1 and localhost only, serverAuth only, CA:FALSE and no keyCertSign. It's valid for 397 days, and setup renews it within 30 days of expiry.
    • On macOS, setup adds it to the login keychain with SSL-only trust (security add-trusted-cert -r trustRoot -p ssl), which asks for the user's password.
    • Other platforms get no trust step, since their browsers already accept http loopback.
  • agent-runtime serve
    • Also listens on https://127.0.0.1:9478 when the certificate exists (--https-port, --no-https, AGENT_RUNTIME_HTTPS_PORT).
    • Both listeners share one app object, so pairing, tokens and kernels are shared and enforced the same way. Only the HTTP listener runs the lifespan.
    • One signal handler stops both listeners (serving.py). Each uvicorn server installs its own handlers, and the last one would win.
    • It falls back to HTTP only, with a message, when 9478 is taken or the certificate has expired. --debug keeps the single auto-reload listener.
  • Docs
    • Removes the targetAddressSpace: "local" hint. Chrome 151 rejects it for 127.0.0.1, which is in the loopback address space, and no annotation is needed.
    • integration.md gets the browser table and an HTTPS-then-HTTP detection example.
    • security.md gets a certificate section; the installer mentions tls setup.
  • New dependency: cryptography>=42.

Tests

  • tests/test_tls.py: certificate names and constraints, validity, key permissions, reuse and renewal, the macOS keychain commands (mocked), and the CLI.
  • tests/test_serving.py: an end-to-end agent-runtime serve subprocess. It checks HTTP, verified HTTPS for 127.0.0.1 and localhost, that pairing is enforced on HTTPS (401 without a token), and that one SIGINT stops both listeners with exit 0.
  • 132 passed locally on Linux; ruff, format and mypy (strict) are clean. CI adds macOS.

Not tested here

The real keychain trust (tls setup on a Mac) hasn't been run through this code. The manual version it automates (same certificate profile, same security add-trusted-cert flags) is what made Safari pass. Please run agent-runtime tls setup on a Mac before merging.

🤖 Generated with Claude Code

Safari blocks an HTTPS page from calling http://127.0.0.1 as mixed content; Chrome and Firefox
allow it. Measured on macOS 2026-09-19 with a test page on a public HTTPS origin: Chrome 152 and
Firefox 155 passed against http://127.0.0.1:9477, Safari 26.6 failed ("Load failed"), and Safari
passed against https://127.0.0.1:9478 with a trusted self-signed loopback certificate.

- `agent-runtime tls setup` creates that certificate in <runtime dir>/tls (key 0600, dir 0700):
  RSA 2048, SAN 127.0.0.1 / ::1 / localhost, serverAuth only, CA:FALSE, 397 days. On macOS it
  adds it to the login keychain trusted for SSL only (macOS asks for the password). It reuses a
  valid certificate and renews one within 30 days of expiry. `tls status` shows expiry and
  trust; `tls remove` untrusts and deletes it.
- `agent-runtime serve` also listens on https://127.0.0.1:9478 (--https-port, --no-https,
  AGENT_RUNTIME_HTTPS_PORT) when the certificate exists. Both listeners serve the same app, so
  pairing and tokens are shared and enforced alike. One signal handler stops both: uvicorn
  servers each install their own, and the last one would win. It falls back to HTTP only if the
  port is taken or the certificate expired, and says why. --debug keeps the single auto-reload
  listener.
- Docs: the `targetAddressSpace: "local"` hint is removed. Chrome 151 rejects it for 127.0.0.1,
  which is in the loopback address space, and no annotation is needed. integration.md has the
  measured browser table and an HTTPS-then-HTTP detection example; security.md covers the
  certificate; the installer mentions `tls setup`.
- New dependency: cryptography>=42.

Tests: certificate properties, reuse and renewal, the macOS keychain commands (mocked), the CLI,
and an end-to-end `serve` run. It checks HTTP, verified HTTPS for 127.0.0.1 and localhost, and
pairing enforced on HTTPS, and that one SIGINT stops both listeners. 132 passed; ruff, format
and mypy clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thinkinginmath
thinkinginmath merged commit 17ef67b into main Sep 19, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant