Skip to content

feat: first-class Tailcat networking and server-to-server federation - #9525

Open
BearHuddleston wants to merge 12 commits into
pingdotgg:mainfrom
BearHuddleston:t3code/add-tailcat-networking
Open

feat: first-class Tailcat networking and server-to-server federation#9525
BearHuddleston wants to merge 12 commits into
pingdotgg:mainfrom
BearHuddleston:t3code/add-tailcat-networking

Conversation

@BearHuddleston

@BearHuddleston BearHuddleston commented Sep 4, 2026

Copy link
Copy Markdown

Problem

Reaching a T3 server on another machine today needs a LAN address, a tailnet, a manually managed SSH tunnel, or T3 Connect. Users on different networks without a VPN account have no first-class path, and two T3 environments cannot coordinate work with each other at all.

What this adds

Tailcat (the open-source point-to-point tunnel CLI) becomes a first-class, bundled transport, with server-to-server federation built on top of it. Tailcat is a transport underneath T3, never a trust boundary: it exposes the unchanged loopback listener, and pairing, sessions, scopes, and RPC run inside it exactly as before. The Tailcat allowlist is derived from T3 pairing state and never edited on its own.

Server (Machine B)

  • t3 serve --tailcat, or the new Remote access via Tailcat card in Settings → Connections, exposes the loopback listener through tailcat serve. Identity is a 0600 key file in the secrets dir with a fixed relay region, so the address is stable across restarts.
  • One-time connection codes (t3c://tailcat/…, text + QR) carry the address, port, and a single-use five-minute pairing token. Never a private key or reusable secret.
  • The listener runs open only while an unredeemed code exists; otherwise it runs with exactly the trusted node keys. Trust is recorded during the ordinary /oauth/token exchange when the redeemed credential was a Tailcat code and the client sent its node key (client_tailcat_node_key). Revoking a trusted device revokes its sessions and relocks the listener. Regenerate identity, rename, revoke, diagnostics, and a typed failure model (binary-missing, version-incompatible, process-exited, …) with jittered backoff are all included.

Client (Machine A, desktop)

  • Add environment → Tailcat: paste a code, the main process starts tailcat forward on a reserved loopback port, probes readiness, then the existing descriptor → pairing → session path runs over http://127.0.0.1:<port>.
  • New TailcatConnectionTarget persists the logical endpoint (address + remote port), never the ephemeral local port. Saved rows show "Tailcat · Direct / Relay", with a details dialog (forwarder status, path probe, restart, re-pair, copy diagnostics, forget).
  • The client identity is encrypted with Electron safeStorage and only materialized as a 0600 temp file while a tailcat process starts. Web and mobile recognize codes and point to the desktop app.

Bundled runtime

  • New packages/tailcat supervises resolve / serve / forward / ping with a version check against native/tailcat/manifest.json (pinned version + per-platform SHA-256). Resolution order: T3CODE_TAILCAT_BINARY override → bundled → PATH.
  • scripts/fetch-tailcat.ts downloads pinned release assets (macOS builds from the pinned source, since upstream ships no darwin archive), verifies checksums, stages into desktop extraResources and the CLI dist, and --update <version> re-pins. No runtime downloads. CI validates the manifest; release workflows fetch per matrix.

Federation (protocol v1)

  • Peer codes (t3c://peer/…) pair two environments with explicit, per-side scopes (environment.read, projects.read, runs.read, runs.start, runs.cancel, artifacts.read). Identity reuses the existing Ed25519 environment key; calls use a challenge/JWT assertion that yields an ordinary T3 session carrying the marker scope federation:peer with the peer's environment id as its subject, confined to the federation HTTP group and refused at /ws.
  • Vertical slice: hello, projects, start / status / cancel run, event summaries, artifact refs and turn diffs with origin identity. Peers only see runs they started. Runs are ordinary threads on the executing environment. UI shows "Runs on · origin " on every run and artifact.
  • CLI: t3 remote tailcat status|enable|disable|code|peers|revoke and t3 peer code|add|list|remove|projects|run --wait.

Docs: docs/user/tailcat.md, docs/internals/tailcat.md (lifecycle, threat model, upstream limitations), docs/internals/federation.md, and ADR 001.

Verification

  • Typecheck clean for contracts, shared, tailcat, client-runtime, server, desktop, web, scripts, mobile. Targeted lint and format clean on changed files.
  • The 28 test files this PR adds or touches: 456 tests pass, 12 of them desktop main-process tests (mocked spawner, TestClock-driven backoff), including a runtime timeout-race regression test. The single failure is the pre-existing build-desktop-artifact case noted below.
  • Live, with the real Tailcat v0.5.0 binary: headless --tailcat server, client forward, token exchange with node key, allowlist relock, untrusted key denied, revoke → session revoked and key locked out, missing binary → unavailable, override honored.
  • Two real machines: an x64 Linux host and a DGX Spark (arm64) paired over Tailcat with a direct LAN path; Codex runs started on each executed on the other and completed. A macOS arm64 DMG built from this branch (Tailcat built from pinned source) installed on a Mac mini; its packaged backend resolved the bundled binary and served, and the Electron forwarder connected to the Spark server (screenshots below).

Screenshots (macOS desktop, built from this branch)

Remote access card with a code and QR (code expired):

Remote access via Tailcat

Add environment → Tailcat, empty and with a pasted code preview:

Add environment Tailcat card

Pasted code preview

Saved Tailcat environment details after connecting to the Spark server:

Tailcat environment details

Compatibility and notes for reviewers

  • Opt-in everywhere: the toggle is off by default, --tailcat is explicit, and clients hide the UI unless the server advertises capabilities.tailcatRemoteAccess / capabilities.federation. Older desktop shells lack the optional bridge methods, so the Tailcat card is hidden there.
  • Allowlist changes restart the Tailcat listener and drop tunnels; clients reconnect through the connection supervisor. Documented as an upstream limitation.
  • Windows has not been exercised beyond typecheck and tests. macOS signing follows the resource-monitor precedent (unverified on a signed build).
  • Shipping all six platform binaries in the npm package adds roughly 40 MB compressed; the platform list in apps/server/scripts/cli.ts is the knob if that is too much.
  • scripts/build-desktop-artifact.test.ts has one pre-existing failure on Linux ("skips the primary native probe for cross-architecture Windows payloads") that reproduces on main.

Built with Claude Fable 5.1 in Claude Code.

🤖 Generated with Claude Code

Note

Add Tailcat remote-access transport and server-to-server federation

  • Introduces the @t3tools/tailcat package providing address decoding, runtime supervision, version/manifest management, and typed errors for the Tailcat mesh transport
  • Adds server-side Tailcat remote-access lifecycle (TailcatRemoteAccess), federation identity (Ed25519 challenge/sign), durable peer store, transport, and HTTP/WS RPC surfaces; server startup now launches Tailcat after port binding and prints a connection code in headless mode
  • Adds CLI commands t3 remote tailcat and t3 peer that authenticate against the running server via short-lived admin sessions to manage remote access, peers, and delegated runs
  • Extends client-runtime, web settings UI, desktop IPC, and mobile pairing with Tailcat connection profiles, onboarding flows, diagnostics, trusted-peer management, and federation peer/remote-run controls
  • Adds build-time Tailcat runtime fetching/verification (scripts/fetch-tailcat.ts), desktop artifact staging, CI manifest validation, and release workflow steps for all platforms
  • Risk: WebSocket RPC auth now rejects sessions carrying AuthFederationPeerScope in websocketRpcRouteLayer (apps/server/src/ws.ts); federation state files written before pending-code support are accepted with empty pending codes but older server versions cannot read the new pendingPeerCodes field. Bootstrap credential exchange (exchangeBootstrapCredential) now returns BootstrapCredentialExchange metadata in addition to the access token, and the auth HTTP handler records a trusted Tailcat peer on connection-code grants, logging (not failing) on record errors.

Macroscope summarized a162dcf.


Note

High Risk
Changes authentication/token exchange, remote-access allowlisting, federation RPC authorization, and ships supervised native tunnel binaries across desktop, server, and npm publish paths.

Overview
Adds first-class Tailcat remote access and server federation on top of a new pinned, bundled @t3tools/tailcat runtime (native/tailcat/manifest.json, scripts/fetch-tailcat.ts, CI verify + release caching/source builds for macOS).

Desktop depends on @t3tools/tailcat, wires a main-process Tailcat environment (loopback tailcat forward, identity via encrypted safeStorage, diagnostics/restart IPC), and passes a resolved bundled binary path into the backend bootstrap so server and client share the same build.

Server gains tailcatEnabled / bootstrap tailcatBinaryPath, exchangeBootstrapCredential (grant + session metadata), and on token exchange records trusted Tailcat peers when a connection-code grant is redeemed with client_tailcat_node_key. RPC authorization maps new Tailcat admin and federation WebSocket methods to access/orchestration scopes. CLI: shared jsonFlag, t3 remote / t3 peer (federation over authenticated /ws RPC), npm publish stages all platform Tailcat binaries into dist/tailcat.

Mobile stubs TailcatEnvironmentGateway and rejects t3c:// codes in pairing with desktop-specific guidance instead of invalid-URL errors.

Reviewed by Cursor Bugbot for commit a162dcf. Bugbot is set up for automated code reviews on this repo. Configure here.

BearHuddleston and others added 10 commits September 3, 2026 19:08
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… main process

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ion UI

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…dist

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Sep 4, 2026
@macroscopeapp

macroscopeapp Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Macroscope skipped reviewing this pull request. Per-review cost limit exceeded (workspace setting).

This review would cost an estimated $31.41, which exceeds your per-review limit of $10.00.

The top 3 files driving up this estimate:

File Diff Size Estimate
apps/server/src/federation/FederationService.ts 56.99KB $2.85
apps/web/src/components/settings/FederationSection.tsx 45.40KB $2.27
packages/tailcat/src/runtime.ts 35.14KB $1.76

Tip

To get this pull request reviewed, you can:

  1. Comment @macroscope-app on this PR to request a manual review (monthly spend limits still apply).
  2. Exclude the file(s) above from review by adding a pattern to your .macroscope/ignore.md — note that creating this file replaces Macroscope's built-in default ignores rather than extending them.
  3. Raise your cost limit in your workspace billing settings.

Turn off this reminder going forward

Comment thread apps/desktop/src/tailcat/DesktopTailcatEnvironment.ts
Comment thread apps/server/src/federation/FederationService.ts
@macroscopeapp

macroscopeapp Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR introduces substantial Tailcat networking and server-to-server federation capabilities across the server, desktop, client runtime, UI, authentication, and release packaging paths. Its authentication/trusted-peer changes, product-default changes, and newly added static-analysis suppression directives make the scope and risk unsuitable for automatic approval.

Not approved because:

  • Per-review cost limit exceeded (workspace setting). Approvability relies on correctness review in order to determine eligibility

Review your spending limits in Billing settings, or comment @macroscope-app review this PR to bypass the limit and review now. You can add or adjust custom eligibility rules. Learn more.

…, and clients

Consolidates duplicated helpers into their owning packages (node-key
fingerprint in contracts, connection-code preview in shared, platform keys in
the tailcat manifest, the environment RPC subscription family in
client-runtime, one absolute timestamp formatter in web) and removes dead
state: the desktop runtime-availability IPC chain, the forward entry's stopping
flag and duplicated connection id, the Tailcat "none" allow policy, unused
transport labels, and the federation session subject prefix (peer sessions are
now identified by the marker scope plus the peer's environment id).

Federation polling no longer rewrites and republishes idle runs, keeps run
events in memory instead of the peer store, caches HTTP clients per forward,
gates the periodic refresh on live forwards, and reads local run events from
the run's start sequence. Tailcat serve tracks the running handle instead of a
generation counter, and both one-shot timers share one arm/disarm pair.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

There are 4 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a8394e9. Configure here.

Comment thread apps/server/src/federation/FederationTransport.ts
Comment thread apps/server/src/federation/FederationTransport.ts
…er codes

Per-connection and per-peer locks were created with a read, a yield, and a
write, so two first callers could each get their own semaphore and both spawn
a forward. They are now created and published in a single Ref.modify.

The idle sweeper snapshotted lastUsedAtMs and then dropped the forward without
re-checking, so a call that reused the forward in between lost its tunnel. The
drop now re-evaluates the idle condition under the peer lock.

Offered peer-code scopes lived only in memory, so a restart during a code's
lifetime consumed the pairing token and then rejected the code. They are now
persisted in federation.json next to the peers and pruned on expiry, with a
store test covering restart, redemption, expiry, and pre-existing state files.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant