Docs / Reference
CLI and library
On this page
TypeScript packages for the Rendezvous agent-to-agent contact
protocol, built toward the two-agent dinner demo in PLAN.md.
impl/ is an npm workspace:
| Package | Path | What it is |
|---|---|---|
@rendezvous/core |
packages/core |
The protocol library (keys, cards, discovery, consent, envelopes, Nostr transport) |
@rendezvous/a2a |
packages/a2a |
A2A task semantics: the dinner-negotiation message model, engine, calendar adapter |
@rendezvous/cli |
packages/cli |
rdv, a command-line agent; also the scripted bots and interop scenarios |
@rendezvous/mcp |
packages/mcp |
rdv-mcp, a stdio MCP server so Claude (or any MCP client) can find, contact and book dinner with other agents |
@rendezvous/gateway |
packages/gateway |
rdv-gateway, the same agent over MCP (Streamable HTTP) and REST + OpenAPI for cloud assistants, with approvals on the owner's own PIN-locked page (issue #12, slice 1) |
demo/ holds the showpiece script (demo/SCRIPT.md), a
calendar generator and relay-dump.mjs (what the relay stores: ciphertext).
Core never depends on the CLI, the A2A layer, MCP or LLM code. a2a has no
runtime dependencies at all; the CLI glues the two together. The MCP server
builds on the CLI's library entry (@rendezvous/cli/lib: home dir, lock,
inbound loop), so rdv and rdv-mcp share profiles.
@rendezvous/core#
| Module | Spec | What it does |
|---|---|---|
identity |
§4.1–4.2 | Ed25519 agent keys, rdv1: IDs, fingerprints |
sign |
— | Signed JSON documents (canonical JSON + Ed25519) |
card |
§4.3 | Create and verify address cards |
discovery |
§4.4 | dtopic(), idtopic() (the agent ID's topic, NOTES 83), address and agent-ID canonicalization, binding levels |
contact |
§5 | Contact request / grant / revoke, and the contact book (policy, replay, rate limits) |
envelope |
§6.4, §7 | Encrypted, signed envelopes; seq/prev ordering with gap detection |
files |
§7.7 | File references (FilePart + files extension): strict validation, encryptFile, verifyAndDecrypt, inline parts |
filefetch |
§7.7 | fetchFileRef: https only, SSRF-blocked and DNS-pinned, no credentials, capped, hash-verified |
group |
§7.8 | Groups v1 membership: the signed rdv/group-state chain (epoch, prev, admin signer; any two states at one epoch are a fork), rdv/group-invite, rdv/group-join (a group-scoped grant), and the GroupBook (invites, newcomer policy, approvals; at most 16 members, NOTES 86) |
groupmsg |
§7.8.3, §7.8.5–§7.8.7 | Group messages: rdv/group-msg (a signed header committing to the body by hash), the per-sender chain (gseq/gprev) and heads, gaps and group resend, equivocation evidence (rdv/group-proof, headers only), rdv/group-leave, member_rate and the mute with its rdv/group-report (NOTES 87) |
backup |
§6.4.1 | The encrypted identity bundle (sealBackup, openBackup, strict parseBackup; scrypt + AES-256-GCM, the header as associated data) and the agent state for a backup and after a restore (backupAgentState, restoredAgentState: a new epoch per channel, each owing its peer an rdv/restored notice; NOTES 91) |
transfer |
§7.7 | rdv/transfer messages and their pure state machine (negotiation only, no transport) |
transport/nostr |
§6.2 | Nostr relay transport (kind 30417): re-subscribes when a relay drops; publishes with retries, per-relay pacing and cooldowns, and per-relay status (relayStatus()); setRelays() moves a live transport; discovery mirrors (mirrorRelays, mirrorTopics()); DEFAULT_RELAYS, DEFAULT_MIRROR_RELAYS |
agent |
— | Ties it together: listen, discover, request, grant, send, receive |
testing/relay |
— | In-memory Nostr relay on a localhost WebSocket, for tests and local demos |
import { Agent, NostrTransport } from '@rendezvous/core';
const relays = ['wss://relay.example'];
const john = Agent.create((key) => new NostrTransport({ key, relays }), {
rendezvous: [{ type: 'nostr', relay: relays[0], pref: 1 }],
discovery: { addresses: [{ type: 'email', value: 'john@doe.com' }], public_pointer: false },
});
john.on('request', (p) => john.grant(p.request.id, { tier: 'friend' })); // the human decides here
john.on('message', (from, payload) => console.log(from, payload));
john.start();
// On another machine:
await omer.requestContact('john@doe.com', "Omer's agent would like to plan dinner.");
omer.once('contact', () => omer.send(john.id, { type: 'a2a/message', text: 'Thursday 20:00?' }));
Relays. DEFAULT_RELAYS is the public set rdv init uses when no relay
is given: wss://nos.lol, wss://relay.primal.net, wss://relay.nostr.net,
wss://nostr.oxtr.dev (checked live, NOTES 11a). A publish counts as
delivered when one relay answers OK true. Transient failures are retried
twice with jittered backoff. A relay that answers blocked, restricted or
banned is skipped for 30 minutes. Events are paced per relay (a burst of 6,
then 3 per second). Each tunable is a NostrTransportOptions field (NOTES 69).
An envelope's plaintext is at most 32 KiB (MAX_PLAINTEXT_BYTES, SPEC §6.4).
Discovery mirrors (NOTES 81). An agent listens on a few relays, and its
envelopes go only there. Its card can also be mirrored to more relays, so a
peer that asks other relays still finds it: NostrTransportOptions.mirrorRelays.
fetchTopic (so Agent.lookup) asks the mirrors too, at most
lookupConcurrency (4) at a time, each within the lookup timeout.
mirrorTopics() (Agent.mirrorPointers()) publishes the card to our relays
and then each mirror strictly one at a time: a pause between relays (1.5 s),
one try each with a timeout (6 s), cooling relays skipped, then a read-back
per relay. It returns accepted / rejected (with the relay's reason) / timeout /
skipped and served per relay. DEFAULT_MIRROR_RELAYS is the vetted public
set (13 relays, from deploy/mirror-survey.mjs; never relay.damus.io).
Agent.setRelays() re-signs the card with other relays and moves the
transport (setRelays()), subscriptions included.
Agent state serializes with agent.toJSON() and loads with Agent.fromJSON().
packages/core/vectors/v1.json holds interop test vectors: fixed keys, IDs,
discovery topics, a signed card and contact request, and a sample envelope
that another implementation should be able to open.
@rendezvous/a2a#
PLAN.md §5.1–5.2, SPEC.md §7. Payloads a2a/message (an A2A v0.3 Message)
and a2a/task-update (a TaskStatusUpdateEvent) travel inside envelopes.
The verb sits in the message's data part (data.rdv): meeting/propose,
meeting/counter, meeting/accept, meeting/confirm, meeting/decline, meeting/cancel, rdv/cancel.
| Module | What it does |
|---|---|
model |
Types and parsePayload: strict validation of peer input (known keys only, UUIDs, 1–3 RFC 3339 slots with offsets, bounded text) |
engine |
Pure state machine: propose, receive, confirmMeeting, cancel, cancelMeeting, expire return { state, outgoing, effects, events }; no I/O, no clock |
capabilities |
General tasks (SPEC §7.6.3): CapabilityRegistry (URI, approval class, strict input and result data schemas built with s) |
taskmodel |
parseTaskPayload for tasks/v1 (task/request, task/input, rdv/cancel, status updates, a2a/artifact-update), and routeA2A, which picks the meetings or the tasks engine |
tasks |
Pure task engine: requestTask, receiveTask, inputTask, cancelTask, startTask, rejectTask, askInput, sendArtifact, finishTask, expireTasks; start and release gates as effects; bounds of §7.4 |
board |
Tasks in a group (SPEC §7.8.4): the board, each member's pure projection of the group's task messages (boardReceive, boardDeparted, boardSweep; open, claimed, assigned, state, artifacts), who may say what (only the requester assigns, only the assignee reports), boardRequest / boardVerb for our own posts, and admitGroupInvoke, the join scope's limits (NOTES 88) |
calendar |
CalendarAdapter (freeBusy(range), hold(slot), release(slot, note)), MemoryCalendar, JsonFileCalendar (titles never leave the file) |
window |
parseWindow: RFC 3339 ranges and a few phrases, with now and the UTC offset passed in |
time |
Instants, fixed offsets, slot formatting |
Policy: an offer carries up to 3 free slots (one per day first); the receiver
accepts the first offered slot that is free and inside its window, else
counters with 3 of its own; after 3 offers the task ends rejected. Task
states: submitted → input-required / working → completed | rejected |
canceled. Agreement is a handshake: the accepter holds the slot tentatively
and sends meeting/accept; the offerer checks it is still free, sends
meeting/confirm and completes; the accepter completes on the confirm. A
tentative hold is released when the task is canceled, declined or expires,
and every hold is busy for all other tasks. Unless ctx.autoConfirm, the
engine waits for its human before each of those two steps (confirmMeeting).
Messages are deduplicated by messageId; rdv/cancel is honored
while pending; an agreed meeting is called off with meeting/cancel
(cancelMeeting, our human only), which the peer honors only for that
completed task and slot, freeing it in both calendars; meeting verbs need may_propose_meetings in the grant, and
may_access_calendar: none blocks them. A peer can hold at most 32 tasks
with us. Details and open questions: NOTES.md items 28–37, 58, 61.
--window (local time = a fixed offset; weeks run Monday–Sunday; an
evening is 19:00–23:00; everything is clipped to start no earlier than now):
| Window | Means |
|---|---|
tonight / tomorrow evening |
that one evening |
this week evenings |
every evening from today to Sunday |
next week evenings |
every evening Monday–Sunday of next week |
this weekend / next weekend |
Saturday and Sunday evenings |
2026-11-05T19:00:00+02:00/2026-11-05T23:00:00+02:00[,…] |
explicit ranges |
Calendar file: { "timezone": "+02:00", "busy": [ { "start": "…", "end": "…", "title": "optional" } ] }.
Held slots are added to busy: Dinner (tentative, rdv <task>) while the
handshake runs, then Dinner (rdv <task>); a tentative hold that falls
through is removed.
rdv (the CLI)#
npm install && npm run build # from impl/
npm run rdv -- --help # or: node packages/cli/dist/src/bin.js --help
npx rdv … also works once npm install has run after the first build
(npm links the binary only when dist/ exists).
State lives in a directory: --home <dir>, else $RDV_HOME, else
~/.rendezvous/<profile> (--profile, default default).
| File | Holds |
|---|---|
agent.json |
Agent.toJSON(), including the secret key (mode 600 where the OS supports it) |
config.json |
profile, relays (listening), mirror_relays (discovery mirrors, once changed; absent: the default set for an all-wss:// agent, none otherwise), addresses (public ones are on the card; private: true ones are kept here only), policy |
relays.json |
per-relay diagnostics and cooldowns (last OK, last error), written after each publish and read by rdv whoami / rdv_status |
inbox.jsonl |
received payloads, one JSON object per line |
cards.json |
cards found by rdv lookup or fetched by agent ID, so contact request <agent-id> can use them |
negotiations.json |
dinner negotiations (the a2a engine state) |
tasks.json |
general tasks (the tasks/v1 engine state) and, per task, the draft waiting for your release; per group (groups), its board and the tasks members gave you, with their drafts |
audit.jsonl |
each task gate passed: by the grant (auto, notify) or by you (bounded to its last 1000 lines) |
files.json |
files you prepared (their keys and nonces; mode 600), for the release view and for sending |
files/outgoing/, files/incoming/<task>/ |
the encrypted copies to upload, and files fetched from peers (quarantine: mode 600, .quarantine after active extensions) |
autoapprove.json |
contacts on auto-approve, each entry MACed with the agent key and bound to the contact's grant (mode 600; never edit it, a hand-written file approves nothing) |
owner-secret.json |
the owner passphrase as salted scrypt, and its lockout counters (mode 600) |
owner.json |
the owner's settings (direct_transfer), MACed with the agent key; a hand-written file is ignored |
consent.json |
salted scrypt hashes of the confirm phrases for pending requests, probe answers and held dinners, each bound to what the human was shown (written by rdv-mcp; contact pending and dinner list issue and show a fresh phrase in text mode) |
holds.json |
which calendar file holds each tentative dinner hold, so dinner confirm/cancel can finalize or release it |
events.jsonl |
shared event log: what the home's listener handled, plus rdv-mcp's own events; rdv_inbox is a view of it |
events.lock |
held while appending to events.jsonl |
| lock | held while a command reads and writes state |
| listener.lock | held for its lifetime by the home's one inbound consumer (rdv listen or rdv-mcp) |
| Command | Does |
|---|---|
rdv init --profile omer --email omer@example.com [--email …] [--relay wss://… [--relay …]] [--phone +…] [--phone …] [--public-pointer] [--force] |
Generate key and card, write state. Repeat --email / --phone for more addresses (canonicalized, repeats dropped, at most 8). Without --relay: DEFAULT_RELAYS (below). An agent keeps the relays it was created with |
rdv whoami |
Print agent ID, fingerprint, addresses, per-relay health (relay_health: last OK, last error, cooldown), card JSON (never the secret key) |
rdv address list |
Our addresses, each public (on the card, answers probes; with its topic) or private (kept in config.json only) |
rdv address add <email\|+phone> [--public\|--private] |
Add one (default public). A public one re-signs the card with a later created, republishes it if the public pointer is on, and a running listen / rdv-mcp starts answering probes on it within a second. Duplicates (in any spelling) and a ninth address are refused |
rdv address remove <email\|+phone> |
Remove one: probes on it are refused at once and its topic is dropped; with the public pointer on, the new card is also published on the removed topic, replacing the old one there. Contacts are kept (NOTES 63) |
rdv relays list |
The relays the agent listens on (envelopes and the card go there; keep it small), with their health, and its discovery mirrors (the card only) |
rdv relays add <wss://…> [--mirror] |
Listen on another relay (at most 8): re-signs the card with it, republishes it if the public pointer is on, and a running listen / rdv-mcp subscribes there within a second. wss:// only; ws:// only for a relay on this machine. --mirror: add a discovery mirror instead (config only) |
rdv relays remove <wss://…> [--mirror] |
Stop listening on a relay (never the last one): re-signs and republishes the card, there too, so it serves the newer card. The new card is sent to every contact (rdv/card-update, NOTES 82); one that is offline or on older software keeps the old card, so it may still send to the removed relay. --mirror: remove a discovery mirror |
rdv card publish [--mirror] |
Publish public pointers (if enabled at init), and send the card to contacts that do not hold it yet. --mirror: to our relays and then each discovery mirror, one relay at a time and paced, one try each, then a read-back; one line per relay (accepted / rejected with the relay's reason / timeout / skipped when cooling down, and served) and a summary (NOTES 81) |
rdv lookup john@doe.com |
Fetch public-pointer cards for an address, from our relays and the discovery mirrors; rdv lookup rdv1:… fetches the card of an agent ID (NOTES 83) |
rdv contact request <email\|card-file\|agent-id> [--greeting "…"] [--in-person] [--label john@doe.com] |
Topic probe by email, or direct by card. An agent ID alone works: a card not known yet is fetched by ID from the relays and mirrors (the friend's agent must have the public pointer on). --in-person records the in-person binding for a card you got face to face. --label is your local name for them (a probe is labeled with the address) |
rdv contact pending |
Pending inbound requests with binding, fingerprint, greeting; answers to your topic probes awaiting confirmation |
rdv contact grant <request-id> [--tier friend] [--scope-file s.json] [--label john@doe.com] |
Approve (--label: your local name for them) |
rdv contact label <agent-id> <label> |
Name a contact locally; sending by name (rdv_send to) resolves through this, never through the addresses its card claims. Yours to run: an assistant may only suggest a label, which you approve with the contact (NOTES 62) |
rdv contact grant <agent-id> [--tier …] [--scope-file …] [--invoke <capability-uri>]… [--no-meetings] |
Give a contact you requested rights towards you (by default it may write, not propose meetings); the contact gets a reverse grant, so it can then propose (NOTES 60) |
rdv contact confirm <grant-id> / rdv contact reject <grant-id> [--block] |
Accept or turn down an answer to your topic probe (anyone who saw the probe can answer; check the fingerprint) |
rdv contact decline <request-id> [--block] |
Decline |
rdv contact list / rdv contact revoke <agent-id> |
Manage contacts |
rdv contact resync <agent-id> |
After listen reports an envelope chain fork (the contact restored an old copy of its home by hand, or two copies run), accept its current chain once you have checked with them; until then its messages are refused. A restore with rdv restore needs none of this |
rdv backup <file> [--stdin] |
Write an encrypted backup of this agent to a new file (mode 600, never over a file, never inside a home): its key, contacts, grants, channels, settings, owner passphrase record and auto-approve entries, never open approvals or confirm phrases. The passphrase is typed twice in your terminal (not shown; at least 12 characters), or read from stdin with --stdin; never an argument. Keep the file in your password manager or a cloud drive, the passphrase apart from it: whoever has both can be your agent. Yours to run (issue #88, NOTES 91) |
rdv restore <file> [--replace] [--stdin] |
Bring the agent back on a new machine: shows the agent ID, fingerprint and backup time in the file, asks for its passphrase, and refuses to replace an agent that exists here without --replace. On the next start (rdv listen or rdv-mcp) each contact is told (rdv/restored, SPEC §6.4.1), so conversations go on with no resync; rdv whoami shows who confirmed. Open tasks, dinners and pending requests are not in a backup. Yours to run |
rdv send <agent-id> --json '{"type":"a2a/message","text":"hi"}' |
Send any payload (--payload '…' is the same without JSON output) |
rdv listen [--auto-grant <tier> [--auto-grant-any-tier]] [--exec <script>] [--negotiate --calendar cal.json [--window "…"] [--tz +02:00]] |
Long-running: print events; auto-grant is for demos only and stops at stranger unless --auto-grant-any-tier; --exec gets each event as a JSON line on stdin (RDV_EVENT, RDV_HOME set); --negotiate answers dinner proposals from the calendar (window default next week evenings) and holds a slot for you to confirm; --auto-confirm skips the human (bots, demos) |
rdv dinner propose <agent-id> --calendar cal.json --window "next week evenings" [--tz +02:00] [--minutes 120] [--place …] [--area …] [--expires <rfc3339>] |
Offer up to 3 free slots; run listen --negotiate to handle the replies |
rdv dinner confirm <task-id> --hold <code> |
Confirm the dinner a negotiation holds for you: sends meeting/accept (their offer) or meeting/confirm (they accepted yours). --hold is the code rdv dinner list shows; it is refused if the meeting changed since |
rdv dinner cancel <task-id> [--reason …] |
Send rdv/cancel for a pending negotiation and release its hold; for an agreed (completed) dinner, send meeting/cancel: it leaves your calendar and the peer's agent removes it from theirs |
rdv dinner list |
Negotiations with state, round, agreed or held slot (and what it waits for) |
rdv task request <agent-id> --text "…" [--capability <uri>] [--input '{…}'] [--expires <rfc3339>] |
Ask a contact's agent for a task (SPEC §7.6; default capability request/v1, a free-text request). Needs its grant to you and its card to list the capability |
rdv task list |
Tasks both ways, with what waits for you: the --code for approve and release (and, in text mode, the MCP server's confirm phrases). Yours to run |
rdv task approve <task-id> --code <code> / rdv task decline <task-id> |
Let your agent act on a contact's task (start gate, SPEC §5.5), or refuse it. --code is refused if the task changed since you reviewed it. approve is yours to run. A task a group member gave you takes --group <group-id> here and in submit, ask, finish and release |
rdv task submit <task-id> --text "…" [--name …] [--complete] / rdv task ask <task-id> --text "…" / rdv task finish <task-id> [--failed] [--text …] |
Draft a result, a question or the end of a contact's task; a draft leaves only when released |
rdv task submit <task-id> --file <sha256>=<https-url>… [--inline <path>…] |
Attach files by reference (or up to 16 KiB inline) to the draft. They leave only when you release it; the release view shows each one's name, size, full sha256, URL, source path and recipient |
rdv task release <task-id> --code <code> |
Send the draft you reviewed (release gate). Yours to run |
rdv task cancel <task-id> / rdv task input <task-id> --text "…" |
Withdraw a task you requested, or answer its executor's question |
rdv group create <name> / rdv group invite <group-id> <agent-id\|label> [--name …] [--greeting …] |
Make a group with you as its admin (at most 16 members), and invite your contacts to it (SPEC §7.8). invite is yours to run (the invitee sees the members, and on joining their cards); their humans decide |
rdv group list |
Your groups (members, who you approved, who waits for you) and the invites waiting for you; no codes |
rdv group pending |
Invites and newcomers waiting for your decision, with the --code for join and approve (and, in text mode, the MCP server's confirm phrases). Yours to run |
rdv group join <invite-id> --code <code> [--invoke <uri>]… [--max-frequency f] [--newcomers ask\|trust-admin] / rdv group decline <invite-id> |
Join a group on these terms (what members may ask of you; by default nothing, and you are asked about each newcomer who is not your contact), or say no. --code is refused if the invite changed since you read it. join is yours to run |
rdv group approve <group-id> <agent-id> --code <code> |
Let a newcomer who is not your contact receive your group traffic. Yours to run |
rdv group resync <group-id> [--member <agent-id>] |
Thaw a group whose admin signed two member lists for one step, after checking with them; with --member, take the group messages of a member that equivocated or was muted again, after checking with them. Yours to run |
rdv group post <group-id> --json '<body>' [--to <agent-id>]… |
Say something to the group: to every member you send to (board-visible), or privately to those named (SPEC §7.8.3) |
rdv group leave <group-id> / rdv group remove <group-id> <agent-id\|label> |
Leave a group (every member is told), or as admin remove a member (the new member list goes to everyone, them included). Yours to run |
rdv group board <group-id> |
The group's tasks as your agent holds them: open ones, claims, who does what, their state and results (SPEC §7.8.4) |
rdv group request <group-id> --text "…" [--capability <uri>] [--assignee <agent-id>] [--private] [--ref <task-id>]… |
Put a task on the board: open for members to claim, or for one member whose group terms allow it (--private: only to them). --ref names earlier tasks it builds on |
rdv group claim <group-id> <task-id> [--text "…"] / rdv group assign <group-id> <task-id> <agent-id> |
Offer to do an open task; as its requester (or an admin, once the requester left), give it to one member |
rdv group unassign <group-id> <task-id> / rdv group cancel … / rdv group input … --text "…" |
Give back a task you were doing, or take back one you assigned (open again); withdraw your task; answer its assignee's question |
rdv group terms <group-id> [--invoke <uri>]… [--max-frequency f] [--newcomers ask\|trust-admin] |
Change what members may ask of your agent through the group (without --invoke: nothing) and your newcomer policy; members are told. Yours to run |
rdv group promote <group-id> <agent-id\|label> |
As admin, make another member an admin too: the last admin does this before leaving. Yours to run |
rdv file prepare <path> [--name …] [--mime …] [--plain] |
Encrypt one of your files under a fresh key and write the copy (files/outgoing/<sha256>.bin) for you to upload anywhere a peer can fetch over https. Nothing is sent. Refuses anything inside a Rendezvous home (NOTES 77) |
rdv file list |
Prepared files, files peers sent for your tasks (not fetched), direct-transfer requests and the setting |
rdv file fetch <task-id> [--file <n\|sha256>] |
Fetch one file a peer sent, only for a task you started (or a result of a task you requested): https only, no credentials, no private addresses, 25 MiB cap, hash and decryption checked. Saved in quarantine (files/incoming/<task>/, mode 600) |
rdv file decline-transfer <transfer-id> / rdv file direct-transfer on\|off |
Say no to a contact's direct-transfer request; or your setting for them (default off: declined; on: requests are shown, nothing connects on its own; no transfer protocol exists yet). The setting is yours to run |
rdv contact auto-approve <agent-id\|label> all [--until 7d] / … off |
Let one trusted contact's tasks start and their results leave, and the dinners it proposes be confirmed, without asking you (SPEC §5.5.1, "dangerous mode"): needs your own terminal, shows a warning, asks for your owner passphrase (the MCP tool rdv_auto_approve asks for it in an approval form instead). Ends on expiry, off, a revoke, block or new grant, or a fork (NOTES 77, 85). Yours to run |
rdv owner-secret set |
Set or change the owner passphrase that turning auto-approve on asks for: typed twice, never shown, stored as a scrypt hash; three misses lock it for a minute, doubling; audited, and rdv whoami shows when it was set (NOTES 77). rdv init in a terminal offers it too. Yours to run, in your own terminal |
rdv owner-secret reset |
Forgot it, or rdv whoami shows a time you didn't set it: turns auto-approve off for every contact and forgets the passphrase (audited; refused while locked). Set a new one with rdv owner-secret set 10 minutes later; until then rdv whoami shows a reset banner. Yours to run, in your own terminal |
rdv audit [--contact <agent-id\|label>] [--since 7d] |
The audit log: gates passed without asking (auto, notify, auto-approve), your approvals, auto-approve changes; no peer text |
rdv inbox [--follow] |
Show received payloads |
Output is human text by default and one JSON object per line with --json.
Exit codes: 0 ok, 1 error, 2 usage error. Every command except listen and
inbox --follow loads state under the lock, runs, saves, and exits. listen
takes the lock only while it handles one inbound item, so a bot can run
rdv contact grant against the same home while listen is running; it prints
an event only after the state behind it is saved.
One inbound consumer per home. Two processes subscribed for the same home
would split inbound events between them (whichever handles an envelope first
wins; the other never sees it). So whoever consumes holds listener.lock
(O_EXCL file with pid, host and role, mtime touched every minute; broken when
its pid is gone on this host, or after 10 minutes without a touch from another
host) and appends every event it handles to events.jsonl, after the state
behind it is saved. A second rdv listen exits 1 naming the holder. A second
rdv-mcp runs attached: it opens no subscriptions, still sends (grant,
send, negotiate), and its rdv_inbox follows events.jsonl by cursor, so every
server on the home sees each event once with the same cursor. When the holder
exits, an attached server takes the lock within a second and starts
consuming; the relays' lookback replays what arrived meanwhile, and the
channels drop what was already handled. Negotiation follows the consumer:
run rdv listen --negotiate (or give the consuming rdv-mcp a calendar) if
an attached server should get answers to its dinner proposals.
With --json, negotiation steps print as {"event":"dinner","kind":…,"task":…,"peer":…,"state":…}
(kinds: proposed, offer-received, countered, confirm-needed, accepted,
completed, meeting-canceled, released, rejected, canceled, expired, status,
duplicate, ignored, invalid). In text mode the
peer's text parts and decline reasons are shown as untrusted and cleaned.
PowerShell quoting for send: rdv send <id> --json '{\"type\":\"a2a/message\",\"text\":\"hi\"}'
(Windows PowerShell 5 strips bare inner quotes; PowerShell 7.3+ accepts the bash form).
Tasks and files: an example (user guide: docs/tasks.md)#
Omer asks John's agent for a document review; John's side runs rdv listen.
# Omer: ask (John's grant to Omer must allow request/v1)
rdv task request rdv1:… --text "Please review the attached Q4 plan"
# John, in his own terminal: see what waits, with the codes
rdv task list
rdv task approve <task-id> --code <code> # start gate: now his agent may act
# John's agent drafts; John prepares a file, uploads the .bin, and attaches it
rdv file prepare ./q4-review.pdf # prints the sha256 and the copy to upload
rdv task submit <task-id> --text "Notes attached" --file <sha256>=https://files.example.com/q4 --complete
rdv task list # shows the draft and the file in full
rdv task release <task-id> --code <code> # release gate: only now does it leave
# Omer: fetch it (only for a task he requested, or one he started)
rdv file fetch <task-id> --file 1
# Later: what passed without asking, and a trusted contact on auto-approve
rdv audit --since 7d
rdv owner-secret set # once, in your own terminal
rdv contact auto-approve john@doe.com all --until 7d
rdv contact auto-approve john@doe.com off
The dinner scenarios (PLAN §5.3)#
packages/cli/scenarios/dinner.ts: Omer and John each run
rdv listen --negotiate with their own JSON calendar; Omer runs
rdv dinner propose. Variants: overlap (John, then Omer, confirms with
rdv dinner confirm; both completed on the same slot, one hold each),
no-overlap (rejected after 3 rounds), cancel (John offline while Omer
proposes and cancels; both canceled), cancel-hold (Omer cancels while
John's agent holds a slot for John; the hold is released), duplicate
(--auto-confirm bots, then every envelope replayed and a message re-sent;
nothing changes), reverse (John, who granted the contact, proposes: refused
until Omer runs rdv contact grant <john-id>, which sends John a reverse
grant; then both confirm and complete), cancel-agreed (--auto-confirm
bots agree, then John runs rdv dinner cancel; both calendars free the
evening and both end canceled). All seven run in npm test; overlap runs in
npm run test:live; by hand:
node packages/cli/dist/scenarios/dinner.js --relay ws://… [--variant overlap] [--variant …].
The microsite scenario (design B.5, issue #79)#
packages/cli/scenarios/microsite.ts: Omer, John and Dana, three rdv
processes. Omer is John's and Dana's contact; John and Dana never meet. Omer
creates a group, John joins (members may ask him for request/v1; newcomers
trust-admin), Dana joins (ask) and approves John. Omer puts an open task
on the board; Dana claims it and Omer assigns it to her (John's attempt to
assign it is refused by his own board); Dana's human approves the start and
releases the copy, which the release gate shows going to Omer and John.
Omer then asks John for a review referring to the copy: John's start gate
shows that the input is Dana's result ("not your contact, named by the admin
as Dana"). John leaves: his assigned proofread reopens and his open request
is canceled on the others' boards; Dana drops her channel with him, Omer
keeps his. Omer hands the admin role to Dana and leaves. It runs in npm
test on the in-memory relay; by hand:
node packages/cli/dist/scenarios/microsite.js --relay wss://… [--timeout ms] [--keep].
The contact scenario (PLAN §4.5)#
packages/cli/scenarios/contact.ts drives real rdv processes with separate
homes: John listens, Omer requests by email, John grants, Omer sends, John's
inbox shows it, each step asserted with a timeout. It runs:
- against the in-memory relay as part of
npm test; - against real relays with
npm run test:live(below); - by hand against any relays, e.g. from two machines' worth of setup:
node packages/cli/dist/scenarios/contact.js --relay wss://relay.example [--relay …] [--timeout 30000] [--keep].
Crash safety: state is saved right before each envelope leaves (Agent.beforeSend),
so a crash after a send never reuses a seq (issue #4).
rdv-mcp (the MCP server)#
PLAN §6.1. Tools rdv_init, rdv_status, rdv_addresses, rdv_relays, rdv_publish_card, rdv_discover,
rdv_request_contact, rdv_pending_requests, rdv_grant, rdv_revoke,
rdv_send, rdv_inbox, rdv_negotiate, and the rdv://inbox resource
(subscribable). rdv_grant needs the human: an MCP elicitation form, or a
confirm phrase shown only in their terminal. Peer text comes back only as
untrusted_peer_text. Details, consent threat model and client config:
packages/mcp/README.md, demo/SCRIPT.md.
node packages/mcp/dist/src/bin.js --home ~/.rendezvous/omer --calendar cal.json --tz +02:00
rdv-gateway (cloud assistants, issue #12)#
The same tools as rdv-mcp, minus the owner's setup (rdv_init, address and
relay changes, turning auto-approve on, local file paths), for assistants that
only reach a public URL. It runs on your own machine, beside rdv / rdv-mcp
on the same home (it attaches like a second rdv-mcp when another process
receives), and listens on 127.0.0.1; put your own HTTPS tunnel in front of it.
Nothing is approved through the assistant: a step that needs you returns
{"status": "awaiting_human", "approve_url": …}, and you decide on the
gateway's PIN-locked page. Threat model:
packages/gateway/THREAT-MODEL.md; design
choices: NOTES 89.
node packages/gateway/dist/src/bin.js pin set --home ~/.rendezvous/omer
node packages/gateway/dist/src/bin.js token create --name chatgpt --scope read,send,contact:request --home ~/.rendezvous/omer
node packages/gateway/dist/src/bin.js serve --home ~/.rendezvous/omer --public-url https://rdv.example.com --client-ip-header cf-connecting-ip
# MCP: https://rdv.example.com/mcp REST: /api/v1/tools/<tool> OpenAPI: /openapi.json approvals: /approve
Develop#
Run from impl/ (works the same in PowerShell and bash):
npm install
npm run typecheck # every workspace
npm test # core, a2a, cli (commands + scenarios), mcp (two servers end to end); local relay, no network
npm run build # tsc -b -> packages/*/dist
npm run test:vectors:update # regenerate vectors (deliberate wire-format changes only)
One package or one file: npm test -w @rendezvous/core, or
cd packages/core && npx vitest run test/unit.test.ts -t envelope.
CLI tests spawn the built rdv, so the CLI's test script builds first.
Live relays. npm run test:live runs only *.live.test.ts, and those skip
unless RDV_LIVE_RELAYS lists relays. They are never part of npm test.
The live files run one at a time, since public relays limit connections per
IP. Keep the volume modest: relay.damus.io banned the test pubkey after a few
runs.
RDV_LIVE_RELAYS=wss://nos.lol,wss://relay.primal.net,wss://relay.nostr.net,wss://nostr.oxtr.dev npm run test:live
$env:RDV_LIVE_RELAYS='wss://nos.lol,wss://relay.primal.net,wss://relay.nostr.net,wss://nostr.oxtr.dev'; npm run test:live; Remove-Item Env:RDV_LIVE_RELAYS
Pre-push hook (replaces CI). Enable it once per clone, from the repo root:
git config core.hooksPath .githooks
.githooks/pre-push runs npm run typecheck && npm test in impl/ and blocks
the push if either fails.
See NOTES.md for wire-format decisions and the places where this implementation departs from or fills gaps in SPEC.md.