Skip to main content
Glama
README.md
# zahadun

**A self-hosted mesh of full-peer AI agents — Claude Code and OpenCode talking
across your machines over A2A, with real mTLS identity and no API keys.**

Every machine is an equal peer: it can *ask* (an MCP bridge inside Claude
Code / OpenCode) and *answer* (an A2A server executing tasks with the local
`claude` CLI, OpenCode, or plain scripts). There is no controller node, no
cloud relay, and no per-token billing — the Claude track runs on your existing
subscription via `claude -p`.

```
you, on machine A                                machine B
┌───────────────────────┐                        ┌───────────────────────┐
│ Claude Code / OpenCode│   A2A JSON-RPC         │ reverse proxy :8443   │
│   │ MCP bridge        │ ──────────────────────▶│   mTLS, CN → identity │
│   ▼                   │   over WireGuard mesh  │   ▼                   │
│ "@bob audit the SEO   │   (NetBird/Tailscale)  │ A2A server :9990      │
│  of example.com"      │                        │   ├─ script           │
│                       │◀── task id, then ──────│   ├─ claude -p        │
│ zahadun_task(id)      │    artifacts           │   └─ opencode         │
└───────────────────────┘                        └───────────────────────┘
```

## Why this exists

Multi-machine agent collaboration is a
[frequently requested](https://github.com/anthropics/claude-code/issues/28300)
capability. The pieces all exist — A2A↔MCP bridges, WireGuard meshes,
workload identity (SPIFFE), agent memory servers — but as separate,
mostly cloud- or Kubernetes-shaped projects. zahadun is the whole thing in
~1,500 lines of Python with **two dependencies** (`httpx`, `mcp`), sized for
one operator and a handful of machines.

### What's different here

- **Subscription, not API keys.** The Claude executor is the CLI in `-p`
  mode. If you pay for Claude Code, your mesh costs nothing extra.
- **One conversation, two machines.** The A2A `contextId` *is* the Claude
  session UUID on every machine in a task's path. `claude --resume <id>` on
  either end shows that machine's half of the same conversation.
- **Deterministic `@peer` addressing.** A `UserPromptSubmit` hook parses
  mentions *before the model sees the prompt*. Routing is code, not an LLM
  decision: unknown peer → hard block with the roster; unreachable peer →
  the prompt never reaches the local model (you can't mistake a local answer
  for the peer's).
- **Conflict-free mesh memory, no database.** Shared memory is a git-synced
  directory of markdown files named `<peer>-<timestamp>.md`. A peer only ever
  creates its own files, so replication can't conflict — `git pull --rebase`
  never meets a merge.
- **Real identity, home-lab sized.** mTLS everywhere; each machine's cert is
  signed by your own root CA (an [OpenBao](https://openbao.org) PKI mount
  works well — see [docs/CA-RUNBOOK.md](docs/CA-RUNBOOK.md)). CSR authenticity
  is attested with an SSH signature (`ssh-keygen -Y`) from the operator's
  personal key. Client-only machines get `clientAuth`-only certs: even a
  stolen key can't impersonate a server.
- **Honest cards.** A peer's AgentCard lists only skills that work today.
  A planned skill is a promise the caller can't distinguish from a working
  one — they just get `FAILED`.

## Architecture in five decisions

1. **The router is a human.** Pure A2A: addressing means choosing a peer.
   No broadcasts, no capability matching. You say `@bob`, code delivers to bob.
2. **Tracks are pure end-to-end.** A task submitted from Claude Code executes
   in Claude on the target; OpenCode-to-OpenCode likewise. Mixing tracks would
   orphan the session history that makes `--resume` work.
3. **The bridge has no model.** The MCP server is an HTTP client plus a file
   layer. All intelligence lives in the tool that loaded it or on the target
   peer. It detects its own track from the MCP handshake's `clientInfo`.
4. **The risky part is code; the convenient part is the model.** Delivery and
   addressing: hook, deterministic. Result pickup, catalogs, memory: MCP
   tools, model-driven.
5. **Peer input is untrusted.** The Claude executor runs headless with **no
   tools** by default (permission prompts auto-deny). You grant tools per
   skill, explicitly, in `skills.json`. Loops are cut by an `X-Zahadun-Trace`
   header; caller identity comes from the client cert's CN via the proxy —
   never from the request body.

## Quick start

See [INSTALL.md](INSTALL.md). The short version, per machine:

```bash
# module
python3 -m venv /opt/zahadun-a2a/venv
/opt/zahadun-a2a/venv/bin/pip install zahadun-a2a   # or from a checkout

# client side (every machine): MCP bridge + @peer hook
claude mcp add zahadun --scope user -- /opt/zahadun-a2a/venv/bin/python -m zahadun_a2a.mcp
# + hook in ~/.claude/settings.json, + block in opencode.json  → INSTALL.md

# server side (machines that answer): systemd unit + reverse proxy with mTLS
# examples/ has units and nginx/Apache/Caddy configs
```

Everything runs as a regular user. No dedicated system account, no root
services — the Claude executor needs the user's `~/.claude` anyway.

## MCP tools exposed to your agent

| tool | purpose |
|---|---|
| `zahadun_peers()` | who is in the mesh, what they can do (live AgentCards) |
| `zahadun_ask(peer, task, …)` | delegate; returns a task id |
| `zahadun_task(id)` | poll result / status |
| `zahadun_models()` | live model catalog of the local OpenCode engine |
| `zahadun_memory_search/read/write/topics` | shared mesh memory |

## Status

Working: the full client+server loop, three executors (script /
`claude -p --session-id` / OpenCode `prompt_async` with a model-fallback
ladder), task persistence across restarts, audit log, loop detection.

Not yet: SSE streaming (cards honestly say `streaming: false`),
`input-required` pauses (skills marked as needing human confirmation are
refused, not hung), per-caller rate limits.

## Security model

Read [SECURITY.md](SECURITY.md) before exposing anything. Summary: designed
for a **single operator's** machines on a private WireGuard mesh; peers are
semi-trusted (authenticated, but their task content is not); it is *not* a
multi-tenant system and was never designed as one.

## A note on language

The project was built for a Polish-speaking mesh: code comments, error
messages and some config keys (`drabina` = model ladder, `wykonawca` =
executor, `potwierdzenie_czlowieka` = human confirmation) are Polish. The
docs you're reading, the wire protocol (A2A v1.0) and the MCP tool names are
English. Translating internals is on the table if anyone actually needs it —
open an issue.

## License

Apache-2.0.

TDQS

B3.4/5.0

Scored across 8 tools

Disambiguation5/5

Each tool targets a distinct function: peer discovery, task submission, task status retrieval, model catalog, memory search, read, write, and topic listing. There is no overlap between task and memory operations, and request/response pair (ask/task) is clearly separated.

Naming Consistency4/5

All tools share the 'zahadun_' prefix, and the memory tools follow a 'memory_<action>' pattern. However, some names are bare nouns (peers, task, models) while others are verbs (ask) or compound nouns (memory_topics), creating minor inconsistency in naming style.

Tool Count5/5

With 8 tools, the set is well-scoped for the server's purpose of managing an agent mesh network, task delegation, and shared memory. Each tool serves a clear role without redundancy or bloat.

Completeness4/5

Core workflows are covered: task lifecycle (submit, retrieve status/result), peer discovery, and memory operations (write, read, search, list). Minor gaps exist such as no task cancellation or listing of all tasks, but these are not essential for the primary use case.

Maintenance

ActivityMaintained
ResponsivenessSyncing