diff --git a/docs/phase-1-exposure-runbook.md b/docs/phase-1-exposure-runbook.md index 38bcdc2..161d1e0 100644 --- a/docs/phase-1-exposure-runbook.md +++ b/docs/phase-1-exposure-runbook.md @@ -194,10 +194,11 @@ If that hangs or refuses while the Pangolin dashboard shows the site online, the *target* is wrong — look at the resource's upstream address (§3.5), not at newt. Note this check needs §3.1 done first: if `mempalace-serve` is not running yet, it fails for that reason alone. -### 3.4 DNS at the web hotel +### 3.4 DNS at the web hotel — ✅ done 2026-08-12 One CNAME: `mempalace` → **the same target your existing Pangolin resources use** (nyvaken's public hostname). RFC §6.2 costed this as *"one DNS record per service on the web hotel is the whole setup cost."* +Done: `mempalace.jordbo.se` resolves and terminates TLS at Pangolin on nyvaken — the §6.2 estimate held. ⚠️ Not verified from here: nyvaken's public FQDN, and whether your web hotel permits a CNAME at that label (some require an A record, or forbid CNAME where other records exist). Confirm before assuming a 5-minute job. @@ -212,20 +213,56 @@ hostname). RFC §6.2 costed this as *"one DNS record per service on the web hote looks perfectly healthy on `curl 172.17.0.1:8765/healthz`. (Got this wrong on the first attempt 2026-08-12, because this line used to omit the scheme.) - **Auth: none at the Pangolin layer** (§1.1). TLS termination only. +- ⚠️ **If resource auth is left on, the signature is a `302`, not a 401 or 403** — hit for real 2026-08-12: + ``` + HTTP/2 302 + location: https://pangolin.jordbo.se/auth/resource/?redirect=https%3A%2F%2Fmempalace.jordbo.se%2Fhealthz + content-length: 0 + ``` + This is §1.1's "browser-shaped auth" arriving as a concrete symptom: Pangolin sends the login redirect + **before** proxying, so the palace never sees the request and its journal stays silent. `curl -s` shows + an empty body and an MCP client sees non-JSON. Diagnose with `-D-` or + `-w '%{http_code} %{redirect_url}'` — a `location:` pointing at `/auth/resource/…` means the fix is in + the Pangolin UI (switch the site's authentication off), not in the palace, the unit, or newt. + **A 302 is unambiguously good news:** DNS, TLS and routing all worked — only the auth layer intervened. - Do **not** attach an `Origin`-injecting proxy or browser client: a *present* non-loopback `Origin` is a hard 403 with no override (runbook §2.4 B3). -### 3.6 Verify end-to-end before flipping any client +**The server's entire HTTP surface is two exact paths**, so path-scoped rules cover it completely +(`mcp_server.py:5299-5318`, read 2026-08-12): + +| Method + path | Auth | Notes | +| --- | --- | --- | +| `GET /healthz` | none (Host/Origin gated only) | the liveness probe; works with no creds by design | +| `POST /mcp` | `Authorization: Bearer `, `hmac.compare_digest` on the exact string | the whole tool surface | +| anything else | — | `send_error(404)` from the palace itself | + +Three consequences worth having in writing: + +- **Path-scoped Pangolin rules are not a compromise here, they are tighter than a host-wide proxy** and + lose nothing — there is no third endpoint to forget. +- **`/mcp` is matched exactly** (`if path != "/mcp"`), so a client URL with a trailing slash gets a 404 + from the palace. Configure clients as `https://mempalace.jordbo.se/mcp` — no trailing slash. +- **There is no `GET /mcp`, no SSE, no session id, no `DELETE`.** This is plain JSON-RPC over POST, not MCP + streamable-HTTP. A strict client that opens with a `GET` handshake will see 404; pi's extension, + opencode `type:remote` and the feeder all POST directly and are fine. + +### 3.6 Verify end-to-end before flipping any client — ✅ passed 2026-08-12 ```sh -curl -s https://mempalace.jordbo.se/healthz # ok +curl -s https://mempalace.jordbo.se/healthz # ok ✓ from devbox AND synlig curl -s -o /dev/null -w '%{http_code}\n' -X POST \ - https://mempalace.jordbo.se/mcp # 401 — the token is doing its job + https://mempalace.jordbo.se/mcp # 401 ✓ the token is doing its job +TOKEN=$(cat ~/.mempalace/server/f5d849287f6d73f0141b29d7/token) # on synlig curl -s -X POST https://mempalace.jordbo.se/mcp \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | head -c 300 # 36 tools ``` +No `initialize` and no `Accept: text/event-stream` needed — see the surface table above; a bare +`tools/list` POST is a complete request. Run the 200 and the 401 from **two different networks** (a client +box and the primary itself): passing from only one leaves split-horizon DNS untested. + The 401 check matters as much as the 200: it is the only evidence that the thing you just published to the internet is not open. Then, and only then, Phase 1 client flip — **one machine first** (RFC §8), and remember opencode containers need the §4.1 sidecar merge before their `.env` takes effect.