feat(access): reach the engines from any PC through the Cloudflare tunnel - #44
Conversation
…nnel
The pilot could only talk to localhost and Tailscale: the manifest listed no
cloud hostname (so MV3 blocked the request before any code ran) and CF-Access
appeared in zero files under src/. The tunnel has worked since t283 — this is
the client half.
- src/state/cf-access.ts: the service-token store on its own storage key.
Deliberately NOT in access-config.ts, whose contract says it never stores a
secret; widening that quietly would have been the wrong move.
- streamable-http-client.js: accessHeaders merged into mcpHeaders() and all
three REST sites, with the JSDoc typedef updated so TS checks it.
- adapter-factory: one injection point at resolveAdapterConfig's single exit,
so every caller is covered and no site can forget.
- manifest: + kernel./api.my-tiny-data-collider.nl.
SCOPING IS THE SAFETY PROPERTY, and it is tested: cfAccessHeadersFor()
returns {} unless the URL is https AND the host ends in the collider domain.
9 cases pass — cloud kernel/api get the pair; localhost, Tailscale, plain
http, a disabled token, no token, a garbage URL, and a LOOKALIKE domain
(kernel.my-tiny-data-collider.nl.evil.com) all get nothing. Local operation
is byte-identical to before.
The token is edge passage only. Writes stay gated by the kernel bearer; the
two must never be interchanged.
Verified: typecheck clean, build green, smoke:live PASS.
authored-by: agent:claude-cowork.hp-z440 / session:sam.z440-cowork-workspace / t283-cf-access
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adds Cloudflare Access service-token support to the Pilot’s HTTP/MCP client path so the extension can reach cloud-hosted engines (via the Cloudflare tunnel) from any machine, while keeping local/Tailscale behavior unchanged.
Changes:
- Introduces a dedicated Cloudflare Access token store (
chrome.storage.local) and URL scoping helper (cfAccessHeadersFor). - Threads
accessHeadersthrough the streamable HTTP adapter/client so MCP + REST requests can include the Cloudflare Access headers when appropriate. - Expands MV3
host_permissionsto allow requests tokernel.my-tiny-data-collider.nlandapi.my-tiny-data-collider.nl.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/state/cf-access.ts | Adds Cloudflare Access token persistence and scoped header construction. |
| src/mcp/streamable-http-client.js | Merges Access headers into MCP and REST requests. |
| src/mcp/streamable-http-adapter.ts | Adds accessHeaders to adapter config and forwards into the shared client. |
| src/mcp/adapter-factory.ts | Injects Access headers during adapter config resolution based on engine URL. |
| public/manifest.json | Adds cloud host permissions and reformats permissions/resources arrays. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| * STORAGE TIER. `chrome.storage.local` is extension-local and not synced. It is readable by | ||
| * anything with access to this profile — same trust tier as the browser's own password store, | ||
| * and strictly better than the alternative of embedding the pair in a committed config. The | ||
| * pair is only ever sent to the two configured Cloudflare hostnames (see `isCloudHost`). |
| const cf = await loadCfAccess(); | ||
| const headers = cfAccessHeadersFor(config.engineUrl ?? "", cf); | ||
| if (Object.keys(headers).length > 0) config.accessHeaders = headers; | ||
| } catch { |
The middle one was a real defect, not a nit: accessHeaders are baked into an adapter at build time, but the worker only dropped its memo on pilot.engine changes — so ROTATING OR DISABLING the token left live adapters sending the old credential until the service worker happened to restart. The memo now invalidates on pilot.cfaccess too, which is what makes the enabled:false off switch actually mean something. - isCfAccessSet is a type predicate, removing the cfg! assertions. - The doc no longer claims chrome.storage.local is the browser password store (it is not encrypted at rest) and no longer says 'two hostnames' when isCloudHost is a suffix match over the whole zone. Both said plainly, with the rotation note that the Access policy binds the token by name. Verified: typecheck + build clean, smoke:live PASS, scoping still 9/9 (cloud yes; localhost, Tailscale, http, disabled, absent, garbage, and a lookalike domain all no). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
All three addressed in Stale token (the bug). You're right that Type predicate. Overstated doc. Both claims corrected rather than softened: Verified: typecheck + build clean, |
The pilot could only talk to localhost and Tailscale: the manifest listed no
cloud hostname (so MV3 blocked the request before any code ran) and CF-Access
appeared in zero files under src/. The tunnel has worked since t283 — this is
the client half.
Deliberately NOT in access-config.ts, whose contract says it never stores a
secret; widening that quietly would have been the wrong move.
three REST sites, with the JSDoc typedef updated so TS checks it.
so every caller is covered and no site can forget.
SCOPING IS THE SAFETY PROPERTY, and it is tested: cfAccessHeadersFor()
returns {} unless the URL is https AND the host ends in the collider domain.
9 cases pass — cloud kernel/api get the pair; localhost, Tailscale, plain
http, a disabled token, no token, a garbage URL, and a LOOKALIKE domain
(kernel.my-tiny-data-collider.nl.evil.com) all get nothing. Local operation
is byte-identical to before.
The token is edge passage only. Writes stay gated by the kernel bearer; the
two must never be interchanged.
Verified: typecheck clean, build green, smoke:live PASS.
authored-by: agent:claude-cowork.hp-z440 / session:sam.z440-cowork-workspace / t283-cf-access
Co-Authored-By: Claude Fable 5 noreply@anthropic.com