mcpField
A
airlock

airlock

本地服务
airlock-web.vercel.appAI 与智能体219 周前更新

关于 airlock

airlock 是一个常驻本地的 Go 守护进程,为多个 AI Agent 提供锁、共享状态、事件和任务队列,适用于共同操作代码库的场景。其 MCP 服务支持服务端阻塞等待,并将状态保存在 SQLite 中。

主要功能

  1. 01命名资源锁与阻塞等待
  2. 02原子共享状态和计数器
  3. 03Agent 在线状态登记
  4. 04事件通知与等待
  5. 05带租约的任务队列
  6. 06SQLite 持久化状态

适用场景

  1. 01协调多个 Agent 对同一文件的修改
  2. 02避免并发生成迁移编号冲突
  3. 03在规划 Agent 与工作 Agent 间分派任务
  4. 04等待其他 Agent 完成事件或释放锁

MCP 相关

本地 HTTP MCP 服务向客户端提供锁、状态、在线登记、事件和任务队列工具。守护进程持续运行,状态存储于 SQLite。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"adamorad-airlock": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"ghcr.io/adamorad/airlock:2.0.0"
]
}
}
}
启动命令docker run -i --rm ghcr.io/adamorad/airlock:2.0.0

本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
ocighcr.io/adamorad/airlock:2.0.0

工具

还没有获取到工具列表

本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。

详细介绍

Airlock

在同一个代码仓库中运行 5 个 Agent,不让它们互相干扰。

面向 AI Agent 的命名资源锁、原子共享状态、在线状态、事件和任务队列——通过始终运行的本地 HTTP MCP 服务提供,因此任何会话、终端或 CI 作业都能与其他会话、终端或作业协调。跨平台 Go 守护进程,基于 SQLite,支持真正的阻塞等待——不再需要轮询循环。

CI License Platform MCP

Airlock 演示

两个 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_resourcename、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_resourcename、lock_token(首选)或 agent_id{released: bool}
renew_lockname、lock_token(首选)或 agent_id、ttl_seconds?=900{renewed:true, expires_in_seconds} 或 {renewed:false, error}
list_locks—[{name, agent_id, expires_in_seconds}]
lock_resourcesnames:[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_notekey、value、author?、ttl_seconds?{saved:true}
get_notekey{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 构建。

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。