Skip to content

[PR -- Features] Tailscale detection core: tailnet status, serve, doctor (mesh 1/5) - #2512

Open
sethkarten wants to merge 3 commits into
mainfrom
rsi/tailscale-detect
Open

sethkarten wants to merge 3 commits into
mainfrom
rsi/tailscale-detect

Conversation

@sethkarten

@sethkarten sethkarten commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Motivation

User-requested feature: first-class Tailscale support as the foundation of the Tailscale agent mesh — see your agents running across your tailnet, message and spawn them, with the UI showing which tailscale connection each remote agent runs on. This is PR 1 of the 5-PR mesh stack: the detection core, trimmed from #2451.

Stack (each PR bases on the previous):

  1. this PR — detection core: tailnet probing, tailscale command, doctor facts, mesh docs
  2. daemon TCP listener (per-machine token auth, same JSON protocol as the unix socket)
  3. tailnet peer discovery (on-demand roster scan)
  4. agents-view remote sessions labeled with their tailscale connection
  5. cross-machine messaging + spawn

What it adds

  • prime-agent tailscale — tailnet state, MagicDNS suffix, this node's hostname, and served endpoints; --json machine-readable form (valid JSON even on failure paths)
  • prime-agent tailscale serve --port <n> [--funnel] — wraps tailscale serve|funnel --bg localhost:<n>; requires an explicit --port (never guesses a default), validates 1-65535 before any side effect, exits 1 on every failure
  • post-serve verification: tailscale can exit 0 after only printing an interactive enable URL without configuring anything; the wrapper re-reads serve status --json and refuses success when the target is absent (exact port match — 80 does not match localhost:8000, default ports 80/443 mapped)
  • probeTailscale() exported as the detection seam the mesh PRs build on: ENOENT (absent) vs EACCES/hang (installed-but-unusable), BackendState vs Self.Online (stopped backend vs up-but-offline), CurrentTailnet.MagicDNSSuffix preferred over the deprecated top-level field, hostname suffix trimming
  • prime-agent doctor surfaces the same detection in human mode (doctor --json stays pure JSON)
  • docs/tailscale.md describes the mesh feature (detection + remote agents); the manual-workaround patterns from First-class Tailscale support (status, serve/funnel, doctor, docs) #2451 (Tailscale SSH, container MagicDNS, Tailscale MCP connector) are superseded by the mesh and are no longer documented

Verification

  • 21 vitest tests in test/tailscale.test.ts: probe states, serve argv pinned exactly (serve|funnel --bg localhost:<n>), post-serve verification (substring trap, default ports incl. https 443), parser forms, unparseable status output, status failure diagnostics
  • related suites green: test/public-command.test.ts (55), test/owned-session-worker.test.ts (4)
  • npm run check green: biome, test-line budget vs origin/main, tsgo, installer, push-guard, browser-smoke
  • npm run build green
  • dual-reviewer gate (DeepSeek v3.2 + v4-pro, empirical probes): no blockers, no majors; coverage gaps closed in-branch (unparseable-status test, https-default-port test, --json in the command registry); remaining findings judged intended design (tailscale exit-code passthrough on serve failure is the best diagnostic; internal runTailscale args are hardcoded literals with no user input)

No-Ticket: user-requested feature (Tailscale agent mesh), PR 1 of 5; supersedes #2451's scope for this stack.


Note

Medium Risk
tailscale serve/--funnel changes network exposure and spawns the Tailscale CLI; verification can fail after a partial serve, leaving cleanup to the user.

Overview
Adds first-class Tailscale support as mesh PR 1/5: a new prime-agent tailscale command, shared detection via probeTailscale(), and human-only Tailscale lines in prime-agent doctor.

prime-agent tailscale (default status) reports CLI presence, tailnet up/offline state, MagicDNS suffix, hostname, and local serve mappings; --json is status-only. tailscale serve --port <n> [--funnel] wraps tailscale serve|funnel --bg localhost:<n> with explicit port validation, preflight probes, and post-command verification so a zero exit from Tailscale cannot count as success unless serve status --json shows an exact local port match (including default 80/443).

probeTailscale() is exported as the internal seam for later mesh work (peer discovery, remote session labels). Docs in docs/tailscale.md and a changelog entry describe the staged agent mesh; vitest coverage uses PATH shims for probe, parser edge cases, serve argv, and verification traps.

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

Note

Add tailscale CLI command with status, serve, and doctor integration

  • Introduces prime-agent tailscale with status (default) and serve/funnel subcommands, plus JSON output mode for machine-readable state
  • probeTailscale detects CLI availability, tailnet membership, MagicDNS suffix, hostname, and up-but-offline state via bounded Tailscale CLI invocations (15s timeout)
  • runTailscaleServe validates ports, checks probe state, runs serve/funnel in background mode, and verifies the requested local port is actually exposed
  • runDoctor output now includes a one-line Tailscale fact; tailscale.md documents all modes and the staged mesh plan
  • Behavioral Change: runPublicCommand now handles tailscale directly instead of forwarding it downstream; status exits non-zero when the CLI is missing or the node is not on a tailnet

Macroscope summarized 863620a.

@sethkarten
sethkarten enabled auto-merge (squash) September 21, 2026 20:50
@github-actions

github-actions Bot commented Sep 21, 2026

Copy link
Copy Markdown

Prime Agent performance — completed

PR 863620a7 compared with main 18a56bf3.

Overall: 0 regressed · 0 improved · 41 no clear change.

Metric Main This PR Change
Cold startup 683.6 ms 681.0 ms ≈ -2.6 ms (-0.38%)
Warm startup 512.5 ms 515.7 ms ≈ +3.1 ms (+0.61%)
Installation 6.34 s 6.14 s ≈ -0.20 s (-3.09%)
Compressed release artifacts 73.09 MB 73.09 MB ≈ +0.0021 MB (+0.00%)
Installed footprint 594.56 MB 594.61 MB ≈ +0.05 MB (+0.01%)
Idle memory, summed RSS 643.30 MB 642.71 MB ≈ -0.59 MB (-0.09%)

Python runtime

Metric Main This PR Change
Python kernel startup 37.0 ms 32.9 ms ≈ -4.1 ms (-11.09%)
Python cell round trip 0.079 ms 0.081 ms ≈ +0.001 ms (+1.84%)
Empty bash command 2.0 ms 2.2 ms ≈ +0.2 ms (+7.50%)
Bash git status 2.8 ms 2.8 ms ≈ +0.095 ms (+3.45%)
Bash 32 KiB output 2.0 ms 2.2 ms ≈ +0.2 ms (+8.94%)
35 cells / 9 shell calls 25.9 ms 28.8 ms ≈ +2.9 ms (+11.36%)
Python interrupt to done 0.510 ms 0.534 ms ≈ +0.024 ms (+4.80%)
Python state snapshot 10.1 ms 10.0 ms ≈ -0.1 ms (-1.26%)
Python state restore 136.1 ms 132.2 ms ≈ -3.9 ms (-2.86%)
Python idle RSS 21.08 MB 21.09 MB ≈ +0.01 MB (+0.07%)
Python RSS after pandas workload 75.61 MB 75.54 MB ≈ -0.06 MB (-0.08%)

Session transport

Metric Main This PR Change
Private frame decode, 32 MiB in 8 KiB chunks 15.2 ms 15.9 ms ≈ +0.7 ms (+4.69%)

UI interactions

Metric Main This PR Change
Resume large session (cold) 1,717.0 ms 1,649.9 ms ≈ -67.1 ms (-3.91%)
CPU, resume large session 1,930.0 ms 1,990.0 ms ≈ +60.0 ms (+3.11%)
Switch into large session 1,739.7 ms 1,696.9 ms ≈ -42.8 ms (-2.46%)
CPU, switch into large session 1,980.0 ms 1,960.0 ms ≈ -20.0 ms (-1.01%)
Open agents view from a session 472.0 ms 443.4 ms ≈ -28.6 ms (-6.05%)
CPU, open agents view 810.0 ms 660.0 ms ≈ -150.0 ms (-18.52%)
Full agents roster, many sessions 4.04 s 4.03 s ≈ -0.0063 s (-0.16%)
CPU, full agents roster 1.01 s 1.03 s ≈ +0.02 s (+1.98%)
Open another session from agents view 2,128.5 ms 1,944.8 ms ≈ -183.8 ms (-8.63%)
CPU, open from agents view 980.0 ms 1,000.0 ms ≈ +20.0 ms (+2.04%)
Reopen resident large session 302.3 ms 280.6 ms ≈ -21.7 ms (-7.17%)
CPU, reopen resident session 300.0 ms 330.0 ms ≈ +30.0 ms (+10.00%)
Open subagent session at depth 6 17,854.7 ms 17,673.1 ms ≈ -181.6 ms (-1.02%)
CPU, open subagent at depth 6 4,700.0 ms 4,630.0 ms ≈ -70.0 ms (-1.49%)
Open chain parent from agents view 3,171.5 ms 3,192.9 ms ≈ +21.4 ms (+0.68%)
CPU, open chain parent 1,440.0 ms 1,420.0 ms ≈ -20.0 ms (-1.39%)
Scheduled catalog, first request 455.1 ms 445.9 ms ≈ -9.2 ms (-2.03%)
CPU, scheduled catalog 860.0 ms 850.0 ms ≈ -10.0 ms (-1.16%)
Scheduled catalog, repeated request 0.4 ms 0.5 ms ≈ +0.063 ms (+14.28%)
CPU, repeated catalog 0.0 ms 0.0 ms ≈ +0.0 ms (N/A)
Cold worker with three catalog scans 569.0 ms 411.5 ms ≈ -157.5 ms (-27.68%)
CPU, cold worker and scans 420.0 ms 360.0 ms ≈ -60.0 ms (-14.29%)
UI memory after interactions 1,948.70 MB 1,852.90 MB ≈ -95.81 MB (-4.92%)

Sandbox cost: ~$0.1043 — no inference calls.
Run, logs, and downloadable raw results

Methodology and samples

Main resolved at 2026-09-22T17:19:56.008699+00:00. Harness 18a56bf3.
Linux x64, 4 vCPU, 8 GB RAM, 20 GB disk; region us.
Image: node:24-bookworm@sha256:be23f54a88d34e8824c741b19b91064094f92c1c97b194144bfc8b50d67258e2.
Stock tools, skills, daemon, and Python bootstrap enabled; fresh homes and a fixed Git fixture.
Onboarding is dismissed; the editor starts without a selected model or submitted prompt.
Medians shown. Arrows require a 20% timing/memory change plus absolute floors and IQR.
These practical noise floors are not a statistical significance test.
Cold means stopped Prime processes; OS filesystem caches are not flushed.
No model requests or credentials. Installation excludes build/setup time.
Installer tarballs use loopback; npm/Python downloads use the network with fresh caches.
Artifact size counts release tarballs; footprint after first use includes registry packages.
MB is decimal. Summed RSS can double-count shared pages; PSS is recorded when available.
Provisioning, setup, and build durations are recorded separately in the raw results.
Kernel probes use the installed JSONL runtime, outside the TUI/TypeScript host.
Per trial: 50 Python cells, 5 calls per shell case, and one 35-cell mix (9 git status calls).
Cell/shell values are batch means; other runtime timings are single operations.
State fixture: a 10,000-row × 8-column integer DataFrame and a 10,000-integer list.
Restore runs in a fresh kernel, including pandas imports; kernel startup is excluded.
Kernel RSS covers the isolated Python process; loaded RSS follows the pandas workload.
Transport benches run node against the prepared source build, outside the installed home.
Frame decode times one 32 MiB private frame, snapshot-chunk header, pushed in
8 KiB chunks; the wire shape of multi-MB frames on the daemon-worker channels.
UI trials use a fresh fixture set: 194 top-level sessions including one ~40 MB transcript,
40 ledger fan-out children, and a 6-deep subagent chain (~46 spawn edges).
Large fixtures hold 1,999 complete triples (~5 MB JSONL); medium 119; subagents 399 each.
Interactions: cold --resume of a large session, warm /resume switch, left-arrow to agents view,
roster settle with many saved sessions, search-and-open of another large session,
reattaching to that resident session, opening the chain parent, and drilling to depth 6.
Readiness is the rendered transcript tail plus a confirmed editor echo.
CPU metrics sum utime+stime across the whole benchmark-user process tree per interaction.
UI memory sums RSS after the interactions; PTY byte counts are in the raw results.
A separate catalog fixture has 2,300 sessions, 2,298 edges, and 13 paused scheduled-job owners.
Catalog timings cover first/repeated reads and cold worker creation under three pending scans.
All expected jobs and owner metadata are checked; worker readiness excludes TUI rendering.
Costs estimate full sandbox lifetimes at configured rates, including setup and build.
Budget target: $1; not a billing cap. Performance changes are informational.
Failed or incomplete execution fails the workflow; saved artifacts remain available.
Each side stops a phase after 2 identical consecutive failures.
Skipped trials are not attempted samples. Warm startup requires a successful cold launch.

Metric Main successful/attempted PR successful/attempted Main spread PR spread
Cold startup 10/10 10/10 IQR 89.7 ms IQR 244.5 ms
Warm startup 10/10 10/10 IQR 59.4 ms IQR 31.2 ms
Installation 3/3 3/3 range 0.71 s range 0.60 s
Compressed release artifacts 1/1 1/1
Installed footprint 1/1 1/1
Idle memory, summed RSS 10/10 10/10 IQR 3.53 MB IQR 3.05 MB
Python kernel startup 10/10 10/10 IQR 9.4 ms IQR 4.0 ms
Python cell round trip 10/10 10/10 IQR 0.040 ms IQR 0.033 ms
Empty bash command 10/10 10/10 IQR 0.1 ms IQR 0.4 ms
Bash git status 10/10 10/10 IQR 0.3 ms IQR 0.6 ms
Bash 32 KiB output 10/10 10/10 IQR 0.3 ms IQR 0.3 ms
35 cells / 9 shell calls 10/10 10/10 IQR 5.1 ms IQR 3.1 ms
Python interrupt to done 10/10 10/10 IQR 0.062 ms IQR 0.083 ms
Python state snapshot 10/10 10/10 IQR 0.8 ms IQR 0.6 ms
Python state restore 10/10 10/10 IQR 15.9 ms IQR 10.5 ms
Python idle RSS 10/10 10/10 IQR 0.24 MB IQR 0.13 MB
Python RSS after pandas workload 10/10 10/10 IQR 0.12 MB IQR 0.19 MB
Private frame decode, 32 MiB in 8 KiB chunks 10/10 10/10 IQR 3.3 ms IQR 2.4 ms
Resume large session (cold) 3/3 3/3 range 368.8 ms range 480.3 ms
CPU, resume large session 3/3 3/3 range 50.0 ms range 250.0 ms
Switch into large session 3/3 3/3 range 266.2 ms range 326.0 ms
CPU, switch into large session 3/3 3/3 range 270.0 ms range 170.0 ms
Open agents view from a session 3/3 3/3 range 174.7 ms range 27.4 ms
CPU, open agents view 3/3 3/3 range 130.0 ms range 180.0 ms
Full agents roster, many sessions 3/3 3/3 range 0.009 s range 0.0049 s
CPU, full agents roster 3/3 3/3 range 0.06 s range 0.11 s
Open another session from agents view 3/3 3/3 range 275.3 ms range 101.8 ms
CPU, open from agents view 3/3 3/3 range 140.0 ms range 180.0 ms
Reopen resident large session 3/3 3/3 range 25.8 ms range 25.0 ms
CPU, reopen resident session 3/3 3/3 range 60.0 ms range 30.0 ms
Open subagent session at depth 6 3/3 3/3 range 96.1 ms range 259.9 ms
CPU, open subagent at depth 6 3/3 3/3 range 200.0 ms range 310.0 ms
Open chain parent from agents view 3/3 3/3 range 204.2 ms range 173.8 ms
CPU, open chain parent 3/3 3/3 range 190.0 ms range 110.0 ms
Scheduled catalog, first request 3/3 3/3 range 82.5 ms range 60.9 ms
CPU, scheduled catalog 3/3 3/3 range 70.0 ms range 80.0 ms
Scheduled catalog, repeated request 3/3 3/3 range 0.1 ms range 0.5 ms
CPU, repeated catalog 3/3 3/3 range 0.0 ms range 20.0 ms
Cold worker with three catalog scans 3/3 3/3 range 167.1 ms range 129.6 ms
CPU, cold worker and scans 3/3 3/3 range 50.0 ms range 20.0 ms
UI memory after interactions 3/3 3/3 range 192.46 MB range 137.87 MB

Comment thread packages/coding-agent/src/cli/tailscale.ts
…doctor, mesh docs)

First PR of the Tailscale agent-mesh stack: detection only.

- prime-agent tailscale: tailnet state, MagicDNS suffix, node hostname (human or --json)
- prime-agent tailscale serve --port <n> [--funnel]: wraps tailscale serve/funnel --bg
  with pre-flight checks and post-serve verification
- probeTailscale() exported as the detection seam for the mesh PRs (peer discovery,
  remote-session labels, cross-machine messaging/spawn)
- doctor surfaces the same detection in human mode
- docs/tailscale.md describes the mesh (detection + remote agents); the manual
  workaround patterns (SSH, container MagicDNS, MCP connector) are superseded
  by the mesh and removed
- dual-reviewer findings closed: unparseable-status and https-default-port tests,
  --json listed in the command registry

@cursor cursor Bot left a comment

Copy link
Copy Markdown

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 and found 1 potential issue.

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 248a36e. Configure here.

Comment thread packages/coding-agent/src/cli/tailscale.ts Outdated
…le after successful probe

Human `prime-agent tailscale` (no --json) returned exit 1 when
`tailscale serve status` failed or was unparseable, even after the probe
confirmed the node is on a tailnet. --json already returned 0 for the same probe.

Return 0 after printing the yellow warning so older or serve-less Tailscale
installs do not look down despite a successful detection. Pinned by a
regression test (serve-status exits non-zero after probe succeeds).
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