スマホのブラウザからリモートのCLIエージェントを動かす|agent-start のアーキテクチャ
自宅のLinuxマシンでClaude / Codex のCLIセッションを起動・管理し、tailnet越しにスマホからでも操作できるOSS「agent-start」。単一のRustバイナリでPTYとWebSocketをどう束ねているのか、設計を解説します。
agent-start は、リモートマシン上で
claude / codex の CLI セッションを起動・管理するためのセルフホスト型 Web ランチャーです。
自宅の Linux マシンで動かしておけば、tailnet(Tailscale などの仮想プライベートネットワーク)越しに
スマホのブラウザからでも エージェントを立ち上げられます。
この記事では、agent-start がどんな設計でこれを実現しているのかを掘り下げます。

何を解決するのか
AI コーディングエージェントの CLI は強力ですが、実行には常に「動かしっぱなしの端末」が要ります。 ノート PC を閉じればセッションは死に、外出先からは触れません。agent-start は、
- プロジェクトを選んで
claude/codexを 永続的な PTY セッションとして起動 - 実行中セッションの一覧・プレビュー・停止
- ブラウザを閉じてもセッションは生き続ける(PTY はホストプロセスが保持)
を、1 つのバイナリで提供します。
全体像
flowchart LR
Phone[スマホ / PC のブラウザ] -- tailnet --> Host
subgraph Host[agent-start-host : Rust :3030]
API["/api/* /v1/* /ws/*"]
SPA[埋め込み SPA<br/>rust-embed]
PTY[PTY マルチプレクサ]
DB[(SQLite host.db)]
end
PTY --> Shell["bash -lc 'claude ...' / 'codex --full-auto'"]
agent-start-host は 単一の Rust バイナリです。HTTP/WebSocket の配信、複数 PTY の多重化、
状態の永続化までを 1 プロセスで担います。技術スタックは次の通りです。
| 役割 | 採用 |
|---|---|
| HTTP/WS サーバー | axum |
| 非同期ランタイム | tokio |
| 擬似端末(PTY) | portable-pty |
| 永続化 | sqlx + SQLite |
| SPA の埋め込み | rust-embed |
単一バイナリに SPA を埋め込む
フロントエンドは Vite+ + React + TanStack Router の SPA です。開発時は別プロセス
(vp dev の :5173)で動き、/api/* /v1/* /ws/* をホストの :3030 にプロキシします。
一方、本番ではビルド済みの front/dist/ を rust-embed でバイナリに焼き込みます。
これにより、配布物は「静的アセット用ディレクトリを一切持たない単一実行ファイル」になります。
# ソースからビルドする場合(cd front && vp install && vp build) # SPA を front/dist に出力(cd server-rs && cargo build --release) # rust-embed が front/dist を取り込む./server-rs/target/release/agent-start-host --port 3030配布バイナリは自己完結しているので、運用は「置いて実行するだけ」。デプロイ対象が 1 ファイルで済むのは、セルフホストするうえで大きな利点です。
PTY を WebSocket で多重化する
エージェント CLI は対話的な端末プログラムなので、ただ標準入出力をパイプするだけでは足りません。
agent-start はセッションごとに 擬似端末(PTY)を割り当て、その読み書きを WebSocket
(/ws/*)でブラウザのターミナルへ橋渡しします。
ポイントは、PTY の寿命を WebSocket 接続から切り離していることです。 ブラウザを閉じても(= WS が切れても)、ホストプロセスが PTY を握り続けるため、 エージェントは動き続けます。再接続すれば続きから画面に出力が流れてきます。
これを支えるのが SQLite への履歴保存です。状態とデータはすべて ~/.agent-start/ 配下に集約されます。
~/.agent-start/├── config.json # CLI プリセット├── preferences.json # UI から保存した起動フラグ├── host.db # セッション + pty_history(SQLite)├── runtime/manifest.json# 稼働中ホストの URL / PID├── projects/ # 取り込んだプロジェクト└── worktrees/<session>/ # セッションごとの git worktreehost.db の pty_history に出力を蓄えておくことで、後から接続したクライアントにも
直近のターミナル内容を再生できます。runtime/manifest.json は稼働中ホストの URL と PID を
持ち、後述の薄い CLI クライアントが「どこに繋ぐか」を知るための入口になります。
薄いクライアント CLI
リリースには agent-start-host 本体に加えて、HTTP 越しに操作する 薄いクライアント CLI
agent-start が同梱されています。ローカルの manifest を見て自動で接続先を決めるほか、
--url で tailnet 越しのリモートホストも叩けます。
agent-start start --daemon # ホストをバックグラウンド起動agent-start session create --project /path/to/project --cli claude --worktreeagent-start session listagent-start --url http://server:3030 status # リモートホストの状態確認サーバーもクライアントも Rust で書かれ、API は HTTP に統一されています。ホストは「単一バイナリ・ HTTP/WS の口を持つデーモン」、CLI は「その口を叩くクライアント」という素直な分離です。
まとめ
- 単一の Rust バイナリ(axum + tokio + portable-pty + sqlx)に SPA を
rust-embedで同梱 - PTY を WebSocket で多重化し、接続と寿命を分離することでブラウザを閉じてもセッションが生存
- 状態は
~/.agent-start/の SQLite に集約、薄い CLI クライアントから HTTP で操作
次の記事では、複数エージェントを安全に並列実行するための git worktree による分離について書きます。