Skip to content

Commit fc4db61

Browse files
authored
Merge pull request #42 from Zeus-Deus/mcp-on-remote
MCP on remote: headless daemon + auto-provisioning everywhere
2 parents 079bf0a + 02ab669 commit fc4db61

24 files changed

Lines changed: 5324 additions & 150 deletions

docs/features/remote-hosts.md

Lines changed: 139 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,23 +50,157 @@ Wiring **deferred** to a follow-up (small UX work, no architectural risk):
5050
- Workspace header badge for non-local workspaces.
5151
- Workspace list filter dropdown.
5252

53-
## `codemux-remote` slim binary (2c)
53+
## `codemux-remote` binary
5454

55-
`src-tauri/src/bin/codemux_remote.rs`. New `[[bin]]` target in `Cargo.toml`. Same `codemux_lib` crate, no UI deps.
55+
`src-tauri/src/bin/codemux_remote.rs`. `[[bin]]` target in `src-tauri/Cargo.toml`. Same `codemux_lib` crate, no UI deps.
5656

5757
CLI:
5858
```
5959
codemux-remote version
60-
→ {"name":"codemux-remote","version":"0.3.1","protocol_version":1}
60+
→ {"name":"codemux-remote","version":"<v>","protocol_version":1}
6161
6262
codemux-remote pty-daemon --socket /tmp/codemux-ptyd-<rand>.sock
63-
→ binds the socket, runs the daemon server, never returns
63+
→ binds the Unix socket the laptop's "Push workspace" tunnel uses,
64+
runs the PTY daemon server, never returns.
6465
6566
codemux-remote scheduler
6667
→ runs the Automations reconcile + pull + tick + execute loop on an
67-
always-on host; never returns
68+
always-on host; never returns.
69+
70+
codemux-remote serve [--port <n>] [--state-dir <path>]
71+
→ runs the headless Codemux daemon: axum HTTP server on 127.0.0.1
72+
with bearer-token auth (manifest at <state-dir>/manifest.json,
73+
mode 0600). Tracks workspaces in <state-dir>/codemux.db. Survives
74+
SIGTERM/SIGINT cleanly (manifest is removed on shutdown). v1
75+
listens loopback-only; the desktop tunnels in over SSH.
76+
77+
codemux-remote serve status [--state-dir <path>]
78+
→ prints {endpoint, pid, started_at, host_id, alive} as JSON.
79+
Exit 0 if alive, exit 1 otherwise.
80+
81+
codemux-remote serve stop [--state-dir <path>]
82+
→ SIGTERM the running daemon (pid read from manifest).
83+
84+
codemux-remote mcp [--state-dir <path>]
85+
→ stdio JSON-RPC MCP server. Reads <state-dir>/manifest.json for the
86+
daemon's endpoint + secret, then bridges agent CLI tool calls to
87+
the daemon's HTTP API. Configure your CLI agent with:
88+
{"command": "codemux-remote", "args": ["mcp"]}
6889
```
6990

91+
### `serve` mode overview
92+
93+
The `serve` subcommand is the headless equivalent of running the
94+
desktop Codemux app on this host — an MCP-aware agent on this machine
95+
can drive Codemux locally without any UI. Module layout:
96+
97+
- `src-tauri/src/remote/manifest.rs` — manifest read/write, pid liveness.
98+
- `src-tauri/src/remote/auth.rs` — bearer-token axum middleware,
99+
attaches `Identity::Local` to the request.
100+
- `src-tauri/src/remote/identity.rs``Identity::Local | Cloud{…}`.
101+
v1 only ever produces `Local`; the `Cloud` variant is here so a
102+
future optional relay layer can forward verified identity without
103+
changing handler signatures.
104+
- `src-tauri/src/remote/workspace.rs` — SQLite workspace registry
105+
with nullable `owner_id` column for future relay use.
106+
- `src-tauri/src/remote/pty.rs` — minimal portable-pty wrapper with
107+
per-terminal ring buffer.
108+
- `src-tauri/src/remote/server.rs` — axum routes (`/health`,
109+
`/tools/list`, `/tools/call`).
110+
- `src-tauri/src/remote/mcp.rs` — stdio MCP server.
111+
- `src-tauri/src/remote/tools/mod.rs` — 11-tool catalog.
112+
113+
Headless tool surface (advertised via `tools/list`):
114+
115+
| Tool | Purpose |
116+
|---|---|
117+
| `workspace_create` | Register a new workspace. |
118+
| `workspace_list` | All workspaces, newest first. |
119+
| `workspace_info` | One workspace by id. |
120+
| `workspace_update` | Mutate name/branch/notes. |
121+
| `workspace_close` | Remove from registry. |
122+
| `terminal_spawn` | Spawn a shell PTY. |
123+
| `terminal_write` | Write bytes to PTY stdin. |
124+
| `terminal_read` | Read accumulated PTY output. |
125+
| `terminal_list` | List open PTYs. |
126+
| `terminal_close` | SIGHUP the shell. |
127+
| `app_status` | Daemon version, uptime, counts. |
128+
129+
Deliberately *not* in the headless surface: `pane_*`, `browser_*`
130+
(no UI on a headless host).
131+
132+
### Process supervision
133+
134+
Sample systemd user unit at `scripts/codemux-remote.service.example`.
135+
Install:
136+
137+
```
138+
cp scripts/codemux-remote.service.example ~/.config/systemd/user/codemux-remote.service
139+
loginctl enable-linger $USER
140+
systemctl --user daemon-reload
141+
systemctl --user enable --now codemux-remote.service
142+
```
143+
144+
`Restart=on-failure`, `StandardError=journal`, `loginctl enable-linger`
145+
so the daemon survives ssh logouts and reboots.
146+
147+
### Designing for a future optional cloud relay
148+
149+
`docs/plans/mcp-on-remote.md` details the four design choices baked
150+
into v1 so that a paid-tier cloud relay (team collaboration, "control
151+
from phone without SSH") can be added later as a purely additive
152+
feature, not a rewrite: HTTP transport + bearer-token, `Identity`
153+
passthrough, nullable `owner_id`, no Better-Auth coupling in the
154+
daemon. None of those four costs anything today.
155+
156+
### Auto-provisioning on push (the "it just works" flow)
157+
158+
The desktop's push flow does the following automatically, so the user
159+
never has to know about manifests, secrets, or systemd:
160+
161+
1. **Binary install.** `ssh::bootstrap::bootstrap_remote` scp's the
162+
matching `codemux-remote-<target>` to `~/.local/bin/codemux-remote`
163+
and chmods it. Skipped if `version` already returns the right value.
164+
2. **`serve` systemd unit.** `ssh::bootstrap::provision_serve` writes
165+
`~/.config/systemd/user/codemux-remote.service`, runs
166+
`loginctl enable-linger` so it survives logout, and
167+
`systemctl --user enable --now codemux-remote`. The daemon binds an
168+
ephemeral loopback port and writes its manifest under
169+
`~/.local/share/codemux-remote/`.
170+
3. **`.mcp.json` in the pushed workspace.** `ssh::push::push_workspace`
171+
drops a `.mcp.json` into the rsynced workspace directory pointing
172+
`codemux` at `codemux-remote mcp`. Any CLI agent (Claude Code,
173+
Codex, Gemini) launched in that directory on the host auto-discovers
174+
Codemux as an MCP server with zero further config.
175+
4. **Workspace registration.** After rsync,
176+
`ssh::bootstrap::register_workspace_on_remote` runs `codemux-remote
177+
workspace register --path ... --name ... --branch ...` on the host
178+
via SSH. That command talks to the local daemon over loopback HTTP
179+
and inserts the workspace into its registry, so it shows in
180+
`workspace_list` from any MCP-aware agent on the host.
181+
182+
Net effect: user clicks "Push workspace to host" once, gets back a
183+
working MCP control plane on the remote without ever having to know
184+
the words "manifest" or "systemd."
185+
186+
### Controlling the daemon from your phone (Tailscale)
187+
188+
v1's transport is SSH-only — there is no cloud relay. The cleanest
189+
way to drive your home/VPS Codemux daemon from a phone today is
190+
**Tailscale**:
191+
192+
1. Install Tailscale on both the host running `codemux-remote serve`
193+
and your phone (Tailscale has iOS and Android apps).
194+
2. Both devices join the same tailnet.
195+
3. From any agent app on the phone that can SSH (or any web shell
196+
pointed at the host's tailnet name), `ssh <host>` works without
197+
any port forwarding or DDNS.
198+
199+
This costs nothing, works today, and is what we recommend until the
200+
optional cloud relay (paid tier) ships. The relay would replace the
201+
SSH-from-phone requirement, not the Tailscale-or-similar mesh — some
202+
users will always prefer mesh VPN over a hosted relay for privacy.
203+
70204
The `scheduler` subcommand was added by the Automations feature, not by
71205
2c. Host bootstrap provisions it — it writes the scheduler token + host
72206
identity and registers a systemd user service so it survives reboots.

0 commit comments

Comments
 (0)