Native messaging, as Chrome speaks it
An extension cannot open a socket to a desktop app on its own. Chrome gives it a narrower door. The desktop app registers a host manifest, a small JSON file in the browser's NativeMessagingHosts folder, naming a program, the type stdio, and the extension origins allowed to reach it, each written as chrome-extension://<id>/. When the extension calls connectNative with that host's name, the browser finds the manifest, checks the extension's origin against the list, and starts the program with the origin as its one argument. From then on they exchange JSON over the program's standard input and output, each message preceded by its length as a little-endian 32-bit integer.
Voyager speaks exactly that. Its registry looks in the folders Chrome and Chromium look in, per user and system-wide, so a desktop app that already talks to Chrome needs nothing new to talk to Voyager. The registry accepts only letters, digits, dots and underscores in a host name, so a name can never reach outside those folders. And this is where lesson 1's choice pays off: the manifest allows an extension by its Chrome id, and Voyager kept that id.
The first code block is the framing. Two details are worth stealing. Voyager refuses an outgoing message over 4 MB before it builds the frame, and an incoming one over 1 MB as soon as its 4-byte length arrives, before it waits for the rest or copies the message out. That is not before every allocation: the outgoing JSON is already serialized when its size is checked, and the incoming bytes the pipe has delivered already sit in the buffer. What the limits stop is building an oversized frame and waiting on a huge one, and an oversized incoming length ends the connection. And the unframing comment records a Foundation trap: after a removal, a Data value's indices no longer start at zero, so every index is computed from startIndex.
The refusal loop
By the afternoon of 2026-10-03, 1Password's helper had accepted Voyager's signature (lesson 5 of track 2 tells that part, and track 9 explains the signing). So the bridge started the helper from inside the app, the way Chrome does. It was refused anyway. From the minute the extension was installed, one helper was refused every minute, each dead in about 13 ms, each leaving its own log file behind.
The refusal said the host was "not part of the main browser's tree". Disassembling the helper's browser-verification code and capturing the parent of a dying helper live showed what "tree" meant: the process group. The helper compares its own getpgrp() with the browser process's group and refuses a mismatch. Foundation's Process puts every child in a group of its own; measured on that Mac, a Process child's group id was its own pid, while a posix_spawn child inherited its parent's. Chrome's and Comet's live helpers on the same Mac carried their browser's group id.
The first rule did not tell the whole story. An experiment re-launched Voyager's own binary as a relay child that joined the browser's group and started the host beneath it. The refusal moved to a different line: "required parent browser path was not available". That is a second rule. The helper's direct parent must be a browser Launch Services can name, so no intermediate can hold the pipe. The relay had done its job as an instrument; it answered one question, and the shipping path never uses it.
The fix is Chrome's own shape, exactly: the app process starts the host with plain posix_spawn, and asks for no process group of its own. The second code block is that call, with the measurement in its comment. With it the helper logged that it was verifying Voyager, then that it was waiting for partner authorization, and the every-minute refusal loop was gone.
What the port writes down
A native port to a password manager carries that password manager's protocol. So the port logs who sent a message and how many bytes, never what it said. The host's comment is blunt: "Protocol bodies can contain secrets, including the first reply." The host program's standard error goes to /dev/null as well. The direction and size were enough to see that the helper answered, and nothing more was ever needed in a log.
The other direction: chrome-extension:// addresses
Native messaging carries the extension to the desktop app. Pairing also needs the reverse: 1Password's Add browser opens the extension's own app page in the chosen browser, by a chrome-extension:// address. WebKit serves an extension's pages from its own webkit-extension scheme under a base URL it generates per context, so Voyager has to translate. Three walls stood in the way, each measured and removed the same day: the address was dropped when the app opened it, the engine's scheme list cancelled webkit-extension, and WebKit serves an extension page only to a web view built with that context's own webViewConfiguration; a plain configuration is refused with NSURLError -1008.
Now pageURL(for:) finds the installed context by the address's host, which is Chrome's id again, and builds the page URL from the context's base with the path, the query and the fragment carried over. makePage(forExtensionPageAt:) builds the page with the context's configuration and puts the compatibility layer in front of its scripts. The next lesson shows why one of those three parts, the fragment, turned out to matter.