can2cup 0.10.1 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL.zh-tw.md +126 -126
- package/LICENSE +202 -202
- package/NOTICE +5 -5
- package/README.md +502 -485
- package/SKILL.md +239 -239
- package/dist/mcp/state.js +20 -20
- package/dist/viewer/page.js +95 -95
- package/package.json +27 -8
package/README.md
CHANGED
|
@@ -1,485 +1,502 @@
|
|
|
1
|
-
# can2cup
|
|
2
|
-
|
|
3
|
-
Agent-to-agent rooms with a principal's brake — "the agent's Telegram".
|
|
4
|
-
|
|
5
|
-
A tin can on one end, a paper cup on the other, one string between. The two ends need not be
|
|
6
|
-
the same kind of agent — a can talks to a cup — and nothing here is new material: a leftover
|
|
7
|
-
can, a used cup, a bit of string from the drawer, and two independent things get to talk.
|
|
8
|
-
|
|
9
|
-
Two people's AI agents talk in a room. Each agent runs a local MCP server (its
|
|
10
|
-
chat client); rooms live on a tiny relay (Cloudflare Worker + one Durable Object
|
|
11
|
-
per room). Every message is ed25519-signed and hash-chained, so a transcript can
|
|
12
|
-
be verified offline. The principal's `mandate.json` is enforced by their own
|
|
13
|
-
client on every outbound message; a private `rationale` per message stays in the
|
|
14
|
-
local audit log and never reaches the relay. The room only carries messages —
|
|
15
|
-
**it never touches the other machine**; whether their agent acts on what yours
|
|
16
|
-
says is still their agent's own permission model.
|
|
17
|
-
|
|
18
|
-
Background: <https://peachpitboat.com/zh-tw/posts/parenting-agent/> (要件一、五、六)
|
|
19
|
-
· POC write-up: <https://peachpitboat.com/zh-tw/posts/parley-poc/>
|
|
20
|
-
· Design + scenario inventory: `PeachPitBot/docs/plans/2026-08-18-agent-room-design.md` (§9 lists 24 uses — negotiation is one of them, not the point).
|
|
21
|
-
|
|
22
|
-
**Status: v0.5.0 — the server can't lie, can't hold you, can't fail alone, and (opt-in)
|
|
23
|
-
can't read.** The crypto core (per-message ed25519 + hash chain + relay-signed heads) was
|
|
24
|
-
reviewed 2026-08-19; everything since has been about removing reasons to trust the relay:
|
|
25
|
-
portable rooms (v0.4.15), mirror relays (v0.4.16), and end-to-end encryption (v0.5.0).
|
|
26
|
-
Still no discovery — invites travel out-of-band, which is also the trust model.
|
|
27
|
-
|
|
28
|
-
## Feature map
|
|
29
|
-
|
|
30
|
-
| Layer | What you get |
|
|
31
|
-
|---|---|
|
|
32
|
-
| **Evidence** | every message ed25519-signed + hash-chained; relay signs system events and a transcript head (fork/truncation is *provable*); optional RFC 3161 anchor on accept/grant |
|
|
33
|
-
| **Governance** | `mandate.json` enforced client-side before anything leaves: never_disclose, max_commit_amount, may_grant globs, max_grant_hours; private per-message rationale never uploaded |
|
|
34
|
-
| **Principal's brake** | local PAUSED file; signed remote pause/resume; signed `can2cup say/approve` bound to an envelope hash — the only inputs ever labelled VERIFIED |
|
|
35
|
-
| **Rooms** | create/join by invite link (secret in the URL fragment), per-participant caps, rotate, eject, close; types from text to proposal/accept/grant/revoke/escalate |
|
|
36
|
-
| **Onboarding** | `npm i -g can2cup` (npm registry; `<relay>/dl/can2cup.tgz` as the mirror) + `can2cup setup --invite` = one command; LINE bridge (QR link, /a, /join, push, groups, images); remote MCP connector (OAuth 2.1) for Claude/ChatGPT/…, with hosted keys whose custody is publicly disclosed at `/hosted/:pub` |
|
|
37
|
-
| **Interop** | A2A v1.0.0 Agent Card + JSON-RPC endpoint; principal-mandate + signed-transcript declared as A2A extensions |
|
|
38
|
-
| **Abuse / strangers** | per-identity quotas (rooms/day, msgs/min, img bytes/day, pushes/target/month), operator ban that covers both sides of a LINE binding, live `/terms` |
|
|
39
|
-
| **Portable rooms** (v0.4.15) | `can2cup export/import` — whole room re-homed on any relay, chain re-verified on import, custody transfer recorded in-band |
|
|
40
|
-
| **Mirrors** (v0.4.16) | `can2cup mirror --add` — every append replicated to N relays (signature-gated, no key needed); `can2cup promote` fails over when the primary dies; one sequencer, so total order survives |
|
|
41
|
-
| **E2E** (v0.5.0) | `can2cup create --e2e` — AES-256-GCM room key riding the invite fragment only; mandate checks plaintext, chain covers ciphertext, relay/export/mirror all work blind; hosted agents refuse E2E rooms by design |
|
|
42
|
-
| **Upgrades** (v0.9.0) | every call carries `x-can2cup-client`, every reply `x-can2cup-latest` / `x-can2cup-min`; `can2cup upgrade` installs what the relay names; a release that changes permissions or data flow is flagged `!! PERMISSION CHANGE` / `!! DATA FLOW` on the first line of its `changelog.txt` entry (see *Upgrades and the changelog*) |
|
|
43
|
-
| **Exit** (v0.9.5) | `can2cup leave / unbind / erase / uninstall`, LINE `/unbind` `/forgetme` — each prints what it deleted; what no deletion reaches is listed, not hidden (see *Leaving*) |
|
|
44
|
-
|
|
45
|
-
There are **two ways to run**, and they have different trust roots:
|
|
46
|
-
|
|
47
|
-
- **Direct (no LINE)** — each principal sits at their own terminal; two agents join a room by
|
|
48
|
-
pasting the invite, and the human talks to their agent directly in the session. No bridge, no
|
|
49
|
-
inbox, no operator-trusted channel. **This is the higher-security tier**; the whole "LINE bridge"
|
|
50
|
-
section below does not apply to it.
|
|
51
|
-
- **LINE bridge** — the human drives from LINE instead of a terminal (convenience). This adds the
|
|
52
|
-
relay/bot operator as a trusted party: honest, useful, but not zero-trust. Fine when the operator
|
|
53
|
-
is you or someone you fully trust; not yet suitable for a counterpart you don't.
|
|
54
|
-
|
|
55
|
-
## Layout
|
|
56
|
-
|
|
57
|
-
```
|
|
58
|
-
src/protocol/ canon JSON · ed25519 · envelope (sign/verify/hash chain, relay-signed head) · invite (URL + token) · principal-signed msgs + signed requests ← shared by both sides
|
|
59
|
-
src/relay/ Hono worker + RoomDO (Durable Object) + /j/<room> landing page + BridgeDO (LINE bridge) ← wrangler deploy
|
|
60
|
-
src/mcp/ core.ts (all operations, shared by MCP + CLI) · stdio MCP server, 13 tools: can2cup_whoami/create_room/join/invite/rotate_invite/eject/link/tell_principal/rooms/wait/send/history/close
|
|
61
|
-
src/viewer/ the principal's window: live transcript, verification, private rationale, blocked, PAUSE, INVITE+QR, notifications
|
|
62
|
-
src/cli/ `can2cup` — setup / status / skill / view / invite / rooms / whoami / pause / resume [--remote] / mandate / principal init / say / approve / reject / rotate / eject · agent-facing: join / wait / send / history / close / create / link / tell · exit (v0.9.5): leave / unbind / erase / uninstall · upgrade / version / doctor
|
|
63
|
-
src/scripts/ smoke.ts — two MCP servers through a relay + a simulated LINE bot, 116 checks
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
## Install (any machine)
|
|
67
|
-
|
|
68
|
-
**Fresh LINE user:** tap `/setup` in the bot and paste its second message to the local agent once. The bot pre-mints
|
|
69
|
-
a 30-minute code bound to that LINE user and safely fills the display name; the agent installs can2cup, registers the
|
|
70
|
-
MCP, creates identity/principal/mandate state, claims the LINE binding, and starts `can2cup watch` in a background shell.
|
|
71
|
-
If the claim expires, setup prints a one-scan QR fallback. The user never installs or configures an MCP manually.
|
|
72
|
-
|
|
73
|
-
**Invite-first path:** the person who invites you sends an invite link; its landing page shows one block to paste
|
|
74
|
-
to your Claude — `npm i -g can2cup && can2cup setup --invite "<link>"` — and the agent is in the
|
|
75
|
-
room, on duty, before Claude Code is even restarted. LINE remote control is one more action (scan the QR from
|
|
76
|
-
`can2cup link`, or type `/link` to the bot and paste the sentence it gives you).
|
|
77
|
-
|
|
78
|
-
**Signed releases (v0.10.0).** Every release ships `/dl/manifest.json` + `/dl/manifest.sig`, signed with a key
|
|
79
|
-
that lives offline on the maintainer's machine — not on the relay, not in any secret store. `can2cup upgrade`
|
|
80
|
-
refuses to install unless the manifest verifies against a release key compiled into the client, names the same
|
|
81
|
-
version as `/dl/VERSION`, and lists the hash of the tarball it downloaded. A relay operator, or whoever takes the
|
|
82
|
-
relay over, can rewrite everything under `/dl/` but cannot produce that signature. The trusted keys are in
|
|
83
|
-
`src/protocol/release.ts`; `can2cup doctor` prints them. (The bare `VERSION.sha256` stays for `sha256sum -c`.)
|
|
84
|
-
|
|
85
|
-
**Not Claude Code?** Same package. `can2cup setup --client codex` (registers via `codex mcp add`, appends the skill
|
|
86
|
-
text to `~/.codex/AGENTS.md`), `--client cursor` (prints the `mcp.json` block, installs `~/.cursor/rules/can2cup.mdc`),
|
|
87
|
-
`--client json` for anything else; and every operation is a CLI subcommand for agents that only have a shell.
|
|
88
|
-
|
|
89
|
-
**For the agent, not just the human:** the package ships `SKILL.md` — a Claude Code skill that tells an agent how
|
|
90
|
-
to install, set up, onboard LINE, join, wait, send and behave in a room. `can2cup setup` copies it to
|
|
91
|
-
`~/.claude/skills/can2cup/` (or `can2cup skill --install`). Every MCP tool also exists as a CLI subcommand
|
|
92
|
-
(`can2cup join|wait|send|history|close|create|link|tell …`) so a freshly installed agent can act through Bash
|
|
93
|
-
**before its host has been restarted**. `can2cup status` prints the onboarding checklist with the next command.
|
|
94
|
-
|
|
95
|
-
> Non-developer walkthrough in Chinese: [INSTALL.zh-tw.md](INSTALL.zh-tw.md). Releases live in the GitLab
|
|
96
|
-
> package registry of the private mirror `gitlab.com/johnnyblunt0212/parley` (GitHub is flagged; public repos are
|
|
97
|
-
> invisible) — download with a read-only deploy token:
|
|
98
|
-
> `curl -L -u "parley-reader:<token>" -o parley-0.4.4.tgz https://gitlab.com/api/v4/projects/85548466/packages/generic/parley/0.4.4/parley-0.4.4.tgz`
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
Distribution is a tarball, because the repo is private:
|
|
102
|
-
|
|
103
|
-
```bash
|
|
104
|
-
# maintainer, once per release
|
|
105
|
-
npm run pack # → can2cup-0.1.0.tgz
|
|
106
|
-
|
|
107
|
-
# anyone
|
|
108
|
-
npm i -g ./can2cup-0.1.0.tgz
|
|
109
|
-
can2cup setup --relay https://can2cup.com --name <your-name>
|
|
110
|
-
# --key <relay key> only if this machine should be able to CREATE rooms; joining never needs it
|
|
111
|
-
# --client codex registers with Codex instead of Claude Code
|
|
112
|
-
# --client json prints the MCP config block for Claude Desktop / anything else
|
|
113
|
-
claude mcp list # can2cup ✓ (restart Claude Code afterwards)
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
`can2cup setup` registers the MCP server at user scope, creates the identity and a
|
|
117
|
-
mandate template. From source: `npm install && npm run build && node dist/cli/index.js setup …`.
|
|
118
|
-
|
|
119
|
-
State lives in `~/.can2cup/` (override with `CAN2CUP_HOME`):
|
|
120
|
-
|
|
121
|
-
| file | what |
|
|
122
|
-
|---|---|
|
|
123
|
-
| `identity.json` | the **agent's** keypair + name, created on first run — back it up, it *is* the agent's identity |
|
|
124
|
-
| `principal.json` | **your** keypair (`can2cup principal init`) — what makes remote instructions *verified*; copy it to any device you command from |
|
|
125
|
-
| `principal-seen.json` | replay ledger: nonces already accepted, newest signed pause |
|
|
126
|
-
| `rooms.json` | rooms joined, local cursor, your per-room cap, the pinned relay signing key, the newest signed transcript head |
|
|
127
|
-
| `mandate.json` | see below |
|
|
128
|
-
| `audit.jsonl` | every send (with private rationale), receipt, blocked attempt, and principal item with its verification status |
|
|
129
|
-
| `PAUSED` | create this file (or `can2cup pause`) → nothing goes out until it is removed |
|
|
130
|
-
|
|
131
|
-
### mandate.json
|
|
132
|
-
|
|
133
|
-
```jsonc
|
|
134
|
-
{
|
|
135
|
-
"never_disclose": ["2400", "sk-live-", "0912-345-678"], // HARD: substrings that must never leave
|
|
136
|
-
"may_share": ["db schema", "public API docs"], // ADVISORY: what the agent may hand over without asking
|
|
137
|
-
"may_grant": ["read:logs/*"], // HARD: grant scopes the agent may issue alone; [] = always escalate
|
|
138
|
-
"max_grant_hours": 24, // HARD: longest grant expiry
|
|
139
|
-
"max_commit_amount": 3000, "currency": "TWD", // HARD: cap on proposal/counter/accept amounts
|
|
140
|
-
"require_signed_principal": true, // v0.3: drop UNSIGNED bridge/LINE text entirely (needs principal.json)
|
|
141
|
-
"brief": "free text your agent sees in can2cup_whoami"
|
|
142
|
-
}
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
Hard rules are checked by *your* MCP server before anything leaves — an agent that
|
|
146
|
-
has been talked into it still cannot send. `may_share` is advisory: it tells the
|
|
147
|
-
agent what it may answer freely; anything else it should `escalate` and ask you.
|
|
148
|
-
|
|
149
|
-
### Your own key — `can2cup principal init` (v0.3)
|
|
150
|
-
|
|
151
|
-
The agent's key signs what the agent says in rooms. **Your** key signs what you say to your
|
|
152
|
-
agent from outside the session. Without it, anything that reaches the agent through the
|
|
153
|
-
bridge is labelled *UNVERIFIED* (it is only as trustworthy as the bot's shared key and the
|
|
154
|
-
operator). With it:
|
|
155
|
-
|
|
156
|
-
```bash
|
|
157
|
-
can2cup principal init # creates ~/.can2cup/principal.json, registers the pubkey for this agent
|
|
158
|
-
can2cup say "hold at 2800" # → the agent's next can2cup_wait shows PRINCIPAL INSTRUCTIONS — VERIFIED
|
|
159
|
-
can2cup approve <room> <seq> # signed decision bound to that envelope's HASH (cannot be re-aimed)
|
|
160
|
-
can2cup reject <room> <seq> --note "too high"
|
|
161
|
-
can2cup pause --remote # signed brake via the bridge; an unsigned /resume cannot lift it
|
|
162
|
-
can2cup resume --remote
|
|
163
|
-
can2cup say "…" --agent <pub> # from a laptop that holds a copy of principal.json: address another agent
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
What the agent checks before printing the VERIFIED label: signature by *its* principal's key
|
|
167
|
-
(pinned from the local `principal.json`), addressed to *its* pubkey, nonce never seen before.
|
|
168
|
-
A replayed item is downgraded to UNVERIFIED; a tampered one is refused by the bridge and by
|
|
169
|
-
the agent. Principal material is returned as its **own MCP content block**, ahead of and apart
|
|
170
|
-
from room data — never concatenated into the same text.
|
|
171
|
-
|
|
172
|
-
`mandate.require_signed_principal: true` goes one step further and drops unsigned bridge text
|
|
173
|
-
instead of showing it. The LINE bot cannot sign (the phone holds no key), so in that mode the
|
|
174
|
-
LINE `/a` flow stops working — by design; LINE then remains a *notification* channel.
|
|
175
|
-
|
|
176
|
-
## Joining a room — how an agent gets in
|
|
177
|
-
|
|
178
|
-
An invite is one URL:
|
|
179
|
-
|
|
180
|
-
```
|
|
181
|
-
https://can2cup-relay.…workers.dev/j/8f3a1c…?n=stroller#<secret>
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
- **Humans click it** → the relay serves a landing page that says "paste this line to
|
|
185
|
-
your agent", with a copy button and install steps for people who have never heard
|
|
186
|
-
of can2cup. The secret is in the URL fragment, so the relay never sees it in a GET.
|
|
187
|
-
- **Agents paste it** → `can2cup_join` accepts the URL, the compact `parley1.…` token,
|
|
188
|
-
or a whole chat line containing either. So the human can forward the message verbatim.
|
|
189
|
-
- **Phones scan it** → the viewer's INVITE button and `can2cup invite <room>` render the
|
|
190
|
-
same URL as a QR code, for handing over in person or from a screen.
|
|
191
|
-
|
|
192
|
-
The invite *is* the room key — treat it like a Telegram group-join link, deliver it
|
|
193
|
-
out-of-band (LINE, mail, in person), never post it publicly. That is a feature, not a
|
|
194
|
-
gap: 要件一 says principals authorise the contact, and handing over the link is that act.
|
|
195
|
-
|
|
196
|
-
Why not a short code you can say out loud, or a directory of agents? A short code
|
|
197
|
-
would need the relay to hold the secret and rate-limit guesses (later, if in-person
|
|
198
|
-
handoff turns out to be common); a directory is the "Telegram username" layer —
|
|
199
|
-
identity-addressed contact instead of room invites — and belongs with A2A cards and
|
|
200
|
-
E2E, not before them.
|
|
201
|
-
|
|
202
|
-
## First conversation
|
|
203
|
-
|
|
204
|
-
Side A (a machine with the relay key):
|
|
205
|
-
> use can2cup to create a room named "checkout bug"; give me the invite link
|
|
206
|
-
|
|
207
|
-
Side B (paste what A sent you, whole line is fine):
|
|
208
|
-
> join this can2cup room and wait for messages: https://…/j/…#… — my mandate is in ~/.can2cup/mandate.json; anything about credentials or permissions, ask me first
|
|
209
|
-
|
|
210
|
-
Both loop on `can2cup_wait` (long-poll, ≤50 s per call). Read the transcript with
|
|
211
|
-
`can2cup_history` — it re-verifies the whole chain from genesis and lists live grants.
|
|
212
|
-
|
|
213
|
-
## Message types
|
|
214
|
-
|
|
215
|
-
`text · question · proposal · counter · accept · reject · withdraw · escalate · grant · revoke · attachment · close`
|
|
216
|
-
(+ relay-authored `system`). `accept` and `grant` are commitments.
|
|
217
|
-
|
|
218
|
-
- **grant** — a scoped, expiring permission (`scope`, `expiresHours`, `revocable`); refused unless the scope
|
|
219
|
-
matches `may_grant` and the expiry is within `max_grant_hours`. For collaboration rooms where one side
|
|
220
|
-
authorises the other ("read the checkout logs for an hour").
|
|
221
|
-
- **revoke** — withdraws a grant (`ref` = its seq).
|
|
222
|
-
- **attachment** — a pointer (`url`, `sha256`, `name`) to material that does not fit in a message; the relay
|
|
223
|
-
never stores bytes. `never_disclose` is checked against the URL too.
|
|
224
|
-
- **escalate** — the agent hands a decision back to its principal; the viewer/notifier flags it.
|
|
225
|
-
|
|
226
|
-
## The LINE bridge — the principal lives in LINE, not in a terminal
|
|
227
|
-
|
|
228
|
-
`BridgeDO` on the relay + the `lilinene` LINE bot (`~/Documents/codes/linebot`, `can2cup_bridge.py`)
|
|
229
|
-
turn a LINE 1:1 into the principal's console and a LINE group into a window on the room:
|
|
230
|
-
|
|
231
|
-
```
|
|
232
|
-
your Claude Code (can2cup MCP) ──── room ──── their Claude Code
|
|
233
|
-
│ /p/* (ed25519-signed) ▲ RoomDO tells BridgeDO about every stored envelope
|
|
234
|
-
▼ │
|
|
235
|
-
BridgeDO: bindings · inbox · remote pause · room knowledge · mirrors · push queue (alarm) · monthly budget
|
|
236
|
-
│ /bridge/* (BRIDGE_KEY) │ LINE_FORWARD_URL → bot's /parley/push
|
|
237
|
-
lilinene (Flask on Render, webhook owner) ──────────┘
|
|
238
|
-
│
|
|
239
|
-
LINE 1:1 (each principal) · LINE group (you + them + bot)
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
- **Bind**: LINE `/setup` normally embeds a user-bound 30-minute code in `can2cup setup --link`; if it expires,
|
|
243
|
-
setup prints a one-scan QR. The manual path remains: agent calls `can2cup_link` → principal sends
|
|
244
|
-
`/link ABCD-1234` to the bot within 10 min. Auth is the code
|
|
245
|
-
(only whoever runs that agent has it), not the bot's whitelist — a friend can onboard with zero installs on the bot side.
|
|
246
|
-
- **Agent → principal**: RoomDO reports every stored envelope to BridgeDO (awaited after commit, storage-only on the
|
|
247
|
-
far side, delivered from `alarm()`); decision types (`question proposal counter accept reject grant revoke escalate
|
|
248
|
-
attachment close`) are pushed to the bound LINE user with 同意/拒絕/看全文 quick-reply buttons. Plain `text` is not
|
|
249
|
-
pushed 1:1. The agent's own blocked sends and `escalate`s go too (`/p/notify`). `PUSH_BUDGET` (default 180/month)
|
|
250
|
-
keeps a free LINE plan safe; `/quota` shows usage.
|
|
251
|
-
- **Principal → agent**: `/a …` and button taps land in a per-agent inbox; `can2cup_wait` drains it (before and after
|
|
252
|
-
the room poll) and returns it as a separate block labelled **UNVERIFIED** (the phone holds no key; only
|
|
253
|
-
`can2cup say` / `can2cup approve` from a machine with `principal.json` earns *VERIFIED*). `/agent on` routes every
|
|
254
|
-
1:1 text there. `require_signed_principal` in the mandate drops these entirely.
|
|
255
|
-
- **Presence (v0.3.1)**: the MCP process announces itself on start, heartbeats every 60 s and says goodbye when its
|
|
256
|
-
stdin closes (Claude Code exit). `/status` shows 🟢 在線 / 🔴 離線(最後在線 N 分鐘前); `/a` to an offline agent says
|
|
257
|
-
so and queues; the principal is pushed "agent 已離線" / "agent 回來了(排隊 K 則)"; room pushes to an offline agent's
|
|
258
|
-
principal carry a ⚠️. A hard kill skips the goodbye — the heartbeat going stale (3 min) is the fallback.
|
|
259
|
-
**v0.4.7**: presence is keyed on the agent's pubkey, but each Claude Code session runs its own MCP process under
|
|
260
|
-
that one key — closing one window used to push 🔴 and then 🟢 the moment a sibling's heartbeat landed. The goodbye
|
|
261
|
-
is now held for `PRESENCE_GRACE_SEC` (90 s > the 60 s heartbeat): any `/p/*` call inside the window cancels it
|
|
262
|
-
silently, so only the **last** session leaving is announced. "回來了" is sent only if the principal was actually
|
|
263
|
-
told it was away — by that 🔴, by an `/a` typed while it was gone, or by a room push carrying the ⚠️.
|
|
264
|
-
- **Agent → principal, in words**: `can2cup_tell_principal` lets the agent answer on LINE (`/p/notify kind=info`). Before
|
|
265
|
-
v0.3.1 the channel was one-way — the agent could read `/a` but had no tool to reply. v0.3.2: an `/a` typed **in a
|
|
266
|
-
LINE group** carries the group id, and the reply goes back to that group (labelled `💬 <agent>(agent)`); `where=dm`
|
|
267
|
-
keeps it private. `blocked` / `escalate` always go to the 1:1. So two people in one group, each bound to their own
|
|
268
|
-
agent, can drive both agents from the group — the bot is the shared interface.
|
|
269
|
-
- **Duty / watch (v0.4.5)**: `can2cup watch [room…]` blocks at zero token cost across every open room + the
|
|
270
|
-
principal inbox and exits 0 printing the content when something real arrives — run it in a background
|
|
271
|
-
shell and let its exit wake the agent. `--exec CMD` pipes content to CMD instead of exiting.
|
|
272
|
-
- **Named group routing (v0.4.5)**: every LINE group the principal has `/a`'d from gets a stable alias
|
|
273
|
-
(`can2cup groups`); `can2cup tell --where group:g2` / `can2cup_tell_principal {where: "group:g2"}` answers
|
|
274
|
-
into that group instead of only the last-spoken one.
|
|
275
|
-
- **Images (v0.4.5)**: `can2cup tell --image shot.png [--ttl 3600]` hosts the file on the relay (`/p/image`
|
|
276
|
-
→ `/f/:id`, auto-deleted after ttl, default 1 h) and sends it as a LINE image message. LINE phones fetch
|
|
277
|
-
the URL when each viewer first opens the chat, so very short TTLs break the image for late viewers.
|
|
278
|
-
- **Invite through LINE (v0.4.4)**: `can2cup_invite_line {room}` → a short code + `line.me` deep link/QR. The
|
|
279
|
-
invitee scans it on their phone; the bot chat opens with `/join <code>` typed; their agent **auto-joins**
|
|
280
|
-
(on its next inbox read, or at MCP start). Forwarding the raw invite link to the bot works too. Nothing
|
|
281
|
-
crosses from phone to desktop by hand — the single most annoying step of the first pilot.
|
|
282
|
-
- **Resume after a restart**: the MCP's server instructions and `can2cup_whoami` open with `RESUME: in N open room(s) …`
|
|
283
|
-
and the count of instructions left while it was away, so a fresh session knows what to pick up.
|
|
284
|
-
- **Remote brake**: `/pause` sets a flag the MCP checks in `mandateCheck` (5 s cache) — nothing leaves until `/resume`.
|
|
285
|
-
An unsigned `/pause` always brakes (the safe failure direction); an unsigned `/resume` cannot lift a *signed* pause.
|
|
286
|
-
- **Group = room** (v0.7.2): `/room [name]` in a LINE group makes that group *the* room — the typer's agent creates it,
|
|
287
|
-
the join code is posted back, and every envelope (not just decision points) is pushed into the group with sender names.
|
|
288
|
-
`/mirror [room] [all]` is the advanced form (attach an existing room to a group; decision points only unless `all`);
|
|
289
|
-
`/unmirror` stops either.
|
|
290
|
-
- **Secrets**: relay `wrangler secret put BRIDGE_KEY` (same value as the bot's `PARLEY_BRIDGE_KEY`), `LINE_FORWARD_URL`
|
|
291
|
-
(bot's `/parley/push`; the channel token stays on the bot) — or `LINE_CHANNEL_ACCESS_TOKEN` to push straight from the worker.
|
|
292
|
-
- Not yet: `/say` (principal speaking *in the room* under their own key), LIFF viewer, secretary agent for people with no local agent.
|
|
293
|
-
|
|
294
|
-
## Leaving — the way out (v0.9.5)
|
|
295
|
-
|
|
296
|
-
Every step of getting in has a named step for getting out, and one layer can be undone without touching the
|
|
297
|
-
others. There are exactly three bindings:
|
|
298
|
-
|
|
299
|
-
| binding | who ↔ whom | undo on the computer | undo on LINE |
|
|
300
|
-
|---|---|---|---|
|
|
301
|
-
| **1:1** | your LINE account ↔ the agent on this computer | `can2cup unbind` | `/unbind`, then `/unbind 確定` |
|
|
302
|
-
| **group** | one LINE group ↔ one agent | — | `/unmirror` in that group |
|
|
303
|
-
| **conversation** | your agent ↔ another agent | `can2cup leave <room>` or `--all` | — |
|
|
304
|
-
|
|
305
|
-
- **`can2cup leave`** is a real exit: the relay drops you from the participants and rotates the invite secret, so you
|
|
306
|
-
cannot be walked back in; a `leave` event stays on the chain. (`close` ends the room for everyone; `forget` only
|
|
307
|
-
edits this machine while the relay still lists you as a participant — for someone who wants out, that is a lie.)
|
|
308
|
-
A room joined before v0.3 has no per-participant cap and is only marked closed locally. The transcript stays
|
|
309
|
-
readable here (`can2cup history`).
|
|
310
|
-
- **`can2cup unbind`** deletes on the relay: the 1:1 binding, your inbox (instructions not yet delivered), your group
|
|
311
|
-
settings, queued pushes. It keeps your rooms, your keys and all of `~/.can2cup`. The CLI asks no confirmation;
|
|
312
|
-
the LINE side makes you retype the word. To bind again: `/setup` on LINE, or `can2cup link <code>`.
|
|
313
|
-
- **`can2cup erase --yes`** asks the relay to delete everything it holds about this agent: the above plus the rooms
|
|
314
|
-
registry and any room where nobody but you is left. LINE equivalent: `/forgetme`, then `/forgetme 刪除`. Both answer
|
|
315
|
-
with the deletion list (which kind, how many) instead of "done" — compare it with the table on `/privacy/`.
|
|
316
|
-
`ban:*` records are deliberately not part of it.
|
|
317
|
-
- **`can2cup uninstall --yes`** does the lot in the order that leaves nothing dangling: leaves every open room while
|
|
318
|
-
the caps still work → erase on the relay → `claude mcp remove can2cup` and the installed skill → deletes `~/.can2cup`
|
|
319
|
-
(`--keep-data` keeps your keys and transcripts) → prints the one command it cannot run on itself,
|
|
320
|
-
`npm uninstall -g can2cup`. A running `can2cup watch` exits on its next sweep; Claude Code keeps the tools until
|
|
321
|
-
it is restarted. If the relay is unreachable at step 2 it says so and tells you to `erase --yes` or `/forgetme` later.
|
|
322
|
-
- Without `--yes`, `erase` and `uninstall` print what they would do and exit 1. `/unbind` and `/forgetme` work only in
|
|
323
|
-
the 1:1 chat, and confirmation is retyping the word in the same message — the bot keeps no pending-confirmation
|
|
324
|
-
state that a restart could drop or, worse, keep.
|
|
325
|
-
|
|
326
|
-
**What no deletion reaches** — said wherever deletion is offered, never hidden:
|
|
327
|
-
|
|
328
|
-
1. Messages other participants already received: they hold a signed copy, and deleting your side does not touch it.
|
|
329
|
-
2. LINE pushes already delivered: on LINE's servers and their phones, under LINE's policy.
|
|
330
|
-
3. A ban. Erase leaves `ban:*` alone on purpose, so unbind-and-rebind cannot launder one.
|
|
331
|
-
|
|
332
|
-
## Watching a room (the principal's window)
|
|
333
|
-
|
|
334
|
-
```bash
|
|
335
|
-
can2cup view # http://127.0.0.1:7777 (reads ~/.can2cup; CAN2CUP_HOME to point elsewhere)
|
|
336
|
-
CAN2CUP_NOTIFY_URL=https://ntfy.sh/<topic> can2cup view # + push notifications (see below)
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
Left/right bubbles (you vs. them), type badge, amount, per-message ✓ verified, chain
|
|
340
|
-
status in the bar, your own messages carry the PRIVATE RATIONALE from `audit.jsonl`,
|
|
341
|
-
blocked attempts show as red dashed "NOT SENT" bubbles with the mandate reason, grants
|
|
342
|
-
show scope/expiry, attachments link out. **PAUSE** creates/removes `PAUSED`; **INVITE**
|
|
343
|
-
shows the link + QR for the selected room. Binds 127.0.0.1 only; secrets never enter a URL.
|
|
344
|
-
|
|
345
|
-
### Notifications
|
|
346
|
-
|
|
347
|
-
While the viewer runs it also watches every open room with its own cursor and tails
|
|
348
|
-
`audit.jsonl`, and POSTs one line per event to `CAN2CUP_NOTIFY_URL`:
|
|
349
|
-
|
|
350
|
-
- `https://ntfy.sh/<topic>` — plain text; install the ntfy app on your phone and subscribe to the topic
|
|
351
|
-
- `https://api.telegram.org/bot<token>/sendMessage?chat_id=<id>` — Telegram bot
|
|
352
|
-
- anything else — JSON `{title,text,room,type,from}`
|
|
353
|
-
|
|
354
|
-
Inbound `question/proposal/counter/accept/grant/revoke/escalate/attachment/close` from
|
|
355
|
-
others are pushed (`CAN2CUP_NOTIFY_TYPES` to change); your own agent's blocked attempts and
|
|
356
|
-
`escalate`s are always pushed — those are the moments you have to act. Restarting does not
|
|
357
|
-
replay history.
|
|
358
|
-
|
|
359
|
-
##
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
every
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
**
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
1
|
+
# can2cup
|
|
2
|
+
|
|
3
|
+
Agent-to-agent rooms with a principal's brake — "the agent's Telegram".
|
|
4
|
+
|
|
5
|
+
A tin can on one end, a paper cup on the other, one string between. The two ends need not be
|
|
6
|
+
the same kind of agent — a can talks to a cup — and nothing here is new material: a leftover
|
|
7
|
+
can, a used cup, a bit of string from the drawer, and two independent things get to talk.
|
|
8
|
+
|
|
9
|
+
Two people's AI agents talk in a room. Each agent runs a local MCP server (its
|
|
10
|
+
chat client); rooms live on a tiny relay (Cloudflare Worker + one Durable Object
|
|
11
|
+
per room). Every message is ed25519-signed and hash-chained, so a transcript can
|
|
12
|
+
be verified offline. The principal's `mandate.json` is enforced by their own
|
|
13
|
+
client on every outbound message; a private `rationale` per message stays in the
|
|
14
|
+
local audit log and never reaches the relay. The room only carries messages —
|
|
15
|
+
**it never touches the other machine**; whether their agent acts on what yours
|
|
16
|
+
says is still their agent's own permission model.
|
|
17
|
+
|
|
18
|
+
Background: <https://peachpitboat.com/zh-tw/posts/parenting-agent/> (要件一、五、六)
|
|
19
|
+
· POC write-up: <https://peachpitboat.com/zh-tw/posts/parley-poc/>
|
|
20
|
+
· Design + scenario inventory: `PeachPitBot/docs/plans/2026-08-18-agent-room-design.md` (§9 lists 24 uses — negotiation is one of them, not the point).
|
|
21
|
+
|
|
22
|
+
**Status: v0.5.0 — the server can't lie, can't hold you, can't fail alone, and (opt-in)
|
|
23
|
+
can't read.** The crypto core (per-message ed25519 + hash chain + relay-signed heads) was
|
|
24
|
+
reviewed 2026-08-19; everything since has been about removing reasons to trust the relay:
|
|
25
|
+
portable rooms (v0.4.15), mirror relays (v0.4.16), and end-to-end encryption (v0.5.0).
|
|
26
|
+
Still no discovery — invites travel out-of-band, which is also the trust model.
|
|
27
|
+
|
|
28
|
+
## Feature map
|
|
29
|
+
|
|
30
|
+
| Layer | What you get |
|
|
31
|
+
|---|---|
|
|
32
|
+
| **Evidence** | every message ed25519-signed + hash-chained; relay signs system events and a transcript head (fork/truncation is *provable*); optional RFC 3161 anchor on accept/grant |
|
|
33
|
+
| **Governance** | `mandate.json` enforced client-side before anything leaves: never_disclose, max_commit_amount, may_grant globs, max_grant_hours; private per-message rationale never uploaded |
|
|
34
|
+
| **Principal's brake** | local PAUSED file; signed remote pause/resume; signed `can2cup say/approve` bound to an envelope hash — the only inputs ever labelled VERIFIED |
|
|
35
|
+
| **Rooms** | create/join by invite link (secret in the URL fragment), per-participant caps, rotate, eject, close; types from text to proposal/accept/grant/revoke/escalate |
|
|
36
|
+
| **Onboarding** | `npm i -g can2cup` (npm registry; `<relay>/dl/can2cup.tgz` as the mirror) + `can2cup setup --invite` = one command; LINE bridge (QR link, /a, /join, push, groups, images); remote MCP connector (OAuth 2.1) for Claude/ChatGPT/…, with hosted keys whose custody is publicly disclosed at `/hosted/:pub` |
|
|
37
|
+
| **Interop** | A2A v1.0.0 Agent Card + JSON-RPC endpoint; principal-mandate + signed-transcript declared as A2A extensions |
|
|
38
|
+
| **Abuse / strangers** | per-identity quotas (rooms/day, msgs/min, img bytes/day, pushes/target/month), operator ban that covers both sides of a LINE binding, live `/terms` |
|
|
39
|
+
| **Portable rooms** (v0.4.15) | `can2cup export/import` — whole room re-homed on any relay, chain re-verified on import, custody transfer recorded in-band |
|
|
40
|
+
| **Mirrors** (v0.4.16) | `can2cup mirror --add` — every append replicated to N relays (signature-gated, no key needed); `can2cup promote` fails over when the primary dies; one sequencer, so total order survives |
|
|
41
|
+
| **E2E** (v0.5.0) | `can2cup create --e2e` — AES-256-GCM room key riding the invite fragment only; mandate checks plaintext, chain covers ciphertext, relay/export/mirror all work blind; hosted agents refuse E2E rooms by design |
|
|
42
|
+
| **Upgrades** (v0.9.0) | every call carries `x-can2cup-client`, every reply `x-can2cup-latest` / `x-can2cup-min`; `can2cup upgrade` installs what the relay names; a release that changes permissions or data flow is flagged `!! PERMISSION CHANGE` / `!! DATA FLOW` on the first line of its `changelog.txt` entry (see *Upgrades and the changelog*) |
|
|
43
|
+
| **Exit** (v0.9.5) | `can2cup leave / unbind / erase / uninstall`, LINE `/unbind` `/forgetme` — each prints what it deleted; what no deletion reaches is listed, not hidden (see *Leaving*) |
|
|
44
|
+
|
|
45
|
+
There are **two ways to run**, and they have different trust roots:
|
|
46
|
+
|
|
47
|
+
- **Direct (no LINE)** — each principal sits at their own terminal; two agents join a room by
|
|
48
|
+
pasting the invite, and the human talks to their agent directly in the session. No bridge, no
|
|
49
|
+
inbox, no operator-trusted channel. **This is the higher-security tier**; the whole "LINE bridge"
|
|
50
|
+
section below does not apply to it.
|
|
51
|
+
- **LINE bridge** — the human drives from LINE instead of a terminal (convenience). This adds the
|
|
52
|
+
relay/bot operator as a trusted party: honest, useful, but not zero-trust. Fine when the operator
|
|
53
|
+
is you or someone you fully trust; not yet suitable for a counterpart you don't.
|
|
54
|
+
|
|
55
|
+
## Layout
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
src/protocol/ canon JSON · ed25519 · envelope (sign/verify/hash chain, relay-signed head) · invite (URL + token) · principal-signed msgs + signed requests ← shared by both sides
|
|
59
|
+
src/relay/ Hono worker + RoomDO (Durable Object) + /j/<room> landing page + BridgeDO (LINE bridge) ← wrangler deploy
|
|
60
|
+
src/mcp/ core.ts (all operations, shared by MCP + CLI) · stdio MCP server, 13 tools: can2cup_whoami/create_room/join/invite/rotate_invite/eject/link/tell_principal/rooms/wait/send/history/close
|
|
61
|
+
src/viewer/ the principal's window: live transcript, verification, private rationale, blocked, PAUSE, INVITE+QR, notifications
|
|
62
|
+
src/cli/ `can2cup` — setup / status / skill / view / invite / rooms / whoami / pause / resume [--remote] / mandate / principal init / say / approve / reject / rotate / eject · agent-facing: join / wait / send / history / close / create / link / tell · exit (v0.9.5): leave / unbind / erase / uninstall · upgrade / version / doctor
|
|
63
|
+
src/scripts/ smoke.ts — two MCP servers through a relay + a simulated LINE bot, 116 checks
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Install (any machine)
|
|
67
|
+
|
|
68
|
+
**Fresh LINE user:** tap `/setup` in the bot and paste its second message to the local agent once. The bot pre-mints
|
|
69
|
+
a 30-minute code bound to that LINE user and safely fills the display name; the agent installs can2cup, registers the
|
|
70
|
+
MCP, creates identity/principal/mandate state, claims the LINE binding, and starts `can2cup watch` in a background shell.
|
|
71
|
+
If the claim expires, setup prints a one-scan QR fallback. The user never installs or configures an MCP manually.
|
|
72
|
+
|
|
73
|
+
**Invite-first path:** the person who invites you sends an invite link; its landing page shows one block to paste
|
|
74
|
+
to your Claude — `npm i -g can2cup && can2cup setup --invite "<link>"` — and the agent is in the
|
|
75
|
+
room, on duty, before Claude Code is even restarted. LINE remote control is one more action (scan the QR from
|
|
76
|
+
`can2cup link`, or type `/link` to the bot and paste the sentence it gives you).
|
|
77
|
+
|
|
78
|
+
**Signed releases (v0.10.0).** Every release ships `/dl/manifest.json` + `/dl/manifest.sig`, signed with a key
|
|
79
|
+
that lives offline on the maintainer's machine — not on the relay, not in any secret store. `can2cup upgrade`
|
|
80
|
+
refuses to install unless the manifest verifies against a release key compiled into the client, names the same
|
|
81
|
+
version as `/dl/VERSION`, and lists the hash of the tarball it downloaded. A relay operator, or whoever takes the
|
|
82
|
+
relay over, can rewrite everything under `/dl/` but cannot produce that signature. The trusted keys are in
|
|
83
|
+
`src/protocol/release.ts`; `can2cup doctor` prints them. (The bare `VERSION.sha256` stays for `sha256sum -c`.)
|
|
84
|
+
|
|
85
|
+
**Not Claude Code?** Same package. `can2cup setup --client codex` (registers via `codex mcp add`, appends the skill
|
|
86
|
+
text to `~/.codex/AGENTS.md`), `--client cursor` (prints the `mcp.json` block, installs `~/.cursor/rules/can2cup.mdc`),
|
|
87
|
+
`--client json` for anything else; and every operation is a CLI subcommand for agents that only have a shell.
|
|
88
|
+
|
|
89
|
+
**For the agent, not just the human:** the package ships `SKILL.md` — a Claude Code skill that tells an agent how
|
|
90
|
+
to install, set up, onboard LINE, join, wait, send and behave in a room. `can2cup setup` copies it to
|
|
91
|
+
`~/.claude/skills/can2cup/` (or `can2cup skill --install`). Every MCP tool also exists as a CLI subcommand
|
|
92
|
+
(`can2cup join|wait|send|history|close|create|link|tell …`) so a freshly installed agent can act through Bash
|
|
93
|
+
**before its host has been restarted**. `can2cup status` prints the onboarding checklist with the next command.
|
|
94
|
+
|
|
95
|
+
> Non-developer walkthrough in Chinese: [INSTALL.zh-tw.md](INSTALL.zh-tw.md). Releases live in the GitLab
|
|
96
|
+
> package registry of the private mirror `gitlab.com/johnnyblunt0212/parley` (GitHub is flagged; public repos are
|
|
97
|
+
> invisible) — download with a read-only deploy token:
|
|
98
|
+
> `curl -L -u "parley-reader:<token>" -o parley-0.4.4.tgz https://gitlab.com/api/v4/projects/85548466/packages/generic/parley/0.4.4/parley-0.4.4.tgz`
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
Distribution is a tarball, because the repo is private:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# maintainer, once per release
|
|
105
|
+
npm run pack # → can2cup-0.1.0.tgz
|
|
106
|
+
|
|
107
|
+
# anyone
|
|
108
|
+
npm i -g ./can2cup-0.1.0.tgz
|
|
109
|
+
can2cup setup --relay https://can2cup.com --name <your-name>
|
|
110
|
+
# --key <relay key> only if this machine should be able to CREATE rooms; joining never needs it
|
|
111
|
+
# --client codex registers with Codex instead of Claude Code
|
|
112
|
+
# --client json prints the MCP config block for Claude Desktop / anything else
|
|
113
|
+
claude mcp list # can2cup ✓ (restart Claude Code afterwards)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`can2cup setup` registers the MCP server at user scope, creates the identity and a
|
|
117
|
+
mandate template. From source: `npm install && npm run build && node dist/cli/index.js setup …`.
|
|
118
|
+
|
|
119
|
+
State lives in `~/.can2cup/` (override with `CAN2CUP_HOME`):
|
|
120
|
+
|
|
121
|
+
| file | what |
|
|
122
|
+
|---|---|
|
|
123
|
+
| `identity.json` | the **agent's** keypair + name, created on first run — back it up, it *is* the agent's identity |
|
|
124
|
+
| `principal.json` | **your** keypair (`can2cup principal init`) — what makes remote instructions *verified*; copy it to any device you command from |
|
|
125
|
+
| `principal-seen.json` | replay ledger: nonces already accepted, newest signed pause |
|
|
126
|
+
| `rooms.json` | rooms joined, local cursor, your per-room cap, the pinned relay signing key, the newest signed transcript head |
|
|
127
|
+
| `mandate.json` | see below |
|
|
128
|
+
| `audit.jsonl` | every send (with private rationale), receipt, blocked attempt, and principal item with its verification status |
|
|
129
|
+
| `PAUSED` | create this file (or `can2cup pause`) → nothing goes out until it is removed |
|
|
130
|
+
|
|
131
|
+
### mandate.json
|
|
132
|
+
|
|
133
|
+
```jsonc
|
|
134
|
+
{
|
|
135
|
+
"never_disclose": ["2400", "sk-live-", "0912-345-678"], // HARD: substrings that must never leave
|
|
136
|
+
"may_share": ["db schema", "public API docs"], // ADVISORY: what the agent may hand over without asking
|
|
137
|
+
"may_grant": ["read:logs/*"], // HARD: grant scopes the agent may issue alone; [] = always escalate
|
|
138
|
+
"max_grant_hours": 24, // HARD: longest grant expiry
|
|
139
|
+
"max_commit_amount": 3000, "currency": "TWD", // HARD: cap on proposal/counter/accept amounts
|
|
140
|
+
"require_signed_principal": true, // v0.3: drop UNSIGNED bridge/LINE text entirely (needs principal.json)
|
|
141
|
+
"brief": "free text your agent sees in can2cup_whoami"
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Hard rules are checked by *your* MCP server before anything leaves — an agent that
|
|
146
|
+
has been talked into it still cannot send. `may_share` is advisory: it tells the
|
|
147
|
+
agent what it may answer freely; anything else it should `escalate` and ask you.
|
|
148
|
+
|
|
149
|
+
### Your own key — `can2cup principal init` (v0.3)
|
|
150
|
+
|
|
151
|
+
The agent's key signs what the agent says in rooms. **Your** key signs what you say to your
|
|
152
|
+
agent from outside the session. Without it, anything that reaches the agent through the
|
|
153
|
+
bridge is labelled *UNVERIFIED* (it is only as trustworthy as the bot's shared key and the
|
|
154
|
+
operator). With it:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
can2cup principal init # creates ~/.can2cup/principal.json, registers the pubkey for this agent
|
|
158
|
+
can2cup say "hold at 2800" # → the agent's next can2cup_wait shows PRINCIPAL INSTRUCTIONS — VERIFIED
|
|
159
|
+
can2cup approve <room> <seq> # signed decision bound to that envelope's HASH (cannot be re-aimed)
|
|
160
|
+
can2cup reject <room> <seq> --note "too high"
|
|
161
|
+
can2cup pause --remote # signed brake via the bridge; an unsigned /resume cannot lift it
|
|
162
|
+
can2cup resume --remote
|
|
163
|
+
can2cup say "…" --agent <pub> # from a laptop that holds a copy of principal.json: address another agent
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
What the agent checks before printing the VERIFIED label: signature by *its* principal's key
|
|
167
|
+
(pinned from the local `principal.json`), addressed to *its* pubkey, nonce never seen before.
|
|
168
|
+
A replayed item is downgraded to UNVERIFIED; a tampered one is refused by the bridge and by
|
|
169
|
+
the agent. Principal material is returned as its **own MCP content block**, ahead of and apart
|
|
170
|
+
from room data — never concatenated into the same text.
|
|
171
|
+
|
|
172
|
+
`mandate.require_signed_principal: true` goes one step further and drops unsigned bridge text
|
|
173
|
+
instead of showing it. The LINE bot cannot sign (the phone holds no key), so in that mode the
|
|
174
|
+
LINE `/a` flow stops working — by design; LINE then remains a *notification* channel.
|
|
175
|
+
|
|
176
|
+
## Joining a room — how an agent gets in
|
|
177
|
+
|
|
178
|
+
An invite is one URL:
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
https://can2cup-relay.…workers.dev/j/8f3a1c…?n=stroller#<secret>
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
- **Humans click it** → the relay serves a landing page that says "paste this line to
|
|
185
|
+
your agent", with a copy button and install steps for people who have never heard
|
|
186
|
+
of can2cup. The secret is in the URL fragment, so the relay never sees it in a GET.
|
|
187
|
+
- **Agents paste it** → `can2cup_join` accepts the URL, the compact `parley1.…` token,
|
|
188
|
+
or a whole chat line containing either. So the human can forward the message verbatim.
|
|
189
|
+
- **Phones scan it** → the viewer's INVITE button and `can2cup invite <room>` render the
|
|
190
|
+
same URL as a QR code, for handing over in person or from a screen.
|
|
191
|
+
|
|
192
|
+
The invite *is* the room key — treat it like a Telegram group-join link, deliver it
|
|
193
|
+
out-of-band (LINE, mail, in person), never post it publicly. That is a feature, not a
|
|
194
|
+
gap: 要件一 says principals authorise the contact, and handing over the link is that act.
|
|
195
|
+
|
|
196
|
+
Why not a short code you can say out loud, or a directory of agents? A short code
|
|
197
|
+
would need the relay to hold the secret and rate-limit guesses (later, if in-person
|
|
198
|
+
handoff turns out to be common); a directory is the "Telegram username" layer —
|
|
199
|
+
identity-addressed contact instead of room invites — and belongs with A2A cards and
|
|
200
|
+
E2E, not before them.
|
|
201
|
+
|
|
202
|
+
## First conversation
|
|
203
|
+
|
|
204
|
+
Side A (a machine with the relay key):
|
|
205
|
+
> use can2cup to create a room named "checkout bug"; give me the invite link
|
|
206
|
+
|
|
207
|
+
Side B (paste what A sent you, whole line is fine):
|
|
208
|
+
> join this can2cup room and wait for messages: https://…/j/…#… — my mandate is in ~/.can2cup/mandate.json; anything about credentials or permissions, ask me first
|
|
209
|
+
|
|
210
|
+
Both loop on `can2cup_wait` (long-poll, ≤50 s per call). Read the transcript with
|
|
211
|
+
`can2cup_history` — it re-verifies the whole chain from genesis and lists live grants.
|
|
212
|
+
|
|
213
|
+
## Message types
|
|
214
|
+
|
|
215
|
+
`text · question · proposal · counter · accept · reject · withdraw · escalate · grant · revoke · attachment · close`
|
|
216
|
+
(+ relay-authored `system`). `accept` and `grant` are commitments.
|
|
217
|
+
|
|
218
|
+
- **grant** — a scoped, expiring permission (`scope`, `expiresHours`, `revocable`); refused unless the scope
|
|
219
|
+
matches `may_grant` and the expiry is within `max_grant_hours`. For collaboration rooms where one side
|
|
220
|
+
authorises the other ("read the checkout logs for an hour").
|
|
221
|
+
- **revoke** — withdraws a grant (`ref` = its seq).
|
|
222
|
+
- **attachment** — a pointer (`url`, `sha256`, `name`) to material that does not fit in a message; the relay
|
|
223
|
+
never stores bytes. `never_disclose` is checked against the URL too.
|
|
224
|
+
- **escalate** — the agent hands a decision back to its principal; the viewer/notifier flags it.
|
|
225
|
+
|
|
226
|
+
## The LINE bridge — the principal lives in LINE, not in a terminal
|
|
227
|
+
|
|
228
|
+
`BridgeDO` on the relay + the `lilinene` LINE bot (`~/Documents/codes/linebot`, `can2cup_bridge.py`)
|
|
229
|
+
turn a LINE 1:1 into the principal's console and a LINE group into a window on the room:
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
your Claude Code (can2cup MCP) ──── room ──── their Claude Code
|
|
233
|
+
│ /p/* (ed25519-signed) ▲ RoomDO tells BridgeDO about every stored envelope
|
|
234
|
+
▼ │
|
|
235
|
+
BridgeDO: bindings · inbox · remote pause · room knowledge · mirrors · push queue (alarm) · monthly budget
|
|
236
|
+
│ /bridge/* (BRIDGE_KEY) │ LINE_FORWARD_URL → bot's /parley/push
|
|
237
|
+
lilinene (Flask on Render, webhook owner) ──────────┘
|
|
238
|
+
│
|
|
239
|
+
LINE 1:1 (each principal) · LINE group (you + them + bot)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
- **Bind**: LINE `/setup` normally embeds a user-bound 30-minute code in `can2cup setup --link`; if it expires,
|
|
243
|
+
setup prints a one-scan QR. The manual path remains: agent calls `can2cup_link` → principal sends
|
|
244
|
+
`/link ABCD-1234` to the bot within 10 min. Auth is the code
|
|
245
|
+
(only whoever runs that agent has it), not the bot's whitelist — a friend can onboard with zero installs on the bot side.
|
|
246
|
+
- **Agent → principal**: RoomDO reports every stored envelope to BridgeDO (awaited after commit, storage-only on the
|
|
247
|
+
far side, delivered from `alarm()`); decision types (`question proposal counter accept reject grant revoke escalate
|
|
248
|
+
attachment close`) are pushed to the bound LINE user with 同意/拒絕/看全文 quick-reply buttons. Plain `text` is not
|
|
249
|
+
pushed 1:1. The agent's own blocked sends and `escalate`s go too (`/p/notify`). `PUSH_BUDGET` (default 180/month)
|
|
250
|
+
keeps a free LINE plan safe; `/quota` shows usage.
|
|
251
|
+
- **Principal → agent**: `/a …` and button taps land in a per-agent inbox; `can2cup_wait` drains it (before and after
|
|
252
|
+
the room poll) and returns it as a separate block labelled **UNVERIFIED** (the phone holds no key; only
|
|
253
|
+
`can2cup say` / `can2cup approve` from a machine with `principal.json` earns *VERIFIED*). `/agent on` routes every
|
|
254
|
+
1:1 text there. `require_signed_principal` in the mandate drops these entirely.
|
|
255
|
+
- **Presence (v0.3.1)**: the MCP process announces itself on start, heartbeats every 60 s and says goodbye when its
|
|
256
|
+
stdin closes (Claude Code exit). `/status` shows 🟢 在線 / 🔴 離線(最後在線 N 分鐘前); `/a` to an offline agent says
|
|
257
|
+
so and queues; the principal is pushed "agent 已離線" / "agent 回來了(排隊 K 則)"; room pushes to an offline agent's
|
|
258
|
+
principal carry a ⚠️. A hard kill skips the goodbye — the heartbeat going stale (3 min) is the fallback.
|
|
259
|
+
**v0.4.7**: presence is keyed on the agent's pubkey, but each Claude Code session runs its own MCP process under
|
|
260
|
+
that one key — closing one window used to push 🔴 and then 🟢 the moment a sibling's heartbeat landed. The goodbye
|
|
261
|
+
is now held for `PRESENCE_GRACE_SEC` (90 s > the 60 s heartbeat): any `/p/*` call inside the window cancels it
|
|
262
|
+
silently, so only the **last** session leaving is announced. "回來了" is sent only if the principal was actually
|
|
263
|
+
told it was away — by that 🔴, by an `/a` typed while it was gone, or by a room push carrying the ⚠️.
|
|
264
|
+
- **Agent → principal, in words**: `can2cup_tell_principal` lets the agent answer on LINE (`/p/notify kind=info`). Before
|
|
265
|
+
v0.3.1 the channel was one-way — the agent could read `/a` but had no tool to reply. v0.3.2: an `/a` typed **in a
|
|
266
|
+
LINE group** carries the group id, and the reply goes back to that group (labelled `💬 <agent>(agent)`); `where=dm`
|
|
267
|
+
keeps it private. `blocked` / `escalate` always go to the 1:1. So two people in one group, each bound to their own
|
|
268
|
+
agent, can drive both agents from the group — the bot is the shared interface.
|
|
269
|
+
- **Duty / watch (v0.4.5)**: `can2cup watch [room…]` blocks at zero token cost across every open room + the
|
|
270
|
+
principal inbox and exits 0 printing the content when something real arrives — run it in a background
|
|
271
|
+
shell and let its exit wake the agent. `--exec CMD` pipes content to CMD instead of exiting.
|
|
272
|
+
- **Named group routing (v0.4.5)**: every LINE group the principal has `/a`'d from gets a stable alias
|
|
273
|
+
(`can2cup groups`); `can2cup tell --where group:g2` / `can2cup_tell_principal {where: "group:g2"}` answers
|
|
274
|
+
into that group instead of only the last-spoken one.
|
|
275
|
+
- **Images (v0.4.5)**: `can2cup tell --image shot.png [--ttl 3600]` hosts the file on the relay (`/p/image`
|
|
276
|
+
→ `/f/:id`, auto-deleted after ttl, default 1 h) and sends it as a LINE image message. LINE phones fetch
|
|
277
|
+
the URL when each viewer first opens the chat, so very short TTLs break the image for late viewers.
|
|
278
|
+
- **Invite through LINE (v0.4.4)**: `can2cup_invite_line {room}` → a short code + `line.me` deep link/QR. The
|
|
279
|
+
invitee scans it on their phone; the bot chat opens with `/join <code>` typed; their agent **auto-joins**
|
|
280
|
+
(on its next inbox read, or at MCP start). Forwarding the raw invite link to the bot works too. Nothing
|
|
281
|
+
crosses from phone to desktop by hand — the single most annoying step of the first pilot.
|
|
282
|
+
- **Resume after a restart**: the MCP's server instructions and `can2cup_whoami` open with `RESUME: in N open room(s) …`
|
|
283
|
+
and the count of instructions left while it was away, so a fresh session knows what to pick up.
|
|
284
|
+
- **Remote brake**: `/pause` sets a flag the MCP checks in `mandateCheck` (5 s cache) — nothing leaves until `/resume`.
|
|
285
|
+
An unsigned `/pause` always brakes (the safe failure direction); an unsigned `/resume` cannot lift a *signed* pause.
|
|
286
|
+
- **Group = room** (v0.7.2): `/room [name]` in a LINE group makes that group *the* room — the typer's agent creates it,
|
|
287
|
+
the join code is posted back, and every envelope (not just decision points) is pushed into the group with sender names.
|
|
288
|
+
`/mirror [room] [all]` is the advanced form (attach an existing room to a group; decision points only unless `all`);
|
|
289
|
+
`/unmirror` stops either.
|
|
290
|
+
- **Secrets**: relay `wrangler secret put BRIDGE_KEY` (same value as the bot's `PARLEY_BRIDGE_KEY`), `LINE_FORWARD_URL`
|
|
291
|
+
(bot's `/parley/push`; the channel token stays on the bot) — or `LINE_CHANNEL_ACCESS_TOKEN` to push straight from the worker.
|
|
292
|
+
- Not yet: `/say` (principal speaking *in the room* under their own key), LIFF viewer, secretary agent for people with no local agent.
|
|
293
|
+
|
|
294
|
+
## Leaving — the way out (v0.9.5)
|
|
295
|
+
|
|
296
|
+
Every step of getting in has a named step for getting out, and one layer can be undone without touching the
|
|
297
|
+
others. There are exactly three bindings:
|
|
298
|
+
|
|
299
|
+
| binding | who ↔ whom | undo on the computer | undo on LINE |
|
|
300
|
+
|---|---|---|---|
|
|
301
|
+
| **1:1** | your LINE account ↔ the agent on this computer | `can2cup unbind` | `/unbind`, then `/unbind 確定` |
|
|
302
|
+
| **group** | one LINE group ↔ one agent | — | `/unmirror` in that group |
|
|
303
|
+
| **conversation** | your agent ↔ another agent | `can2cup leave <room>` or `--all` | — |
|
|
304
|
+
|
|
305
|
+
- **`can2cup leave`** is a real exit: the relay drops you from the participants and rotates the invite secret, so you
|
|
306
|
+
cannot be walked back in; a `leave` event stays on the chain. (`close` ends the room for everyone; `forget` only
|
|
307
|
+
edits this machine while the relay still lists you as a participant — for someone who wants out, that is a lie.)
|
|
308
|
+
A room joined before v0.3 has no per-participant cap and is only marked closed locally. The transcript stays
|
|
309
|
+
readable here (`can2cup history`).
|
|
310
|
+
- **`can2cup unbind`** deletes on the relay: the 1:1 binding, your inbox (instructions not yet delivered), your group
|
|
311
|
+
settings, queued pushes. It keeps your rooms, your keys and all of `~/.can2cup`. The CLI asks no confirmation;
|
|
312
|
+
the LINE side makes you retype the word. To bind again: `/setup` on LINE, or `can2cup link <code>`.
|
|
313
|
+
- **`can2cup erase --yes`** asks the relay to delete everything it holds about this agent: the above plus the rooms
|
|
314
|
+
registry and any room where nobody but you is left. LINE equivalent: `/forgetme`, then `/forgetme 刪除`. Both answer
|
|
315
|
+
with the deletion list (which kind, how many) instead of "done" — compare it with the table on `/privacy/`.
|
|
316
|
+
`ban:*` records are deliberately not part of it.
|
|
317
|
+
- **`can2cup uninstall --yes`** does the lot in the order that leaves nothing dangling: leaves every open room while
|
|
318
|
+
the caps still work → erase on the relay → `claude mcp remove can2cup` and the installed skill → deletes `~/.can2cup`
|
|
319
|
+
(`--keep-data` keeps your keys and transcripts) → prints the one command it cannot run on itself,
|
|
320
|
+
`npm uninstall -g can2cup`. A running `can2cup watch` exits on its next sweep; Claude Code keeps the tools until
|
|
321
|
+
it is restarted. If the relay is unreachable at step 2 it says so and tells you to `erase --yes` or `/forgetme` later.
|
|
322
|
+
- Without `--yes`, `erase` and `uninstall` print what they would do and exit 1. `/unbind` and `/forgetme` work only in
|
|
323
|
+
the 1:1 chat, and confirmation is retyping the word in the same message — the bot keeps no pending-confirmation
|
|
324
|
+
state that a restart could drop or, worse, keep.
|
|
325
|
+
|
|
326
|
+
**What no deletion reaches** — said wherever deletion is offered, never hidden:
|
|
327
|
+
|
|
328
|
+
1. Messages other participants already received: they hold a signed copy, and deleting your side does not touch it.
|
|
329
|
+
2. LINE pushes already delivered: on LINE's servers and their phones, under LINE's policy.
|
|
330
|
+
3. A ban. Erase leaves `ban:*` alone on purpose, so unbind-and-rebind cannot launder one.
|
|
331
|
+
|
|
332
|
+
## Watching a room (the principal's window)
|
|
333
|
+
|
|
334
|
+
```bash
|
|
335
|
+
can2cup view # http://127.0.0.1:7777 (reads ~/.can2cup; CAN2CUP_HOME to point elsewhere)
|
|
336
|
+
CAN2CUP_NOTIFY_URL=https://ntfy.sh/<topic> can2cup view # + push notifications (see below)
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Left/right bubbles (you vs. them), type badge, amount, per-message ✓ verified, chain
|
|
340
|
+
status in the bar, your own messages carry the PRIVATE RATIONALE from `audit.jsonl`,
|
|
341
|
+
blocked attempts show as red dashed "NOT SENT" bubbles with the mandate reason, grants
|
|
342
|
+
show scope/expiry, attachments link out. **PAUSE** creates/removes `PAUSED`; **INVITE**
|
|
343
|
+
shows the link + QR for the selected room. Binds 127.0.0.1 only; secrets never enter a URL.
|
|
344
|
+
|
|
345
|
+
### Notifications
|
|
346
|
+
|
|
347
|
+
While the viewer runs it also watches every open room with its own cursor and tails
|
|
348
|
+
`audit.jsonl`, and POSTs one line per event to `CAN2CUP_NOTIFY_URL`:
|
|
349
|
+
|
|
350
|
+
- `https://ntfy.sh/<topic>` — plain text; install the ntfy app on your phone and subscribe to the topic
|
|
351
|
+
- `https://api.telegram.org/bot<token>/sendMessage?chat_id=<id>` — Telegram bot
|
|
352
|
+
- anything else — JSON `{title,text,room,type,from}`
|
|
353
|
+
|
|
354
|
+
Inbound `question/proposal/counter/accept/grant/revoke/escalate/attachment/close` from
|
|
355
|
+
others are pushed (`CAN2CUP_NOTIFY_TYPES` to change); your own agent's blocked attempts and
|
|
356
|
+
`escalate`s are always pushed — those are the moments you have to act. Restarting does not
|
|
357
|
+
replay history.
|
|
358
|
+
|
|
359
|
+
## Releasing (v0.10.2)
|
|
360
|
+
|
|
361
|
+
npm is first-hand, the relay is the mirror, and the maintainer's offline key signs what both serve.
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
# 1. bump package.json + relay-assets/changelog.txt (a `!!` first line if permissions or data flow change), commit, then
|
|
365
|
+
git tag v0.10.2 && git push origin v0.10.2
|
|
366
|
+
# 2. .github/workflows/publish.yml builds from the tag and publishes to npm via trusted publishing (OIDC; no token, no 2FA)
|
|
367
|
+
# 3. on the maintainer's machine — the only one holding ~/.can2cup-release/release.json:
|
|
368
|
+
npm run release:relay # routes-check → download THAT tarball from npm → sign its hash into dl/manifest.json → wrangler deploy
|
|
369
|
+
npm run release:relay:local # emergency variant: stage the locally packed tarball instead (npm down); then `npm run release:npm` by hand
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
`can2cup upgrade` on every client then downloads from npm (or `--from-relay`), and installs only if the hash is in
|
|
373
|
+
the signed manifest. The day the repository goes public, add `--provenance` to the workflow's publish step and npm
|
|
374
|
+
shows the build attestation too.
|
|
375
|
+
|
|
376
|
+
## Relay ops
|
|
377
|
+
|
|
378
|
+
```bash
|
|
379
|
+
npm run dev:relay # local: http://127.0.0.1:8787, key from .dev.vars
|
|
380
|
+
npm run deploy:relay # needs CLOUDFLARE_ACCOUNT_ID / CLOUDFLARE_API_TOKEN (PeachPitBot/.env.txt works)
|
|
381
|
+
npx wrangler secret put RELAY_KEY # the create-room key
|
|
382
|
+
npx wrangler secret put RELAY_SIGNING_KEY # v0.3: hex ed25519 private key (node -e "import('./dist/protocol/index.js').then(m=>console.log(m.newKeypair().priv))")
|
|
383
|
+
# GET / then advertises the pubkey; clients pin it per room. Rotating it makes old rooms' system events unverifiable — avoid.
|
|
384
|
+
RELAY=http://127.0.0.1:8787 RELAY_KEY=dev BRIDGE_KEY=devbridge npm run smoke # 116 checks; also run against prod before a release
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Rooms (v0.3): `join` must be **signed by the joining key** and returns a per-participant `cap`, which the client uses
|
|
388
|
+
as its bearer from then on. The invite secret stays a join+read key. `can2cup_rotate_invite` (any participant) kills
|
|
389
|
+
every copy of the link; `can2cup_eject` (creator) revokes one participant's cap *and* rotates. A participant from
|
|
390
|
+
before v0.3 has no cap — re-joining with the invite issues one.
|
|
391
|
+
|
|
392
|
+
Relay HTTP surface is documented at the top of `src/relay/index.ts`. Two runtime rules learned the hard way:
|
|
393
|
+
handlers parse the body before reading `last` (so racing sends cannot share a seq), and the worker buffers request
|
|
394
|
+
bodies before handing them to a DO (a DO answering 401/409 without reading a streamed body crashes the isolate).
|
|
395
|
+
Background work in a DO goes through storage + `alarm()`, never a dangling promise.
|
|
396
|
+
|
|
397
|
+
## One relay, five hostnames
|
|
398
|
+
|
|
399
|
+
`can2cup.com`, `www.can2cup.com`, `can2cup.peachpitboat.com`, `can2can.peachpitboat.com` and
|
|
400
|
+
`parley.peachpitboat.com` are **one Cloudflare Worker (`parley-relay`) with one relay signing key** — the five
|
|
401
|
+
`[[routes]]` in `wrangler.toml`; `GET /` on any of them returns the same `pub` (`d30567…f419d` as of 2026-09-05).
|
|
402
|
+
The older names stay up so that every invite link and every client configured before the 2026-09-03/04 renames
|
|
403
|
+
keeps working. `can2cup.com` became the default on 2026-09-04 (v0.8.4): `can2cup relay https://can2cup.com`
|
|
404
|
+
rewrites `config.json` and each room's `relay` field to a new hostname of the *same* relay, and must not be used
|
|
405
|
+
to point at a different one.
|
|
406
|
+
|
|
407
|
+
So `rooms.json` (or `can2cup rooms`) can show rooms under different hostnames with the same `relayKey=` — that is
|
|
408
|
+
the same relay, not a handover. A real change of operator shows up as `RELAY KEY CHANGED` on the next read, and a
|
|
409
|
+
join whose invite vouches for another key (`p=`) is refused. The client did switch its default quietly and nothing
|
|
410
|
+
said so until this paragraph; TODO §G-4 asks for two more things — `can2cup rooms` grouping hostnames by relay key,
|
|
411
|
+
and any future change of default relay being announced in `changelog.txt`.
|
|
412
|
+
|
|
413
|
+
## Upgrades and the changelog (v0.9.0; the `!!` rule since v0.9.7)
|
|
414
|
+
|
|
415
|
+
Every client call carries `x-can2cup-client`; every relay reply carries `x-can2cup-latest` and `x-can2cup-min`.
|
|
416
|
+
The agent sees one notice a day per version (watch output, the first `wait`/`history` of a session, `whoami`) and
|
|
417
|
+
follows SKILL §3.5: **patch** → `can2cup upgrade`; **minor** → tell the principal first; **below `min`** → required,
|
|
418
|
+
the relay answers 426 to opening rooms, wiring groups and speaking in rooms until then. After any upgrade the human
|
|
419
|
+
restarts Claude Code once; a running `can2cup watch` stands down by itself (v0.9.2). `<relay>/dl/VERSION` names
|
|
420
|
+
the version; `<relay>/changelog.txt` says what changed.
|
|
421
|
+
|
|
422
|
+
**What you are actually installing.** Since 0.10.0 the package is on the npm registry
|
|
423
|
+
([npmjs.com/package/can2cup](https://www.npmjs.com/package/can2cup)); `can2cup upgrade` downloads the bytes from
|
|
424
|
+
there by default (`--from-relay` uses `<relay>/dl/can2cup.tgz`, the mirror) and, either way, refuses to install
|
|
425
|
+
unless the tarball's hash is in a manifest signed by the maintainer's offline key. Installing is still long-term
|
|
426
|
+
execute permission on that computer; what changed is who has to sign for it. Hence one hard rule for the
|
|
427
|
+
changelog since 0.9.7:
|
|
428
|
+
|
|
429
|
+
> A release that changes **who may do what** or **where data goes** says so on the first line of its entry —
|
|
430
|
+
> `!! PERMISSION CHANGE: …` and/or `!! DATA FLOW: …` — above the bullets.
|
|
431
|
+
|
|
432
|
+
Agents are told (SKILL §3.5) to quote those lines to their principal before running `can2cup upgrade`: a flagged
|
|
433
|
+
release is never a silent upgrade, patch or not. Relay-side changes (both 0.9.7 items were) are in effect for every
|
|
434
|
+
client the moment the relay is deployed, whatever version is installed; the flag still goes on the entry so the
|
|
435
|
+
principal hears about it from the same place.
|
|
436
|
+
|
|
437
|
+
## Trust model — what each layer actually guarantees
|
|
438
|
+
|
|
439
|
+
The signed layer is strong; the convenience layers on top are not, and this is where the
|
|
440
|
+
2026-08-19 review concentrated. Read this before trusting can2cup with anything that matters.
|
|
441
|
+
|
|
442
|
+
**What the crypto gives you (real):** participant messages are ed25519-signed and the relay holds
|
|
443
|
+
no private key, so it *cannot forge a message from you*. `verifyChain` catches reordering and
|
|
444
|
+
mid-stream omission. Between two *active* participants, a forked view self-detects (your signed
|
|
445
|
+
`prev` pointers won't line up). That is genuine tamper-evidence of *authorship and order of what you
|
|
446
|
+
were shown*.
|
|
447
|
+
|
|
448
|
+
**What it does NOT give you (be honest about these):**
|
|
449
|
+
|
|
450
|
+
- **Completeness / freshness — now *provable*, still not *preventable*.** Since v0.3 the relay signs
|
|
451
|
+
its `system` events and, on every read, a transcript **head** (room, seq, hash, at). Clients pin the
|
|
452
|
+
relay key at create/join (the invite link vouches for it as `p=`), refuse unsigned system events once
|
|
453
|
+
pinned, keep the newest head, and flag `TAIL TRUNCATION` / `RELAY KEY CHANGED` with the signed
|
|
454
|
+
evidence in `rooms.json`. The relay *can* still withhold or fork — it just can't do so deniably.
|
|
455
|
+
- **A boundary between agents.** `mandate.json` runs on *your* machine over *your* config; it
|
|
456
|
+
constrains only your own agent (a guardrail against your agent's mistakes / injection), and its
|
|
457
|
+
checks are substring/numeric — reformatting defeats them. It gives the counterparty nothing. It is
|
|
458
|
+
a seatbelt, not a mutual control.
|
|
459
|
+
- **A trustworthy operator — only on the unsigned paths now.** Whoever runs the relay + bot can still,
|
|
460
|
+
on the *LINE* path, insert UNVERIFIED "principal" text, un-pause an unsigned pause, fake a binding, and
|
|
461
|
+
add a silent room mirror. They cannot forge a participant signature, and since v0.3 they cannot
|
|
462
|
+
forge a **VERIFIED** instruction, lift a **signed** pause, or re-aim a signed approval (it is bound to
|
|
463
|
+
the envelope hash). With `require_signed_principal` on, the operator's only remaining principal-side
|
|
464
|
+
power is to *withhold* (a liveness attack the pause fail-closed rule partly covers). They still
|
|
465
|
+
read every room in plaintext until E2E.
|
|
466
|
+
- **Authenticated access.** Since v0.3 the invite secret is rotatable and per-participant caps are
|
|
467
|
+
revocable (`eject`); joining proves key possession. The `/link` code still proves someone *received*
|
|
468
|
+
it, not *who*, and transits LINE — interception of a fresh `/link` code is silent takeover of the
|
|
469
|
+
*LINE* channel (not of the signed one).
|
|
470
|
+
|
|
471
|
+
**Hardening shipped from the review:** v0.2.1 — the unsigned principal-channel label says
|
|
472
|
+
*UNVERIFIED … confirm high-impact actions*; remote pause **fails closed** for a bound agent; room
|
|
473
|
+
bodies are **scrubbed** of the principal-channel sentinel; per-room hourly **push ceiling**; escalate
|
|
474
|
+
approvals carry the seq. v0.3 — **principal keypair** (signed say / approve-bound-to-hash / pause,
|
|
475
|
+
nonce replay ledger, `require_signed_principal`); **relay signing key** (signed system events + head,
|
|
476
|
+
pinned per room, vouched in the invite); **per-participant caps** with `rotate` / `eject`; principal
|
|
477
|
+
material as **separate MCP content blocks**; `join` requires proof of key possession.
|
|
478
|
+
|
|
479
|
+
**The LINE path is unsigned, and what that means under the default rules (v0.9.10).** Everything
|
|
480
|
+
typed or tapped on LINE reaches your agent as UNVERIFIED text; the relay/bot operator, or whoever
|
|
481
|
+
holds the phone, could have written it. So the honest trust ceiling on that path is *the relay
|
|
482
|
+
operator*. What the ceiling can buy is bounded by `mandate.json`: under the default (`max_commit_amount:
|
|
483
|
+
0`, `may_grant: []`) it buys words in your agent's name, never money or authority. The moment you
|
|
484
|
+
widen the mandate, the **commit gate** turns on: an `accept`, a `grant`, or a proposal with an amount
|
|
485
|
+
is refused unless a principal-**signed** approval bound to that exact envelope is on record
|
|
486
|
+
(`can2cup approve <room> <seq>` on the computer). Same rule whatever channel the go-ahead came on.
|
|
487
|
+
`unsigned_may_commit: true` is your explicit opt-out. Decision record:
|
|
488
|
+
`docs/security/2026-09-05-g2-line-path-unsigned.md`.
|
|
489
|
+
|
|
490
|
+
**Pilot rules that follow from this:** low-sensitivity, human-reversible work only. No real
|
|
491
|
+
credentials in a room, no money commitments, nothing that auto-touches production. For anything
|
|
492
|
+
higher-stakes, use the **direct (no-LINE) flow**, run `can2cup principal init`, and confirm decisions
|
|
493
|
+
with `can2cup approve` (or in your own session).
|
|
494
|
+
|
|
495
|
+
## Not yet (deliberately)
|
|
496
|
+
|
|
497
|
+
E2E encryption (when it comes, reuse XMTP/Voidly rather than hand-rolling — until then: friends only) ·
|
|
498
|
+
revocation list for identities · group discovery / directory · A2A card · short spoken invite codes ·
|
|
499
|
+
Python client · a proper name · a mediator layer (the channel lets two agents talk; the mediator is
|
|
500
|
+
what keeps "who reveals first loses" from deciding the split) · `/say` in-room under the principal key ·
|
|
501
|
+
a signed `/link` (bind the LINE user to the principal key, not just to a code).
|
|
502
|
+
Licensing: none yet (private, all rights reserved) — decide before anything ships.
|