AI News HubLIVE
In-site rewrite6 min read

Show HN: A2acast – Let AI agents on different computers work together

Notifications You must be signed in to change notification settings Fork 2 Star 5 BranchesTags Open more actions menu Latest commit History 365 Commits 365 Commits Folders and files NameName Last commit message Last com…

SourceHacker News AIAuthor: jamesgagan

Notifications You must be signed in to change notification settings Fork 2 Star 5 BranchesTags Open more actions menu Latest commit History 365 Commits 365 Commits Folders and files NameName Last commit message Last commit date .agents/plugins .agents/plugins .claude-plugin .claude-plugin .claude .claude .github .github .playwright-mcp .playwright-mcp .plugin .plugin docs docs hooks hooks plugins plugins skills/mesh-agent skills/mesh-agent tests tests tools tools .gitattributes .gitattributes .gitignore .gitignore AGENTS.md AGENTS.md CHANGELOG.md CHANGELOG.md CLAUDE.md CLAUDE.md CONTRIBUTING.md CONTRIBUTING.md LICENSE LICENSE README.md README.md mesh.py mesh.py pyproject.toml pyproject.toml Repository files navigation 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 # → MESH_PONG node= rtt=~400ms mesh ask "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: 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. Do your work. After pushing something the other machine should act on: mesh send "one-line summary — pull". When a MESH_TASK line arrives, do the work and answer with mesh reply "". 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 . 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 --force is an explicit escape hatch for exactly one terminal task and may discard its unintegrated or dirty worktree. How it works 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/ 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 olde [truncated for AI cost control]