Three zones on paper
Invariant 5 is the most ambitious sentence in Voyager's architecture: trust is a zone, and a zone is verified. It names three. This Mac and the home network's family origins. Dad's tailnet. The outside world. A tailnet origin is one whose host the local tailscaled names as a peer. A home origin is an exact origin the catalog lists for a fleet Mac, never "any address on the LAN", because guests share the Wi-Fi. Everything else is the outside world, and so is anything that cannot be verified.
Invariant 6 says what a zone buys. Family zones use the microphone and camera without a prompt, every further grant is added by name with its reason, and the outside world is asked as Safari asks. The friction it removes is the one Track 1 opened with: a permission prompt for Dad's own microphone, on his own apps.
One function decides
The first block is the whole policy. A TrustPolicy wraps a function from an origin (scheme, host, port) to a zone, handed in by whoever can check. With nothing handed in, every origin is the outside world. decision(for:) then looks at two origins, the frame that asks and the page it sits in, and grants only when neither is the outside world. A family frame inside an outside page is asked. So is an outside frame inside a family page. Content cannot raise its own zone by being embedded, or by embedding.
Two things are missing on purpose. The policy never answers .deny: the outside world gets WebKit's own prompt and Dad decides, so nothing is refused on his behalf, and a test named nothingIsRefusedOnDadsBehalf walks every kind to prove it. And the list of grantable things is closed. PermissionKind holds microphone, camera and both, and a new kind is added there by name, with its reason. Track 3 showed that enum, and the engine child that turns WebKit's capture question into this request. Trust also decides one quieter thing: Track 4's sleep policy lets only a tab of the outside world fall asleep.
The verifier Voyager installs
The second block is the only verifier the code ever builds. TrustPolicy.family(origins:) makes home of two things: this Mac by any loopback spelling (localhost, ::1, any 127. address), and the exact origins handed in. Everything else is the outside world. A default port is normalised first, because WebKit names HTTPS on 443 with no port while a catalog may write the port out.
The third block hands those origins in: the hub and Pippa that Settings reach, plus every web target in the current catalog projection. The shell calls it whenever Settings or the catalog move. On the Mac that happens in the same place that fences extensions (next lesson), and on the phone in its own family hook. So trust follows the catalog. An app the catalog lists today is home today, and one it drops is the outside world as soon as the projection changes.
Exact means exact. The family trust tests check that a neighbour is not family: another port on the same tailnet host is outside, and so is the same host over plain HTTP. One listed origin vouches for itself and for nothing near it.
Nineteen days with no verifier
The zone type, the decision and their tests existed from the first brick on 2026-10-03. What did not exist was a caller. BrowserServices kept the default TrustPolicy(), the one with no verifier, so every origin was the outside world and every family app got WebKit's prompt like a stranger. Every trust test passed the whole time, because each test built its policy by hand. The fix on 2026-10-22 (3638f97, todo #603) added the verifier in the second block and, just as important, the third block and the shell's calls to it: the code that installs a policy.
The zone that exists only as a type
Now the honest gap. TrustZone declares .tailnet, and its comment says "A host the local tailscaled names as a peer." The architecture, the house sign and the README all describe Voyager as trusting Dad's tailnet without asking. The code never asks tailscaled anything. family(origins:) returns only .home or .outsideWorld, and outside its declaration the app's sources mention .tailnet only in Site Information's text and symbol, for a zone no verifier produces. (One test builds a policy by hand that answers .tailnet, to check that the decision grants it.)
So, today: a family app on the tailnet is trusted because Firelink's catalog, or Settings, names its exact origin. A tailnet host the catalog does not list is asked, exactly like any site on the web. The tailnet zone is not built yet. Because unverifiable already means outside world, the gap fails toward asking, which is the safe direction: what is missing costs Dad a prompt, never a grant he did not mean to give.
A smaller drift sits in the same panel. Site Information describes the home zone as "Family app on this Mac", though a catalog-listed app served by another Mac is home too. That wording was written on the first day, nineteen days before any verifier existed.
Secure contexts first
A grant only matters where a page can ask. WebKit offers capture only to secure contexts and cannot be told to bless plain HTTP. The engine gate measured it (case W6): a family app at its Tailscale HTTPS name can ask, the same app over plain HTTP at its tailnet name has no navigator.mediaDevices at all, and loopback over plain HTTP counts as secure. That is why a family app should open at its HTTPS twin or at loopback, and why Firelink's catalog prefers the HTTPS twin when it answers for the tailnet. Under the hardened runtime the app must also claim the microphone and camera in its entitlements, and macOS's own permission for Voyager still applies. A zone settles the question between Voyager and the page. It never overrides the system.