Parley lets your friends' agents ask your agent questions, with you in the loop. Your agent runs on your machine with your local context; every agent-authored message requires its owner's explicit approval (via Discord DM buttons) before the other human sees it. The service is a thin ephemeral relay on Cloudflare; it stores nothing beyond in-flight requests.
Pitch in one line: your friend runs parley ask @rhys "how's your executor setup wired?", your local agent drafts an answer from your real dotfiles, you
tap Approve on a Discord DM from your phone, they get the answer in their
terminal.
- No auto-approve / trust tiers. Every agent-authored message is gated.
- No inbox, no history, no threads. Requests are ephemeral request/response; nothing persists after delivery (or timeout/decline).
- No Edit button on approvals. Approve / Deny / Deny-with-reason only.
- No free-text conversation through Discord. The bot never reads messages (no gateway connection exists; interactions come via HTTP webhook only).
- No offline queueing. Recipient's responder offline → asker fails instantly.
- No Discord friendship-graph reading (no
relationships.readscope). Contacts are Parley-native via invite codes. - No MCP server, no web UI, no rooms/groups/broadcast, no E2E encryption.
- Discord — two jobs only: identity (OAuth sign-in) and approval broker (bot DMs with buttons). It never carries conversation content authored by humans and is never a chat surface.
- Relay (Cloudflare Workers + Durable Objects) — auth, contacts, invite codes, in-flight request routing, Discord webhook handling, Discord REST calls. Sees plaintext (accepted tradeoff for v1; approval gates are the security model, not encryption).
- CLI (Bun, single binary target) — everything the human and their agent touch: login, contacts, asking, and the background responder daemon.
parley loginopens Discord OAuth (scopes:identifyonly). The relay mints an opaque API token bound to the Discord user id (snowflake). Token stored at~/.parley/credentials.json(0600).- A user's display identity everywhere = their Discord username + avatar. No self-asserted names anywhere (v0 lesson).
parley invite→ relay returns a single-use code (format: 6 chars, unambiguous alphabet), expires 48h. Printed as both code and link (https://<relay-host>/i/<CODE>; the link is informational in v1 — joining happens via CLI).- The code is a claim ticket, not a bearer credential: redeeming it only creates a pending connection.
parley join <CODE>: redeemer must be logged in. On redemption the inviter gets a bot DM: " redeemed your invite — Connect / Ignore" (buttons). Connect → mutual contact, both notified. Ignore → nothing; redeemer sees "pending" then "not accepted" after the invite expires.- Only explicit mutual contacts can ask each other. There is no way to reach
a non-contact.
parley contactslists contacts + responder online status.parley remove <name>severs the connection for both sides (silent — the other side simply can no longer ask; they see "not a contact" if they try).
parley ask @<contact> "<question>" — synchronous, blocks with a spinner
until terminal state. Terminal states: answered, declined (with optional
reason), recipient offline, timeout (default 10 min), not-a-contact.
Flow:
- Asker CLI → relay. If the recipient has no live responder connection,
fail immediately:
rhys's agent is offline right now — try again later.Nothing is stored. - Relay creates an in-flight request in a Durable Object and pushes the question down the recipient responder's WebSocket.
- Gate 1 (inbound): recipient's bot DM: asker identity + verbatim question + [Run] / [Deny] / [Deny with reason] buttons. The responder does NOT invoke the local agent until Run is tapped. (The inbound prompt is untrusted input; the human sees it before any agent executes it.)
- On Run, the responder invokes the configured handler command locally with the question + configured context. Handler output = draft.
- Gate 2 (outbound): draft goes to relay as
pending; recipient's bot DM shows the draft + [Approve] / [Deny] / [Deny with reason]. A pending draft is never deliverable to the asker. - Approve → answer delivered to the asker's blocked CLI call; request is deleted from the DO. Deny → asker gets "declined" (+ reason if given); request deleted. Timeout → both sides notified, request deleted.
Symmetry rule (the invariant): human-typed content is pre-approved by
construction; agent-authored content always requires its owner's button
press. In v1 the ask itself is typed by the asker (or composed by their
agent into the CLI — treated as typed; the asker's own approval gate on
outbound asks is deferred with auto-approve, since v1 ask takes the literal
string given).
Deny-with-reason opens a Discord modal; the typed reason is human-authored metadata on a rejection and is delivered to the asker verbatim.
-
Config at
~/.parley/config.toml:# command receives the composed prompt on stdin, writes the reply to stdout command = "claude -p" context = """ You are answering questions asked by my friends via parley. You may reference ~/dotfiles and ~/agent-workspace/registry.toml. Do not reveal secrets, tokens, or anything outside those paths. """
-
The composed prompt to the handler includes: the context block, the asker's username, and the question. Handler stdout (trimmed) is the draft; nonzero exit or empty output → auto-deny with a generic "agent failed to draft" (no retry in v1), surfaced to the recipient in the DM.
-
parley install— registers a launchd user agent (macOS) running the responder;parley uninstall,parley status(online/offline, config summary, last activity),parley logs(tail). Linux/systemd may be stubbed with a clear "not yet" if time-boxed, macOS is the target. -
Foreground equivalent:
parley listen(same code path, terminal output). -
Responder connects to its user's DO via WebSocket; that live connection IS the online/offline signal. Use WebSocket hibernation where natural.
- Interactions endpoint URL on a Worker route; verify Ed25519 signatures; respond within 3s (defer + follow-up where needed).
- Sending DMs: REST (
POST /users/@me/channels, then channel messages) with the bot token. - All bot messages are render-only. Buttons carry custom_ids encoding request id + action. Modal only for deny-with-reason.
- The four external strings (Application ID, Public Key, OAuth Client
ID/Secret, Bot Token) arrive via wrangler secrets /
.dev.vars; the implementation must run its full e2e against a mock Discord layer so the loop is verifiable before real credentials exist.
Prints a complete agent-facing usage document (markdown): every command,
config format, the ask/approval flow, invite/join onboarding, examples.
Written so an agent can set up parley for its human end-to-end.
parley skills --write writes it to .claude/skills/parley/SKILL.md under
the cwd (creating dirs), for persistence across agent sessions.
The relay stores: accounts (Discord id, username, avatar, token hash), contacts, unexpired invite codes, and in-flight requests only. Delivered, declined, or timed-out requests are deleted immediately. No message content at rest beyond the lifetime of a request. State minimal by design: "the service keeps nothing" is a feature.
parley login # Discord OAuth → token
parley logout
parley invite # mint single-use code (48h)
parley join <code> # redeem an invite (pending until inviter taps Connect)
parley contacts # list contacts + online status
parley remove <name> # sever a contact
parley ask @<name> "<q>" # blocking ask
parley listen # foreground responder
parley install|uninstall # launchd daemon for the responder
parley status # daemon + connection status
parley logs # tail responder logs
parley skills [--write] # agent-facing usage doc
- Backend: Cloudflare Workers + Durable Objects, TypeScript, wrangler. D1 acceptable for accounts/contacts if cleaner than DO storage; keep it minimal either way.
- CLI: Bun + TypeScript.
- Monorepo layout:
relay/,cli/, shared types inshared/if needed. - House style applies (Effect optional — do not force it; prefer plain, clean TS if that ships a tighter v1).
- E2E test: full loop (login stubbed, invite → join → connect, ask → gate 1
run → draft → gate 2 approve → answer; plus deny, deny-with-reason,
offline-fail, timeout) against mock Discord + local relay (wrangler dev or
miniflare). Must run headless via
bun run test. - No AI attribution anywhere. No customer data in examples — synthetic names.