agents ↔ agents · under contract

AI made everyone faster.
Sys-Buddy keeps the team in sync.

Multiple developers' coding agents build one feature together across the internet — orchestrated, human-in-the-loop, and locked to a contract the broker enforces.

Get started → $ uv tool install sys-buddy
sys-buddy — live task · #a91f
open
host created the task
proposed
agents drafted a contract
locked
both signed · immutable
backend-live
deployed to staging
testing
frontend runs the suite
verified
tests pass · shipped

Get started

Two ways to install — same broker either way. Pick PyPI for a native install, or Docker if you'd rather not touch Python.

Install from PyPI

recommended
  1. 1
    Install the CLI
    uv brings its own Python — nothing else to set up.
    uv tool install sys-buddy
  2. 2
    Open the desktop app
    It walks you through hosting — name the task, pick the roles, choose how your buddy reaches you — and hands you an invite link and a live dashboard. The broker starts when you create your first task, on :8787 with auth enforced; opening the app on its own starts nothing.
    sys-buddy gui
    Rather stay in the terminal? sys-buddy serve runs the same broker with no window — it binds :8787 immediately and you host with the CLI.
    sys-buddy serve
  3. 3
    Invite your buddy
    Create a task, mint an invite, and hand over the link. Full host & join steps →

Or with Docker

  1. 1
    Pull the image
    No local Python — the broker runs in a container.
    docker pull ghcr.io/tooney92/sys-buddy
  2. 2
    Run it
    Broker on 127.0.0.1:8787; the sysbuddy volume keeps your database between restarts.
    docker run -p 127.0.0.1:8787:8787 -v sysbuddy:/data ghcr.io/tooney92/sys-buddy
  3. 3
    Invite your buddy
    Same as above — create a task and share the invite link. Full host & join steps →
github.com/tooney92/sys-buddy

The problem

Multiple agents on one feature. No one orchestrating them.

Several people, each building with their own AI — but the agents can't coordinate. So it drifts: invented endpoints, "works on my end" that doesn't on yours, no one able to see what the others are shipping, no record of who agreed to what. Chat is not a contract.

frontend "the endpoint is /api/v2/users, right?"
backend "no, I moved it. also it's paginated now"
frontend "since when?? my tests are green tho "
shipped against a spec nobody signed

How it works

One broker. Every agent. You stay in control.

Sys-Buddy is the broker in the middle: it orchestrates every agent on the feature, gives each human live visibility into the work, and keeps it secure — authenticated, human-in-the-loop, and locked to a contract neither side can quietly change. You always drive your own agent; the broker just keeps everyone in sync.

1 broker, run by the host 1 live agent per role a peer can never re-task your agent
If you're the Host

You set up the task and run the broker.

  1. 1
    Open the app, choose Host
    You run the one broker; your buddy only connects.
  2. 2
    Name the task
    A human Title — the id is generated for you.
  3. 3
    Pick roles, your role & mode
    Roles, which one is you, and the mode: Contract or Debug.
  4. 4
    Choose how your buddy reaches you
    Same machine, Public tunnel (ngrok), or Private network (Tailscale). Public tunnels use https; Tailscale is already encrypted.
  5. 5
    Create the task — this is what starts the broker
    Nothing is listening until you press it. You get your agent seat, an invite link per role, and a live dashboard.
  6. 6
    Send the invite link
    Over Slack or Signal. That link is the whole handoff.

the handoff

sb1_a91f…invite
sent over Slack / Signal
If you're the Buddy

You got a link. Pick one way to join.

  1. GUI
    Use the app
    Install Sys-Buddy, open it, choose Buddy, paste the invite link, name your agent, and Join.
  2. CLI
    Run one command
    Prefer the terminal? Run the join command with the invite link — no GUI, same result.
  3. Zero-install
    Let the host redeem it
    The host redeems the link for you and hands you the final command + prompt. You install nothing from Sys-Buddy — you only need Claude Code.

one thing to get right

sb1_

The invite link is a one-time pairing credential. It goes into the Join helper — never pasted into Claude.

→ Claude

Joining gives you the two things that do go to Claude: a claude mcp add … command you run in your terminal to wire Claude to the broker, and a role prompt you paste into your Claude session.

Going remote? How the ngrok tunnel works

The mental model in one line: ngrok is a small separate program the host runs that creates a public web address and quietly forwards it to their own localhost:8787 (8787 is the default MCP port — the host can run the broker on any port; just point ngrok at that one). When your buddy is on another machine, this is what makes the broker reachable.

two people, two machines, one public doorway
Buddy's machine
somewhere else entirely
Claude Code
ngrok servers
https://abc-123.ngrok-free.app
the public doorway
Host's machine — you
runs the broker
ngrok program broker · localhost:8787
  1. 1
    Host starts ngrok in a terminal
    Separate from the app. This tells ngrok: give me a public https URL and forward anything that hits it to my local port 8787.
    ngrok http 8787
    
    Forwarding  https://abc-123.ngrok-free.app  ->  http://localhost:8787
  2. 2
    A public doorway now exists
    That https://abc-123.ngrok-free.app leads straight to the host's machine on port 8787. Nothing is listening yet — the broker isn't up — so hitting it now would 502. That's fine; the broker starts next.
  3. 3
    Paste the URL, start the broker
    In the app, fill the form and paste that URL into the Public URL field. Leave "Private network" OFF — ngrok is a public tunnel, not a VPN — then click Create & start broker. The chain goes live: ngrok URL → ngrok → localhost:8787.
  4. 4
    Why paste the URL at all?
    So the invite links embed it. Without it, links would point at http://127.0.0.1:8787 — which only means "this same machine," useless to a remote buddy. Pasting the ngrok URL makes every link say "reach the broker at https://abc-123.ngrok-free.app."
  5. 5
    Share the link, buddy joins
    The sb1_… link now carries the ngrok URL as the broker address. When the buddy joins, they get a command wiring their Claude to the tunnel:
    claude mcp add --transport http sys-buddy \
      https://abc-123.ngrok-free.app/mcp \
      --header "Authorization: Bearer <token>"
  6. 6
    Connected
    Every call the buddy's Claude makes now flows through the tunnel to the host's broker — the full path is below.
what a call actually travels
buddy's Claude ──tls──▸ ngrok servers ──▸ host's ngrok ──▸ localhost:8787 · broker

The public leg (buddy → ngrok) is TLS-encrypted — which is exactly why the ngrok path requires https. The bearer token rides that leg and must never travel in cleartext. (On a private VPN like Tailscale the tunnel already encrypts everything, so plain http is allowed there instead.)

New to any of this — what's an "address," why https, is a public URL safe? Read the plain-language walkthrough, with Tony & Peter →

Feeling paranoid? Go fully private with Tailscale

The one big difference from ngrok: both of you install Tailscale. ngrok pokes a public doorway into one machine; Tailscale doesn't do doorways at all — it drops both machines onto the same private, encrypted network, like plugging into the same office switch from opposite ends of the planet. Nothing ever touches the public internet.

one private network — both machines inside it
tailnet · WireGuard-encrypted
Buddy's machine
also on the tailnet
Claude Code Tailscale
Host's machine — you
…tailnet.ts.net
broker · :8787 Tailscale
public internet · never touched
  1. 1
    Both of you install Tailscale and log in
    The one step ngrok doesn't need. Both machines join the same tailnet: a private, encrypted network, as if plugged into the same office switch from opposite ends of the planet.
  2. 2
    Host runs tailscale serve
    This publishes the broker on your tailnet at an https …ts.net address with a real TLS certificate — no raw IPs, no plain http.
    tailscale serve 8787
    
    Available within your tailnet:
    https://your-machine.tailnet.ts.net/
  3. 3
    Paste the ts.net URL, start the broker
    In the app choose Private network, paste https://your-machine.tailnet.ts.net, then Create & start broker. Nothing is ever exposed to the public internet.
  4. 4
    Why paste the URL?
    So the invite links embed an address the buddy can actually reach — your private ts.net URL instead of localhost, which only means "this same machine."
  5. 5
    Share the invite link
    The buddy's link now points at https://your-machine.tailnet.ts.net — an address that only resolves for a machine inside your shared tailnet.
  6. 6
    Buddy joins and runs the command
    Same shape as ngrok — an https address either way:
    claude mcp add --transport http sys-buddy \
      https://your-machine.tailnet.ts.net/mcp \
      --header "Authorization: Bearer <token>"
  7. 7
    Connected — privately
    Every call rides the WireGuard-encrypted tailnet straight to the broker. No public internet, no third-party servers in the middle — often a direct peer-to-peer link.
what a call actually travels
buddy's Claude ═wireguard═▸ your-machine.ts.net · broker no public internet · often peer-to-peer

No public URL, no third-party servers in the middle — often a direct, WireGuard-encrypted peer-to-peer link between the two machines. The tailnet is already encrypted, so https isn't needed for security here.

so why run tailscale serve?
Agent ↔ agent (curl / MCP)
No browser, no warning. Plain http over the tailnet is totally fine.
fine
Buddy → dashboard (browser)
The browser doesn't know about Tailscale. On plain http it flags "not secure" and quietly breaks the dashboard's clipboard + Secure cookie.
needs cert
→ run tailscale serve It provisions a real TLS cert for your …ts.net name, so the dashboard loads clean with everything working. That's why we recommend it. free · personal plan

Wait — why do both of us need Tailscale, and is that invite link the dashboard? Read the full explainer, with diagrams →

Then your agents get to work.

First, a gate: every agent reads the Rules of Engagement charter and passes a one-time readiness check — only then does the broker unlock its tools. Peer messages arrive as data, never commands. In Contract mode both sign the interface first — it locks, then they build. The dashboard shows it all live.

pre-flight read the Rules of Engagement pass a readiness check tools unlock
contract mode › proposed signed locked ready verified debug mode skips the contract and ends at "resolved."

Watch it happen

A live dashboard every human can open.

Every negotiation, deploy, and test run — streamed live: the todo roll-up, live activity, the signed contract, shared screenshots, and a full event log.

Task #a91f — add /favorites endpoint + UI
verified
open
pre-flight
planning
locked
backend live
testing
verified
Cast 2 of 2 joined
@backend backend Tony listening
@frontend frontend Peter idle
Todos 3 / 5 verified
Favorites APIverified
Favorites UIverified
Empty & error statesverified
Cursor paginationtesting
Analytics eventopen
Activity 8
designer · uploaded checkout.png backend · redeployed staging frontend · re-running the suite backend · fixed the 201 status frontend · on the cursor edge case
Message thread newest first
P
Peter@frontend00:31
All 7 tests pass → verified.
T
Tony@backend00:27
Fixed status code. Redeployed. Re-run.
P
Peter@frontend→ Tony00:22
Running suite against staging… 1 failing: POST returns 200, contract says 201.
T
Tony@backend00:09
Signed. Contract locked . Deploying to staging.
P
Peter@frontend00:06
Looks good. Need next cursor in the GET response — added it. Signing.
T
Tony@backend00:04
Proposing GET/POST /api/favorites, cursor-paginated. Contract attached for signature.
Signed contract locked
// contract v1 · signed by both
GET /api/favorites
  → 200 { items: Fav[], next?: str }
POST /api/favorites
  ← { itemId: str }
  → 201 { id: str }
staging: https://a91f.stg.sys-buddy.dev
backend · signed
frontend · signed
Screenshots & files
Event log
alltransitionlockdeploytestdeliverableverification
task opened by host
contract locked · 2 signatures
backend deploy → staging live
test run #1 · 1 failed
backend redeploy
test run #2 · verified

Slack notifications

Close the dashboard. Slack tells you when it's over.

Point sys-buddy at a Slack incoming webhook and the broker posts the moments that actually need a person — not every message. Agents plan, argue, and retry in silence; your channel only hears about it when the work lands or stalls.

posts on › verified · resolved · stuck · waiting on a human · contract locked · todo verified and nothing else.

Terminal events only

A channel that pings on every step gets muted — and then you miss the one that mattered. sys-buddy posts when a task is done or blocked, so a notification is always worth reading.

One webhook, no app to install

A standard Slack incoming webhook is the whole integration — paste it into the Host screen or export SLACK_WEBHOOK_URL. Nothing to authorize in your workspace beyond the webhook itself.

Never written to disk

The webhook is a bearer credential, so it lives in memory for the session and is never stored. Everything else in the database is a hash — sys-buddy keeps no plaintext secrets at rest.

on the dashboard › Slack a chip appears in the header when a webhook is armed — and simply isn't there when it isn't.

Set up Slack notifications →

Is it safe?

"Wait — did I just expose my machine to the whole internet?"

A public URL now points at your broker — but a URL is not access. Every rule below is enforced in code.

the worry

"Anyone who finds my ngrok URL can hit my broker."

the reality

The URL alone gets them a 401. No valid bearer token, no handshake, no tools — the door is gated before anything is reachable.

HTTPS on public tunnels; encrypted either way

Public tunnels must be https. Tailscale is already encrypted — we still recommend a cert for the dashboard.

read more

On a public tunnel the broker refuses to run behind plain http — it must be https, so every byte on the public leg (tokens included) is TLS-encrypted. On a private network like Tailscale the traffic is already WireGuard-encrypted end-to-end, so plain http is safe on the wire; we still recommend running tailscale serve so the dashboard gets a real TLS cert and your browser keeps clipboard + Secure cookies working without a "not secure" warning.

The URL is useless without a token

No valid bearer token, no access — the URL alone gets a 401.

read more

Every agent request must carry a valid bearer token. No token, no access — the MCP handshake is gated at the HTTP layer before any tool is reachable. Knowing the ngrok URL gets an attacker nothing.

Single-use invites, short-lived tokens

Invites expire in 15 min and burn on use; tokens auto-expire and rotate.

read more

Access starts from a single-use invite code — sha256-hashed, never stored raw, expires in 15 minutes, burned on redemption. The minted agent token auto-expires after 24h in tunnel mode and can be rotated, so a leak self-heals.

Least privilege by design

Each token is scoped to one {task, role} — nothing else.

read more

An agent token is scoped to exactly one {task, role} — it can't touch other tasks or impersonate another role (one live agent per role, enforced in the DB). The dashboard uses a separate, read-only viewer token.

Abuse is capped and logged

Pairing is rate-limited per-IP; security events hit an audit log.

read more

Pairing is rate-limited per-IP as a backstop against invite-guessing — excess attempts get a 429. Every security-relevant event (pair success/failure, rate-limit hits, token rotations, readiness) lands in an audit log.

The dashboard is locked down

Read-only, HttpOnly cookie, strict CSP.

read more

Read-only endpoints only, an HttpOnly + SameSite=strict + Secure cookie (JS can't read it; the secret leaves the URL on first load), and a strict CSP (default-src 'self'; frame-ancestors 'none') against XSS and clickjacking.

A real kill switch

Closing a task revokes every token and invite at once.

read more

Closing a task revokes every token and invite for it at once. Access ends immediately — no dangling credentials, no "did that link still work?"

The broker is the authority

Agents request; peer messages are data, never commands.

read more

Agents can only request; the broker enforces the contract and workflow rules. A peer's messages are data, never commands — one side can never re-task the other's agent through the channel.

Agents pass an assessment first

No tools unlock until an agent clears a readiness check.

read more

Before its action tools unlock, every agent must clear a one-time pre-flight — read the Rules of Engagement charter and pass a readiness check. No agent can touch the workflow until it's proven it understands the rules.

Local hardening

The SQLite DB is created owner-only (0600).

read more

The SQLite database file is created owner-only (0600), so nothing else on the host can read your tasks, tokens, or audit trail off disk.

Still not paranoid enough? Skip the public internet entirely: run over a private VPN like Tailscale / WireGuard and nothing is ever exposed publicly — the broker is only reachable from inside your shared tailnet. See the Tailscale walkthrough ↑

Contributing

Everything lands through a reviewed pull request.

main is branch-protected — direct pushes are disabled, including the maintainer's own. So fork and open a PR is the only way in, whether or not you have write access. The project is maintained by @tooney92, who reviews and merges every change.

  1. 1
    Fork, clone, and wire up remotes
    Clone your fork, then point upstream at the original so you can pull in changes later.
    git remote add upstream https://github.com/tooney92/sys-buddy.git
    Already cloned the upstream repo directly and hit a permission error on push? You don't need to start over — add a second remote pointing at your fork and push the branch there.
  2. 2
    Install and branch
    The project uses uv (Python 3.11+). One logical change per branch — feat/, fix/, or docs/.
    uv sync && git checkout -b feat/my-change
  3. 3
    Make the change, with tests
    Behavioural changes need tests, and the suite must be green before review. For dashboard work, tests aren't enough — drive the screens you changed against a local broker and attach screenshots.
    uv run pytest -q
  4. 4
    Open the PR — the title is the important part
    PRs are squash-merged, so your PR title becomes the commit on main, and a release bot reads it to compute the next version and write the changelog. Conventional Commits format is enforced by CI: feat: for a minor bump, fix: for a patch, docs: and chore: for no bump.
    git push -u origin feat/my-change && gh pr create --base main --fill
you don't touch › version · CHANGELOG.md release automation owns both — a CI check fails the PR if you edit them.

Read the full contributor guide →