Imanect Engineering

スマホのブラウザからリモートのCLIエージェントを動かす|agent-start のアーキテクチャ

自宅のLinuxマシンでClaude / Codex のCLIセッションを起動・管理し、tailnet越しにスマホからでも操作できるOSS「agent-start」。単一のRustバイナリでPTYとWebSocketをどう束ねているのか、設計を解説します。

後藤柊矢
CTO @ Imanect

agent-start は、リモートマシン上で claude / codex の CLI セッションを起動・管理するためのセルフホスト型 Web ランチャーです。 自宅の Linux マシンで動かしておけば、tailnet(Tailscale などの仮想プライベートネットワーク)越しに スマホのブラウザからでも エージェントを立ち上げられます。

この記事では、agent-start がどんな設計でこれを実現しているのかを掘り下げます。

agent-start の Web UI。左にプロジェクト一覧、中央に claude セッションのターミナル、右にファイルツリー

何を解決するのか

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 でバイナリに焼き込みます。 これにより、配布物は「静的アセット用ディレクトリを一切持たない単一実行ファイル」になります。

Terminal window
# ソースからビルドする場合
(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 worktree

host.dbpty_history に出力を蓄えておくことで、後から接続したクライアントにも 直近のターミナル内容を再生できます。runtime/manifest.json は稼働中ホストの URL と PID を 持ち、後述の薄い CLI クライアントが「どこに繋ぐか」を知るための入口になります。

薄いクライアント CLI

リリースには agent-start-host 本体に加えて、HTTP 越しに操作する 薄いクライアント CLI agent-start が同梱されています。ローカルの manifest を見て自動で接続先を決めるほか、 --urltailnet 越しのリモートホストも叩けます。

Terminal window
agent-start start --daemon # ホストをバックグラウンド起動
agent-start session create --project /path/to/project --cli claude --worktree
agent-start session list
agent-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 による分離について書きます。

後藤柊矢
CTO @ Imanect

I'm Shuya Goto. Software Engineer, Mathematics Major.