Inspiration

Two of us were building against the same deadline, each with a coding agent open. And we spent the whole time being a message bus: read what my agent said, paste it into Slack, wait for you to paste it into yours, paste the reply back. Two systems that could coordinate in milliseconds, throttled to human typing speed.

The obvious fix is to just connect them — it's a socket. But the moment we sketched it, the real problem showed up. If your agent can act in my workspace, I've handed someone else's AI my keyboard, and I'm the one who has to answer for whatever it does. Every multi-agent framework we looked at assumed a single owner orchestrating agents inside one trust domain. None of them had an answer for two people who don't fully trust each other.

That gap is Inzo.

What it does

Inzo pairs two coding agents — Claude Code, Cursor, Codex, anything that speaks MCP — so they can coordinate directly while both humans stay in control and can prove it.

  1. Pair. One agent mints a six-character code; the teammate's agent joins with it. Nothing is open by default.
  2. Negotiate. The agents work out a shared goal and task split between themselves. Both humans watch live in their own terminal via inzo watch, pushed over SSE rather than polled.
  3. Approve. Nothing locks in until both humans approve the same version of the plan. Re-proposing resets every approval, and approving a stale version is rejected — consent is always attached to text a human actually read.
  4. Work, sandboxed. Shared commands run only in a local Docker sandbox: --network none, dropped capabilities, non-root, resource- and time-limited, scoped to one directory you named. There is no host-execution path.
  5. Track the runway. Token burn, cost, and elapsed time become a projection — what's left, how fast it's going, and whether it lasts until your deadline.

The part we care most about: your approval is an Ed25519 signature over a hash of the exact plan text you were shown, made with a key our relay has never held. A relay that lies about consent produces a record that fails verification, and any third party can check that offline against our published keys — no callback to us, no pre-existing agreement between the two organizations.

How we built it

A TypeScript monorepo, six packages, protocol-first. docs/PROTOCOL.md is the authoritative contract — when an implementation disagreed with it, we changed the doc first and then the code.

  • relay — Express + SQLite. Pairing, message relay, plan proposals and approvals, budgets and runway, scope, revocation, and the live SSE stream.
  • relay-cf — a Cloudflare Workers + Durable Objects port, which is what the default hosted relay runs on.
  • mcp-server — what each person adds to their agent. Exposes create_pairing_code, join_pairing, send_message, propose_plan, approve_plan, withdraw_consent, set_budget, get_runway, limit_my_agent, revoke_pairing, run_shared_command.
  • cli — the human surface: watch, approve, withdraw, revoke, audit, status, budget.
  • sandbox — the Docker isolation every shared command runs inside.
  • holder — holder keys, request proofs, and consent signatures, shared by the MCP server and CLI so there's one implementation of the signing rules rather than two that drift. It has no network access by design, so nothing in it can accidentally transmit the private key it exists to protect.

Credentials are compact Ed25519 JWS carrying principal, capabilities, and full delegation chain, verifiable against a cacheable /.well-known/inzo-jwks. Revocation is a second cacheable feed, so a fully offline verifier is stale for at most its cache TTL — bounded by the one-hour ceiling on credential lifetime.

Challenges we ran into

We shipped an impersonation vulnerability and had to tear out our own identity model. In v1, the relay learned who was calling from a field in the request body. It worked, it was simple, and it meant anyone holding a pairing code could impersonate either agent, read the private thread, and forge both approvals to lock in a hostile plan. The feature we were proudest of — human approval — was the thing most completely broken.

The rebuild wasn't a patch. Identity is now derived server-side from a signed credential, always, and a body carrying agentId, fromAgentId, proposedBy, or principalId fails with a 400 rather than being silently ignored. Ignoring it was the tempting, polite choice; we decided a caller who believes those fields are authoritative has a bug worth surfacing loudly, because that exact belief was the original vector.

Consent turned out to be a different problem than delegation. The existing cross-org work we found — draft-reece-wimse-cross-org-delegation — handles authority descending from a single principal. We needed two principals who don't trust each other to jointly authorize one artifact, which nothing covered. We had to design the consent statement ourselves, domain-separated so a signature harvested from elsewhere in the protocol can never be replayed as an approval, and versioned so consent can't survive onto text the human never read.

We almost built a subtle hole into inzo approve. The natural implementation asks the relay for the plan hash and signs it. That would let a hostile relay collect a human's signature over text they never saw. The CLI now re-fetches and re-hashes the plan locally before signing.

Security kept fighting usability. Our first credential and pairing-code TTLs were tight enough to be defensible on paper and useless in practice — agents work asynchronously, and people walk away from their terminals. We had to add silent refresh and retune the lifetimes without giving up the bound on revocation staleness.

One machine, on purpose. In-process SSE fan-out plus SQLite means a second instance would show half the events to half the viewers. We pinned it to one machine in fly.toml and documented why rather than pretending it scaled.

Accomplishments that we're proud of

The relay cannot lie about authority or consent, and you don't have to take our word for it. POST /consent/verify takes no authentication and re-derives whether consent is satisfied from the signatures alone. Anyone can audit our records without trusting us.

Capabilities that actually constrain. A credential can attenuate to a subset of its capabilities, and narrowing is one-way and re-checked by every verifier. Strip plan:approve and your agent cannot approve on your behalf however it is prompted — a structural answer to prompt injection rather than a filter. Without the subset check, scope would have been decoration.

A migration path with a deliberate hole in it. v2 bearer tokens still work, but a bearer caller is marked assurance: "bearer" in the audit log and is structurally barred from giving consent — because a bearer token cannot produce a signature you can't repudiate. Keeping compatibility while withholding exactly one capability, for a stated cryptographic reason, is the design decision we'd defend hardest.

A real kill switch. Either human can revoke either credential instantly, without the other side's cooperation. It kills the whole credential subtree, fails every route including reads, closes open streams server-side, and withdraws any consent given with that credential — because an approval is only worth what the credential behind it is worth.

196 tests, including the SSE stream over real sockets, the CLI driven against a real relay, and the entire v3 trust surface exercised over HTTP.

What we learned

Identity must be derived, never asserted. The single line "read the caller's ID from the body" was the whole vulnerability. Anything self-reported by a caller is a claim, not a fact.

Fail loudly rather than ignoring silently. Quietly dropping an unauthorized field hides the bug from the person who most needs to know about it.

Capability and consent are different questions, and you need both. Capability says the peer may run commands. Consent says they may run this work. Checking only one gets you either a system that can't move or a system you can't trust.

Tamper-evidence is cheap if you design for it early and expensive to retrofit. Hash-chaining the audit log from the start cost us very little; it was motivated by EU AI Act Articles 12 and 14, and it turned out to be the feature people ask about first.

Stating your limits precisely makes the rest more credible. v3 removes the relay's ability to lie about authority or consent. It does not remove the operator's ability to drop messages or stall. Saying that plainly got us taken more seriously than claiming more would have.

What's next for Inzo

Past one machine. Postgres plus an out-of-process event bus, which is what the current SQLite and in-process SSE design deliberately defers. That's also the boundary of the open-core model: self-hosting stays free under Apache-2.0, and a managed multi-tenant relay is the commercial path.

More than two. Consent is already modeled as a required set of principals with signatures collected against it, so extending from pairs to N-party groups is a protocol change rather than a rewrite.

Interoperability rather than competition. A2A handles agent discovery and task delegation across vendors but has no notion of two humans co-signing an artifact. We'd rather slot into that gap than fight its momentum.

Design partners in the cases that already have the trust problem — agency to client, contractor to enterprise, vendor to customer. Those relationships already carry audit and legal requirements, which is exactly what the tamper-evident log is for.

Built With

Share this project:

Updates

Submission history