docs blog library
github

Setting up peers

Connect two Jazz installations so their agents can ask each other for help. Read Agent-to-agent for disclosure tiers and request logging.

Use peers when each installation needs to keep its own credentials and permissions:

  • a personal planning agent can ask a company-hosted engineering agent for repository status without receiving company credentials;
  • a laptop agent can ask a home-server agent to inspect local services without exposing that machine’s filesystem tools to the laptop;
  • a low-cost local agent can escalate one difficult question to a separately operated specialist;
  • two collaborators can let their agents coordinate while each side controls what its own agent may disclose and execute.

The receiving Jazz installation chooses the model, persona, tools, and disclosure ceiling. The caller sends a request; it never lends its permissions or dictates how the other machine executes it.

Choose the setup that matches your network:

Where the daemon bindsEncryptionNeeds a domain?
One machineloopbacknone neededno
A tailnetthe tailnet interfaceTailscale’s WireGuardno
The internetloopback, behind a proxyTLS at the proxyyes

The answering side creates an invite link; the caller accepts it to configure the connection and shared credential. With a daemon already serving peers at a reachable address, the minimal command is:

jazz peers invite create alice

It detects your public IPv4 address, uses port 4747, grants internal disclosure, and expires after three hours. --host, --port, --disclosure, and --expires override those defaults. --public-url takes precedence over host and port for a proxy or tunnel.

Public-IP discovery makes a credential-free HTTPS request to ipify, with a five-second timeout and a bounded response. No invite secret or peer token is sent. If discovery fails, Jazz asks for an explicit address rather than printing a loopback link. Explicit --host or --public-url skips discovery entirely. For a same-machine demo, pass --host 127.0.0.1; for a LAN or tailnet, pass the reachable address on that network. Discovery does not change the daemon’s bind address or open firewall/router ports. Each setup below also includes manual configuration.

Where the token lives: an OS keyring when one’s reachable (Keychain on macOS, secret-tool/libsecret on Linux), otherwise a chmod 600 file at $JAZZ_HOME/secrets.json. Not in config.json. The file fallback needs no D-Bus session or keyring unlock, so it works the same on a workstation and a headless server. jazz peers invite accept stores the shared token automatically. jazz peers set-token still exists for manual setups. $JAZZ_DISABLE_KEYRING turns off both if you’d rather manage tokens yourself.


One machine, two agents

Use two separate JAZZ_HOME directories to test both sides on one machine.

export ALICE=/tmp/jazz-alice
export BOB=/tmp/jazz-bob
JAZZ_HOME=$ALICE jazz agent create   # name it "alice"
JAZZ_HOME=$BOB   jazz agent create   # name it "bob"

1. Bob starts serving

JAZZ_HOME=$BOB jazz daemon --serve-peers bob --port 4748

The daemon runs in the background and listens only on loopback. That is fine for a same-machine demo, but it is not a good default for a real invite you want to share with another machine. If you want a real remote peer, pass --public-url or bind the daemon to a reachable host instead of relying on loopback. Stop it with JAZZ_HOME=$BOB jazz daemon stop --port 4748.

2. Bob invites Alice

JAZZ_HOME=$BOB jazz peers invite create alice --host 127.0.0.1 --port 4748 --as bob

disclosure is the tier. See the tier table. Start at internal, not private: you want to see a refusal happen before you see an answer. This prints a link; send it to Alice out of band (a chat message, not a commit).

3. Alice accepts

JAZZ_HOME=$ALICE jazz peers invite accept <the-link-bob-sent>

Alice sees who invited her, at what endpoint, and what tier, confirms once, and both sides are paired: her config now has Bob as a peer she can ask, and his has her as a peer who may learn internal, with a token stored in each machine’s keyring that neither of you had to generate. Agents without a saved tools list inherit Peers automatically. Acceptance also adds ask_peer to existing agents with an explicit tools list. Jazz prints only agents whose updates were saved. If agent storage cannot be read or written, it exits with status 1 and explains that the peer and token were saved but tool setup is incomplete. Fix storage and enable Peers with jazz agent edit <name>; the one-time invite is already consumed, so do not accept it again. With --json, backfilled lists successful updates and backfillErrors lists failures; ok is false when tool setup is incomplete.

Prefer to do it by hand?

Skip steps 2–3 above and do this instead, before step 1 (starting the daemon):

Edit $BOB/config.json and add Alice as a peer:

{
  "peers": [{ "name": "alice", "url": "http://127.0.0.1:4748/peer/ask", "disclosure": "internal" }],
}

Generate a shared token and put it on both sides: both store it under the other’s name, since Bob’s copy answers “is this really Alice?” and Alice’s copy answers “here’s what I present as Alice”:

export TOKEN=$(openssl rand -hex 24)

JAZZ_HOME=$BOB   JAZZ_PEER_TOKEN=$TOKEN jazz peers set-token alice
JAZZ_HOME=$ALICE JAZZ_PEER_TOKEN=$TOKEN jazz peers set-token bob

Then add the same peer to $ALICE/config.json, this time from her side (url pointing at Bob’s daemon, no disclosure needed: tiers only matter to whoever is answering):

{ "peers": [{ "name": "bob", "url": "http://127.0.0.1:4748/peer/ask" }] }

4. Give Alice the tool

JAZZ_HOME=$ALICE jazz agent edit alice

For a manual setup, select Peers in the toolset. Invite acceptance already enables ask_peer on existing agents. The tool appears only after a peer with a URL is configured.

5. Ask

JAZZ_HOME=$ALICE jazz run --agent alice "ask bob's agent what time it is on his machine"

Approve Alice’s outgoing ask_peer call when prompted. internal admits Bob’s get_time tool, so the request should return his answer, attributed and quoted. Now try something the tier doesn’t cover:

JAZZ_HOME=$ALICE jazz run --agent alice "ask bob's agent to read his ~/.bashrc and summarize it"

Bob’s agent should say it cannot, not because it decided to refuse, but because read_file was never in its toolset for this run. There is no prompt to argue with.

6. Read the ledger, both sides

JAZZ_HOME=$BOB   jazz peers log   # what Bob was asked, and what he said
JAZZ_HOME=$ALICE jazz peers log   # what Alice asked, and what came back

Refused requests are logged with the reply.


Over a tailnet

The setup for two machines that are both already on the same Tailscale network: a friend’s laptop, a home server, a personal fleet. No public exposure, no domain, no certificate: the tailnet is already a private, encrypted network, so the daemon just binds to it directly instead of to loopback.

Alice and Bob each need Jazz installed and Tailscale connected to the same tailnet.

1. Bob finds his tailnet address

tailscale ip -4
# 100.101.102.103

MagicDNS gives the same machine a name too (bob-machine.tailnet-name.ts.net), if you’d rather not hardcode an IP that Tailscale could reassign.

2. Bob installs the daemon as a persistent service on the tailnet interface

sudo jazz daemon install --serve-peers bob --host 100.101.102.103 --yes

Bind the specific tailnet address, not 0.0.0.0. If this machine also has a public interface (a cloud VM with a tailnet sidecar, say), 0.0.0.0 would listen on that too: binding the 100.x address keeps the daemon reachable only from the tailnet, which is the whole reason to use one.

daemon install provisions an operator token in /etc/jazz/daemon.env, installs the system service, and starts it. If startup fails, it prints a command for reading the service logs.

Check on it anytime with jazz daemon status (or systemctl status jazz-daemon, or launchctl list | grep jazz on macOS), read its output with jazz daemon logs -f, and remove it again with sudo jazz daemon uninstall.

To test in the foreground first, run jazz daemon --foreground --serve-peers bob --host 100.101.102.103. Stop it with Ctrl+C.

3. Bob invites Alice

jazz peers invite create alice --host 100.101.102.103 --disclosure internal --expires 1h

The link uses http://; Tailscale encrypts traffic between the two machines. As with loopback, the invite should point at a reachable host, not 127.0.0.1.

4. Alice accepts

jazz peers invite accept <the-link-bob-sent>
Prefer to do it by hand?

In Alice’s ~/.jazz/config.json:

{ "peers": [{ "name": "bob", "url": "http://100.101.102.103:4747/peer/ask" }] }

In Bob’s ~/.jazz/config.json:

{
  "peers": [
    {
      "name": "alice",
      "url": "http://<alice's-tailnet-ip>:4747/peer/ask",
      "disclosure": "internal",
    },
  ],
}

Then the shared token, one command per side (both stored under the other’s name):

JAZZ_PEER_TOKEN=<shared-secret> jazz peers set-token alice   # on Bob's machine
JAZZ_PEER_TOKEN=<shared-secret> jazz peers set-token bob     # on Alice's machine

5. Ask, from Alice’s machine

jazz agent edit alice   # enable Peers if you used the manual setup
jazz run --agent alice "ask bob's agent what time it is on his machine"

Same verification as the one-machine walkthrough: jazz peers log on both sides afterward.


Over the internet

For a peer that isn’t on a private network with you at all. This needs a domain and TLS, but the daemon itself never has to leave loopback: a reverse proxy on Bob’s box terminates TLS and forwards only the paths that peers actually need, so the daemon’s operator routes (/runs, /health) remain private. Jazz provisions the daemon’s operator token even on loopback; peer requests use their separate peer credential.

This assumes Bob has a server with a public domain (bob-agent.example.com below) and a reverse proxy already fronting it. Caddy is used here because it gets you automatic TLS from a three-line config; nginx or anything else works the same way.

1. Bob starts the daemon, loopback only

jazz daemon --serve-peers bob

The daemon starts in the background on loopback. For restarts after a crash or reboot, install it with sudo jazz daemon install --serve-peers bob --yes, or use --foreground under your existing process supervisor.

2. Bob’s proxy forwards two paths

bob-agent.example.com {
    reverse_proxy /peer/ask 127.0.0.1:4747
    reverse_proxy /peer-invites/* 127.0.0.1:4747
}

Anything else gets Caddy’s default 404. /runs and /health are never proxied, so they simply don’t exist from the internet’s point of view, whatever the daemon itself is willing to answer on loopback. /peer-invites/* only ever needs to be reachable long enough for one redemption; nothing stops you from removing that line again afterward.

3. Bob invites Alice

jazz peers invite create alice --public-url https://bob-agent.example.com --disclosure internal --expires 1h

--public-url overrides what would otherwise be http://127.0.0.1:4747: the daemon’s real bind address, which Alice cannot reach: with the domain the proxy actually fronts. Without it, the printed link would point nowhere useful to her.

4. Alice accepts

jazz peers invite accept <the-link-bob-sent>
Prefer to do it by hand?
{ "peers": [{ "name": "bob", "url": "https://bob-agent.example.com/peer/ask" }] }
{
  "peers": [
    {
      "name": "alice",
      "url": "https://alice-agent.example.com/peer/ask",
      "disclosure": "internal",
    },
  ],
}
JAZZ_PEER_TOKEN=<shared-secret> jazz peers set-token alice   # on Bob's machine
JAZZ_PEER_TOKEN=<shared-secret> jazz peers set-token bob     # on Alice's machine

Send that secret out of band: a chat message, not a commit, not a URL. It’s a bearer credential for someone else’s agent to use on yours.

5. Ask, from Alice’s machine

jazz agent edit alice   # enable Peers if you used the manual setup
jazz run --agent alice "ask bob's agent what time it is on his machine"
jazz peers log

If it doesn’t answer

  • POST /peer/ask returns 404: the daemon wasn’t started with --serve-peers, the proxy isn’t forwarding that path (internet setup), or you hit the wrong port/host.
  • 401: the token presented doesn’t match what’s stored for that peer’s name on the answering side. If you set it up by hand, re-run peers set-token on both ends with the exact same value; if you used an invite, the link may have been redeemed already: create a new one. The command uses the other machine’s name locally: Alice runs set-token bob, while Bob runs set-token alice, both with the same JAZZ_PEER_TOKEN value.
  • 403, "not accepting questions": the peer exists in config but has no disclosure, which defaults to none. Add a tier.
  • 403, "could not answer": authentication succeeded, but the answering run failed. Ask the other operator to check jazz peers log and the daemon logs for the actual cause. Re-pairing or replacing the token will not fix a failed run.
  • 413: the question (or the A2A request carrying it) is over 64 KB.
  • 429: that peer already has maxConcurrentRuns questions in flight (4 unless its config says otherwise). Retry after the Retry-After seconds.
  • 500, "could not answer": the answering run failed. The peer is never told why; the reason is in the answering daemon’s log.
  • A new token is still refused: the daemon trusts a token it read from the keyring for 30 seconds. Wait that long after peers set-token, or restart the daemon.
  • ask_peer doesn’t show up in the toolset: configure a peer with a URL on the asking side, then check the agent’s tools and deniedTools (and any persona restrictions). The remote peer’s disclosure controls what they may learn from you; it does not prevent you from asking them. For manual setup, enable Peers with jazz agent edit <name>. If invite acceptance reported backfill errors, fix agent storage and enable Peers manually.
  • The daemon refuses to start: read the message; it’s almost always --host set to something other than loopback with no token available. That check exists because a daemon on a reachable interface is an agent with filesystem access that anyone reaching the port can drive. Should be rare (see above): it means $JAZZ_DISABLE_KEYRING is set or $JAZZ_HOME isn’t writable. Fix: set $JAZZ_DAEMON_TOKEN yourself and persist it the way you’d persist any other server secret.
  • The invite link doesn’t work: check it hasn’t expired or already been redeemed (jazz peers invite list on the inviter’s machine), and that the inviter’s daemon is actually running at the address embedded in the link.

Next steps

  • Agent-to-agent: the tier model, the ledger, and what this does not protect you from
  • jazz daemon: the HTTP server peers runs on top of
  • Tools: what public/internal/private mean, and why tiers are built on that axis instead of risk

machine-readable: /docs/guides/connect-peers.md · /llms.txt · /llms-full.txt