Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/coding-agent/.changes/tailscale-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Added first-class Tailscale support: `prime-agent tailscale` reports tailnet and MagicDNS state, `prime-agent tailscale serve --port <n> [--funnel]` exposes a local port on the tailnet via `tailscale serve --bg`, doctor surfaces Tailscale detection, and docs cover the tailnet patterns (Tailscale SSH remote control, serve/funnel exposure, MagicDNS reach from cloud containers, and adding Tailscale's MCP server).
41 changes: 41 additions & 0 deletions packages/coding-agent/docs/tailscale.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Tailscale

Prime Agent is tailnet-aware: it detects Tailscale, reports your tailnet state, and can expose local ports on your tailnet with one command. This page documents the three supported patterns.

## 1. Reach your agent from anywhere (Tailscale SSH)

The agents view is local-first: it renders in your terminal over the daemon's unix socket. To control it from any device on your tailnet, use Tailscale SSH into the host and run `prime-agent` there:
Comment thread
macroscopeapp[bot] marked this conversation as resolved.

```sh
# from your laptop or phone terminal, on any tailnet device:
ssh your-agent-host
prime-agent agents
```

No port forwarding, no public exposure - Tailscale SSH authenticates with your tailnet identity. Prerequisites: Tailscale SSH must be enabled on the agent host (`tailscale up --ssh` on it, and your tailnet ACL must allow `autogroup:member` ssh access to it); plain `ssh` without Tailscale SSH enabled would fall back to a normal SSH server that may not exist or use different credentials.

## 2. Expose a local port on your tailnet (`prime-agent tailscale serve`)

Wrap `tailscale serve` for any local bridge, API, or dev server:

```sh
prime-agent tailscale serve --port 3000 # https://<host>.<tailnet>.ts.net
prime-agent tailscale serve --port 3000 --funnel # public via tailscale funnel
prime-agent tailscale # status: tailnet, MagicDNS name, served endpoints
```

The command refuses with a teaching error when the tailscale CLI is missing or the machine is not up on a tailnet. `prime-agent doctor` includes the same detection in its report.

## 3. Let the agent reach tailnet services (MagicDNS + containers)

A process on a tailnet machine reaches every other device by MagicDNS name (`http://db.tailnet.ts.net:5432`) with no extra wiring - the agent can already do this from the kernel. To give a CLOUD-hosted agent container tailnet access, join it to your tailnet: install the Tailscale CLI (or sidecar container) and run `tailscale up` with an auth key in the container bootstrap, then MagicDNS names resolve from inside the agent. Container note: without `/dev/net/tun` (typical for hosted containers), run tailscaled in userspace-networking mode (`tailscaled --tun=userspace-networking`) - the container then dials out through userspace networking and MagicDNS still works; `tailscale up` alone cannot create the tunnel interface in that environment.

## 4. Operate your tailnet from the agent (Tailscale MCP)

Tailscale publishes an MCP server for AI agents to operate a tailnet (list devices, manage serve/funnel). Find the current endpoint in Tailscale's docs (https://tailscale.com/kb - search "MCP"), then add it as a remote MCP server:

```sh
prime-agent mcp add remote --url https://<current-tailscale-mcp-endpoint>
```

References: https://tailscale.com/kb (Serve/Funnel, MagicDNS, container patterns, MCP).
10 changes: 10 additions & 0 deletions packages/coding-agent/src/cli/command-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,16 @@ export const COMMAND_SPECS: readonly CommandSpec[] = [
summary: "Inspect and safely clean up background services",
options: ["--fix Remove stale sockets and stop idle orphaned services", "--json Print JSON"],
},
{
path: ["tailscale"],
usage: "tailscale [status] | tailscale serve --port <n> [--funnel]",
summary: "Tailscale tailnet support: status, and expose a local port via serve/funnel",
options: [
"status (default) show tailnet state, MagicDNS name, and served endpoints",
"serve --port <n> expose localhost:<n> on your tailnet (wraps `tailscale serve --bg`)",
"--funnel with serve: expose publicly via tailscale funnel",
],
},
{
path: ["shutdown"],
usage: "shutdown [--force] [--json]",
Expand Down
23 changes: 23 additions & 0 deletions packages/coding-agent/src/cli/public-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { handleDaemonCommand } from "./daemon-command.js";
import { runPs, runReap, runShutdownAll } from "./daemon-ps.js";
import { DAEMON_UPDATE_RESTART_COORDINATOR_FLAG } from "./daemon-update-restart.js";
import { extractHelpCommandPath, rotateGlobalFlagsBeforeCommand } from "./global-flags.js";
import { parseTailscaleArgs, runTailscaleServe, runTailscaleStatus, tailscaleDoctorFacts } from "./tailscale.js";

export interface PublicCommandResult {
handled: boolean;
Expand Down Expand Up @@ -113,6 +114,8 @@ async function runPublicCommand(args: string[]): Promise<PublicCommandResult> {
return runStatus(args.slice(1));
case "doctor":
return runDoctor(args.slice(1));
case "tailscale":
return runTailscaleCommand(args.slice(1));
case "shutdown":
return runShutdown(args.slice(1));
case "package":
Expand Down Expand Up @@ -255,7 +258,27 @@ async function runDoctor(args: string[]): Promise<PublicCommandResult> {
await runReap(options.has("--json"), false);
} else {
await runPs(options.has("--json"));
if (!options.has("--json")) {
for (const fact of tailscaleDoctorFacts()) {
console.log(fact);
}
}
}
return HANDLED;
}

function runTailscaleCommand(args: string[]): PublicCommandResult {
const parsed = parseTailscaleArgs(args);
if (parsed.kind === "error") {
console.log(chalk.red(parsed.message));
process.exitCode = 1;
return HANDLED;
}
if (parsed.kind === "serve") {
process.exitCode = runTailscaleServe(parsed.port, parsed.funnel);
return HANDLED;
}
process.exitCode = runTailscaleStatus(parsed.json);
return HANDLED;
}

Expand Down
Loading
Loading