Skip to content
C.W.K.
Lesson 02 of 05 · published

One Owner, Two Wires

~12 min · wire, appkit, daemon, ios, shared-types

Level 0Spark
0 XP0/48 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

The Mac App Is a Client Too

Once a phone exists, it is tempting to treat the Mac app as the real owner and the phone as a special case bolted on. That split recreates the glued factory: desktop types leak into the conversation, the phone becomes a secretly-Mac program, and a type that imports AppKit cannot travel. Smolder's answer is one owner and two deliberately different wires. The Mac app is a client of the daemon too, speaking the owner's own versioned protocol over a private socket. The phone speaks a narrower remote wire through its gateway, and that wire's types live in their own target that compiles without AppKit, without local process inspection, without a local-filesystem assumption. Two surfaces. Two wires. One owner behind both.

"Without AppKit" is not an aesthetic. A type that names NSView cannot build for iOS. A type that asks "what is the local PID of this pane" cannot run on a phone that has no pane PIDs. A type that assumes /Users/... is here cannot describe a folder on a host the phone is only watching. Those assumptions are how a shared module becomes a Mac module with an #if os costume. The costume always tears on the day you need a field the phone cannot fill.

What the Wire Is Allowed to Say

Session identity and the owner's incarnation, journal chunks, input as a surface's act, a size request from a surface that is typing, receipts for launches. Things the daemon knows. Not pixels. Not an NSFont. Not a bookmark into the Mac's open-panel. The emulator on each surface turns bytes into glyphs with the same pin; the wire does not ship a screenshot. If you find a screenshot on the wire, you have given up on the emulator and started a remote-desktop, which resizes wrong and copies wrong and cannot tell a spectator from a typist.

The gateway in a later track speaks a bounded WebSocket to approved devices, and it is itself just another client of the owner's private socket. The remote wire is kept separate from that socket on purpose: it has no prompt nonce, no process argument list, no arbitrary launch command, no family credential and no terminate. That is not translation into "phone-safe" shadows of desktop types; the narrow wire never had desktop shapes to translate. The full protocol stays where the caller is the same Mac's own app; the narrow one is what a pocket gets. Zero AppKit on the types that cross.

Local Inspection Stays Local

The daemon, on the Mac, may inspect its own processes. That inspection does not travel. A phone that needs to show a title asks the wire for the title the daemon already holds, not for a sysctl it cannot run. A Mac window that needs to draw chrome asks AppKit, then sends only the facts the daemon must know (focused pane, the size it asks for when it types). Mixing those layers is how a resize from a mere observer sneaks through as "just a window callback."

This lesson is the type-level version of shells-stay-on-the-host. If the wire cannot compile on iOS, the phone is not a surface. It is a remote-control script pretending to share a model. Keep the compiler honest and the ownership stays honest.

Shared wire types compile without AppKit. The Mac app is a client of the same daemon over its own, fuller protocol. Local process inspection, local-filesystem assumptions and the owner's private verbs stay off the phone's wire, or the phone becomes a secretly-Mac program.

Code

Wire types that can travel; Mac types that must not·swift
// The phone's remote wire (abridged from the shipped types).
// Its own target: no AppKit, no SwiftTerm, no local process inspection.
public struct RemoteSession: Codable, Sendable {
    public var id: UUID            // session identity
    public var incarnation: UUID   // which owner process this belongs to
    public var host, folder, title, process: String  // facts the daemon already holds
    public var columns, rows: Int
    public var sequence: UInt64
    public var writer: Bool        // this device may type now
    public var held: Bool?         // a hold is set
    public var surfaces: [String]? // every attached input surface
}
// RemoteEvent .output carries bytes: journal / live output, never pixels.

// Deliberately NOT on this wire (the owner's private socket keeps them):
//   prompt nonce, process argv, arbitrary launch command,
//   family credential, terminate
// Illegal anywhere on it:
//   import AppKit / NSView / pid_t / a URL bookmark into this device's files

// Mac app:  owner's versioned protocol over a private socket, sends its input
// Gateway:  a client of that same socket, speaks the remote wire to the phone
// Daemon:   may inspect local processes; that inspection does not travel

External links

Exercise

List five facts a phone might want (pane title, glyph pixels, local PID, folder path on the host, current journal chunk). Put each on the wire or keep it off. For every off item, write one sentence naming the lie you would tell if you put it on anyway.
Hint
Pixels and PIDs are the easy refusals. Folder path is legal only as a host-named string the daemon already knew, not as a bookmark from the phone's Files app pretending to be the Mac.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.