Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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