Skip to content

feat(apple): read peers' Hostinfo.App and admit by the tag first (T8, the Swift read half) - #245

Merged
jamesyong-42 merged 1 commit into
mainfrom
feat/apple-app-tag-read
Nov 1, 2026
Merged

jamesyong-42 merged 1 commit into
mainfrom
feat/apple-app-tag-read

Conversation

@jamesyong-42

@jamesyong-42 jamesyong-42 commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

What

The Swift node reads each peer's Hostinfo.App and admits app peers by that tag first, by the truffle-{appId}-
prefix only for an untagged row. This is the Apple plane's half of #243 (sidecar protocol 12). A Swift node now sees a
desktop that chose its own hostname prefix.

  • BackendPeer gains app.
  • Hostname.isAppRow(hostname:app:appId:) mirrors provider.rs::is_app_row:
    • A present tag must be exactly truffle:{appId}, whatever the name.
    • An untagged or empty-tagged row falls back to the prefix.
    • It replaces isAppPeer at MeshNode admission (upsertFromLayer3) and in the login-gate notice.
  • TailscaleKit's IpnState.PeerStatus binding has no Hostinfo.App, and the status JSON has none either. So the iOS
    backend reads it over the LocalAPI loopback it already uses for WhoIs and the login overlay:
    • The status-row decoder also keeps NodeID and HostName.
    • GET /localapi/v0/peer-by-id?id=N returns the peer's tailcfg.Node, whose Hostinfo.App control relays to peers
      (measured on a real tailnet for feat(core): an app-chosen hostname prefix, with app membership from Hostinfo.App (sidecar protocol 12) #243: a peer's row keeps App, Hostname, OS and Services).
    • Each peer is asked once per (node ID, hostname), so a peer that renames itself, the upgrade case, is asked again.
    • A failed read records nothing, is asked again at the next refresh, and leaves the row to the prefix.
    • A peer only ever carries the answer for its current hostname.
  • PeerAppCache is pure and lives outside TailscaleKitBackend.swift's #if os(iOS) && canImport(TailscaleKit), as
    LocalAPIIdentity does, so the macOS test target pins it.
  • LoopbackNetwork models a node's app (join(app:), setApp).

Additive: BackendPeer.init's new parameter has a default.

Not here (held)

Announcing a tag from the Swift node, and a Swift hostname prefix.

  • hostinfo.SetApp has no environment knob, and libtailscale exports no setter.
  • So it needs a fourth apple/patches/ patch (a cgo export calling hostinfo.SetApp) and a rebuilt
    TailscaleKit.xcframework, published under a new dependency-keyed release, with the root Package.swift URL and
    checksum moved together.
  • Until then the Swift node keeps truffle-{appId}-…, which every release admits by the prefix.

Rollout: the chain before an app turns a desktop prefix on

A Swift peer must read the tag, so the chain (VibeField's, for its phone) is:

  1. this change ships in a truffle release (0.7.26, or 0.7.25 if it lands before chore(main): release truffle 0.7.25 #244);
  2. a ghosttea release whose manifest declares that truffle exactly (VibeField's Swift plane moves only with it);
  3. that app's upgrade event;
  4. every desktop on 0.7.25 or later.

Only then may the desktop's hostname_prefix go on.

Tests

The tests were written first and run red against stubs, an isAppRow that ignored the tag and an empty cache:

  • the tag tests failed on their expectations;
  • the decoders (status rows' NodeID/HostName, peer-by-id's App) passed, as wire facts should.

New:

  • HostnameAppRowTests:
    • a present tag decides whatever the hostname;
    • a foreign or non-truffle tag under our prefix is refused;
    • a same-prefix node of another app is refused;
    • no tag or an empty tag falls back;
    • appTag is truffle:{appId}.
  • NodeLoginGateTests.aTaggedPeerIsAdmittedByItsTagNotItsName (loopback):
    • vibefield-studio tagged truffle:demo is admitted;
    • truffle-demo-mallory tagged truffle:chat is not;
    • untagged vibefield-old is not, until it announces the tag.
  • PeerAppTagsTests:
    • status rows carry NodeID and HostName, and the login overlay still reads them;
    • peer-by-id yields the app (absent, empty and no-Hostinfo all give nil);
    • each peer is asked once, an answer of "none" is kept, and a rename asks again;
    • tags are applied, a renamed peer with no new answer gets nil, and departed peers are forgotten.

Run locally at this commit:

  • swift test (root manifest): 115 tests in 19 suites passed.
  • swift test in apple/ (the shim, with a copied, gitignored TailscaleKit): 115 passed.
  • xcodebuild -scheme TruffleTailscale -destination 'generic/platform=iOS' build CODE_SIGNING_ALLOWED=NO: BUILD
    SUCCEEDED.
  • The same for generic/platform=iOS Simulator: BUILD SUCCEEDED. Both compile TailscaleKitBackend.swift and
    PeerAppTags.swift, with no new warnings.

Not run here: a device against a live tailnet. The peer-by-id path is the LocalAPI tsnet serves at v1.104.1, the
version the sidecar's own PeerByID read uses in #243.

Petition: VibeField's T8, the Apple plane's half A.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

… the Swift read half)

- BackendPeer gains `app`, the peer's Hostinfo.App.
- Hostname.isAppRow(hostname:app:appId:) mirrors provider.rs::is_app_row: a
  present tag must be exactly truffle:{appId}, whatever the name; an untagged
  or empty-tagged row falls back to the truffle-{appId}- prefix. Admission
  (MeshNode upsertFromLayer3) and the login-gate notice use it in place of
  isAppPeer.
- TailscaleKit's PeerStatus binding drops Hostinfo.App, so the iOS backend
  reads it over the LocalAPI loopback it already uses for WhoIs and logins:
  - the status-row decoder also keeps NodeID and HostName;
  - GET /localapi/v0/peer-by-id?id=N is asked once per (node ID, hostname),
    so a renamed peer is asked again;
  - a failed read records nothing and is asked at the next refresh;
  - a peer applies only the answer for its current hostname.
- PeerAppCache is pure (outside the iOS #if), so the macOS test target pins
  it from literal JSON. LoopbackNetwork models a node's app (join(app:),
  setApp).
- Not here: announcing a tag from the Swift node, or a Swift prefix. Both
  need a libtailscale export of hostinfo.SetApp and a new TailscaleKit. The
  Swift node keeps truffle-{appId}-…, which every release admits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jamesyong-42
jamesyong-42 merged commit 6d57b60 into main Nov 1, 2026
13 checks passed
@jamesyong-42
jamesyong-42 deleted the feat/apple-app-tag-read branch November 1, 2026 03:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant