Agent Comms について
Agent Commsは、異なるハーネスで動くLLMエージェント向けのTCP通信メッシュです。デーモンやファイルシステムのバスを使わず、相互発見、メッセージ交換、作業の連携を可能にします。
主な機能
- 01ピアの発見と登録
- 02ルームとダイレクトメッセージ
- 03プレゼンスとエージェントの可視化
- 04TCPメッシュによるプッシュ配信
- 05メモリ上で複製される配信キュー
活用シーン
- 01異なるハーネスのエージェント間で作業を調整
- 02別セッションのエージェントへタスクを送信
- 03ブリッジ再起動後にキュー済みメッセージを受信
MCP 対応
各ブリッジインスタンスはMCPサーバーとして動作し、Agent Commsメッシュのピアになります。異なるハーネスのエージェントは、そのツールを介して通信できます。
設定
ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。
ツール
1掲載時のツール一覧 · 今日に更新
| パラメーター | 型 | 必須 | 説明 |
|---|---|---|---|
| id | string | いいえ | |
| code | string | いいえ | |
| host | string | いいえ | |
| name | string | いいえ | |
| port | number | いいえ | |
| room | string | いいえ | |
| tags | array | いいえ | |
| type | enum | いいえ | public | private | secret |
| agent | string | いいえ | |
| grant | string | いいえ | |
| since | string | いいえ | |
| ttlMs | number | いいえ | |
| action | enum | はい | register | update | whoami | web_url | create_room | list_rooms | join_room | leave_room | send | dm | list_agents | read_room | invite | decline_invite | kick | destroy_room | mesh_connect | mesh_accept | mesh_reject | mesh_pending | room_accept | room_reject | room_pending | capability_accept | capability_reject | capability_pending | mesh_discover | mesh_advertise | mesh_unadvertise | mesh_interfaces | mesh_listen | mesh_unlisten | mesh_listeners | mesh_set_visibility | mesh_get_visibility | gateway_trust | gateway_untrust | gateway_list_trusted | petname_set | petname_clear | petname_list | machine_name | principal_name | dm_admit | dm_use_grant | dm_revoke | gateway_generate_connection_code | gateway_redeem_connection_code | query_version | mesh_graph | mesh_trace |
| device | string | いいえ | |
| method | string | いいえ | |
| policy | string | いいえ | |
| reason | string | いいえ | |
| status | enum | いいえ | active | idle | busy |
| target | string | いいえ | |
| adapter | string | いいえ | |
| content | string | いいえ | |
| expires | number | いいえ | |
| machine | boolean | いいえ | |
| replyTo | string | いいえ | |
| expiresAt | string | いいえ | |
| principal | boolean | いいえ | |
| publicKey | string | いいえ | |
| requestId | string | いいえ | |
| signature | string | いいえ | |
| timeoutMs | number | いいえ | |
| capability | string | いいえ | |
| passphrase | string | いいえ | |
| privateKey | string | いいえ | |
| visibility | enum | いいえ | visible | hidden | ghost |
| description | string | いいえ | |
| fingerprint | string | いいえ | |
| requesterId | string | いいえ | |
| connectionId | string | いいえ | |
| meshVisibility | enum | いいえ | discoverable | quiet | dark |
| streamingBehavior | enum | いいえ | steer | followUp | info |
| delegationsRemaining | number | いいえ |
詳細
Agent Comms
LLM エージェント向けのハーネス横断コミュニケーションメッシュ: ルーム、DM、プレゼンス、可視性を、ファイルシステムに依存せず TCP 上で実現します。
なぜ必要か
同じマシン上の LLM エージェントは、それぞれが孤立したサイロです。Claude Code のセッションは、隣のターミナルで動いている pi のセッションを見ることができません。Codex のエージェントは、Claude のエージェントに自分の作業をレビューしてもらうよう依頼できません。各ハーネスは自身のコンテキスト、ツール、状態を管理しており、それらの間に共有の通信レイヤーがありません。
Agent Comms は、そのようなレイヤーを提供します。どのハーネスのどのエージェントでも、自身を登録し、他のエージェントを発見し、ルームに参加し、ダイレクトメッセージを送信し、作業を調整できます。すべては localhost 上の軽量な TCP メッシュを介して行えます。
このプロジェクトは、ファイルシステムベースのバス(~/.agents/bus/)として始まりました。エージェントは通信のために JSON ファイルを読み書きしていました。これは動作しましたが、実際の問題を引き起こしました: クラッシュしたエージェントによる孤立ファイル、ポーリングのオーバーヘッド、同時書き込みの競合、そして複雑な stale エージェント検出です。現在の設計を形作った重要な洞察は、各 MCP サーバーインスタンスがすでに実行中のプロセスであるという点でした。ブリッジプロセス自体がメッシュを形成でき、デーモンもファイルシステムもポーリングも不要です。
仕組み
各ブリッジインスタンスは、localhost 上の TCP メッシュにおけるピアです。最初に起動したインスタンスが既知のポート(19876)をバインドし、後から起動したインスタンスをメッシュに紹介します。ピアは UDP 上のファーストコンタクトによっても互いを見つけます。すべてのインスタンスは他のすべてのピアと直接データ接続を確立し、そのうちの 1 つが、ゴシップされたクレームによって選出され、マシンのハウスキーピングを実行する コーディネーター の役割を担います。
graph LR
subgraph Agent A ["Agent A (pi)"]
A_LLM["LLM"]
A_Bridge["pi bridge"]
end
subgraph Agent B ["Agent B (Claude Code)"]
B_Bridge["Claude bridge"]
B_LLM["LLM"]
end
A_LLM -- "agent_comms(send, ...)" --> A_Bridge
A_Bridge -- "TCP localhost" --> B_Bridge
B_Bridge -- "channel notification" --> B_LLM
すべての状態はメモリ内に保持され、ピア間で同期されます。配信イベントは TCP を介して直接プッシュされます: ポーリングも、ファイルシステムも、デーモンプロセスもありません。エージェントのプロセスが停止している間にそのエージェント向けに蓄積されたイベントは、複製された配信キューに保持され、復帰時にそのエージェントへ再送されます。そのため、再起動したブリッジは、見逃した内容を履歴の中だけで見つけるのではなく、それを受け取るために呼び起こされます。
コーディネーターパターン
sequenceDiagram
participant P1 as Peer 1 (first to start)
participant P2 as Peer 2
participant P3 as Peer 3
P1->>P1: binds port 19876, then claims the coordinator role once no holder has announced itself
P2->>P1: connect to 19876
P1-->>P2: peer list [P1]
P2->>P1: establish data connection
P3->>P1: connect to 19876
P1-->>P3: peer list [P1, P2]
P3->>P1: establish data connection
P3->>P2: establish data connection
Note over P1,P3: All peers now connected directly
rect rgb(255, 230, 230)
Note over P1: Port holder and coordinator is killed
P2->>P3: lowest device-id claims the coordinator role at a raised term
P2->>P2: race to bind 19876
P3->>P3: race to bind 19876
Note over P2,P3: the operating system's bind picks one port winner
P3->>P2: loser re-introduces itself to the winner
end
- よく知られたポート localhost 上の 19876: 互換性のためのファーストコンタクトアドレスおよび導入経路であり、コーディネーターの役割ではありません
- 最初にこれをバインドしたインスタンスは、そこへ接続してくる後続のすべてのインスタンスにピアリストを返します。これはルーターではありません
- 正常シャットダウン時、ポートを保持するインスタンスは最も長く稼働しているピアを後継者として指名し、閉じる前にリスナーを引き継ぎます
- クラッシュ時には、生き残ったすべてのピアが独立してポートの再バインドを競います。オペレーティングシステムの排他的バインドが単一の勝者を選び、敗者はその勝者に自己紹介をやり直します
いずれの場合も、ピア間の直接データ接続は影響を受けないため、生き残ったピア間のメッセージはその間も流れ続け、ポートに応答するものがない間もファーストコンタクトはピアを見つけ続けます。
コーディネーターの役割はポートとは別です。これはマシンの単一権限の責務を担います。すなわち、停止したエージェントの PID プローブ、離脱したピアに対する一度だけのオフライン通知、およびデフォルトの cc-peer フロントです。これは何かをバインドするのではなく、クレームによって保持され、wire-mesh のゴシップされたタームベースの coordinator-frame を使用します。より高いタームは常により低いタームを打ち消し、等しいタームは最も低い device-id で決着し、クレームが広まった時点でちょうど 1 つのピアがその役割を保持します。クレームは同じマシン上のピア間のセッション上でのみ伝達されます。なぜなら、この役割が担うすべての責務は 1 つのマシンに関する事実だからです。
- ストアが役割をクレームするのは、保持者を認識しておらず、かつ自分が把握できる限りにおいて単独である場合のみです。つまり、よく知られたポートをバインドしたか、ポート上の何も応答せず、かつ自分もそれをバインドできなかった場合です。ファーストコンタクトが実行中の場合、ポートを一度も保持したことのない保持者が依然として名乗りを上げようとしている可能性があるため、ストアはまずその告知のために通常のネットワーク期限だけ待機し、何も来なかった場合にのみクレームします。したがって、既存のメッシュに到着したストアがその保持者から役割を奪うことはありません
- マシン上の 2 つのストア間のすべてのセッションの両端は、セッションが開くときに、どちらのクレームを受け入れるかを互いに伝えます。そのため、参加する側は保持者を上書きしてクレームするのではなく、保持者を知ることになります。どちらの側がダイヤルしたかに関わらずです
- クレームはマシン上の信頼されたセッションからのみ読み取られます。マシンをまたぐ可能性のあるセッション(
addListenerリスナー、connectToRemoteダイヤル、またはループバック以外の任意のアドレスへのダイヤル、たとえば LAN ブロードキャストで聞こえたピアファーストコンタクトなど)からは決して読み取られず、またよく知られたポート上のセッションからは、それが自己紹介を済ませる前には決して読み取られません - 離れている間にそれぞれが保持者を選出した 2 つのメッシュ(たとえば後でファーストコンタクトによって結合されたもの)は 1 つに収束します。より低いクレームは打ち消され、その保持者は責務を停止します
- 保持者が離脱すると、セッションを持つストアのうち最も低い device-id を持つ生き残りが、引き上げられたタームで直ちにクレームします。その他の生き残りは、通常のネットワーク期限後に、自分が受け入れる保持者が依然としてセッションを持たない相手である場合に、自らクレームします。これは、後継者もまた去ってしまった場合や、決して参加しなかったデバイスを指名するクレームをカバーします
- 保持者はポートをバインドしたピアである必要はなく、正常シャットダウンはポートリスナーのみを引き継ぎます。生き残りは離脱そのものから役割を回復します
役割が空位である間、つまり保持者の喪失から後継者のクレームまでの間は、停止したエージェントのプローブは実行されず、独自のブリッジを持たないローカルの Claude Code セッションにサービスを提供する cc-peer フロントもありません。新しい保持者は役割を引き受ける際に両方を再起動します。リレーハブ上の各ストア自身のセッションは影響を受けないため、他のマシンからのメッセージはその間も届き続けます。
コーディネーターなしで初めて接続する
よく知られたポートは、ピア同士が互いを見つける方法の1つですが、それだけではありません。各ブリッジは UDP ポート 19877 でも小規模なプレゼンス機能を実行します。ビーコンで自身のデータポートを通知し(起動時と、その後30秒ごとに送信)、起動時にプローブを1つブロードキャストします。プローブを受信したピアは、新規参加者が次の送信間隔まで待たずに済むよう、新しいビーコンで応答します。ビーコンを受信したピアは、通知されたデータポートに直接接続します。また、確立済みの各接続は、認識しているピアを共有するため、ランデブー機構がなくても接続は推移的に形成されます。そのため、よく知られたポートに応答するものがあるかどうかにかかわらず、メッシュは形成され、修復されます。
ビーコンとプローブは、ループバックインターフェース上で参加するマルチキャストグループ(239.255.19.77)と、限定ブロードキャストアドレスに送信されます。ユニキャストアドレスには送信されません。1台のマシン上にあるブリッジは同じ UDP ポートを共有します。共有ポートに送信されたユニキャストデータグラムは、そのポートにバインドされたソケットのうち1つだけに届くため、プローブを送信した側宛ての応答が、応答側に届いてしまう可能性があります。ループバックマルチキャストはネットワークの状態にかかわらずホスト上のすべてのブリッジに届き、ブロードキャストは LAN 上の他のマシンに届きます。ループバックインターフェースでマルチキャストを送信できないホストでも、ブロードキャスト経由でピアを検出でき、ブリッジのエラーチャネルに失敗が通知されます。
ID
各ブリッジは、自身の鍵ペアのデバイス ID(公開鍵の生データの SHA-256)からピア ID を導出します。鍵ペアは ECDSA P-256 で、自己署名され、ローカルで生成されます。鍵の素材はブリッジスロットごとに保持されます(~/.agent-comms/identity-<harness>--<cwd>.json、所有者のみがアクセス可能な権限)。そのため、デバイス ID、ひいてはエージェント ID、ルームへの参加状態、ピアがエージェントへの配信を継続できる状態は、再起動後も維持されます。メッシュの状態自体はメモリー内にとどまり、ディスク上に保存されるのはローカルの鍵認証情報だけです。これは SSH キーと同じ信頼モデルです。ロックファイルでスロットを保護します。同じハーネスとディレクトリ内で2つ目のブリッジが稼働すると、ピア ID の重複を避けるため、一時的な ID で動作します。また、記録された PID を調べることで、古いロックは自動的に修復されます。
スロットファイルの隣には、さらに2つの鍵が置かれます。それぞれ初回使用時にローカルで生成され、ハードウェアに関する情報から導出されることはありません。~/.agent-comms/user-identity.json はアカウントのユーザープリンシパルで、そのマシン上でアカウントが実行するすべてのブリッジで共有されます。~/.agent-comms/machine-identity.json はマシンを表し、ホスト上でアカウントが実行するすべてのブリッジで共有されます。アカウントのホームディレクトリに保存されるため、同じホスト上の別の OS アカウントは独自のマシンキーを作成し、別のマシンとして表示されます。また、プリンシパルがホスト間で共有されるようになるまでは(#344)、2つの鍵のスコープは同じです。ホスト上のすべてのアカウントでマシンキーを1つだけ使う仕組みは、#367 で追跡されています。どちらもグループ化の発行者です。各ブリッジは、デバイスの所有者と実行中のホストをピアが識別できるよう、各発行者からの有効期間の短い証明をゴシップで共有します。whoami では Principal: と Machine: の行に表示されます。また、list_agents は各エージェントを、そのエージェントを証明するマシンの下にグループ化します。現在のマシン証明がないエージェント(マシン証明の導入前から存在するエージェントなど)は、「Machine not proven」の下に表示されます。再構築したホストにマシン ID を移す場合は、SSH ホストキーと同じ方式で、ファイルを意図的にコピーします。
破壊的変更(v2): 以前のバージョンでは、ピア ID を公開鍵の生データではなく、ピアの自己署名 X.509 証明書の SHA-256 フィンガープリントから導出していました。同じ鍵ペアでも値が異なるため、アップグレードすると、v2 より前の ID に紐付いたすべてのエージェント ID、ルームへの参加状態、保留中の配信キューが孤立します。既存のピアはアップグレード後のピアに以前の ID でアクセスできなくなるため、移行手段はありません。v2 ブリッジは v1 ブリッジと一切相互運用できません。ワイヤーフレーミング、トランスポート、ピア ID のいずれも一致しなくなったためです。
データディレクトリ
ブリッジまたは account コマンドを起動する前に AGENT_COMMS_DATA_DIR を設定すると、永続的なメッシュデータを別のディレクトリに保存できます。未設定または空白の場合は ~/.agent-comms が使用されます。このディレクトリには、デバイス、アカウント、マシンのキー、IDロック、ルームとグループのトークン、ゲートウェイの信頼情報、接続コード、名前、ルーム通知ログ、アカウント許可台帳が保存されます。
環境変数と明示的に指定するディレクトリには、絶対パスを指定する必要があります。相対パス、リテラルの ~ パス、空の明示的なディレクトリは、AGENT_COMMS_DATA_DIR の名前を含むエラーで拒否されます。作業ディレクトリを基準に解決されることはありません。
ライブラリを呼び出す側は IdentitySlot に dataDir を設定できます。この値はIDローダーとロックヘルパーでも使用されます。
const slot = { harness: "my-bridge", cwd: process.cwd(), dataDir: config.dataDir };
const { store, tool } = await createBridgeMesh(slot);
明示的な dataDir は、環境変数と既存の slot.dir の設定より優先されます。dataDir がない場合、slot.dir は従来の適用範囲であるデバイススロットのファイル、信頼情報、ペットネームに使用されます。アカウントキーとマシンキーには、環境変数のディレクトリまたはホームディレクトリのデフォルトが使用されます。同じデータディレクトリを使うブリッジは、アカウントキーとマシンキーを共有します。一方、各ハーネス/作業ディレクトリはそれぞれ独自のデバイスキーを保持します。ディレクトリを分けると別々のIDが作成され、既存データのコピーや移行は行われません。これはストレージの分離です。メッシュポート、検出、ハブは別途設定します。
インストール
pi
pi install npm:agent-comms
pi マニフェストによって、拡張機能が自動的に登録されます。マニフェストはTypeScriptのソースを指定しており、piはビルド手順なしで読み込むため、同じエントリが pi install npm:agent-comms と pi install git:github.com/ExaDev/agent-comms の両方で使われます。このため、npmパッケージには src が含まれています。gitインストールではビルド成果物がないため、ブリッジ、API、メッシュは動作しますが、ダッシュボードのルートはフロントエンドのビルドが見つからないことを示す503を返します。npmパッケージにはビルド済みのダッシュボードが含まれています。
この拡張機能は @earendil-works/pi-coding-agent と @earendil-works/pi-ai からインポートします。これらはpiホストから提供され、ピア依存関係の範囲は 0.79.0 以降です。旧パッケージの @mariozechner/pi-coding-agent と @mariozechner/pi-ai は非推奨で、パッチも適用されていないため、引き続きそのスコープを使うホストはピア依存関係の範囲を満たさなくなりました。@earendil-works/pi-coding-agent にアップグレードしてください。0.73.1 の @mariozechner ホストでは、新しいパッケージをその node_modules から解決できる場合、移行済みの拡張機能を読み込めましたが、これはサポート対象の組み合わせではありません。
Claude Code
claude plugin marketplace add https://github.com/ExaDev/agent-comms
claude plugin install agent-comms@agent-comms
このリポジトリは、独自のマーケットプレイスとして機能します。プラグインマニフェストでMCPサーバーを定義しています。
または、claude mcp add を使ってMCPサーバーを直接登録できます。
claude mcp add agent-comms -- npx -y agent-comms
単体の agent-comms は、標準入力がターミナルでない場合にMCPを提供します。MCPクライアントは常にそのように起動するためです。npx -y agent-comms bridge mcp は同じサーバーを明示的に指定するもので、引き続き使用できます。
MCP互換の任意のハーネス
MCPサーバー設定に追加します。
{
"mcpServers": {
"agent-comms": {
"command": "npx",
"args": ["agent-comms"]
}
}
}
汎用MCPブリッジは、任意のMCPクライアントで動作します。受信メッセージは、すべてのツールの応答に含まれます。明示的な ["agent-comms", "bridge", "mcp"] も同等です。
このサーバーは、MCP Registryにも io.github.ExaDev/agent-comms として公開されています。
その他のハーネス
npx agent-comms # auto-detect harnesses and configure
npx agent-comms status # check current configuration
npx agent-comms remove # undo configuration
または、依存関係としてインストールします。
npm install agent-comms
pnpm add agent-comms
または、ソースをクローンしてビルドします。
git clone https://github.com/ExaDev/agent-comms.git
cd agent-comms && pnpm install && pnpm build
npx agent-comms # auto-detect and configure
CLIは、インストール済みのハーネス(pi、Claude Code、Codex、OpenCode)を検出し、適切な設定ファイルを自動的に書き込みます。
cc-peer(マシン間 Claude Code リレー)
cc-peer は、Claude Code 独自のローカルなセッション間ピアプロトコルを直接使用します。セッションごとの Unix ソケットを使い、マシン間の通信経路は独自には持ちません。cc-peer ブリッジは、ローカルの Claude Code セッションをこのメッシュに中継するため、メッシュ独自のトランスポートを介して、別のマシン上のものを含む、他のあらゆるエージェント通信ブリッジから認識してメッセージを送信できるようになります。
npx agent-comms bridge cc-peer <local-session-name>
# or address the local session by pid instead of its registered name:
npx agent-comms bridge cc-peer --pid=12345
1 つのブリッジプロセスが中継するのは、ローカルの Claude Code セッション 1 つだけです。これは、ここにある他のすべてのブリッジが従う「1 つのブリッジプロセスが 1 つのエージェントであり、1 つのデバイスである」というモデルと同じです。そのセッションからの受信メッセージは、このブリッジ独自のプロジェクトルームに投稿されます。メッシュからこのブリッジのエージェント宛てに届いた配信は、cc-peer 独自の send() を使って、同じセッションに中継されます。
デフォルトの cc-peer フロント
独自のエージェント通信ブリッジを持たない Claude Code セッションも、メッシュから引き続きアクセスできます。そのマシンで現在選出されたコーディネーターの役割を担っているブリッジが、cc-peer 独自のセッション一覧から見つけたすべてのローカルセッションをフロントします。その際、そのセッション独自の claude-code ブリッジが起動した場合に使用するのと同じ (harness, cwd) の ID スロットを使います。ID はスロットに属し、現在どのプロセスがそのスロットを担当しているかには依存しません。セッション独自のブリッジが稼働している間はそのブリッジがスロットを保持し、それ以外のときはフロントが保持します。また、そのスロットの実際のブリッジが現れるとすぐにフロントはスロットを譲るため、アドレス指定、ルームへの参加、キューに入った配信は、切り替えの前後でそのまま引き継がれます。設定は不要です。このリポジトリ内のすべてのブリッジは、コーディネーターの役割への移行に合わせてフロントを自動的に連携させます。また、ローカルに Claude Code セッションがない(または cc-peer ソケットがまったくない)マシンでは、何もせずに終了します。
cc-peer では 1 プロセスにつき 1 つのピアしか使えないため、bridge cc-peer はコーディネーターとして実行するフロントに自身のピアを貸し出します。フロントは、そのブリッジの対象セッションを操作しません。ブリッジがすでにそのセッションを中継しているためです。
別のローカルプロセスからのメッセージがセッションに届くかどうかは、Claude Code 自身が判断します。ブリッジからのメッセージはピアメッセージとして届き、送信側の権限モードクラスが受信セッションと異なる場合、Claude Code はそのセッションを操作する人が承認するまでメッセージを保留します(「Held peer message ... set "crossSessionInbound" to "accept"」)。その人が承認するか、crossSessionInbound を accept に設定するまで、メッシュからそのセッションには何も届きません。
メッセージを 1 件ずつ承認せずに受け取るには、ユーザー設定の ~/.claude/settings.json で crossSessionInbound を accept に設定します。
{ "crossSessionInbound": "accept" }
--settings フラグで渡すことも、管理対象の設定で指定することもできます。プロジェクト設定ファイルやローカル設定ファイルで指定した値は、意図的に無視されます。この設定は自分に届くものを制御するため、リポジトリがユーザーに代わって有効化できないようになっています。Claude Code 2.1.224 以降が必要です。設定リファレンスと受信メッセージの制御を参照してください。
cc-peer 経由でのみアクセスできるセッションにはエージェント通信ツールがないため、room_accept や room_reject を呼び出せません。ルームへの参加リクエストや初回の DM が届いている場合、中継メッセージには返信先のピアが示され、選択肢として次の 2 行が提示されます。accept <room> <requester> または reject <room> <requester> <optional reason>。中継は、そのセッションのエージェントとして該当するアクションを実行し、結果を返信します。そのセッション自身のソケットから届いたメッセージだけが有効なため、判断するのはそのセッションを操作する人です。
フロントエンドのセッションは、メッセージを受信するだけでなく、返信することもできます。発信元のメッシュエージェントが1つだけのメッセージ(DMまたはルームメッセージ)は、フロントエンド経由ではなく、相手自身の返信エイリアスから配信されます。このエイリアスは cc-peer 独自の AliasPool(cc-peer/alias-pool サブパス)を介して作成される、実際に存在し、ネイティブに検出可能なローカルピアです。そのため、セッションに表示される送信者、そしてセッションが返信する相手は、メッセージを送った相手になります。返信はそのエイリアスに届き、フロントエンドが相手へのメッシュDMに変換します。メッセージ本文でセッションに特別な操作を求める必要はありません。送信者に返信するのが、すでに正しい動作だからです。以前のバージョンではフロントエンドから配信し、本文でエイリアスを指定していましたが、モデルがそれを無視することもあり、返信が送信者ではなくフロントエンド自身のプロジェクトルームに届いていました(issue #284)。
エイリアスは、その相手からの受信があった場合にのみ作成され、より広範なメッシュのメンバー一覧から事前に登録されることはありません。また、一時的なもので、フロントエンド自身のメモリ内にのみ保持されます。そのため、再起動すると失われますが、次にその相手からメッセージを受信すると、同じエイリアスが再作成されます。フロントエンドが認識できなくなったエイリアス(たとえば、再起動後)に返信が届いた場合や、メッシュ自体がメッセージの中継を拒否した場合は、黙って破棄せず、明確なエラーとしてセッションに報告します。エイリアスを起動できない場合でも、メッセージはセッションに届きます。その際、返信しても相手に届かないことを簡潔に伝え、失敗はブリッジ独自のエラーチャネルに報告します。
新しいハーネスの追加
ブリッジは次の2つで構成されます。
- ツール。LLM が
agent_comms({ action: "send", ... })を呼び出せるようにします - プッシュ機構。受信イベントを LLM のコンテキストに届けます
Core は共通ヘルパーを提供するため、各ブリッジで実装するのはこの2つだけです。
import {
MeshStore,
CommsTool,
buildAction,
ensureRegistered,
formatDeliveryEvent,
} from "agent-comms";
const store = new MeshStore();
const tool = new CommsTool(store);
// 1. Initialise mesh and register identity
await store.init();
const { agentId } = await ensureRegistered({
store,
harness: "my-harness",
defaultName: "my-agent",
});
// 2. Wire delivery callback for real-time push
store.onDelivery = (_targetId, event) => {
const line = formatDeliveryEvent(event);
yourHarness.push(`📬 ${line}`);
};
// 3. Wire tool into your harness
const action = buildAction(paramsFromToolCall);
const result = await tool.handle(
{ agentId, harness: "my-harness", cwd: process.cwd(), pid: process.pid },
action,
);
動作する例については、src/bridges/ を参照してください。
使い方
# Register yourself
agent_comms({ action: "register", name: "vault-refactor", visibility: "visible", tags: ["obsidian"] })
# List other agents
agent_comms({ action: "list_agents" })
# Create a room
agent_comms({ action: "create_room", room: "code-review", type: "public", description: "Cross-harness review" })
# Join an existing room
agent_comms({ action: "join_room", room: "general" })
# Send a message
agent_comms({ action: "send", target: "code-review", content: "Batch 3 done." })
# Send with delivery timing hint
agent_comms({ action: "send", target: "code-review", content: "Review needed now.", streamingBehavior: "steer" })
# DM another agent
agent_comms({ action: "dm", target: "a1b2c3", content: "Can you review my last commit?" })
# DM with delivery timing hint
agent_comms({ action: "dm", target: "a1b2c3", content: "Urgent: deploy is blocked.", streamingBehavior: "steer" })
# Read room history
agent_comms({ action: "read_room", room: "general" })
# Go dark
agent_comms({ action: "update", visibility: "hidden" })
配信タイミング
send と dm は、受信側のブリッジにメッセージをどの程度急いで表示するかを指示する、省略可能な streamingBehavior フィールドを受け付けます。
| 値 | 意味 | Pi ブリッジ | Claude Code ブリッジ | ドレイン型ブリッジ(MCP、Codex) |
|---|---|---|---|---|
steer | 今すぐ動作します — 次の判断ポイントで対応します | deliverAs: "steer" | [STEER] プレフィックス + meta.streamingBehavior | ドレイン時に [STEER] プレフィックス |
followUp | アイドル状態になったら動作します — 現在のタスクが完了するまで待ちます | deliverAs: "followUp" | [FOLLOWUP] プレフィックス + meta.streamingBehavior | ドレイン時に [FOLLOWUP] プレフィックス |
info | 都合のよいときに対応します(デフォルト。現在の動作に一致します) | 情報バッファ | プレフィックスなし | プレフィックスなし |
streamingBehavior が指定されていない場合、各ブリッジは既存のヒューリスティックに従います。対応が必要なイベント(DM、ルームメッセージ、招待)は steer として扱い、ステータスの変更やメンバーシップのイベントは info として扱います。
Claude Code の配信機構:イベントは ~/.agents/bus/pending/claude-code--<cwd-slug>.jsonl に書き込まれます。3つの Claude Code フック(PostToolUse、Stop、UserPromptSubmit)が hooks/drain.sh を呼び出し、ファイル名をアトミックに変更して、その内容を stderr に書き込み、終了コード 2 で終了します。Claude Code の asyncRewake 機構は stderr の内容を <system-reminder> で囲み、アイドル状態の Claude を起動します。agent_comms ツールが直接呼び出された場合、ツールハンドラーは同じアトミックなファイル名変更を使って同じファイルをドレインします。ファイル名の変更が同期プリミティブとなるため、同時にドレインしても重複は発生しません。[STEER] と [FOLLOWUP] のマーカー、および meta.streamingBehavior はタイミングの意図を伝えますが、それに従って動作するかどうかは受信側のエージェントに委ねられます。pi ブリッジはこのヒントをネイティブに deliverAs 経由で反映します。
Web UI
Web サーバーを起動するブリッジ(npx agent-comms chat、またはそれを起動するその他のコマンド)は、REST API と並んで小さなブラウザダッシュボードを提供します。エージェントとルームの一覧、メッセージ履歴、ライブのメッシュグラフなどです。ダッシュボードは SharedWorker 内で動作し、そのブリッジに対して開かれたすべてのタブで共有されるため、複数のタブがそれぞれメッシュへの個別の接続を開くのではなく、一貫した単一のビューを参照できます。
このダッシュボードには 2 つの独立したトランスポートがあり、どちらも oRPC 上に構築されています。oRPC はコントラクトファーストの RPC レイヤーで、Zod で検証された入力と出力、およびストリーミングプロシージャのネイティブサポートを備えています。
- ワーカー ↔ サーバー:
/ws/meshの実際の WebSocket を介します。ここではワーカーが oRPC クライアントとして動作し、REST API が公開するのと同じコントラクトのプロシージャを呼び出します。さらに、メッシュのイベントを発生と同時にストリーミングするsubscribeEventsプロシージャも呼び出します。 - タブ ↔ ワーカー:
MessagePortを介します。この区間ではワーカーが役割を反転し、oRPC サーバーとして動作して、接続してくる各タブに対して同じコントラクト(およびタブの終了処理用のdisconnectプロシージャ)を実装します。
どちらの区間も独立して再開可能です。ワーカーの各側は、イベント ID でタグ付けされた、バッファリングおよび再生可能な独自のイベントストリームを保持しているため、MessagePort 接続を一時的に失ったタブや、上流の WebSocket が切断されたワーカーは、切断中に発生したことを見逃すのではなく、自身が最後に受信したイベントから再開します。これは上記の「How it works」で説明されているメッシュレベルの deliveryQueues メカニズムとは異なります。あちらはエージェントブリッジの_プロセス_の再起動を対象としており、ブラウザタブが離脱してから戻ってくる場合や、ワーカー自身のソケットが切断される場合は、独自の再開可能なストリームを持つ別の障害モードです。
ただし、ダッシュボードのすべてのデータがこのライブイベントストリームを経由するわけではありません。エージェントとルームは経由します。すべての変更が発生した瞬間にパッチとして届くため、別途取得するものは何もありません。ルームのメッセージ履歴、メッシュの接続グラフ、パストレースは異なります。これらは独自の継続的なサブスクリプションを持たない、真に一発のリクエスト/レスポンスによる読み取りです。これら 3 つは TanStack Query を経由し、上記のタブからワーカーへの oRPC クライアントを介して oRPC 自身の TanStack Query 統合で接続されています。キャッシュ、リクエストの重複排除、およびダッシュボードのライブ側をすでに駆動している同じイベントでのキャッシュ無効化が行われます。
LAN から Web UI にアクセスする
ブリッジの Web サーバーは、別途指定しない限りループバックのみで待ち受けます。同じマシン上のブラウザーからアクセスする場合、認証情報は必要ありません。ネットワーク上の別のデバイスからダッシュボード(および任意のワイヤーメッシュコンソール)を開くには、ブリッジの起動前に AGENT_COMMS_WEB_HOST にバインドする IP アドレスを設定します。すべてのインターフェースで待ち受ける場合は 0.0.0.0(または ::)、特定のネットワークだけに公開する場合は、いずれかのインターフェースのアドレスを指定します。IP アドレス以外の値を指定すると、ループバックにフォールバックせず、サーバーの起動が停止します。
AGENT_COMMS_WEB_HOST=0.0.0.0 npx agent-comms chat
サーバーはすべての変更操作(POST /api/action および /ws/mesh ソケット)を公開するため、ループバックを超えてバインドすると、プロセスごとのシークレットトークンが必ず付与されます。トークンは起動時に生成され、開いてすぐにアクセスできる次の形式の URL として、stderr に一度だけ出力されます(MCP チャネルとなる可能性のある stdout には出力されません)。http://<address>:<port>/?token=<token> このマシン上にないすべてのクライアントは、Authorization: Bearer <token> ヘッダー、または URL を開くと設定される Cookie でトークンを提示する必要があります(サーバーはその後、トークンなしの同じパスにリダイレクトするため、トークンがアドレスバーに残ることはありません)。Cookie の名前は agent_comms_web_token_<port> です。ブラウザーはポートではなくホストごとに Cookie を管理するため、同じマシン上で複数のブリッジを実行しても、それぞれに独自のトークンが割り当てられます。トークンのないリクエストには 401 が返され、比較は定時間で行われます。このマシン上のクライアント(ループバック、またはこのマシン自身のインターフェースアドレスのいずれかから接続するクライアント)にはトークンが不要です。そのため、エージェントに渡される web_url は、1 つのインターフェースアドレスにバインドされたサーバーでも動作します。ループバックを含むすべてのクライアントからのすべてのリクエストには、サーバーがバインドされているアドレス、ループバック名、またはこのマシン自身の名前を指定する Host ヘッダーが必要です。これにより DNS リバインディングを防ぎます。また、ループバックを含むすべてのクライアントからの Origin ヘッダーには、サーバー自身を http で指定する必要があります。これにより、クロスサイトリクエストと、オペレーター自身のブラウザーで開いたページからのクロスサイト WebSocket ハイジャックを防ぎます(サーバーは CORS ヘッダーを送信しません)。例外は、GitHub Pages でホストされているダッシュボードがループバックポートを調べてローカルブリッジに接続する場合のみです。この場合、その Origin はこのマシン上のクライアントからの接続に限り許可されます。アクセス Cookie は SameSite=Lax なので、別のサイト上のリンクから印刷された URL を開いても設定されます。Lax Cookie はクロスサイト POST や WebSocket アップグレードでは送信されず、それ以外のケースは Origin チェックで保護されます。トークンはメモリ上に保存され、起動のたびに変わります。
http は暗号化されていないため、トークンやダッシュボードに表示または送信されるすべての情報は、ネットワーク上を暗号化されずに流れます。そのネットワークセグメント上の通信を監視できる人は誰でもトークンを読み取り、オペレーターとして操作できます。共有ネットワークや公共ネットワークで 0.0.0.0 にバインドするのではなく、信頼できるネットワーク(自宅やオフィスの LAN、プライベートな Tailscale インターフェースアドレスなど)のインターフェースアドレスにバインドしてください。同じマシン上で動作するリバースプロキシはループバック経由でサーバーに接続するため、このマシン上のクライアントとして扱われます。そのため、トークンチェックを回避し、プロキシ独自の認証だけが保護手段となります。ただし、Host チェックは引き続き適用されます。
LAN 経由では http と ws を使用するため、https で配信されるページ(ホストされている mesh.exadev.io インスタンス)からは接続できません。代わりに、ブリッジ自身のアドレスを開いてください。ループバックを超えてバインドするブリッジは、最初の接続用ポート(19877)で小さな agent-comms-web-beacon データグラムもブロードキャストします。このデータグラムにはピア ID と Web ポートが含まれます。受信側はデータグラムの送信元アドレスからホストを取得し、トークンが通知されることはありません。
ネットワーク上のほかのブリッジは、これらのビーコンを受信します。lan_web_uis には、この方法で検出された近隣の Web UI がすべて一覧表示され、それぞれについて、ピア ID の全文と名前に加え、すぐにアクセスできる http://<address>:<port> URL が表示されます。ブリッジがビーコンを数回続けて受信できないと、その項目は一覧から消えます。そのため、停止したダッシュボードが一覧に残り続けることはありません。
代替 UI:wire-mesh の web-console
エージェント通信ノードは、内部的にはすでに wire-mesh ノードです。そのため、独自の高機能なダッシュボードに加えて、必要に応じて wire-mesh 独自の汎用的なプロトコルレベルの web-console も提供できます。これは、agent-comms 独自の製品 UI ではなく、メッシュのリファレンスクライアントのビューを使いたい場合に便利です。
これはオプトイン方式で、デフォルトでは無効です。このパッケージは web-console に依存していないため、自分でビルドしない限り、提供されるものはありません。ブリッジを起動する前に、AGENT_COMMS_WEB_CONSOLE_DIST をローカルでビルドした web-console の dist/ ディレクトリ(wire-mesh/ts/packages/web-console 内で pnpm build を実行)に設定してください。
AGENT_COMMS_WEB_CONSOLE_DIST=/path/to/wire-mesh/ts/packages/web-console/dist npx agent-comms chat
その後、ウェブサーバー(各ブリッジで tryStartBridgeWebServer が選んだポート)が /web-console/* にも応答するようになります。この変数が設定されていない場合、または index.html を含むディレクトリを指していない場合は、ルート自体が登録されません。そのため、/web-console 配下へのすべてのリクエストは、ほかの未知のパスと同様に 404 になります。
ダイレクトメッセージ
エージェントへの最初の dm では、メッセージを送信する前に、そのエージェントにアクセス許可を求めます。この呼び出しは、受信者のエージェントにリクエストが通知され(room_pending で確認できる room_join_request イベント)、room_accept または room_reject で回答されるまで待機します。いったん承認されると、以降のメッセージは直接送信され、受信者からの返信に再度の判断は必要ありません。リクエストが拒否された場合、または承認待ち時間内に誰も回答しなかった場合、dm は失敗し、送信済みとして記録されることもありません。
dm は常に配信済みとして表現するのではなく、メッセージが実際にどうなったかを通知します。受信者のデバイスがメッセージを受け取った場合は、配信済みとして返されます。リクエストがタイムアウトした、デバイスへの経路がなかった、またはメッセージを運ぶ接続が切断されたなどの理由で届かなかった場合は、どの事態が起きたのかを示して、キューに登録済みとして返され、再試行のために保持されます。これは未配信であり、文言にもそのことが示されます。受信者が拒否を返した場合は、再試行しても回答は変わらないため、拒否の内容を示して呼び出し自体が失敗します。
受信者への経路が再び確立されると、ダイレクト接続か、リレーハブがデバイスの接続を許可した場合かを問わず、キューに登録されたメッセージは再試行されます。送信者には、開封確認にも使われる同じ delivery_status イベントで通知されます。キューには、受信者ごとのメッセージ数と、各メッセージの保持時間の両方に上限があります。再試行を断念したメッセージは、保留中のまま残されず、破棄または期限切れとして報告されます。
その場で判断せずにデバイスからの DM を許可するには、事前にそのデバイスを登録します。そのデバイスの ID を指定した dm_admit は、許可証と、それを使って実行する正確な呼び出しを返します。送信者が自分のデバイス ID と許可証を指定して dm_use_grant を呼び出すと、送信者からの最初の DM は、あなた側での判断なしに送信されます。dm_revoke で許可を取り消せます。許可証は、発行対象となったデバイスでのみ有効です。ただし、principal: true と、相手の whoami にある Principal: の行を渡した場合は例外です。その場合、相手が使うすべてのデバイスを許可し、それぞれのデバイスで同じ許可証のテキストを提示できます。
名前
device-id、エージェント、マシン、プリンシパル、信頼済みデバイスのいずれで識別される場合も、表示形式は常に同じです。まず自分が付けた名前、次に相手が自称する名前を引用符で囲んだもの、最後に短い ID の順です。たとえば、work laptop "joe-mbp" 3a8b8ebc6dd3 のようになります。短い ID は先頭部分です。ツールのアクションでは完全な ID が必要なため、対象のエントリに対して操作する一覧では、完全な ID の後に名前だけを表示します。短い ID は先頭部分を繰り返すだけなので表示しません。
エージェントは register と update で自分の名前を付けます。マシンは machine_name で自分の名前を付けます。ホスト上でアカウントが実行する各ブリッジは、その名前にマシンキーで署名し、マシンの証明とともにゴシップするため、ピアはマシンが選んだ名前と、その名前を選んだキーを確認できます。ただし、その名前が示す内容は、依然としてマシン自身の申告です。アカウントも同様に principal_name で自分の名前を付けます。名前は user-identity.json に保存され、ユーザーキーで署名されてメンバーシップ証明の隣に置かれます。whoami、gateway_list_trusted、および「vouched for by」列では、そのデバイスの証明が示すプリンシパルと照合できた場合にのみ、プリンシパル ID の隣に表示されます。名前はアカウントに属するため、アカウントのエクスポートとアカウントの参加によって新しいマシンにも引き継がれます。その後に名前を変更しても複製されないため、名前を使いたい場所ごとに再設定してください。
自分で付ける名前、つまりペットネームは、自分だけが使うものです。~/.agent-comms/petnames.json に保存され、このマシン上のブリッジ間で共有されますが、ゴシップされたり、誰かに送信されたりすることはありません。ペットネームを設定できるのは完全な device-id に対してのみです。誤った ID や短い ID にラベルを付けても何にも適用されないためです。完全な device-id に対する有効な名前ではないファイル内のエントリは、そのエントリだけが無視されます。JSON オブジェクトではないファイルは、上書きせずにエラーとして報告されます。
# Name this machine for everyone who can see it (omit name to clear it)
agent_comms({ action: "machine_name", name: "joe-mbp" })
# Name the account every one of your machines shares (omit name to clear it)
agent_comms({ action: "principal_name", name: "work account" })
# Give any id your own name, and take it away again
agent_comms({ action: "petname_set", device: "3a8b8e...", name: "work laptop" })
agent_comms({ action: "petname_clear", device: "3a8b8e..." })
agent_comms({ action: "petname_list" })
名前は空であってはならず、64 文字以内で、制御文字を含んではなりません。register、update、machine_name、principal_name、petname_set は、これらのルールに違反する名前を拒否します。そのような名前をゴシップするエージェントは一覧に表示されず、ルールに違反するその他の名前も、表示される場所から除外されます。引用符で囲まれた名前の中に引用符が含まれる場合はエスケープされるため、名前の一部を別のラベルのように見せかけることはできません。
ルームの種類
| 種類 | 検出方法 | 参加 | 履歴の閲覧 |
|---|---|---|---|
public | list_rooms に一覧表示 | 誰でも | 誰でも |
private | 名前を表示 | 招待のみ | メンバーのみ |
secret | 非表示 | 招待のみ | メンバーのみ |
可視性レベル
| レベル | 一覧表示 | DM 可能 | ルームのメンバー一覧 |
|---|---|---|---|
visible | ✓ | ✓ | ✓ |
hidden | ✗ | ✓(ID が分かる場合) | メンバーのみ |
ghost | ✗ | ✗ | ✗ |
ルームメンバーの把握
エージェントがルームに参加すると、現在の全メンバーとそのステータスを一覧にした room_members 配信イベントを受け取ります。既存のメンバーには、参加または退出するエージェントを除き、member_joined/member_left 通知が届きます。
sequenceDiagram
participant A as Agent A (in room)
participant Mesh
participant B as Agent B (joining)
B->>Mesh: joinRoom("code-review")
Mesh-->>B: room_members { [{ id: A, status: active }] }
Mesh-->>A: member_joined { agent: B }
Note over A: A knows B arrived, B knows who is already there
rect rgb(255, 245, 230)
Note over B: B goes idle
B->>Mesh: update(status: idle)
Mesh-->>A: member_status { agent: B, status: idle }
end
エージェントのステータスが変わると(active/idle/busy/offline)、そのエージェントが所属するすべてのルームに member_status 通知が届きます。対象となるのは次のとおりです。
- 明示的な
updateアクション - 再登録(offline → active)
- 正常なシャットダウン
- 古くなったエージェントのクリーンアップ(選出されたコーディネーターによる PID プローブ)
ライブラリからの永続化されるルーム送信
ライブラリの呼び出し元は、ルームメッセージを送信者のローカルなルーム通知ログに記録するよう指定できます。
import type { SendRoomMessageOptions } from "agent-comms";
const options: SendRoomMessageOptions = { durable: true };
await store.sendRoomMessage(roomId, senderId, "Keep this message", options);
MeshStore は、durable が true の場合にのみ、署名付きルーム通知を送信者自身のログに記録します。
これは通知をローカルに書き込むだけです。現在、これらのルーム通知エントリを告知したり取得したりする機能はありません。オフライン時の自動追いつきには、引き続き追いつきポリシーとブリッジの配線が必要です。
FileStore はすべてのメッセージをすでに永続化するため、このオプションを無視します。
配信状況と既読通知
メッセージには、どのエージェントがメッセージを確認したかを追跡する readBy フィールドが含まれています。ステータスイベントは送信者に自動的に送信されるため、明示的な操作は不要です。
sequenceDiagram
participant A as Agent A (sender)
participant Mesh
participant B as Agent B (recipient)
A->>Mesh: send("Hello")
Mesh->>B: queue room_message
Mesh-->>A: delivery_status { delivered }
alt Push bridge (pi, Claude Code)
Mesh->>B: onDelivery fires
else Drain bridge (MCP, Codex, OpenCode)
B->>Mesh: drainDelivery()
end
Mesh->>Mesh: markRead(msgId, B)
Mesh-->>A: delivery_status { read }
Mesh->>Mesh: broadcast message_read patch
| タイミング | 送信者が受け取る内容 |
|---|---|
| メッセージが宛先へのキューに追加されたとき | delivery_status { delivery: { status: "delivered" } } |
| 宛先のブリッジがメッセージを受信したとき | delivery_status { delivery: { status: "read" } } |
| キューに入っていたメッセージが最終的に配信されたとき | delivery_status { delivery: { status: "delivered" } } |
| 再試行が拒否されたとき | delivery_status { delivery: { status: "refused", code } } |
| 再試行キューがメッセージを断念したとき | delivery_status { delivery: { status: "dropped" } } または { status: "expired" } |
既読通知は、onDelivery が呼び出されたとき(プッシュブリッジ:pi、Claude Code)、または drainDelivery が呼び出されたとき(ドレインブリッジ:MCP、Codex、OpenCode)に送信されます。ピア間の既読通知は、message_read メッシュパッチを介して伝播します。
これはルームメッセージとDMの両方で機能します。
古くなったエージェントのクリーンアップ
選出されたコーディネーター(「コーディネーターパターン」を参照)は、ピアの接続が終了するとすぐにそのエージェントを退役させ、オフラインとしてマークして、そのことをほかのすべてのピアにブロードキャストします。これに加えて、登録済みエージェントのPIDを5秒ごとにシグナル0(存在確認)で調べ、プロセスが終了しているのにソケットが閉じられていないエージェントも検出します。どちらの場合も通知を送るのはコーディネーターだけなので、離脱時に生存中の各ピアから個別にステータス変更が発生するのではなく、1回だけ通知されます。周知のポートを保持するピアも含め、ほかのピアは受動的に動作します。離脱したピアがコーディネーターだった場合は、後任が役割を引き継いだ時点で一度だけ通知します。
すべてのブリッジはSIGTERM、SIGINT、SIGHUPに応答して、自身のエージェントをオフラインとしてマークし、ストアをシャットダウンします。これにより、コーディネーターの役割を担っていた場合はその役割を放棄し、周知のポートのリスナーをクラッシュ時の競合に委ねることなく引き継ぎます。自分のプロセスを所有するブリッジはその後終了しますが、ホストのプロセス内で動作するOpenCodeプラグインは、代わりにシグナルを再送し、ホストに判断を委ねます。pi拡張機能は、同じ処理にpi独自の session_shutdown フックを使用します。
ゲートウェイの信頼と接続コード
各ストアはリレーハブ(デフォルトは wss://mesh.exadev.io/。セルフホストのハブを使用するには、AGENT_COMMS_HUB_URL に ws:// または wss:// のURLを設定します。たとえば、npx wire-mesh で起動したノードを指定できます)上に自身のセッションを保持します。デフォルトのcc-peerフロントが処理する各Claude Codeセッションも同様です。ハブから切断されると、ストアは間隔を徐々に長くしながら再接続します。したがって、別のマシンから到達できるかどうかは、どのストアがローカルのコーディネーター役を担っているかに依存しません。また、コーディネーターが引き継がれても、ほかのすべてのストアのハブセッションはそのまま維持されます。ストアがセッションを保持するのは、ゴーストではないエージェントが存在し、そのマシンが少なくとも1つのリモートデバイス、プリンシパル、またはマシンを信頼している間だけです。そのため、それまではデバイスIDさえも、ストアに関する情報がハブに送られることはありません。表示状態のエージェントは自身の情報、プレゼンス、ホストするルームをアドバタイズします。非表示状態のエージェントがアドバタイズするのはデバイスIDだけであり、これによってそのIDで到達できるようになります。
ハブは単一のアドレスではなく、複数のアドレスの集合です。ストアは、自身が最も優先するリレーを経由して、ハブへのセッションを1つ保持します。優先順位は、自身のマシン上で稼働するリレー、信頼するデバイスが提供するローカルネットワーク上のリレー、リレーサービスを提供する任意の信頼済みデバイス、最後にフォールバックとして設定済みハブの順です。デバイスへの直接接続は常にすべてのリレーより優先されます。直接接続されたデバイスへのリクエストがハブセッションを経由することはないためです。接続に失敗したリレーやセッションが切断されたリレーは、約1分間は再試行されません。
自身のマシンの選出済みコーディネーター役を担うストアは、設定済みハブとのセッションを保持しながら、リレーの役割も担います。すべてのインターフェースで WebSocket リスナーを介して稼働し、パブリックハブが稼働させるものと同じリレーロジックを実行し、それをゴシップ広告(wire-mesh/relay-offer 拡張)で提供します。この広告はピアに届き、ハブを介して別の場所にある信頼済みデバイスにも届きます。リレーが接続を許可するのは、自身のマシンのメッシュメンバーと、このマシンがデバイスまたはプリンシパル単位で信頼するデバイスだけです。また、自身に直接接続されたクライアント間の転送も行います。リレーのペイロードは、リレーを利用する2台のデバイス間でエンドツーエンド暗号化されます。そのため、リレーを信頼するかどうかは、可用性とメタデータ(誰が誰と通信するか、リレー自身のアドレス)についての判断であり、内容についての判断ではありません。
リレーは、自身のハブセッションの接続をアップリンクとして使い、その上でクライアントとの通信を仲介します。そのため、自身のマシンのリレーの背後にあるストアは、設定済みハブを通じてのみ到達できるデバイスからも到達可能であり、その逆も可能です。それぞれの接続先は、リレーではなく自身のデバイス ID として認識されます。リレー自身のクライアントに関する広告がハブを経由して戻ってきても、そのクライアントの登録が置き換わることはありません。また、リレーに移ったばかりのデバイスも、最初の広告が受理されればルーティング可能になります。広告が受理されるのは、次のゴシップまでかかる場合があります。
ハブ経由でのみ認識され、このメッシュに直接接続されていないデバイスは、最後にゴシップされた広告に記載されていた状態で報告されます。ハブセッションでは要約情報しか運ばれないため、そのデバイスの作業ディレクトリ、ルーム、タグの一覧は共有されていないものとして表示されます。このマシンが信頼すると選択したブリッジには、両者がすでに確立しているエンドツーエンドセッションを通じて、これらの情報が直接問い合わせられます。list_agents には、次回の一覧取得以降に返された内容が表示されます。応答するブリッジも問い合わせ元を信頼していない限り、この問い合わせは拒否されます。そのため、オペレーターがピン留めしていないデバイスに詳細情報が送られることはありません。このようなデバイスが切断されても、メッシュのほかの部分には通知されません。そのため、list_agents は古い広告を信頼し続けず、再広告の間隔が数回過ぎても更新されない場合は、デバイスを無期限にアクティブとして表示するのではなく、オフラインとして報告します。
異なるマシン上の2台のデバイスが、互いのハブ経由でトラフィックを中継するには、それぞれの側で相手のデバイス ID を明示的に信頼する必要があります。これは、ディレクトリ検索を行わず、デフォルトで拒否し、キーをピン留めするモデルで、通常のピア接続がすでにピン留めされている方式を意図的になぞらえています。
信頼済みデバイスの一覧は、1つのファイル ~/.agent-comms/gateway-trust.json に保存されます。このファイルは、マシン上のすべてのブリッジと、デフォルトの cc-peer フロントが処理する各セッションで共有されます。そのため、いずれか1つでデバイスを信頼すると、すべてに適用されます。
# Trust a remote device you already know the device-id for
agent_comms({ action: "gateway_trust", device: "1a2b3c..." })
# Trust a person rather than one device: pass the Principal line from their whoami
agent_comms({ action: "gateway_trust", device: "<their principal>", principal: true })
# Trust a machine: pass the Machine line from its whoami
agent_comms({ action: "gateway_trust", device: "<their machine>", machine: true })
# Stop trusting it
agent_comms({ action: "gateway_untrust", device: "1a2b3c..." })
# Revoke every device that machine vouches for in one act
agent_comms({ action: "gateway_untrust", device: "<their machine>", machine: true })
# List every currently trusted device
agent_comms({ action: "gateway_list_trusted" })
プリンシパルを信頼すると、その人が使用するすべてのデバイスが対象になるため、その人の各セッションを個別に追加する必要はありません。各ブリッジは、アカウントのユーザー ID で署名された、自身がそのプリンシパルに属することを証明する短期間有効な証明をゴシップで配布します。プリンシパルを信頼するゲートウェイはその証明を検証し、証明の有効期限が切れるか、プリンシパルの信頼を解除するまで、そのデバイスを list_agents に表示し、リクエストの送信先にできるようにします。gateway_list_trusted には、各デバイスを保証するプリンシパルとともに、そうしたデバイスが表示されます。
1つのアカウントがホスト上で使うすべてのデバイスについて、マシンを信頼する仕組みは同じです。各ブリッジは自身のマシン ID による証明によって、マシンが信頼されている間は到達可能になり、マシンの信頼を解除すると、そのマシン経由でしか到達できないすべてのデバイスが一度に到達不能になります。ID または信頼されたプリンシパル経由でも信頼されているデバイスは、その経路から引き続き到達できます。証明は、表示状態のエージェントのアドバタイズにのみ含まれるため、非表示のエージェントからはデバイス ID 以外は何も分からず、プリンシパルもマシンも明らかになりません。
デバイスがゴシップするすべてのアドバタイズには、そのデバイス自身の鍵による署名が付いており、ハブと各受信セッションの両方で、送信元を信頼する前に署名が検証されます。これにより、エントリーがどのデバイスに関するものかが認証されるため、誰も別のデバイスのディレクトリーエントリーを公開したり、置き換えたりできません。ただし、エントリーに記載される内容は、あくまでそのデバイスが主張するものです。
知っておくべき制限が3つあります。証明が示すのは、プリンシパルまたはマシンがデバイス ID を保証しているということであり、ゴシップの送信者がそのデバイスであるということではありません。ハブに接続している人なら誰でも証明を読んで再送できるため、証明は一覧への掲載とルーティングには十分ですが、中継フレームを信頼済みとして受け入れさせるには不十分です。また、デバイスへのすべてのリクエストは引き続きエンドツーエンドで認証されます。証明は取り消せず、有効期限切れを待つしかありません。そのため、アカウントからデバイスを削除しても、そのデバイスがアカウントの鍵を保持している限り、効果はありません。また、ホストは自身のマシン鍵を保持している限り、自分のデバイスを保証し続けます。さらに、証明にはプリンシパルまたはマシンが平文で記載されるため、ハブ上の誰もが、どのデバイスが同じ人に属するのか、またどのデバイスが同じホスト上で動作しているのかを確認できます。これを明かさないようにするには、エージェントを非表示にします。
通常、デバイス ID は、gateway_trust に貼り付ける前に、Slack、メール、電話などを通じて別途入手する必要があります。接続コードは、使い捨てで有効期間が短いアーティファクトです。これを使うと、単なる未認証の文字列を渡す代わりに、その受け渡し自体を検証可能にできます。
ConnectionCode {
code: nonce # single-use
expiresAt: timestamp # short-lived
deviceId: hex # the device being vouched for
signature?: PgpSignature over (code, expiresAt, deviceId)
}
code/expiresAt は、常に確認される部分です。コードを引き換える人が、最近行われたこの特定のやり取りの相手側にいたことを証明します。signature は任意で、別の問いに答えるものです。つまり、この主張が特定の長期使用 PGP 鍵の保持者によって行われたことを示します。署名がある場合にのみ検証され、必須ではありません。PGP ID を持たないデバイスでも、署名のないコードを生成して引き換えられます。
# Generate a code vouching for this device, unsigned
agent_comms({ action: "gateway_generate_connection_code" })
# ...or signed with a PGP private key you already have (agent-comms never generates or stores PGP key material itself -- you supply it per call, the same way you would to `gpg --sign` directly)
agent_comms({ action: "gateway_generate_connection_code", privateKey: "-----BEGIN PGP PRIVATE KEY BLOCK-----..." })
# Relay the printed code to the counterpart out of band, then redeem it on their side -- a successful redemption trusts the device it vouches for, exactly as if gateway_trust had been called directly
agent_comms({
action: "gateway_redeem_connection_code",
code: "...", expiresAt: "2026-01-01T00:15:00.000Z", device: "1a2b3c...",
})
# Redeeming a signed code needs the signer's public key to verify against -- paste it directly, or supply a fingerprint you already trust and let the redeemer fetch it from keys.openpgp.org
agent_comms({
action: "gateway_redeem_connection_code",
code: "...", expiresAt: "...", device: "1a2b3c...", signature: "...",
fingerprint: "aabbccddeeff00112233445566778899aabbccd",
})
フィンガープリントは、キーサーバーが提供する信頼の基点ではありません。名刺、以前の検証などを通じて、引き換える側がすでに独立して信頼しているものです。そして引き換える側が確認するのは、実際に取得または貼り付けた鍵が、その正確なフィンガープリントに一致することです。単に何らかの鍵が見つかっただけでは不十分です。コードを生成した側にコールバックすることはありません。2台のデバイス間に接続が存在する前に信頼を確立することが、そもそもの目的だからです。
1つのアカウントを複数のマシンで使う
ユーザープリンシパルは、あなたのアカウントです。~/.agent-comms/user-identity.json にある鍵が、デバイスのメンバーシップ証明と、あなたが発行するすべての DM 許可に署名します。同じアカウントを複数のマシンで使うことができ、その場合、それぞれのマシンが1つのプリンシパルとして自身のデバイスを保証します。そのため、あなたのプリンシパルを信頼する人は、マシンごとやペアごとの手順なしに、あなたが使うすべてのマシンを信頼できます。
新しいマシンの参加には、意図的な操作が1つ必要です。すでにアカウントを保持しているマシンで、接続コードとなる招待を発行し、別の手段で新しいマシンに渡します。
npx agent-comms account invite # on a machine that holds the account; keep it running
npx agent-comms account join # on the new machine; paste the invite at the prompt
account join --label <name> を実行すると、発行側のマシンにも新しいマシンの呼び名が伝わります。発行側のマシンはそのラベルを、新しいマシンに対する独自のペットネームとして保存するため(「名前」を参照)、参加させたマシンは以後、その名前で表示されます。ラベルには通常の名前の規則が適用され、規則に違反する場合は、コードを引き換える前に拒否されます。ラベルが発行側のマシンのペットネームファイルから外部に出ることはありません。
どちらも実行するコマンドであり、agent_comms のツールアクションではありません。招待は、最初に引き換えた人がアカウントを取得できるキーであり、参加すると、そのマシンが保持しているアカウントが置き換わります。そのため、エージェントに何を指示しても、どちらも実行できません。また、招待はターミナルにのみ表示され、エージェントのトランスクリプトに表示されることはありません。join は入力内容が表示されないプロンプトで招待を読み取るため、シェルの履歴にも残りません。
account invite は専用のストアを実行し、招待が引き換えられるか期限切れになるまで稼働させます。発行側のデバイスは招待をメモリ内にのみ保持し、それぞれ一度だけ応答し、期限切れ後は応答しないためです。招待の有効期間は接続コードと同じで、--ttl-minutes <n> を指定すれば短くできますが、長くはできません。引き換え時には、gateway_redeem_connection_code と同じように発行側のデバイスを信頼し、その後、メッシュのエンドツーエンド認証済みリクエスト経路を使って、直接またはハブ経由で中継して、そのデバイスにアカウントキーを要求します。キーは招待から導出されたキーで暗号化されて返され、招待を持つ人だけが復号できます。発行側のデバイスが参加を拒否するか、応答しない場合、その信頼は取り消されるため、参加に失敗しても、新しいマシンが以前は信頼していなかった相手を信頼することはありません。招待には --sign-key <armored private key file> を指定して PGP 署名を付けることができ、署名付きの招待は join 時に --public-key <file> を使って検証されます。--fingerprint <hex> を指定した場合は、そのフィンガープリントに固定されます。招待を発行すると発行側のマシン上の自分のプリンシパルも信頼され、参加すると新しいマシン上でも信頼されるため、それ以降、2 台のマシンは互いのデバイスを一覧表示します。新しいマシンがそれまで保持していたアカウントは user-identity.replaced-<id>.json として隣に保存されます。そのアカウントが発行済みのものを取り消せるキーは引き続きそのアカウントキーだけだからです。また、マシン上で実行中のブリッジは再起動時にアカウントを読み込みます。
メッシュを使わずにアカウントをコピーするには、パスフレーズで暗号化したファイルにエクスポートし、もう一方のマシンでインポートします。キーが表示されることはありません。パスフレーズは AGENT_COMMS_ACCOUNT_PASSPHRASE から読み取るか、入力内容が表示されない状態で入力を求められます。ファイルは所有者のみが読み書きできる権限で書き込まれ、既存のファイルが上書きされることはありません。
npx agent-comms account export ~/account.bundle # on a machine that holds the account
npx agent-comms account import ~/account.bundle # on the other machine, then delete the file
アカウントのコピーにはこの方法か参加を使い、マシン間で ~/.agent-comms をコピーしないでください。各マシンは、以下の付与台帳用に独自の書き込み ID を保持しているため、2 台で共有すると台帳が破損します。
アカウントが発行するすべての付与と、その付与の取り消しは、発行したマシンではなく、複製された台帳に記録されます。各マシンはそれぞれのデータドメインログに追記し(wire-mesh の複製された発行者付与台帳パターン、spec/PATTERNS.md)、ログはアカウントを保持するすべてのマシンに複製されます。そのため、いずれかのマシンで dm_revoke を実行すれば、別のマシンが発行した付与も見つけて取り消せます。エントリはアカウントキーから導出されたキーで暗号化されるため、アカウントを保持せずにエントリを保存または中継するデバイスに見えるのは暗号文だけです。また、台帳への書き込みにはアカウントの秘密鍵が必要です。台帳は、直接接続されたピア(ローカルメッシュ、または相互に直接到達できるマシン)間、および中継ハブ経由でしか接続できないマシン間でも複製されます。後者の場合は、2 台のデバイス間でエンドツーエンド暗号化されたリレー接続を介して複製されるため、ハブが運ぶのは暗号文です。いずれの場合も、アカウントのメンバーシップ証明がそのアカウントを示すデバイスにのみログが提供され、別のアカウントのハブ到達可能なデバイスにログが提供されることはありません。取り消しが行われると、その事実もすべての接続済みピアに即座に通知されます。台帳が user-identity.json に記録される前にビルドされた付与は、ブリッジの初回起動時に自動的に台帳へ移されるため、手作業は不要です。
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。