仕組み
他のページでは説明のつかない挙動に遭遇したときに参照してください。
構成要素
minato (CLI) ──────┐
minato-desktop ────┼── Unix socket / JSON-RPC ──┐
SKILL.md (agent) ──┘ │
┌───────────┐
│ minatod │
└─────┬─────┘
┌──────────────┬──────────────────┼──────────────┬─────────────┐
▼ ▼ ▼ ▼ ▼
DNS (:53) Proxy (:80/:443) Runtime Env resolver Tunnel
*.localhost → Host で振り分け Docker / Apple 3 層 + cloudflared
127.0.0.1 シークレット参照daemon の API が本体です。 CLI、デスクトップアプリ、Skill はいずれも 同格のクライアントであり、独自のロジックを持ちません。あるクライアントで できる操作は、他のクライアントでも同様に行えます。
daemon を置いている理由
常駐が必要な機能が 4 つあります。80/443 番ポートを保持し続けるプロキシ、 53 番ポートを保持する DNS、scale-to-zero が依存するアイドル判定のタイマー、 そして cloudflared のプロセス管理です。実行のたびに終了する CLI では、 いずれも実現できません。
リクエストの処理経路
https://web.feature-auth.myapp.localhost にアクセスした場合の流れです。
DNS。 macOS は
*.localhostを標準では解決しません。Chrome は独自に 解決しますが、curl、Safari、Node の fetch は解決せず、エージェントは curl を使用します。そこで daemon が DNS サーバを持ち、/etc/resolver/localhostからそこへ向けます。この DNS は、接尾辞配下の すべての名前に対して 127.0.0.1 を返します。環境が存在しない名前も 含まれます。名前解決に失敗しても情報は得られませんが、プロキシまで到達 すれば、稼働中の workspace を示す 404 が返るためです。TLS。 プロキシは SNI で要求された名前に対する証明書を、
~/.minato/ca/のローカル CA からその場で発行します。ワイルドカード証明書 では対応できません。*.localhostではweb.feature-auth.myapp.localhostを カバーできず、worktree が増えるたびに階層の深さが異なる名前が生成される ためです。利用者は CA を 1 つ信頼するだけで済みます。ルーティング。 プロキシは daemon が保持するテーブルからホスト名を検索 します。稼働中のサービスにはアドレスが登録され、停止中のサービスはアドレス なしで登録されています。「停止中」と「存在しない」を区別する必要があるため です。前者は起動し、後者は 404 を返します。
起動。 停止中であれば起動します。ブラウザには約 1.5 秒後に自動リロード するページを返し、それ以外は応答可能になるまで最大 120 秒待機させます。 エージェントに 503 を返すと、サーバの故障と判断されるためです。
転送。 プロキシは対象のアドレスに接続し、データを中継します。WebSocket の upgrade にも対応しています。HMR がこれに依存しており、また upgrade は HTTP/1.1 の機構であるため、HTTP/2 は意図的にアドバタイズしていません。
Host ヘッダは書き換えません。 Vite などは Host ヘッダを見て許可判定を 行うため、アプリケーションからはブラウザで開いた URL がそのまま見える必要が あります。
状態の保持場所
daemon は実行時の状態をファイルに保存しません。 コンテナが稼働している かどうか、どのアドレスが割り当てられたかは、コンテナ自身のラベル (dev.minato.*)から取得します。daemon を再起動しても、一覧を 1 回取得 するだけで状態を復元できます。
状態ファイルが保持するのは 2 つだけです。Minato が管理している worktree の 一覧と、それぞれに発行した URL ラベルです。ラベルを永続化しているのは、後から 命名規則を変更しても既存の workspace の URL が変わらないようにするためです。
状態の複製が存在しないため、クラッシュ後に整合性を取る処理も不要です。
命名規則
feature/user-auth は feature-user-auth に変換されます。
- 小文字化し、
[a-z0-9-]以外の文字を-に置換する - 連続する
-を 1 つにまとめ、前後の-を削除する - 63 文字を超える場合は切り詰め、元の名前のハッシュを付加する
- 既存の workspace と衝突する場合もハッシュを付加する
区切り記号以外の文字が失われた場合にもハッシュを付加します。 /、_、 -、.、空白は区切り記号として扱いますが、それ以外の文字が失われた場合は 情報が欠落しています。この処理がないと feature/デモ環境 と feature/検証環境 がどちらも feature になり、URL が衝突します。日本語の ブランチ名は実際に使用されます。
ランタイム抽象が隠蔽しているもの
バックエンドの責務は、サービスを 1 つ起動し、その到達先を返すことだけです。
pub struct RunningService {
pub endpoint: SocketAddr,
}Docker ではフォワードされた 127.0.0.1:49312、Apple Container ではコンテナ 自身の 192.168.64.3:3000 が返ります。プロキシはどちらであるかを認識しません。 この戻り値の設計により、Apple Container の追加時に他の変更が不要でした。 ポートフォワードを前提とした型であれば、作り直しが必要になっていたはずです。
特権ポート
1024 番未満のポートは root 権限がないとバインドできません。launchd が root として 53/80/443 をバインドし、daemon にはファイルディスクリプタのみを渡し ます。plist の UserName により daemon 自体は利用者の権限で動作するため、 作成されるコンテナやファイルの所有者も正しくなります。
macOS は systemd の LISTEN_FDS 規約を使用しないため、 launch_activate_socket() を呼び出し、plist に記述した名前でファイル ディスクリプタを取得します。
SockNodeName に localhost を指定すると、launchd は ::1 と 127.0.0.1 の 両方にソケットを開きます。両方が必要です。macOS は *.localhost を両方に 解決し、クライアントは IPv6 を優先するためです。IPv4 のみで待ち受けていた際に、 たまたま [::1] を使用していた無関係のアプリケーションへリクエストが送られる という問題が実際に発生しました。
さらに詳しく
覆した設計判断とその理由を含む記録は docs/DESIGN.md にあります。