商业产业 Hacker News AI 2026-08-28T22:12 正文 ok 3819 words

Show HN:A2acast——让不同电脑上的 AI Agent 协同工作

英文标题:🔶 Show HN: A2acast – Let AI agents on different computers work together

A2acast 是一个仅依赖 Python 标准库的单文件工具,可让不同机器上的 Claude Code、ChatGPT(Codex CLI)和 Copilot CLI 通过端到端加密消息及 A2A 任务协作,无需自建服务器、账号或开放入站端口。它通过 ntfy 中继和仅出站 HTTPS 连接实现 NAT 后设备之间的通信,并支持节点发现、广播、任务委派及本地 A2A 桥接。项目还提供可选的 Codex 自主执行模式与机器级 Worker Pool,但明确强调允许列表并非基于每节点密码学身份,接收任务也应视为不可信输入。对于开发者而言,它提供了一种把本地 AI 编程 Agent、CLI 工具和 A2A 框架连接成跨设备协作网络的轻量方案,同时也要求用户谨慎管理加入代码、共享密钥及自动执行权限。
打开原文相关 3 · 机会 1
media139.7 KB

中文

不同机器上的 AI Agent 之间通信——无需服务器、无需账号、无需开放端口。 一个仅依赖标准库的 Python 文件。端到端加密。Linux 笔记本上的 Claude Code、MacBook 上的 ChatGPT(Codex CLI)、Windows PC 上的 Copilot——都可以交换消息和 A2A 任务。

快速开始(两台机器,一分钟)

机器 A ——创建 mesh:

uv tool install a2acast   # 或:pipx install a2acast ——它已发布到 PyPI
mesh init home   # 打印一段可粘贴到机器 B 的内容,然后开始监听

(如需使用最新开发版本:pipx install git+https://github.com/husker/a2acast。)

机器 B ——粘贴 mesh init 输出的内容。它看起来像这样:

curl -fsSLO https://raw.githubusercontent.com/husker/a2acast/vX.Y.Z/mesh.py
python3 mesh.py join mesh1-XXXX...

就这样:完成下载、加入网络(节点以其主机名命名)并开始监听。B 加入的瞬间,机器 A 会打印 MESH_NODE_JOINED。

通信(在任意一台机器的新终端中):

mesh send all "hello mesh"     # 约一秒后,B 的 watcher 会打印该消息
mesh ping <b-name>             # → MESH_PONG node=<b-name> rtt=~400ms
mesh ask <b-name> "run the tests and summarize failures" --wait 300

无需预先声明机器列表:任何拥有加入代码的机器都可以加入,自行选择名称,并且每个节点都会自动获知它。请通过私密渠道分享加入代码——它就是 mesh 的密钥。

与 Claude Code、Codex 或 Copilot CLI 一起使用

安装插件(让会话了解该协议,并在项目是 mesh 节点时自动提醒):

# Claude Code
/plugin marketplace add husker/a2acast
/plugin install a2acast
mesh claude-setup             # 每个项目执行一次——在会话开始时启用在线状态

# Codex CLI / ChatGPT desktop
codex plugin marketplace add husker/a2acast
codex plugin add a2acast@a2acast
mesh codex-setup              # 每台机器执行一次——在会话开始时启用在线状态

# GitHub Copilot CLI
copilot plugin marketplace add husker/a2acast
copilot plugin install a2acast@a2acast
mesh copilot-setup            # 每个项目执行一次——将 watcher 固定到此节点

每个插件都会在会话开始时加载 mesh 安全规则。Claude 使用带有 asyncRewake 的异步 Stop;Codex 使用 Stop。Copilot 将 watcher 作为 MCP server 运行(mesh mcp-serve,由 mesh copilot-setup 为每个项目连接配置——见下文):Copilot 会在会话开始时启动它、在会话结束时停止它——包括 Ctrl-C 和崩溃的情况。由于它不是 Agent shell,会话在监听时不会显示“working”旋转指示器。消息到达时,服务器会自行唤醒空闲会话(通过 MCP sampling),会话再通过 mesh_pending / mesh_reply / mesh_send 工具处理消息——这是一次真实的交互回合,因此 MESH_TASK 会被实际完成,而不是仅得到确认。(第一次使用时,Copilot 可能会要求你批准该服务器进行 sampling;批准后,之后的唤醒将静默进行。)

每个会话运行的循环:

  1. 使用插件后,请遵循上述特定 harness 的配置方式。Claude 和 Codex 无需手动启动 watcher;对于 Copilot,在项目中运行一次 mesh copilot-setup(Copilot 向插件 MCP server 不提供项目相关信息,也没有可移植的方式推断项目信息,因此该操作会将节点固定在工作区的 .github/mcp.json 中)。此后,其 MCP-server watcher 会自动监听并唤醒会话——无需额外启用。处理会在后台进行(不会出现“working”旋转指示器);你之后发送的下一条提示会以一行说明开头,告知你离开期间 a2acast 已处理的内容。
  2. 完成你的工作。在推送了需要另一台机器处理的内容后,执行:mesh send <node> "one-line summary — pull"。
  3. 当收到一行 MESH_TASK 时,完成任务,并使用 mesh reply <task-id> "<result>" 回答。

支持任意数量的节点;每个节点都有一个收件箱主题,all 则用于广播。mesh claude-setup 通过写入项目的 .mcp.json 注册 a2acast 在线状态 watcher——CLAUDE.md 协议片段现已位于 mesh integrate --format claude。(无论是否使用插件,你仍需在每台机器上执行一次 mesh init/mesh join:插件让会话了解协议,但不会创建 mesh。对于 Copilot,你还需要在每个项目中执行一次 mesh copilot-setup。插件的 hooks 和 MCP server 会调用 PATH 中的 mesh CLI——即执行 mesh init 所安装的同一个 CLI——因此它在 macOS、Linux 和 Windows 上的工作方式相同。在 Windows 上,私有状态依赖账户 ACL,而不是 POSIX 文件权限;证据文件会使用已验证身份检查打开,而非内核 O_NOFOLLOW;完整测试套件在 CI 的 windows-latest 上全部通过。更新插件时请保持其最新:运行 pipx upgrade a2acast,或 uv tool upgrade a2acast。)

自主 Codex 节点(选择启用)。 Claude 和 Copilot 会在进程内唤醒空闲会话以处理 MESH_TASK;Codex 没有这样的路径,因此已加入的 Codex 机器可以改为运行后台 supervisor,在没有会话打开的情况下执行委派任务。使用 mesh codex-setup --supervise 启用它,然后用 mesh codex-allow <node> 指定你信任、允许其在这台机器上运行代码的对等节点。在同时完成这两步之前,不会执行任何内容——supervisor 默认关闭,其允许列表初始为空。该授权以发送方名称字符串为键,而不是会话身份,也不单独依赖经过验证的节点密钥。请参阅下方安全模型。

机器级 Worker Pool(选择启用)

Worker Pool 通过本地 Codex、Copilot 和 Goose/Ollama CLI 执行代码仓库任务。仅当所有三个 CLI 都已安装后,才可在已加入的 Worker 主机上运行它;为当前用户完成 Codex 和 Copilot 身份验证,并启动 Ollama 且确保配置的模型可用(默认是 qwen3:4b)。协调器必须已经是当前已知的 mesh 身份;请在配置前使用 mesh status 确认。

mesh pool-setup --workspace-root ~/Projects \
  --coordinator jamess-macbook-air-2
mesh pool-start
mesh pool-status
mesh delegate auto "add a regression test" --repo /abs/repo --wait 300

pool-setup 仅允许位于所列工作区根目录之下的仓库,并将 exec_allow 设为指定的唯一协调器。它不会启动 Worker;pool-start 是显式启用步骤。由于每个 mesh 成员都拥有共享密钥,并且可以声明任意发送方名称,这个允许列表并非按节点密码学身份验证。请仅配置你信任的协调器。

在 macOS 上,pool-start 管理当前用户的 LaunchAgents。在其他操作系统上,pool-start 和 pool-stop 会打印供你或服务管理器运行的前台 supervisor 命令;它们不会安装服务。

对于常规 auto 作业,调度器会按照 Goose/Ollama、Copilot、Codex 的顺序选择第一个符合条件的后端,并跳过被阻塞、忙碌、不可用或处于冷却期的 Worker。非阻塞 CLI 调用(默认的 --wait 0)以及非阻塞 MCP 工具仅分派一个 Worker,不会自动重新分派。使用正数 --wait 时,CLI 只有在收到经认证的 quota 或 unavailable 结果后,才能在同一总等待预算内尝试下一个符合条件的后端。安全和集成作业只选择 Codex,除非显式指定某个后端。

每项作业都在单独的 Git worktree 中运行。worktree 可以避免 checkout 冲突;worktree 并不是安全沙箱。Worker 进程仍拥有本地用户的操作系统权限,而仓库任务属于不可信输入。结果会报告执行结果、分支、提交、worktree、摘要和验证信息。分支和提交包含建议的生产变更;在你自行审查并合并或 cherry-pick 之前,它们不会被集成。

该 Pool 会创建 worktree 和本地提交。其 Worker 指令禁止 merge、push、PR、deploy、publish 和删除 worktree,a2acast 也不会将其中任何操作作为自动后处理执行。清理必须显式执行:

mesh pool-clean --integrated-into main
mesh pool-stop

常规清理仅会删除已终止、干净、具有一致持久记录,且其提交已集成到指定 ref 的 worktree;不确定或未集成的工作会被保留。mesh pool-clean --task <id> --force 是专门针对单个已终止任务的显式逃生开关,可能丢弃其未集成或未清理的 worktree。

一个文件,即 mesh.py,仅使用 Python 标准库。消息通过 ntfy 中继传递(默认使用公共 ntfy.sh;可使用 mesh init --server 自托管),两端均通过仅出站 HTTPS 连接通信——这正是两台位于 NAT 后的笔记本无需端口转发、无需 VPN、无需你自建服务器即可互通的原因。主题由 mesh 密钥和节点名称派生,因此任何内容都不会在任何地方注册;传递延迟约为一秒。mesh watch --follow 会保持一个流式连接,并在每条消息到达时打印它。

在终端中,init 和 join 完成后会直接进入该 watcher;调用 mesh 的程序(脚本、Agent harness,以及任何没有 TTY 的内容)则会获得普通的立即返回行为。

消息持久性取决于大小。 小于中继约 4 KB 内联限制的消息会以正常保留策略缓存;更大的负载会作为中继的 attachments 传输,其 TTL 约为 3 小时——节点在该时间窗口后才上线时仍会收到唤醒,但内容已消失,发送方必须重发(当可能出现这一情况时,mesh 会在两端发出警告)。对于必须跨越长时间离线窗口的大批量数据,请保持负载较小,或使用共享仓库等持久通道(#66 正在跟踪分块修复)。

委派任务:任意 AI 与任意 AI 通信

节点不只是相互 ping——它们会在 JSON-RPC 信封中交换真实的 A2A protocol 任务:

mesh ask desktop "run the test suite and summarize failures" --wait 300
# → MESH_TASK_RESULT from=desktop state=completed: 2 failures, both in auth...

# 在接收端(其 Agent 通过 `mesh watch --follow` 看到此消息):
# MESH_TASK from=laptop task=5e52304e... state=submitted: run the test suite...
mesh reply 5e52304e "2 failures, both in auth: ..."

mesh tasks             # 所有已提问/已回答内容的账本
mesh card desktop      # 其 A2A Agent card

并且,由于其线路格式是真正的 A2A,mesh a2a-serve 会运行一个 localhost bridge,使任何兼容 A2A 的框架(LangGraph、Google ADK、Microsoft Agent Framework 等)都可以将远程 mesh 节点作为普通 A2A 服务器通信——通过 Agent card 发现、message/send、tasks/get:

mesh a2a-serve         # → http://127.0.0.1:4737/agents/<node>,每个远程节点一个

请参阅 docs/AGENTS.md,了解各 harness 的连接方式(Codex CLI、Copilot CLI、Gemini CLI、A2A 框架、cron)。默认情况下,mesh 仅传递消息;显式启动的 Worker Pool 是例外,它会调用配置好的本地 CLI。交互式节点则使用自身 harness 所具备的模型、工具和权限进行响应。

安全模型(请阅读)

消息经过端到端加密和认证。 mesh 密钥由 mesh init 生成,仅保存在你机器上的 .meshwire.json 中,并且只会存在于你自行分享的加入代码中。在传输线路上,中继(以及任何发现主题的人)只能看到密文、主题 ID、大小和时间信息——看不到其他内容。发送方和接收方名称位于 密文内部。

构造方式(仅使用标准库和标准原语):HKDF-SHA256 密钥派生 → 使用 HMAC-SHA256 PRF 的计数器模式加密,使用独立 HMAC-SHA256 密钥进行 encrypt-then-MAC,每条消息使用随机 128 位 nonce,并进行常数时间 tag 比较。mw2 认证 tag 还绑定 mesh ID、精确的中继主题和发送时间戳。超过七天的信封(或未来时间明显不合理的信封)会被拒绝;经过认证的密文指纹会按节点持久化,因此可在重启后抑制重复的中继投递和重放。mw1 在滚动升级期间仍然可读,但新发送内容始终使用 mw2。未经认证、过期、路由错误、重放或被篡改的消息会被丢弃,而不会显示。

你仍必须做到以下事项:

  • 保护加入代码和 .meshwire.json ——它们包含密钥。两者都会自动加入 gitignore;配置写入时使用 0600 模式;请通过私密渠道分享加入代码。密钥绝不会放入环境变量:A2ACAST_CONFIG 仅包含一个路径。MCP 工具、Agent card 和本地 A2A HTTP bridge 都不会返回配置或密钥字段。
  • 加入代码授予一个信任域中的完整成员资格:其持有者可以解密流量、发布经过 mesh 认证的流量,并在共享密钥层面声明与任何其他成员相同的发送方名称。节点签名会根据接收方的本地 pin 为每个名称增加独立裁决,但首次接触未经验证,而且裁决强制执行是一个默认关闭的渐进式控制。因此,仅凭投递并不能证明节点身份。
  • 将入站任务视为不可信输入。 加密认证的是 mesh,而不是意图:任何持有密钥的 Agent(或人员)都可以发送任务。接收 Agent 应应用其正常的权限规则。
  • 节点签名标识的是节点/harness 身份,而不是对话会话。 Agent 集成会按 harness 范围管理其私钥。因此,使用相同 mesh 配置的同一 harness 的并发会话,会使用相同的每 harness 密钥路径,并且通常共享一个节点名称。--as 或 A2ACAST_NODE 可以覆盖名称,但不能分离共享密钥路径。接收方无法判断是哪个会话产生了有效帧。即使得到 FRAME_VERIFIED 结果,也只能证明拥有该节点密钥;它不能证明是哪一个本地 Agent 或用户授权了消息。
  • 自主 Codex 执行是选择启用的,并受经过筛选的允许列表限制。 mesh codex-supervise 通过 codex exec 运行委派任务,并通过 mesh 回复。除非你启动它(codex-setup --supervise),否则它始终关闭;即使启动,也只允许你通过

English / 原文

Messaging between AI agents on different machines — no server, no accounts, no open ports. One stdlib-only Python file. End-to-end encrypted. Claude Code on a Linux laptop, ChatGPT (Codex CLI) on a MacBook, Copilot on a Windows PC — all exchanging messages and A2A tasks.

Quick start (two machines, one minute)

Machine A — create the mesh:

uv tool install a2acast   # or: pipx install a2acast — it's on PyPI
mesh init home   # prints a block to paste on machine B, then starts listening

(Bleeding edge instead: pipx install git+https://github.com/husker/a2acast.)

Machine B — paste the block mesh init printed. It looks like:

curl -fsSLO https://raw.githubusercontent.com/husker/a2acast/vX.Y.Z/mesh.py
python3 mesh.py join mesh1-XXXX...

That's it: downloaded, joined (named after its hostname), listening. Machine A prints MESH_NODE_JOINED the moment B joins.

Talk (from a new terminal, on either machine):

mesh send all "hello mesh"     # B's watcher prints it about a second later
mesh ping <b-name>             # → MESH_PONG node=<b-name> rtt=~400ms
mesh ask <b-name> "run the tests and summarize failures" --wait 300

No machine list to declare up front: any machine with the join code can join , picks its own name, and every node learns about it automatically. Share the join code privately — it IS the mesh secret.

Using it with Claude Code, Codex, or Copilot CLI

Install the plugin (teaches sessions the protocol and auto-reminds them when a project is a mesh node):

# Claude Code
/plugin marketplace add husker/a2acast
/plugin install a2acast
mesh claude-setup             # once per project — arms presence at session start

# Codex CLI / ChatGPT desktop
codex plugin marketplace add husker/a2acast
codex plugin add a2acast@a2acast
mesh codex-setup              # once per machine — arms presence at session start

# GitHub Copilot CLI
copilot plugin marketplace add husker/a2acast
copilot plugin install a2acast@a2acast
mesh copilot-setup            # once per project — pins the watcher to this node

Each plugin loads the mesh safety rules at session start. Claude uses asynchronous Stop with asyncRewake; Codex uses Stop. Copilot runs the watcher as an MCP server (mesh mcp-serve, wired per project by mesh copilot-setup — see below) that Copilot starts with the session and stops when it ends — including Ctrl-C and crash. Because it isn't an agent shell, the session shows no "working" spinner while it listens. When a message arrives the server wakes the idle session on its own (via MCP sampling) and the session handles it with the mesh_pending / mesh_reply / mesh_send tools — a real turn, so a MESH_TASK gets done, not just acknowledged. (The first time, Copilot may ask once to approve the server for sampling; approve it and later wakes run silently.)

The loop each session runs:

  1. With the plugin, follow the harness-specific setup above. Claude and Codex need no manual watcher; on Copilot, run mesh copilot-setup once in the project (Copilot hands a plugin MCP server no project info and there's no portable way to guess it, so this pins the node in a workspace .github/mcp.json). After that its MCP-server watcher listens and wakes the session automatically — nothing to arm. Handling happens out of band (no "working" spinner); the next prompt you send opens with a one-line note of anything a2acast handled while you were away.
  2. Do your work. After pushing something the other machine should act on: mesh send <node> "one-line summary — pull".
  3. When a MESH_TASK line arrives, do the work and answer with mesh reply <task-id> "<result>".

Works with any number of nodes; each node has an inbox topic and all broadcasts. mesh claude-setup registers the a2acast presence watcher by writing the project's .mcp.json — the CLAUDE.md protocol snippet itself now lives at mesh integrate --format claude. (You still run mesh init/mesh join once per machine either way: the plugin teaches sessions the protocol, it doesn't create the mesh. On Copilot you also run mesh copilot-setup once per project. The plugin's hooks and MCP server invoke the mesh CLI on your PATH — the same one mesh init installed — so it works the same on macOS, Linux, and Windows. On Windows, private state relies on your account's ACLs rather than POSIX file modes, and evidence files are opened with verified-identity checks in place of kernel O_NOFOLLOW; the full test suite runs green on windows-latest in CI. Keep it current: pipx upgrade a2acast (or uv tool upgrade a2acast) when you update the plugin.)

Autonomous Codex nodes (opt-in). Claude and Copilot wake an idle session in-process to handle a MESH_TASK; Codex has no such path, so a joined Codex machine can instead run a background supervisor that executes delegated tasks with no session open. Enable it with mesh codex-setup --supervise, then name the peers you trust to run code on this machine with mesh codex-allow <node>. Nothing runs until you do both — the supervisor is off by default and its allowlist starts empty. That grant is keyed to the sender-name string, not a conversational session or, by itself, a verified node key. See the security model below.

Machine-wide worker pool (opt-in)

The worker pool runs repository tasks through local Codex, Copilot, and Goose/Ollama CLIs. Run it on a joined worker host only after all three CLIs are installed; authenticate Codex and Copilot for the current user, and start Ollama with the configured model available (default qwen3:4b). The coordinator must already be a current known mesh identity; confirm it with mesh status before setup.

mesh pool-setup --workspace-root ~/Projects \
  --coordinator jamess-macbook-air-2
mesh pool-start
mesh pool-status
mesh delegate auto "add a regression test" --repo /abs/repo --wait 300

pool-setup permits repositories only below the listed workspace roots and sets exec_allow to the single named coordinator. It does not start a worker; pool-start is the explicit activation step. Because every mesh member has the shared key and can assert any sender name, this allowlist is not per-node cryptographic identity. Configure only a coordinator you trust.

On macOS, pool-start manages current-user LaunchAgents. On other operating systems, pool-start and pool-stop print foreground supervisor commands for you or your service manager to run; they do not install a service.

For normal auto jobs, dispatch selects the first eligible backend in Goose/Ollama, Copilot, then Codex order, skipping workers that are blocked, busy, unavailable, or cooling down. Nonblocking CLI calls (the default --wait 0) and the nonblocking MCP tool dispatch that one worker and do not auto-redispatch. With a positive --wait, the CLI can try the next eligible backend only after an authenticated quota or unavailable result, within the same total wait budget. Security and integration jobs select only Codex unless a backend is explicitly named.

Each job runs in a separate Git worktree. A worktree prevents checkout collisions; a worktree is not a security sandbox. Worker processes still have the local user's OS permissions, and repository tasks are untrusted input. Results report an outcome, branch, commit, worktree, summary, and verification. A branch and commit contain proposed production changes; they are not integrated until you review and merge or cherry-pick them yourself.

The pool creates worktrees and local commits. Its worker instructions forbid merge, push, PR, deploy, publish, and worktree deletion, and a2acast performs none of those as automatic postprocessing. Cleanup is an explicit command:

mesh pool-clean --integrated-into main
mesh pool-stop

Normal cleanup removes only terminal, clean worktrees with consistent durable records whose commits are integrated into the named ref; uncertain or unintegrated work is preserved. mesh pool-clean --task <id> --force is an explicit escape hatch for exactly one terminal task and may discard its unintegrated or dirty worktree.

One file, mesh.py, Python stdlib only. Messages travel through an ntfy relay (default: the public ntfy.sh; self-host with mesh init --server) over outbound-only HTTPS connections on both ends — which is why two laptops behind NAT can talk with no port forwarding, no VPN, and no server of yours. Topics are derived from the mesh secret and the node name, so nothing is ever registered anywhere; delivery latency is about a second. mesh watch --follow holds one streaming connection and prints each message as it lands.

In a terminal, init and join flow straight into that watcher when they finish — programs calling mesh (scripts, agent harnesses; anything without a TTY) get the plain return-immediately behavior instead.

Message durability depends on size. Messages under the relay's ~4 KB inline limit are cached with normal retention; larger payloads ride as relay attachments with a ~3 hour TTL — a node offline past that window still gets the wake, but the content is gone and the sender must resend (mesh warns on both ends when this can happen). For bulk that must survive long offline windows, keep payloads small or use a durable channel such as a shared repo (#66 tracks a chunking fix).

Delegating tasks: any AI talking to any AI

Nodes don't just ping each other — they exchange real A2A protocol tasks in JSON-RPC envelopes:

mesh ask desktop "run the test suite and summarize failures" --wait 300
# → MESH_TASK_RESULT from=desktop state=completed: 2 failures, both in auth...

# on the receiving side (its agent sees this via `mesh watch --follow`):
# MESH_TASK from=laptop task=5e52304e... state=submitted: run the test suite...
mesh reply 5e52304e "2 failures, both in auth: ..."

mesh tasks             # ledger of everything asked/answered
mesh card desktop      # its A2A agent card

And because the wire format is real A2A, mesh a2a-serve runs a localhost bridge so any A2A-capable framework (LangGraph, Google ADK, Microsoft Agent Framework, …) can talk to remote mesh nodes as ordinary A2A servers — discovery via agent cards, message/send, tasks/get:

mesh a2a-serve         # → http://127.0.0.1:4737/agents/<node> per remote node

See docs/AGENTS.md for per-harness wiring (Codex CLI, Copilot CLI, Gemini CLI, A2A frameworks, cron). By default, mesh only moves messages; the explicitly started worker pool is the exception and invokes its configured local CLI. Interactive nodes answer with whatever brain, tools, and permissions their own harness has.

Security model (read this)

Messages are end-to-end encrypted and authenticated. The mesh key is generated by mesh init, lives only in .meshwire.json on your machines, and travels only inside join codes you share yourself. On the wire, the relay (and anyone who discovers a topic) sees ciphertext, topic id, size, and timing — nothing else. Sender and recipient names ride inside the ciphertext.

Construction (stdlib-only, standard primitives): HKDF-SHA256 key derivation → HMAC-SHA256 PRF in counter mode for encryption, encrypt-then-MAC with an independent HMAC-SHA256 key, random 128-bit nonce per message, and constant-time tag comparison. The mw2 authentication tag also binds the mesh id, exact relay topic, and send timestamp. Envelopes older than seven days (or implausibly far in the future) are rejected, and authenticated ciphertext fingerprints are persisted per node so duplicate relay deliveries and replays are suppressed across restarts. mw1 remains readable during rolling upgrades but new sends always use mw2. Unauthenticated, stale, misrouted, replayed, or tampered messages are dropped, not displayed.

What you still must do:

  • Guard the join code and.meshwire.json — they contain the key. Both are auto-gitignored; config writes use mode 0600; share join codes over a private channel. The key is never placed in an environment variable: A2ACAST_CONFIG contains only a path. MCP tools, agent cards, and the local A2A HTTP bridge never return config or key fields.
  • A join code grants full membership in one trust domain : its holder can decrypt traffic, publish mesh-authenticated traffic, and assert the same sender names as any other member at the shared-key layer. Node signatures add a separate per-name verdict against the receiver's local pin, but first contact is unverified and verdict enforcement is a default-off rollout control. Delivery alone therefore does not prove a node identity.
  • Treat inbound tasks as untrusted input. Encryption authenticates the mesh , not intent: any agent (or person) holding the key can send tasks. Receiving agents should apply their normal permission rules.
  • A node signature identifies a node/harness identity, not a conversational session. Agent integrations scope their private key per harness. Concurrent sessions of the same harness using the same mesh configuration therefore use the same per-harness key path and normally share one node name. --as or A2ACAST_NODE can override the name, but not separate the shared key path. A receiver cannot tell which session produced a valid frame. Even a FRAME_VERIFIED result proves possession of that node key; it does not prove which local agent or user authorized the message.
  • Autonomous Codex execution is opt-in and gated on a curated allowlist. mesh codex-supervise runs delegated tasks through codex exec and replies over the mesh. It stays off unless you start it (codex-setup --supervise), and even then only peers you add with mesh codex-allow run — a default-empty allowlist (exec_allow), not the roster of everyone who holds the key. mesh codex-allow <node> grants sender-name-wide legacy authority : every accepted inbound task whose persisted sender-name string matches that entry is eligible. It is not a grant to one chat or agent session, and under default-off or unsettled node verification it is not a per-node cryptographic grant. A settled pin plus verdict enforcement can refuse a mismatched key, but first contact remains unverified and accepted. The --sandbox read-only default is defense-in-depth, not the boundary: a read-only task can still read repo secrets and return them in its reply, so only allow peers you actually trust.
  • Hard separation between co-located agents requires separate OS accounts or sandboxes , with distinct mesh identities and private keys protected from one another. Separate processes, temporary key directories, and Git worktrees do not contain a malicious process running as the same OS user. They may improve routing or collision isolation, but must not be treated as an authorization boundary.
  • Someone who learns a topic id (but not the key) can't read or forge messages, but can post garbage that your watcher silently drops. Self-hosting ntfy with auth (mesh init --server https://ntfy.example) closes even that.
  • Upgrade all machines together when moving to a new a2acast version — it's one file. (v0.4 meshes interoperate; v0.4 clients just render the new join/ping control messages as odd one-off messages.)

Incident response: revoke and rotate the mesh key

If a join code, config, or member machine is compromised, rotate both the key and topic capability from one trusted node:

That command switches new commands on the current node to a fresh key and fresh topic id, then prints a private mesh rotate-key mesh1-... command. Run that exact command on every remaining trusted node over a private channel. Do not send it through the compromised mesh. Restart long-running mesh watch, MCP/harness, and supervisor processes on each node so they drop their in-memory copy of the old config. Nodes that have not applied the new code remain on the revoked mesh and cannot read or publish on the new topics. Compare the key: sha256:... line from mesh status out of band to confirm every trusted node completed the cutover.

mesh init <name> [--as NODE] [--server URL]   create a mesh; in a terminal, prints the invite block and keeps listening
mesh join <code> [--as NODE]   join from a code, announce, and (in a terminal) keep listening
mesh invite                    print the join code + paste-able bootstrap block
mesh rotate-key [mesh1-code]   rotate the key/topics, or apply a peer rotation
mesh iam <node>                set/change this machine's identity
mesh send <node|all> <msg...> [--intent request|inform|ack] [--reply-to ID]
                               message a node (or broadcast) with reply intent
mesh presence listening|working|blocked   set and broadcast agent status
mesh watch --follow            stream messages forever (preferred; background task)
mesh watch [--timeout N]       one-shot: block until one message, print, exit
mesh ping <node> [--timeout N] liveness + round-trip time (answered by watchers)
mesh ask <node> <text...> [--wait SECS]   delegate an A2A task
mesh run ensemble [--timeout N] -- "<prompt>"   fan out and collate replies
mesh run cross-review [--timeout N] -- <diff-or-ref>   two independent reviews
mesh reply <task-id> <text...> [--state completed|failed|...]   answer one
mesh tasks [get <id>]          task ledger
mesh tasks --wait <id> [--timeout N]   wait for a terminal task result
mesh card [node] [--name N --description D]   A2A agent card
mesh a2a-serve [--port 4737] [--wait 60]      localhost A2A HTTP bridge
mesh peek [node] [--since S]   show recent messages without consuming
mesh peek --wait [--from NODE] [--timeout N]   wait for the next arrival
mesh status                    mesh, identity, known peers + last seen
mesh fleet [--as NODE]         read-only fleet receiver-health table
mesh integrate [--format codex|copilot|claude|mcp|skill]   print setup for a harness/route
mesh mcp [--config PATH]       stdio MCP tool server for any MCP client (Claude Desktop, Cursor, …)
mesh claude-setup              register the Claude Code presence watcher (writes .mcp.json)
mesh codex-setup [--supervise] arm Codex presence; --supervise also launches the autonomous actor
mesh copilot-setup             register the Copilot CLI presence watcher
mesh codex-allow <node> [--revoke|--list]   trust (or untrust) a peer to run delegated tasks here
mesh codex-supervise [--once] [--sandbox S] [--interval N] [--stop]   the autonomous task actor
mesh pool-setup --workspace-root DIR --coordinator NODE [--model MODEL]   configure the worker pool
mesh pool-start|pool-status|pool-stop   manage or inspect worker supervisors
mesh delegate auto|codex|copilot|goose <task...> --repo ABS [--base COMMIT]
  [--kind implementation|analysis] [--class normal|security|integration]
  [--verify TEXT (repeatable)] [--wait 0..300] [--as NODE]   run an isolated task
mesh pool-clean [--integrated-into REF] [--task ID [--force]]   remove eligible worktrees

The blocking commands use shell-friendly exit codes: tasks --wait exits 0 for completed, 1 for any other terminal state, and 124 on timeout; peek --wait exits 0 on an arrival and 124 on timeout.

mesh run ensemble creates one correlated task per other node in the dynamic roster, waits for all terminal replies or the time window, then prints each answer plus a no-reply list. mesh run cross-review prefers two available peers (blocked nodes rank last), gives both the same independent review brief, and collates their findings. Recipes exit 0 when every task completes, 1 for dispatch or terminal failures, and 124 when any dispatched node does not reply before the timeout.

New messages carry a stable message id and default to inform when intent is missing (including messages from older clients). Use request when a reply is required, inform for FYI traffic, and ack to close a loop; --reply-to correlates a response to the displayed message id. Agent integrations follow a fixed rule: always answer request, answer inform only when it adds value, never answer ack, and do not send filler greetings or thanks.

Presence controls carry listening, working, or blocked through normal announce/ping/pong/ack traffic and explicit mesh presence beacons. Session and stop hooks maintain working/listening; approval integrations can run mesh presence blocked while waiting for permission. mesh status and mesh_list_agents show the latest reported state, and mesh ask warns before sending to a blocked node.

Operational checks: green signals are property-specific

A healthy-looking signal proves only the property it measured. Do not promote one positive observation into a broader liveness or migration claim:

Signal What it proves What it does not prove
Fresh receiver heartbeat / healthy That receive process handled a frame recently That the node can publish a reply
unknown health after five quiet minutes No fresh self-report is available That the node is dead; a quiet healthy node ages to unknown
mesh status shows the new relay A new command read the updated config file That an already-running watcher or MCP server reloaded it
A send was accepted by the relay The relay accepted the publication That the intended listener is subscribed there

Use mesh ping <node> when you need end-to-end evidence: a matching pong proves the target heard the ping and successfully published a reply through the route the caller is currently using. After any .meshwire.json server or key change, restart long-lived receivers (mesh watch, mcp-serve / MCP hosts, codex-supervise, and pool workers); transient lifecycle-hook receivers reload config on their next invocation.

For a relay migration, verify the old connection is actually gone rather than trusting mesh status. On POSIX, capture or resolve the old relay addresses and check the process table; the following should print no matching socket after all long-lived receivers have been cycled:

old_host=old-relay.example
old_ips="$(dig +short A "$old_host"; dig +short AAAA "$old_host")"
test -n "$old_ips" || { echo "could not resolve $old_host" >&2; exit 1; }
old_sockets=0
for old_ip in $old_ips; do
  if lsof -nP -iTCP | grep -F -- "$old_ip:"; then old_sockets=1; fi
done
test "$old_sockets" -eq 0

Use Resolve-DnsName plus Get-NetTCPConnection for the equivalent Windows check. During the cutover, keep private old- and new-relay config files and publish coordination messages once through each (A2ACAST_CONFIG=/path/to/...) until every node has been cycled and passes a ping on the new relay. Config files contain the mesh key; protect both copies and remove the old one after the migration is verified.

To set a stable node name use mesh iam <name> (writes a per-harness pin). Prefer this over the A2ACAST_NODE env var, which is not reliably inherited by harness-spawned processes.

To keep the mesh key outside a project, point A2ACAST_CONFIG at an existing config file. The explicit path takes precedence over ancestor discovery and is inherited by lifecycle hooks:

export A2ACAST_CONFIG=/absolute/path/to/mesh-node/.meshwire.json
# PowerShell: $env:A2ACAST_CONFIG = 'C:\path\to\mesh-node\.meshwire.json'

mesh claude-setup and mesh copilot-setup still write their MCP workspace files in the current project while pinning that isolated config path.

To select a specific OpenSSH binary, set A2ACAST_SSH_KEYGEN to its path. This is useful on Windows builds where the in-box OpenSSH 9.5p2 ssh-keygen -Y sign can take about one minute and race a2acast's signing timeout. For example:

$env:A2ACAST_SSH_KEYGEN = 'C:\Program Files\Git\usr\bin\ssh-keygen.exe'

Set the same variable in an MCP server's env block for long-running integrations. The override drives both signing and verification; an invalid value fails loudly and never falls back to the slower PATH binary.

Onboarding & MCP clients. mesh integrate prints the right setup for whatever you run — a harness plugin (--format codex/copilot), a CLAUDE.md snippet (--format claude), a paste-in skill (--format skill), or the MCP config (--format mcp). For GUI/desktop agents, mesh mcp runs a stdio MCP tool server (add it with mesh integrate --format mcp) exposing mesh_send / mesh_pending / mesh_ask / mesh_reply / mesh_delegate / mesh_list_agents, so Claude Desktop, Cursor, or any MCP host can talk to the mesh — no plugin needed. (This is the pull-mode tool server; the Copilot plugin's mcp-serve is the push-mode watcher that wakes an idle session.)

| a2acast | shared MCP queue | SSH + headless agent | plain git polling
---|---|---|---|---
Infrastructure | none | server to run | SSH + reachable host | none
Wake latency | ~1 s | poll interval | seconds | poll interval
Payload channel | message or your repo | the queue | the SSH pipe | your repo
Audit trail | git history | custom | none | git history
N nodes | yes | yes | pairwise | yes

MIT