Two kinds of caller, two path prefixes
Avatars has two audiences. Dad uses the studio in a browser, on a Mac, an iPad or a phone. My brains, my scripts and cwk-site's build scripts use it from code. They need different things, so they get different doors, and the path prefix says which door a request is knocking on.
Every request first passes one network gate: the peer must be loopback or a Tailscale address (the 100.64.0.0/10 range or Tailscale's IPv6 prefix). Anything else gets Use the family Tailscale connection. and a 403. After that, one middleware splits the traffic by prefix.
| Prefix | Who it is for | What it asks |
|---|---|---|
/api/* | The studio's own web UI and iOS app | Same origin, then the family PIN and Dad's admin capability (a few health and auth routes are exempt) |
/internal/* | Engines and scripts on the same Mac | A loopback peer carrying no browser or proxy headers |
The /internal doors are the ones this track is about. Options, subjects, plans, generations, batches, files, exports and the Sidekick's read-only view all live there. The track's later lessons walk through what they answer. This one is about who may knock.
The header test
require_internal asks two questions. Is the peer a loopback address? And does the request carry any of three headers: x-forwarded-for, origin or sec-fetch-mode? A loopback peer with none of them passes. Everything else gets A local engine request is required.
Each header names a kind of caller that is not an engine:
sec-fetch-modeis a fetch metadata header. Current browsers attach it to every request, so a page that tries to call an internal door from the user's browser announces itself. A browser old enough to predate Fetch Metadata sends none, and Track 8 comes back to that one corner.originis sent by browsers on cross-origin requests and on most POSTs. A script running in a web page cannot hide it.x-forwarded-foris the mark of a proxy. Avatars' HTTPS twin is a proxy on the same Mac, so its requests arrive from loopback. The forwarded header is what gives it away.
That last one is worth a second look, because the same header means opposite things on the two sides. client_ip(), which the /api side uses to ask Pippa's security service about the visiting device, trusts x-forwarded-for only when the peer is loopback: the proxy on this Mac is telling Avatars who is really visiting. On the /internal side that same header disqualifies the request. Pippa's reach module states the consequence in its docstring: the door refuses a tailnet peer and anything carrying X-Forwarded-For, so the twin can never stand in.
The test test_internal_boundary_rejects_browser_and_forwarded_clients pins it. From 127.0.0.1, a request with an origin, one with an x-forwarded-for and one with sec-fetch-mode: cors each get 403; the same request with no extra headers gets 200; and the same request from a non-loopback peer gets 403.
Away from the Mac: a tunnel to loopback
Avatars runs on the registered service host. Pippa's brains often run somewhere else. If the door only opens to loopback, how does a brain on another Mac reach it?
It brings loopback with it. Pippa's avatars_client.origin() asks the family reach module, which answers in order: a pinned address if AVATARS_ENGINE_URL is set, the local loopback if the family registry places Avatars on this Mac, and otherwise an ssh tunnel over the family control route to that Mac's own loopback. The tunnel is opened on demand and left open for the next call. It listens on the member's port plus 10000, the same spot Beacon's reach code uses, so the two share one tunnel. The request then arrives at Avatars from 127.0.0.1 with no forwarded header, which is exactly what the door asks for.
cwk-site's image upload check does the same. Before an image goes to the public CDN, the site asks Avatars' export-policy door about its hash (track 8 explains why). It finds the door through the same reach logic, run with the system Python: that binary is Apple-signed, so the ssh it starts stays outside macOS's Local Network gate.
The bug the header test caught
That export check first used Node's built-in fetch. Every check came back 403, and every new image was blocked. Node's fetch follows the web platform's rules and adds sec-fetch-mode: cors to every request, so to Avatars it looked like a browser. The fix, in the third code block, is a plain node:http GET that sends only the headers it is given. The door was right. The client was wearing a browser's clothes.
What the door does not check
Inside the door, the caller names its actor in actor_soul_id. Avatars uses that actor for visibility and ownership, and it does not ask for proof. The trust is placement. A process that can run on the Avatars Mac, or open a tunnel to it over the family's control route, is already inside the family. The door keeps out the two things that are not: a browser, and anything relayed through the HTTPS twin. If you copy this pattern, copy that assumption knowingly.
The tailnet itself is the first wall around all of this. Solo Auth Quest teaches why Tailscale is the first line and the PIN the second; this door is a third, narrower wall for machine callers.