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

Where Is Home?

~13 min · registry, roles, migration, passkeys

Level 0New Tab
0 XP0/44 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

One address, two right answers

Firelink and cwkPippa run on the Pippa machine, the Mac that holds the family's Pippa role. On that Mac the right address for each is loopback by name, http://localhost:<port>. On every other Mac the same service is reached at its tailnet twin, an HTTPS name that Tailscale serves. A browser that ships one default is wrong on every Mac but one, and Voyager runs on several.

FamilyHome is the answer, an enum with two cases: .pippaMachine and .elsewhere. The first block below is nearly all of it. It reads this Mac's fleet label once, from ~/.config/fleet-label, and asks the kit's registry whether the family's ledgers place cwkPippa on the machine with that label. A Mac without a label is elsewhere, and so is a phone. From that one answer come the hub's default address and the way Pippa is reached: loopback on the Pippa machine, the twins everywhere else. The twins come from the registry too (FamilyEndpoints.twinURL), rendered from the family's port and network ledgers.

A role outlives its machine

The code got here in steps, and each step removed a fixed fact about one machine.

  • The first version compared this Mac's label with one machine's label, written into Voyager.
  • 2026-10-19: Voyager asked the family registry which Mac plays that part, and the kit's own helpers replaced the copies Voyager had vendored (9249152, b829eb8).
  • 2026-10-21: FamilyHome became "the Mac that runs cwkPippa", and its twins became computed values that never trap (29c8224).
  • 2026-10-26: the Pippa machine had moved, the case still carried the first machine's name, and it was renamed .pippaMachine (ac85754).

The move was the test this design had to pass. The Pippa role went to a different Mac, and Voyager's source needed no new machine name, because it never asks for one. The comment in the first block says so: "The registry answers, never a literal label here." Sibling Kit Quest teaches the registry itself.

Old spellings follow the move

A settings file outlives a move too. Dad may have saved the hub as a raw tailnet IP, or as plain HTTP to a name, back when another Mac held the role. When the file is read, canonicalTwin asks the kit whether that spelling, on that member's engine or twin port, names a family Mac the registry knows. Firelink and cwkPippa follow the Pippa machine, so for them any family Mac's spelling counts; the kit keeps no history of which Mac held the role and never asks when the file was saved. It needs neither, because the Mac that held the role that day is one of the family's Macs. If the spelling names one, the address becomes the member's current twin. Anything else stays exactly as Dad typed it.

The bug that taught this shape was a natural-looking check. Voyager compared the saved host with the member's current machine, so a file saved before the move matched nothing, was left alone, and kept dialling the old Mac (0034914, 2026-10-22). Accepting every family Mac on the member's own port, instead of only today's machine, is what lets an old file move with the family.

Two smaller rules sit beside it. On a Mac that is not the Pippa machine, a hub saved as loopback reads as the registered twin, since nothing of the family listens on that Mac's loopback. A Pippa connection saved as "This Mac" becomes the tailnet twin there. An address Dad typed himself stays his.

Loopback by name

On the Pippa machine the defaults say localhost, never 127.0.0.1. Dad found out why on 2026-10-04:

ip 주소 127.0.0.1 같은 걸로는 touchid 자체가 안 되는데

"With an IP address like 127.0.0.1, Touch ID doesn't work at all." A passkey belongs to a host name. WebAuthn scopes a credential to a relying party identified by a domain, and an IP address is not one. The second block keeps both spellings: the named defaults, and the numeric ones as former… values, so a settings file written before the change is read as the named address. The ports in both come from the port ledger through the kit (0d6b545, 2026-10-14). The settings code writes no port number of its own.

A target that belongs to one Mac

Some catalog targets only make sense in one place. When Firelink's catalog gained the CWK site's dev server, its address was the workbench's own loopback, and on any other Mac that address is that Mac's loopback, where nothing listens. Shown everywhere, it would be a dead tile on every Mac but one.

The fix lives in the kit, so every reader applies it the same way (third block). Firelink marks such a target with a role-spelled host_affinity, an at sign and a role name: @workbench. opens(on:) answers true for every other affinity. For a role-bound target it asks the device whether it holds that role, meaning whether its fleet label is the one the family's roles give that machine today. A phone or a tablet has no label and holds no role, so it never sees the dev server. Voyager on the Mac, Voyager on the phone and Firelink's own launcher all apply this one function, and the catalog model takes its device as a parameter, so a test can stand in for any Mac (ac85754, cwkFirelink Coop 675).

Principle: Ask which role a machine plays, never what it is called. A name compared in code is a fact frozen on the day it was written, and a move is exactly the day it stops being true.

Code

cwkVoyager, Sources/VoyagerCore/Settings.swift: FamilyHome asks the registry whether this Mac runs cwkPippa (verbatim, lines 94-112)·swift
    /// This Mac, read once from `~/.config/fleet-label`, the family's name for
    /// each machine. A Mac without the label is not the service host. Tests bind it.
    @TaskLocal public static var current = FamilyHome(
        fleetLabelAt: URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent(".config/fleet-label"))

    /// ``pippaMachine`` is whichever Mac runs cwkPippa.
    /// Its placement follows this Mac's host profile (Pippa's Ark, cwkPippa
    /// todo #549) and the family's Pippa role (#582). The registry
    /// answers, never a literal label here.
    public init(fleetLabelAt url: URL, profile: FamilyEndpoints.HostProfile = FamilyEndpoints.currentHostProfile) {
        let label = (try? String(contentsOf: url, encoding: .utf8))?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
        self = !label.isEmpty && FamilyEndpoints.runs("cwkPippa", on: label, profile: profile) ? .pippaMachine : .elsewhere
    }

    /// Where Firelink's Web Hub and launch catalog are from this Mac.
    public var familyHub: URL { self == .pippaMachine ? VoyagerSettings.defaultFamilyHub : Self.familyHubTwin }

    /// How cwkPippa is reached from this Mac until Dad chooses otherwise.
    public var pippa: PippaConnection { self == .pippaMachine ? .thisMac : .tailnet(Self.pippaTwin) }
cwkVoyager, Sources/VoyagerCore/Settings.swift: loopback by name, the old numeric spelling kept only to be read, ports from the ledger (verbatim, lines 228-239)·swift
    /// The service host's loopback, by name, never by number: a passkey belongs to a host
    /// name, and `127.0.0.1` is not one (Dad, 2026-10-04: "ip 주소 127.0.0.1 같은
    /// 걸로는 touchid 자체가 안 되는데"). The old numeric defaults are read as
    /// these, so a settings file written before this change moves with it. On
    /// any other Mac the defaults are the service host's twins (`FamilyHome`).
    /// Ports by the port ledger (todo #442).
    public static let defaultFamilyHub = URL(string: "http://localhost:\(firelinkPort)/")!
    public static let defaultPippaBase = URL(string: "http://localhost:\(pippaPort)")!
    static let formerFamilyHub = URL(string: "http://127.0.0.1:\(firelinkPort)/")!
    static let formerPippaBase = URL(string: "http://127.0.0.1:\(pippaPort)")!
    private static var firelinkPort: Int { FamilyEndpoints.service("cwkFirelink")!.engine! }
    private static var pippaPort: Int { FamilyEndpoints.service("cwkPippa")!.engine! }
cwkSiblingKit, CwkKit/Sources/CwkKitCore/FamilyCatalog.swift: the one rule every catalog reader applies to a role-bound target (verbatim, lines 31-48)·swift
extension FamilyLaunchTarget {
  /// The family role a device must hold to open this target, read from a role-spelled
  /// `host_affinity` (`@workbench`): Firelink binds a loopback dev server to the one Mac
  /// that holds the role (cwkFirelink Coop 675). Nil for every other affinity (`any`, a
  /// ledger host name, `local-device`), which any device may open.
  public var requiredRole: String? {
    guard hostAffinity.hasPrefix("@"), hostAffinity.count > 1 else { return nil }
    return String(hostAffinity.dropFirst())
  }

  /// Whether `device` may open this target: always, unless the target is bound to a role
  /// the device does not hold. The one rule every catalog consumer applies (Mac and
  /// iPhone/iPad Voyager, the Firelink launcher), so a workbench-only page never shows
  /// as a dead link anywhere else.
  public func opens(on device: KitFamilyMac) -> Bool {
    guard let role = requiredRole else { return true }
    return device.holds(role)
  }

External links

Exercise

Take a client that hard-codes where its server lives, or switches on a machine name. Replace that with a small registry file mapping roles to machines and a function that answers whether this device holds a role. Write three tests: the answer on the role's machine, the answer elsewhere, and a test that moves the role to another machine in the registry and passes with no code change. Then add a settings migration that reads an address saved under the old placement as the role's current address, while leaving an address the user typed by hand alone.
Hint
Decide the migration by asking whether the saved spelling names any registered family host on that service's port, not whether it names the role's current machine; the second check is the bug Voyager shipped, and no role history is needed for the first. Make the device a parameter (or a task-local) so tests can stand in for any machine without touching the real one.

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.