airlock について
airlockは、共有リポジトリで作業する複数のAIエージェントに、ロック、共有状態、イベント、タスクキューを提供する常駐型のローカルGoデーモンです。MCPサーバーはサーバー側のブロッキング待機に対応し、状態をSQLiteに保存します。
主な機能
- 01ブロッキング待機付き名前付きロック
- 02共有状態とカウンターの原子的更新
- 03エージェントの在席登録
- 04イベント通知と待機
- 05リース付きタスクキュー
- 06SQLiteによる状態の永続化
活用シーン
- 01同じファイルを編集するエージェントの調整
- 02マイグレーション番号の重複防止
- 03プランナーとワーカー間のタスク分配
- 04他のエージェントの通知やロック解除を待機
MCP 対応
ローカルHTTP MCPサーバーが、ロック、状態、在席、イベント、タスクキューのツールを提供します。デーモンは常時稼働し、状態をSQLiteに保存します。
設定
ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。
docker run -i --rm ghcr.io/adamorad/airlock:2.0.0ローカルサーバーにはマシン上の実行環境(Node.js 18 以上など)が必要です。設定ファイルの場所はクライアントごとに異なります。
ghcr.io/adamorad/airlock:2.0.0Streamable HTTPツール
ローカルサーバーのツールはマシン上で起動して初めて取得できます。メンテナーはまだツール一覧を提供していません。
詳細
Airlock
1 つのリポジトリで 5 つのエージェントを実行しても、互いの作業を妨げません。
AI エージェント向けの名前付きリソースロック、アトミックな共有状態、プレゼンス、イベント、タスクキューを、常時稼働するローカル HTTP MCP サーバー経由で提供します。これにより、あらゆるセッション、ターミナル、CI ジョブが相互に連携できます。Go 製のクロスプラットフォームデーモンで、SQLite をバックエンドに使用し、真の ブロッキング待機 を実現します。ポーリングループはもう不要です。

2 つのエージェントが同じ npm-install ロックを取りにいきます。一方がロックを取得し、もう一方はサーバー側の待機処理でブロックされ、最初のエージェントがロックを解放した瞬間に取得します。
Agent A Agent B
────── ──────
lock_resource("npm-install") lock_resource("npm-install", wait_seconds=50)
→ { locked: true, lock_token } (parked — the daemon holds the connection open)
npm install ...
unlock_resource(lock_token)
→ { locked: true, lock_token } ← wakes instantly
npm install ...
問題点
同じリポジトリで Claude Code のセッションを 2 つ開き、一方で認証機能を追加し、もう一方で通知機能を追加するとします。両方が同時に npm install を実行し、次のマイグレーションファイル名として両方が 003_ を選びます。どちらも、もう一方の存在を知りません。
共有状態も、作業の引き継ぎもありません。一方のエージェントがもう一方の作業内容を把握する方法もありません。連携は「マイグレーションが終わるまで待ってください」といった、強制力のない脆弱なプロンプト指示に頼っています。
仕組み
Airlock はバックグラウンドサービスとして動作する単一の Go デーモンです。常時稼働し、IDE やターミナルを再起動しても動き続けます。127.0.0.1:27183 で待ち受け、HTTP 経由で MCP を提供します。設定済みのエージェントなら、次の操作ができます。
- ロックを取得 — 競合するロックに対して ブロッキング待機 (
wait_seconds、最大 50 秒) ができます。デーモンが呼び出し元を待機させ、ロックが解放された瞬間に起こします。エージェントがリトライループを書く必要はありません。 - アトミックな状態を共有 — 衝突しない一意の番号を生成する
increment_counterや、比較とスワップを行うset_note_ifを利用できます。読み取り・変更・書き込みは競合状態がなく、ベストエフォートではありません。 - プレゼンスを通知 —
register_agentがハートビートを送信します。エージェントが停止すると、そのエージェントのロックが解放され、待機中のエージェントがすぐに起こされます。 - イベントを通知 — 世代カウント付きの
signal_event/wait_for_eventにより、ポーリングせずに作業を引き継げます。 - 作業をキューに入れる — プレゼンスにひも付いたリースを持つ小規模なタスクキューがあり、クラッシュ時にはタスクが自動的に再キューイングされます。
状態は ~/.airlock/state.db の SQLite(WAL モード) に保存されます。トランザクション対応でクラッシュに強く、複数の読み取りと単一の書き込みをサポートします。ストアは pure-Go (modernc.org/sqlite、cgo 不要) なので、Linux 向けのビルドは簡単なクロスコンパイルで済みます。バイナリは 1 つで、言語ランタイムも外部データベースも必要ありません。
インストール
(a) go install
go install github.com/adamorad/airlock/v2@latest # builds the `airlock` binary
airlock install-service # launchd (macOS) / systemd user unit (Linux)
(b) Homebrew
brew install adamorad/tap/airlock
airlock install-service
(c) ソースからビルド
git clone https://github.com/adamorad/airlock.git
cd airlock
go build -o airlock .
sudo install airlock /usr/local/bin/airlock # or anywhere on your PATH
airlock install-service
install-service は常時稼働するバックグラウンドサービスを登録します(旧 v1 LaunchAgent が存在する場合はアンロードするため、v2 デーモンがポート 27183 を問題なく使用できます)。
動作確認
airlock status
# Airlock — 127.0.0.1:27183
#
# LOCKS (0)
# (none)
# ...
エージェントを設定
Claude Code — コマンド 1 つで設定できます。
claude mcp add --transport http airlock http://localhost:27183
Cursor / Windsurf / その他の MCP クライアント — MCP 設定に次の内容を追加します。
{
"mcpServers": {
"airlock": {
"type": "http",
"url": "http://127.0.0.1:27183"
}
}
}
Linux(および複数ユーザーが使うホスト)では、Bearer トークンが 必須 です。ループバックはすべてのユーザー間で共有されるため、それだけでは認証境界になりません。デーモンは初回起動時に ~/.airlock/token に 0600 のトークンを書き込みます。Authorization: Bearer <token> ヘッダーとして送信してください。
{
"mcpServers": {
"airlock": {
"type": "http",
"url": "http://127.0.0.1:27183",
"headers": { "Authorization": "Bearer <paste contents of ~/.airlock/token>" }
}
}
}
macOS では、デーモンはデフォルトでループバックのみを使用し、トークンは任意です。AIRLOCK_TOKEN を設定すると、どの OS でもファイル内のトークンを上書きします。
ツール
Airlock は MCP 経由で 22 個のツール を公開します。各ツールの結果は JSON オブジェクトです(list_* ツールは JSON 配列を返します)。TTL は ttl_seconds で指定し、非推奨の別名として ttl_minutes も使用できます。
ロック
| ツール | 引数 | 戻り値 |
|---|---|---|
lock_resource | name、agent_id、ttl_seconds?=900、wait_seconds?=0(上限50)、wake_token? | {locked:true, lock_token, expires_in_seconds} — または、競合している場合は {locked:false, held_by, expires_in_seconds} に加え、キューに追加された場合は {wake_token, queue_position, retry_with} |
unlock_resource | name、lock_token(推奨)または agent_id | {released: bool} |
renew_lock | name、lock_token(推奨)または agent_id、ttl_seconds?=900 | {renewed:true, expires_in_seconds} または {renewed:false, error} |
list_locks | — | [{name, agent_id, expires_in_seconds}] |
lock_resources | names:[string]、agent_id、ttl_seconds?=900 | 全件成功または全件失敗:{locked:true, tokens:{name:token}} または {locked:false, held_by} |
wait_seconds は、デフォルトで連携を行うための設定です。50までの値を指定すると、lock_resource はすぐに locked:false を返す代わりに、ロックが解放されるまでサーバー側でブロックします。上限は、Claude Code のツール呼び出しごとのデフォルトタイムアウト60秒を下回る値に設定されています。返される lock_token はケイパビリティです。unlock_resource/renew_lock ではこのトークンが必要です(agent_id を使う方法は v1 との互換性のために維持されています)。lock_resources はドキュメント化されたロック順序(名前の辞書順)でロックを取得するため、2つの呼び出し元がデッドロックすることはありません。
メモと状態
| ツール | 引数 | 戻り値 |
|---|---|---|
set_note | key、value、author?、ttl_seconds? | {saved:true} |
get_note | key | {key, value, author?, expires_in_seconds?} または {found:false} |
list_notes | — | [{key, value, author?, expires_in_seconds?}] |
delete_note (v2) | key | {deleted: bool} |
set_note_if (v2) | key、expected_value、new_value、author?、ttl_seconds? | {swapped: bool}(保存された値が expected_value と一致する場合に限り true。不在または期限切れのメモは "" として扱われます) |
increment_counter (v2) | name、by?=1 | {value}(インクリメント後の値。並行実行時も衝突しません) |
プレゼンス
| ツール | 引数 | 戻り値 |
|---|---|---|
register_agent (v2) | agent_id、ttl_seconds?=60 | {registered:true, expires_in_seconds} — 生存状態を維持するには再度呼び出します。期限が切れると、そのエージェントのロックは自動的に解放されます |
unregister_agent (v2) | agent_id | {unregistered:true} — 保持しているロックも解放します |
list_agents (v2) | — | [{agent_id, expires_in_seconds}] |
イベント
| ツール | 引数 | 戻り値 |
|---|---|---|
signal_event (v2) | name | {generation}(更新された新しい世代番号。すべての待機者を起こします) |
wait_for_event (v2) | name、last_seen_generation?=0、wait_seconds?=25(上限50) | {generation, fired} — 世代番号が last_seen_generation を超えるまで、または待機時間が終了するまでブロックします(fired:false) |
clear_event (v2) | name | {cleared:true} |
イベントは世代番号でカウントされ、状態を保持しません。最後に確認した世代番号を渡すことで、呼び出しの間に発生したシグナルを見逃すことはありません。
タスク
| ツール | 引数 | 戻り値 |
|---|---|---|
push_task (v2) | queue、payload、author?、priority?=0 | {id}(優先度が高いタスク、または古いタスクから取得されます) |
claim_next_task (v2) | queue、agent_id、lease_seconds?=120 | {claimed:true, id, payload, lease_token} または {claimed:false} |
complete_task (v2) | id、lease_token | {completed: bool} |
fail_task (v2) | id、lease_token、requeue?=true | {failed: bool}(requeue=true の場合は保留中に戻り、false の場合は破棄されます) |
list_tasks (v2) | queue | [{id, queue, payload, priority, state, author?, lease_agent?, lease_expires_in_seconds?}] |
タスクの取得はリースです。取得したエージェントが lease_seconds 以内に complete_task/fail_task を実行しない場合、タスクは別のコンシューマー向けに自動的にキューへ戻されます。そのため、ワーカーがクラッシュしても作業が失われることはありません。claim_next_task が返す lease_token はケイパビリティであり、complete_task/fail_task の実行に必要です。
命名規則
エージェント同士が理解し合えるよう、一貫した名前を使用します。
- ファイル:
file:/abs/path/to/package.json - プロセス:
npm-install、db-migrations、tests:unit - エージェント ID:
agent:claude-session-abc
レシピ
(a) ブロッキングロックで1つのファイルへの編集を直列化する
2つのサブエージェントがどちらも package.json を編集する必要があります。1つのロックで直列化され、2つ目は ブロック されます(リトライループなし)。1つ目がロックを解放すると、ただちに再開します。
# Subagent A — block up to 50s to acquire, edit, release.
A=$(curl -s -X POST localhost:27183 -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"lock_resource","arguments":{"name":"file:/repo/package.json","agent_id":"sub-A","wait_seconds":50}}}' \
| jq -r '.result.content[0].text | fromjson | .lock_token')
# ...edit the file...
curl -s -X POST localhost:27183 -H 'Content-Type: application/json' \
-d "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"tools/call\",\"params\":{\"name\":\"unlock_resource\",\"arguments\":{\"name\":\"file:/repo/package.json\",\"lock_token\":\"$A\"}}}" \
| jq -c '.result.content[0].text | fromjson' # → {"released":true}
# Subagent B — the same call with wait_seconds=50 parks until A releases, then
# returns {locked:true, lock_token} the moment the lock frees. No polling.
(b) 一意なマイグレーションファイル名 — 競合なし
従来の v1 における典型的な危険です。2つのエージェントが同時に get_note でカウンターを取得し、どちらも 003 を読み取って、どちらも 004 を書き込みます。v2 では失われる読み取り・変更・書き込みがありません。increment_counter はアトミックなので、100件の同時呼び出しに対して、重複しない番号が100個返されます。
curl -s -X POST localhost:27183 -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"increment_counter","arguments":{"name":"migration-seq"}}}' \
| jq -c '.result.content[0].text | fromjson' # → {"value":4}
# Use the returned value: create 0004_*.sql. The next caller gets 5, never a collision.
(c) タスクキューを介したプロデューサー/コンシューマーの受け渡し
プランナーエージェントが作業を登録し、1つ以上のワーカーエージェントがそれを取得します。リースがあるため、ワーカーがクラッシュしても、そのタスクは消失せず自動的にキューへ戻されます。
# Producer — enqueue a unit of work.
curl -s -X POST localhost:27183 -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"push_task","arguments":{"queue":"build","payload":"compile module X","author":"planner"}}}' \
| jq -c '.result.content[0].text | fromjson' # → {"id":1}
# Consumer — claim the next task, do the work, complete it with the lease token.
CLAIM=$(curl -s -X POST localhost:27183 -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"claim_next_task","arguments":{"queue":"build","agent_id":"worker-1"}}}' \
| jq -c '.result.content[0].text | fromjson') # → {"claimed":true,"id":1,"payload":"compile module X","lease_token":"..."}
ID=$(echo "$CLAIM" | jq -r .id); TOK=$(echo "$CLAIM" | jq -r .lease_token)
# ...do the work...
curl -s -X POST localhost:27183 -H 'Content-Type: application/json' \
-d "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/call\",\"params\":{\"name\":\"complete_task\",\"arguments\":{\"id\":$ID,\"lease_token\":\"$TOK\"}}}" \
| jq -c '.result.content[0].text | fromjson' # → {"completed":true}
airlock status
デーモンが調整しているすべての状態(ロックとその保持者、現在のエージェント、ノート、カウンター、イベント、タスク)を一度にスナップショットとして表示します。~/.airlock/state.db を対象とするため、別のシェルからでも動作します。airlock watch を実行すると、1秒ごとに最新の状態が再表示されます。
Airlock — 127.0.0.1:27183
LOCKS (2)
NAME HELD BY EXPIRES IN
file:/repo/package.json sub-A 4m30s
npm-install session-A 12m
AGENTS (2)
AGENT EXPIRES IN
session-A 48s
worker-1 59s
NOTES (1)
KEY VALUE AUTHOR EXPIRES IN
deploy-status building planner —
COUNTERS (1)
NAME VALUE
migration-seq 4
EVENTS (1)
NAME GENERATION
migrations-done 2
TASKS (1)
ID QUEUE STATE PAYLOAD LEASE AGENT
2 build claimed compile module Y worker-1
アーキテクチャ
Go 製の単一デーモンを、127.0.0.1:27183 上で1つのプロセスとして実行します。
- ストア。
~/.airlock/state.dbに WAL モード の SQLite を使用します(純粋な Go 製のmodernc.org/sqliteを使用し、cgo は不要)。トランザクション対応で、クラッシュに耐えます。専用の単一書き込み接続によって書き込みを直列化し、読み取りには WAL の並行処理パスを使用します。SQLite の単一ライター方式により、increment_counterとset_note_ifが最初から正しく動作します。 - ブロッキング待機。 待機中のゴルーチンは、リソースごとの メモリ内 FIFO チャネル で待機し、
contextの期限を使用します。SQLite のトランザクション外で待機するため、競合時にブロッキング中のデーモンがデータベースを過負荷にすることはありません。バックグラウンドの リーパー が期限切れのロック TTL とプレゼンスリースを定期的に処理し、FIFO キューの先頭を起こすため、終了した保持者の後ろで待つ待機者が待機時間を使い切ることはありません。 - ケイパビリティトークン。
lock_token/lease_tokenによって、unlock/renewとcomplete/failを制御します。agent_idは来歴を示すものであり、認可には使用しません。 - セキュリティ。 ループバックのみへのバインド、
Hostヘッダーの許可リスト、すべてのOriginヘッダーの拒否(DNS リバインディング対策)、POST でのapplication/jsonの必須化に加え、~/.airlock/tokenにある0600のベアラートークンを必須とします(Linux/マルチユーザーホストの場合)。詳しくは SECURITY.md を参照してください。 - ポート 27183。 PortPeek は
27182(e の数字)を使用します。Airlock は次のポートを使い、2つのデーモンが共存できるようにします。
v1(Swift 製、macOS 専用)は legacy/swift/ にアーカイブされ、v1.1.0 のタグが付けられています。v1 の完全なワイヤーコントラクトは、v2 でも変更なく動作します。ポートも JSON も同じです。
ロードマップ/v2 の状況
v2.0.0 では、ブロッキング待機を伴うロック、アトミックな状態管理、プレゼンス、イベントというコア機能に加え、タスクキューを提供します。 すべて Go 製で、クロスプラットフォーム(macOS + Linux)に対応しています。Airlock と隣接するツールとの関係を含む設計仕様とその根拠は、docs/ROADMAP-v2.md にあります。ロードマップがご自身のワークフローに合わない場合は、issue を作成してください。そのための「徹底的に批評してください」という固定スレッドが用意されています。
アンインストール
airlock uninstall-service # remove the launchd / systemd service
brew uninstall airlock # if installed via the tap: brew uninstall adamorad/tap/airlock
rm -rf ~/.airlock # optional: drop the state db + token
FAQ
ポートが27183なのはなぜですか? PortPeekはすでに27182(ネイピア数eの最初の5桁)を使用しています。Airlockは次のポート27183を使用するため、2つのデーモンが同じマシン上で競合せずに共存できます。
セキュリティモデルはどのようなものですか? Airlockはループバックのみでバインドし、Hostヘッダーを検証し、Originヘッダーを拒否し、POSTではapplication/jsonを必須とします。Linux/マルチユーザーホストでは、0600のベアラートークンを~/.airlock/tokenに用意する必要があります(ループバックがユーザー間で共有されるためです)。macOSでは任意です。機能トークン(lock_token/lease_token)により、バグのあるエージェントやプロンプトインジェクションを受けたエージェントが、agent_idを推測するだけで別のエージェントのロックを解除したり盗んだりすることはできません。意図的に設けられた、文書化済みの制限があります。同じローカルユーザーとして実行中のプロセスであれば、どれでもAPIを呼び出せます。Airlockは協調するエージェント間を調整するものであり、エージェントをサンドボックス化するものではありません。ノートやタスクの内容は、エージェント間で受け渡される信頼できない入力です。これを利用するエージェントは、指示ではなくデータとして扱う必要があります。脅威モデルの全文はSECURITY.mdをご覧ください。
PortPeekとはどのような関係ですか? PortPeekは、AIエージェント間のポートの競合(どのエージェントがどのポートをバインドしているか)を解決します。Airlockは、より広範な調整の問題(どのエージェントがどのタスクに取り組んでいるか)を解決します。同じ系譜で、隣接するポート(27182/27183)を使用し、並行して動作するよう設計されています。
貢献
IssueやPRを歓迎します。CONTRIBUTING.mdをご覧ください。
MIT Licenseの下でライセンスされています。
Adam Moradが開発しました。
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。