关于 airlock
airlock 是一个常驻本地的 Go 守护进程,为多个 AI Agent 提供锁、共享状态、事件和任务队列,适用于共同操作代码库的场景。其 MCP 服务支持服务端阻塞等待,并将状态保存在 SQLite 中。
主要功能
- 01命名资源锁与阻塞等待
- 02原子共享状态和计数器
- 03Agent 在线状态登记
- 04事件通知与等待
- 05带租约的任务队列
- 06SQLite 持久化状态
适用场景
- 01协调多个 Agent 对同一文件的修改
- 02避免并发生成迁移编号冲突
- 03在规划 Agent 与工作 Agent 间分派任务
- 04等待其他 Agent 完成事件或释放锁
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
在同一个代码仓库中运行 5 个 Agent,不让它们互相干扰。
面向 AI Agent 的命名资源锁、原子共享状态、在线状态、事件和任务队列——通过始终运行的本地 HTTP MCP 服务提供,因此任何会话、终端或 CI 作业都能与其他会话、终端或作业协调。跨平台 Go 守护进程,基于 SQLite,支持真正的阻塞等待——不再需要轮询循环。

两个 Agent 同时尝试获取同一个 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 会话——一个添加身份验证,另一个添加通知。它们同时尝试运行 npm install,也都将 003_ 选为下一个迁移文件名,却都不知道对方的存在。
没有共享状态,没有交接,也没有办法让一个 Agent 知道另一个正在做什么。协调只能依靠脆弱的提示词指令(“请等迁移完成后再操作”),但没有任何机制会强制执行。
工作原理
Airlock 是一个以后台服务运行的单一 Go 守护进程——始终在线,即使 IDE 和终端重启也能继续运行。它监听 127.0.0.1:27183,通过 HTTP 提供 MCP 服务。配置好它的任何 Agent 都可以:
- 获取锁——遇到竞争时可以阻塞等待(
wait_seconds,最长 50s),由守护进程挂起调用方,并在锁释放后立即唤醒。不需要 Agent 自行编写重试循环。 - 共享原子状态——使用
increment_counter获取不会冲突的唯一编号,使用set_note_if执行比较并交换。读-改-写操作不会发生竞态,而不只是尽力而为。 - 声明在线状态——
register_agent会发送心跳;Agent 退出后,其锁会释放,等待者也会立即唤醒。 - 发送事件信号——使用带代数计数的
signal_event/wait_for_event进行交接,无需轮询。 - 排队任务——通过与在线状态绑定的租约管理小型任务队列,Agent 崩溃后任务会自动重新入队。
状态存储在 ~/.airlock/state.db 中的 SQLite(WAL 模式)——支持事务、可从崩溃中恢复,并发读取且仅允许单个写入者。存储实现为纯 Go(modernc.org/sqlite,无 cgo),因此 Linux 版本可轻松交叉编译。单个二进制文件,无需语言运行时或外部数据库。
安装
(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)
# ...
配置 Agent
Claude Code——一条命令:
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 token——因为所有用户共享 loopback 地址,所以它本身不能作为授权边界。守护进程首次运行时会在 ~/.airlock/token 中写入一个 0600 权限的 token;发送时将其放在 Authorization: Bearer <token> 请求头中:
{
"mcpServers": {
"airlock": {
"type": "http",
"url": "http://127.0.0.1:27183",
"headers": { "Authorization": "Bearer <paste contents of ~/.airlock/token>" }
}
}
}
在 macOS 上,守护进程默认仅监听 loopback 地址,token 为可选项。任何操作系统上,AIRLOCK_TOKEN 都会覆盖文件中的 token。
工具
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 需要该凭证(为兼容 v1,仍保留 agent_id 方式)。lock_resources 会按文档规定的锁顺序(名称按字典序排列)获取锁,因此两个调用方不会陷入死锁。
备注与状态
| 工具 | 参数 | 返回值 |
|---|---|---|
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} — 重新调用以保持在线;过期后,该 Agent 的锁会自动释放 |
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 调用时必须提供。
命名约定
使用一致的名称,让各个 Agent 能够相互理解:
- 文件:
file:/abs/path/to/package.json - 进程:
npm-install、db-migrations、tests:unit - Agent 身份:
agent:claude-session-abc
示例
(a) 使用阻塞锁串行编辑同一文件
两个子 Agent 都需要编辑 package.json。它们通过一个锁串行执行——第二个会阻塞(无需重试循环),并在第一个释放锁的瞬间唤醒。
# 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 隐患:两个 Agent 在同一时刻调用 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) 通过任务队列交接生产者/消费者任务
规划器 Agent 推送工作;一个或多个工作 Agent 领取任务。租约意味着崩溃的工作 Agent 所领取的任务会自动重新入队,而不会消失。
# 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
一次性快照,显示守护进程正在协调的所有内容——锁(及其持有者)、当前 Agent、备注、计数器、事件和任务。该命令针对 ~/.airlock/state.db 运行,因此即使在不同的 shell 中也能正常工作。airlock watch 会每秒实时重新打印快照。
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。
- 存储。 SQLite 以 WAL 模式存储于
~/.airlock/state.db——纯 Go 实现(modernc.org/sqlite,不使用 cgo),支持事务且崩溃安全。单个专用写入连接负责串行化写入;读取者使用 WAL 的并发路径。SQLite 的单写入者模型使increment_counter和set_note_if无需额外处理即可正确运行。 - 阻塞等待。 等待中的调用者是一个 goroutine,在每个资源对应的内存 FIFO 通道上等待,并受
context截止时间限制——它位于任何 SQLite 事务之外,因此守护进程阻塞等待时,不会在高争用下拖垮数据库。后台清理器会清理过期的锁 TTL 和在线状态租约,并唤醒 FIFO 队首,因此排在已失效持有者之后的等待者不会耗尽整个等待时间。 - 能力令牌。
lock_token/lease_token用于限制unlock/renew和complete/fail操作。agent_id仅用于标识来源,不用于授权。 - 安全性。 仅绑定回环地址、使用
Host标头允许列表、拒绝任何Origin标头(防范 DNS 重绑定)、POST 请求要求使用application/json——此外,在 Linux/多用户主机上还要求使用位于~/.airlock/token的0600持有者令牌。请参阅 SECURITY.md。 - 端口 27183。 PortPeek 使用
27182(数字 e 的位数);Airlock 使用下一个端口,因此两个守护进程可以共存。
v1(仅适用于 macOS 的 Swift 版本)已归档至 legacy/swift/,并标记为 v1.1.0。完整的 v1 线协议在 v2 中仍可原样使用——端口相同,JSON 相同。
路线图 / v2 状态
v2.0.0 发布了核心功能——支持阻塞等待的锁、原子状态、在线状态和事件——以及任务队列;全部使用 Go 编写,支持跨平台(macOS + Linux)。设计规范和设计理由(包括 Airlock 与相邻工具的定位)位于 docs/ROADMAP-v2.md。如果路线图不适合你的工作流,请提交问题——置顶的“尽情批判”讨论帖正是为此准备的。
卸载
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
常见问题
为什么使用端口 27183? PortPeek 已使用 27182(e,即欧拉数的前几位数字)。Airlock 使用下一个端口 27183,因此两个守护进程可以在同一台计算机上共存,不会发生冲突。
安全模型是什么? Airlock 仅绑定到 loopback,验证 Host 标头,拒绝任何 Origin 标头,并要求 POST 请求使用 application/json。在 Linux/多用户主机上,必须提供位于 ~/.airlock/token 的 0600 持有者令牌(因为 loopback 在这些环境中由多个用户共享);在 macOS 上则可选。能力令牌(lock_token/lease_token)意味着有缺陷或受到提示词注入的 Agent 无法仅靠猜测另一个 Agent 的 agent_id 来释放或窃取其锁。明确记录的限制是:任何以同一本地用户身份运行的进程都可以调用 API——Airlock 用于协调相互协作的 Agent,不会对它们进行沙箱隔离。备注/任务内容属于不可信的跨 Agent 输入;使用这些内容的 Agent 必须将其视为数据,绝不能当作指令。完整威胁模型:SECURITY.md。
它与 PortPeek 有何关系? PortPeek 解决 AI Agent 的端口冲突问题(哪个 Agent 绑定了哪个端口)。Airlock 解决更广泛的协调问题(哪个 Agent 正在执行哪个任务)。两者属于同一产品系列,使用相邻端口(27182/27183),设计为可并行运行。
贡献
欢迎提交 Issue 和 PR——请参阅 CONTRIBUTING.md。
依据 MIT License 授权。
由 Adam Morad 构建。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。