agent-yes 1.276.0 → 1.277.1

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.
Files changed (76) hide show
  1. package/README.md +10 -9
  2. package/agent-yes.config.schema.json +4 -1
  3. package/default.config.yaml +49 -0
  4. package/dist/{SUPPORTED_CLIS-G5YhpzKV.js → SUPPORTED_CLIS-DMA2T-LF.js} +2 -2
  5. package/dist/{SUPPORTED_CLIS-D-hylNcp.js → SUPPORTED_CLIS-DzVEFVKI.js} +3 -3
  6. package/dist/{agentShare-BmQZ96CJ.js → agentShare-DqT9USG5.js} +3 -3
  7. package/dist/{callback-BEgUEMY3.js → callback-D60U9zFd.js} +3 -3
  8. package/dist/{callback-B2oH6mCo.js → callback-PKIAkhNq.js} +3 -3
  9. package/dist/cli.js +11 -11
  10. package/dist/cmdDsh-BySPA9hP.js +45 -0
  11. package/dist/{cmdPs-E6fcHruY.js → cmdPs-BeUkjSOH.js} +3 -3
  12. package/dist/{cwdConflictWarn-CqN5pJ-7.js → cwdConflictWarn-DIPD6HH5.js} +1 -1
  13. package/dist/{cwdPassthroughHint-DxdqgwEN.js → cwdPassthroughHint-C0tfZlgZ.js} +1 -1
  14. package/dist/dsh-legacy-yes.js +10 -0
  15. package/dist/dsh-tui-yes.js +10 -0
  16. package/dist/dsh-yes.js +10 -0
  17. package/dist/{expose-DTxYWQeW.js → expose-Ds9aUFsq.js} +1 -1
  18. package/dist/{forkNested-CZAvQj0y.js → forkNested-Dt5XP8ZH.js} +1 -1
  19. package/dist/{hist-QkZZBp-y.js → hist-DSYaJEd3.js} +1 -1
  20. package/dist/index.js +2 -2
  21. package/dist/{initMsg-DAwRS89d.js → initMsg-CeF4QNOM.js} +1 -1
  22. package/dist/{initMsg-C8ISz_C7.js → initMsg-DejbYYjX.js} +1 -1
  23. package/dist/{nodeRuntime-C_LtZrpR.js → nodeRuntime-_zeWDISM.js} +1 -1
  24. package/dist/{notifyDaemon-D_x5ow8y.js → notifyDaemon-Bw70QCkC.js} +3 -3
  25. package/dist/{openBrowser-B50Lhaob.js → openBrowser-BNP8xByu.js} +1 -1
  26. package/dist/{parentLink-svMgYhEi.js → parentLink-Bv8ihRcu.js} +1 -1
  27. package/dist/{parentLink-i817PzXK.js → parentLink-HWunrH1s.js} +1 -1
  28. package/dist/{parentPingLoop-DP2y906e.js → parentPingLoop-DJAPbp6D.js} +4 -4
  29. package/dist/{remotes-gAI6-nv9.js → remotes-BlHTR9ow.js} +1 -1
  30. package/dist/{remotes-Cdlaj7AP.js → remotes-b6reVXzo.js} +2 -2
  31. package/dist/{rustBinary-BwgA8XyI.js → rustBinary-CxtSgRSg.js} +2 -2
  32. package/dist/{rustBinary-bXjmCsim.js → rustBinary-DQHcq1Wr.js} +2 -2
  33. package/dist/{schedule-CdEUIaEV.js → schedule-BI7-Pa2F.js} +6 -6
  34. package/dist/{serve-CvLw3YZx.js → serve-Bfxc3VWQ.js} +26 -26
  35. package/dist/{setup-BRyFMtpv.js → setup-Cp31FALW.js} +4 -4
  36. package/dist/{spawnGate-DtC_ffme.js → spawnGate-B2hA_2NQ.js} +2 -2
  37. package/dist/{spawnGate-CdewnL8m.js → spawnGate-FLwTAPW6.js} +1 -1
  38. package/dist/{subcommands-CqBONdKo.js → subcommands-CjgHXjZd.js} +2 -2
  39. package/dist/{subcommands-BtRA2slB.js → subcommands-Cohq91Dx.js} +27 -22
  40. package/dist/{systemPathLink-Df_pstm2.js → systemPathLink-Dl2gJqeg.js} +1 -1
  41. package/dist/{terminal-8jfLRLtF.js → terminal-8ipst6Wf.js} +2 -2
  42. package/dist/{todoCli-Cf0EHvfy.js → todoCli-MWrejmdM.js} +1 -1
  43. package/dist/{tray-BeIs4ffu.js → tray-BhPVpP_r.js} +1 -1
  44. package/dist/{trayApp-CnJiwYjb.js → trayApp-CL_iZpnR.js} +2 -2
  45. package/dist/trayApp-DaekNsVe.js +5 -0
  46. package/dist/{ts-CEtUijwR.js → ts-RBN1OMQ_.js} +5 -5
  47. package/dist/{versionChecker-CL6Dvn--.js → versionChecker-F_WiFsVg.js} +2 -2
  48. package/dist/{webrtcRemote-CIbq5Hdt.js → webrtcRemote-H9Hok0_R.js} +1 -1
  49. package/dist/{widget-DvSx4hyW.js → widget-DTramHv8.js} +3 -3
  50. package/dist/{workspaceConfig-DctWuxUP.js → workspaceConfig-D6QndOCz.js} +1 -1
  51. package/dist/{ws-XngU-GGe.js → ws-dgltfu3A.js} +3 -3
  52. package/dist/{ws-B830G79O.js → ws-sf8JPYPq.js} +3 -3
  53. package/lab/ui/index.html +2 -1
  54. package/lab/ui/lab/build.ts +5 -4
  55. package/lab/ui/lab/index.md +14 -14
  56. package/lab/ui/lab/posts/2026-06-15-e2ee-share-links.html +128 -132
  57. package/lab/ui/lab/posts/2026-07-22-ay-callback.html +214 -167
  58. package/lab/ui/lab/posts/2026-08-11-an-address-space-for-agents.md +9 -9
  59. package/lab/ui/lab/posts/2026-08-11-no-daemon-owns-your-agents.md +13 -13
  60. package/lab/ui/lab/posts/2026-08-11-slack-term-safe-writes.md +26 -26
  61. package/lab/ui/lab/posts/2026-08-11-the-terminal-is-the-api.md +10 -10
  62. package/lab/ui/landing.html +2 -1
  63. package/package.json +4 -1
  64. package/ts/buildRustArgs.spec.ts +3 -0
  65. package/ts/cmdDsh.spec.ts +90 -0
  66. package/ts/cmdDsh.ts +62 -0
  67. package/ts/histStore.spec.ts +8 -4
  68. package/ts/histStore.ts +2 -7
  69. package/ts/identity.spec.ts +4 -1
  70. package/ts/initMsg.spec.ts +12 -2
  71. package/ts/invokedCli.spec.ts +1 -0
  72. package/ts/postbuild.ts +11 -8
  73. package/ts/subcommands.ts +6 -0
  74. package/ts/systemPathLink.spec.ts +12 -2
  75. package/ts/systemPathLink.ts +5 -1
  76. package/dist/trayApp-CUuEodHT.js +0 -5
@@ -19,7 +19,7 @@ That sounds like tooling. It is closer to a change of category.
19
19
 
20
20
  ## Why that is not a small difference
21
21
 
22
- Every agent CLI ships with the same assumption baked in: *a human is sitting right here.* It asks
22
+ Every agent CLI ships with the same assumption baked in: _a human is sitting right here._ It asks
23
23
  before it edits. It draws a full-screen TUI for eyes that are watching. It dies when the terminal
24
24
  closes. Those are reasonable defaults, and they are also a ceiling — they cap an agent at exactly
25
25
  one person's attention, and attention is the scarcest thing in the building.
@@ -27,7 +27,7 @@ one person's attention, and attention is the scarcest thing in the building.
27
27
  agent-yes removes the assumption in the least invasive way available: it does not fork the CLIs, it
28
28
  does not need their APIs, and it does not wait for a vendor to ship a daemon mode. It runs the real
29
29
  binary in a pseudo-terminal and answers the questions a human would have answered. The terminal
30
- *is* the integration surface.
30
+ _is_ the integration surface.
31
31
 
32
32
  What follows is the interesting part. Once one agent no longer needs a human in the loop, each
33
33
  capability forces the next:
@@ -48,16 +48,16 @@ because paying those bills honestly is more interesting than the original trick.
48
48
  The system reads cleanly top-down, and the
49
49
  [interactive architecture map](https://agent-yes.com/architecture.html) draws the same boxes as a graph:
50
50
 
51
- | Plane | What lives there |
52
- | --- | --- |
53
- | **① Agent core** | The PTY wrapper. Spawns any agent CLI, matches ready / enter / fatal patterns, restarts on crash, resumes sessions, propagates terminal size. Rust is the primary binary; TypeScript is the reference implementation. |
54
- | **② Local fabric** | `pids.jsonl` (every agent, both runtimes), a FIFO per pid for stdin, a raw PTY log per agent for stdout. Files, not a daemon. |
55
- | **③ Exposure** | One transport-agnostic handler, `apiFetch(req) → Response`. HTTP and the WebRTC bridge are both just callers; port tunnels and libp2p peers fold in next. |
56
- | **④ Trust & discovery** | Signaling that is a rendezvous only, an HKDF key split so the relay never holds a key, and signed capability tokens that carry their own scope. |
57
- | **⑤ Reach surfaces** | The browser console, the CLI on another box, embeddable widgets, a system tray, peers. |
51
+ | Plane | What lives there |
52
+ | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
53
+ | **① Agent core** | The PTY wrapper. Spawns any agent CLI, matches ready / enter / fatal patterns, restarts on crash, resumes sessions, propagates terminal size. Rust is the primary binary; TypeScript is the reference implementation. |
54
+ | **② Local fabric** | `pids.jsonl` (every agent, both runtimes), a FIFO per pid for stdin, a raw PTY log per agent for stdout. Files, not a daemon. |
55
+ | **③ Exposure** | One transport-agnostic handler, `apiFetch(req) → Response`. HTTP and the WebRTC bridge are both just callers; port tunnels and libp2p peers fold in next. |
56
+ | **④ Trust & discovery** | Signaling that is a rendezvous only, an HKDF key split so the relay never holds a key, and signed capability tokens that carry their own scope. |
57
+ | **⑤ Reach surfaces** | The browser console, the CLI on another box, embeddable widgets, a system tray, peers. |
58
58
 
59
59
  The seam that holds it together is plane ③. Because `Bun.serve` and the WebRTC bridge already call
60
- the *same in-process* `apiFetch`, a new transport is a new caller and never a new control plane.
60
+ the _same in-process_ `apiFetch`, a new transport is a new caller and never a new control plane.
61
61
  That is what turns "three stacks" back into one system.
62
62
 
63
63
  ## What is actually true today
@@ -81,12 +81,12 @@ about the four places the current system is still cluttered.
81
81
 
82
82
  ## The ecosystem
83
83
 
84
- agent-yes is the fleet. Around it sit tools that give the fleet somewhere to *reach* — each a
84
+ agent-yes is the fleet. Around it sit tools that give the fleet somewhere to _reach_ — each a
85
85
  standalone CLI in its own right, each usable without the others:
86
86
 
87
- | | |
88
- | --- | --- |
89
- | **[agent-yes](https://github.com/snomiao/agent-yes)** | run, watch, message and delegate to a fleet of AI coding agents |
87
+ | | |
88
+ | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
89
+ | **[agent-yes](https://github.com/snomiao/agent-yes)** | run, watch, message and delegate to a fleet of AI coding agents |
90
90
  | **[slack-term](https://github.com/snomiao/slack-term)** | the human-comms surface — read and write Slack from a terminal, with a confirm gate built for operators who are sometimes machines |
91
91
 
92
92
  The connective tissue is a shared stance rather than a shared library: file-based state over
@@ -3,148 +3,144 @@ title: End-to-end encryption for agent-yes share links
3
3
  tag: Security
4
4
  summary: How share links stay private even if the signaling relay is compromised: an HKDF key split, AES-256-GCM with per-connection keys, and a fail-closed handshake.
5
5
  ---
6
+
6
7
  <p class="lede">
7
- agent-yes share links now run an end-to-end-encrypted protocol. The signaling server that
8
- introduces your browser to your machine never sees a key — so even if it is fully
9
- compromised, it cannot read your terminal, type into your agents, or spawn new ones.
10
- </p>
8
+ agent-yes share links now run an end-to-end-encrypted protocol. The signaling server that
9
+ introduces your browser to your machine never sees a key — so even if it is fully compromised, it
10
+ cannot read your terminal, type into your agents, or spawn new ones.
11
+ </p>
11
12
 
12
- <h2>What a share link does</h2>
13
- <p>
14
- When you run <code>ay serve --webrtc</code>, your machine connects to a tiny Cloudflare
15
- signaling server and waits for a browser to open the printed link on
16
- <code>agent-yes.com</code>. The signaling server is only a <em>matchmaker</em>: it relays
17
- the WebRTC handshake (SDP + ICE) so the two sides can find each other, then your terminal
18
- traffic flows <strong>peer-to-peer</strong> over an encrypted WebRTC DataChannel — it never
19
- passes through the server. The room link looks like:
20
- </p>
21
- <pre><code>https://agent-yes.com/#&lt;room&gt;:e1.&lt;secret&gt;</code></pre>
13
+ <h2>What a share link does</h2>
14
+ <p>
15
+ When you run <code>ay serve --webrtc</code>, your machine connects to a tiny Cloudflare signaling
16
+ server and waits for a browser to open the printed link on <code>agent-yes.com</code>. The
17
+ signaling server is only a <em>matchmaker</em>: it relays the WebRTC handshake (SDP + ICE) so the
18
+ two sides can find each other, then your terminal traffic flows <strong>peer-to-peer</strong> over
19
+ an encrypted WebRTC DataChannel — it never passes through the server. The room link looks like:
20
+ </p>
21
+ <pre><code>https://agent-yes.com/#&lt;room&gt;:e1.&lt;secret&gt;</code></pre>
22
22
 
23
- <h2>The threat: "what if the matchmaker is hacked?"</h2>
24
- <p>
25
- Previously, the link's token did double duty — it was both the value the server used to
26
- match peers <em>and</em> the only shared secret. That meant the server
27
- <em>saw the secret</em>. A compromised signaling server could therefore impersonate a
28
- browser or man-in-the-middle the connection, and a breach would expose <em>every</em> user's
29
- token at once. The bar we want instead:
30
- </p>
31
- <div class="note">
32
- A fully compromised signaling server (or an active network attacker who can rewrite the
33
- relayed handshake) may slow you down or learn metadata — but must
34
- <strong>never</strong> read terminal I/O, inject input, spawn agents, or recover any key.
35
- </div>
23
+ <h2>The threat: "what if the matchmaker is hacked?"</h2>
24
+ <p>
25
+ Previously, the link's token did double duty — it was both the value the server used to match
26
+ peers <em>and</em> the only shared secret. That meant the server <em>saw the secret</em>. A
27
+ compromised signaling server could therefore impersonate a browser or man-in-the-middle the
28
+ connection, and a breach would expose <em>every</em> user's token at once. The bar we want
29
+ instead:
30
+ </p>
31
+ <div class="note">
32
+ A fully compromised signaling server (or an active network attacker who can rewrite the relayed
33
+ handshake) may slow you down or learn metadata — but must
34
+ <strong>never</strong> read terminal I/O, inject input, spawn agents, or recover any key.
35
+ </div>
36
36
 
37
- <h2>One secret, two derived keys (the HKDF split)</h2>
38
- <p>
39
- The link still carries one 32-byte secret <code>S</code>. We run it through HKDF-SHA256 to
40
- derive two unrelated values:
41
- </p>
42
- <pre><code>authToken = HKDF(S, "ay/ay-e2e-1/auth") → the ONLY value the server sees
37
+ <h2>One secret, two derived keys (the HKDF split)</h2>
38
+ <p>
39
+ The link still carries one 32-byte secret <code>S</code>. We run it through HKDF-SHA256 to derive
40
+ two unrelated values:
41
+ </p>
42
+ <pre><code>authToken = HKDF(S, "ay/ay-e2e-1/auth") → the ONLY value the server sees
43
43
  e2e keys = HKDF(S, "ay/ay-e2e-1/key/…") → never leave your machine or browser</code></pre>
44
- <p>
45
- HKDF is one-way, so from <code>authToken</code> the server cannot recover <code>S</code> or
46
- the encryption keys. The actual terminal frames are sealed with AES-256-GCM under keys the
47
- server has never seen. Because the link is pasted directly between the two endpoints, both
48
- sides already share <code>S</code> without the server ever being told it.
49
- </p>
44
+ <p>
45
+ HKDF is one-way, so from <code>authToken</code> the server cannot recover <code>S</code> or the
46
+ encryption keys. The actual terminal frames are sealed with AES-256-GCM under keys the server has
47
+ never seen. Because the link is pasted directly between the two endpoints, both sides already
48
+ share <code>S</code> without the server ever being told it.
49
+ </p>
50
50
 
51
- <h2>The subtle part nobody gets right: nonce reuse</h2>
52
- <p>
53
- AES-GCM is catastrophic if a <code>(key, nonce)</code> pair ever repeats — it leaks
54
- plaintext <em>and</em> lets an attacker forge messages. The naive design (one key per room
55
- plus a per-connection counter) reuses nonces across reconnects, host restarts, and multiple
56
- browsers in the same room. Our fix: derive the encryption keys
57
- <strong>per connection</strong> by folding the live DTLS handshake fingerprint into HKDF as
58
- the salt. Every session and every peer therefore gets fresh keys, so a counter that restarts
59
- at 0 is always paired with a never-before-used key. We also use
60
- <strong>directional keys</strong>
61
- (host→client and client→host are different keys), so the two senders never share a nonce
62
- space.
63
- </p>
51
+ <h2>The subtle part nobody gets right: nonce reuse</h2>
52
+ <p>
53
+ AES-GCM is catastrophic if a <code>(key, nonce)</code> pair ever repeats — it leaks plaintext
54
+ <em>and</em> lets an attacker forge messages. The naive design (one key per room plus a
55
+ per-connection counter) reuses nonces across reconnects, host restarts, and multiple browsers in
56
+ the same room. Our fix: derive the encryption keys <strong>per connection</strong> by folding the
57
+ live DTLS handshake fingerprint into HKDF as the salt. Every session and every peer therefore gets
58
+ fresh keys, so a counter that restarts at 0 is always paired with a never-before-used key. We also
59
+ use
60
+ <strong>directional keys</strong>
61
+ (host→client and client→host are different keys), so the two senders never share a nonce space.
62
+ </p>
64
63
 
65
- <h2>Binding to the real connection (anti-MITM), done right</h2>
66
- <p>
67
- We hash both peers' DTLS fingerprints — plus the handshake role and ICE ufrag — from the
68
- negotiated session, and use that hash as <strong>both</strong> the HKDF salt for the keys
69
- <strong>and</strong> the authenticated data on every frame. On top of that, the channel runs
70
- a mandatory, bidirectional <strong>key-confirmation handshake</strong> (each side sends a
71
- fresh challenge nonce and must see it echoed back) that has to complete in
72
- <em>both</em> directions before a single byte of terminal I/O is processed. If a relay sat
73
- in the middle, the fingerprints differ, the keys differ, confirmation fails, and the
74
- connection <span class="no">closes</span> — there is no "connected but unverified" window
75
- and no silent fallback.
76
- </p>
64
+ <h2>Binding to the real connection (anti-MITM), done right</h2>
65
+ <p>
66
+ We hash both peers' DTLS fingerprints — plus the handshake role and ICE ufrag — from the
67
+ negotiated session, and use that hash as <strong>both</strong> the HKDF salt for the keys
68
+ <strong>and</strong> the authenticated data on every frame. On top of that, the channel runs a
69
+ mandatory, bidirectional <strong>key-confirmation handshake</strong> (each side sends a fresh
70
+ challenge nonce and must see it echoed back) that has to complete in <em>both</em> directions
71
+ before a single byte of terminal I/O is processed. If a relay sat in the middle, the fingerprints
72
+ differ, the keys differ, confirmation fails, and the connection <span class="no">closes</span> —
73
+ there is no "connected but unverified" window and no silent fallback.
74
+ </p>
77
75
 
78
- <h2>Replay, reordering, and forged aborts</h2>
79
- <p>
80
- The DataChannel is reliable and ordered, and we add a mandatory monotonic frame counter,
81
- 128-bit random request ids, and per-stream sequence numbers. So a captured frame can't be
82
- replayed to re-run a command, a stale "abort" can't cancel a fresh request, and a truncated
83
- stream can't masquerade as a complete response.
84
- </p>
76
+ <h2>Replay, reordering, and forged aborts</h2>
77
+ <p>
78
+ The DataChannel is reliable and ordered, and we add a mandatory monotonic frame counter, 128-bit
79
+ random request ids, and per-stream sequence numbers. So a captured frame can't be replayed to
80
+ re-run a command, a stale "abort" can't cancel a fresh request, and a truncated stream can't
81
+ masquerade as a complete response.
82
+ </p>
85
83
 
86
- <h2>Fail closed, always</h2>
87
- <p>
88
- Encrypted links carry a version marker (<code>#room:e1.&lt;secret&gt;</code>). The grammar
89
- is strict: anything that looks like a marker but isn't exactly <code>e1.</code> + 64 hex is
90
- rejected, never quietly treated as a legacy link. Legacy plaintext is hard-disabled in the
91
- client, the host refuses to bridge a plaintext channel for an encrypted room, and on any
92
- version skew, missing marker, bad tag, fingerprint mismatch, or confirmation timeout we
93
- <span class="no">refuse</span> rather than downgrade.
94
- </p>
84
+ <h2>Fail closed, always</h2>
85
+ <p>
86
+ Encrypted links carry a version marker (<code>#room:e1.&lt;secret&gt;</code>). The grammar is
87
+ strict: anything that looks like a marker but isn't exactly <code>e1.</code> + 64 hex is rejected,
88
+ never quietly treated as a legacy link. Legacy plaintext is hard-disabled in the client, the host
89
+ refuses to bridge a plaintext channel for an encrypted room, and on any version skew, missing
90
+ marker, bad tag, fingerprint mismatch, or confirmation timeout we
91
+ <span class="no">refuse</span> rather than downgrade.
92
+ </p>
95
93
 
96
- <h2>One implementation, no dependencies</h2>
97
- <p>
98
- The crypto is a single WebCrypto module shared by both ends — the browser imports it, and
99
- the host (running on Bun, which ships WebCrypto) bundles the exact same file. One
100
- implementation means the two sides can't drift apart, and there are no new dependencies. A
101
- test suite pins the key derivation, the frame format, and every fail-closed path.
102
- </p>
94
+ <h2>One implementation, no dependencies</h2>
95
+ <p>
96
+ The crypto is a single WebCrypto module shared by both ends — the browser imports it, and the host
97
+ (running on Bun, which ships WebCrypto) bundles the exact same file. One implementation means the
98
+ two sides can't drift apart, and there are no new dependencies. A test suite pins the key
99
+ derivation, the frame format, and every fail-closed path.
100
+ </p>
103
101
 
104
- <h2>What a hacked server gets — and doesn't</h2>
105
- <table>
106
- <tr>
107
- <th></th>
108
- <th>Before</th>
109
- <th>Now</th>
110
- </tr>
111
- <tr>
112
- <td>Read your terminal I/O</td>
113
- <td class="no">possible (sees the secret)</td>
114
- <td class="ok">no — keys never reach the server</td>
115
- </tr>
116
- <tr>
117
- <td>Inject input / spawn agents</td>
118
- <td class="no">possible</td>
119
- <td class="ok">no — frames it forges fail decryption</td>
120
- </tr>
121
- <tr>
122
- <td>Harvest every user's secret in a breach</td>
123
- <td class="no">yes (stores raw tokens)</td>
124
- <td class="ok">no — only one-way authTokens</td>
125
- </tr>
126
- <tr>
127
- <td>DoS / see metadata (rooms, timing, IPs)</td>
128
- <td class="no">yes</td>
129
- <td class="no">still yes</td>
130
- </tr>
131
- </table>
102
+ <h2>What a hacked server gets — and doesn't</h2>
103
+ <table>
104
+ <tr>
105
+ <th></th>
106
+ <th>Before</th>
107
+ <th>Now</th>
108
+ </tr>
109
+ <tr>
110
+ <td>Read your terminal I/O</td>
111
+ <td class="no">possible (sees the secret)</td>
112
+ <td class="ok">no — keys never reach the server</td>
113
+ </tr>
114
+ <tr>
115
+ <td>Inject input / spawn agents</td>
116
+ <td class="no">possible</td>
117
+ <td class="ok">no — frames it forges fail decryption</td>
118
+ </tr>
119
+ <tr>
120
+ <td>Harvest every user's secret in a breach</td>
121
+ <td class="no">yes (stores raw tokens)</td>
122
+ <td class="ok">no — only one-way authTokens</td>
123
+ </tr>
124
+ <tr>
125
+ <td>DoS / see metadata (rooms, timing, IPs)</td>
126
+ <td class="no">yes</td>
127
+ <td class="no">still yes</td>
128
+ </tr>
129
+ </table>
132
130
 
133
- <h2>What changed for you</h2>
134
- <p>
135
- Old share links rotate to a fresh encrypted room automatically on upgrade — re-open the new
136
- printed link (or <code>rm ~/.agent-yes/.share-room</code> to force a rotation). The
137
- signaling server got one small additive change (it pins a protocol version) but still never
138
- sees a key.
139
- </p>
131
+ <h2>What changed for you</h2>
132
+ <p>
133
+ Old share links rotate to a fresh encrypted room automatically on upgrade — re-open the new
134
+ printed link (or <code>rm ~/.agent-yes/.share-room</code> to force a rotation). The signaling
135
+ server got one small additive change (it pins a protocol version) but still never sees a key.
136
+ </p>
140
137
 
141
- <h2>Honest limitations</h2>
142
- <p>
143
- End-to-end encryption protects the <em>channel</em>, not the <em>capability</em>: anyone you
144
- hand the full link to gets control, so treat it like an SSH key. The server can still deny
145
- service and observe metadata (which rooms exist, connection timing, IPs via ICE). The
146
- channel binding uses DTLS fingerprints from the SDP rather than a true RFC 5705 keying
147
- exporter (a limitation of the host's WebRTC stack today), and the secret still sits in your
148
- browser's local storage until it ages out. Forward secrecy / rekeying, and the separate
149
- codehost transport, are future work.
150
- </p>
138
+ <h2>Honest limitations</h2>
139
+ <p>
140
+ End-to-end encryption protects the <em>channel</em>, not the <em>capability</em>: anyone you hand
141
+ the full link to gets control, so treat it like an SSH key. The server can still deny service and
142
+ observe metadata (which rooms exist, connection timing, IPs via ICE). The channel binding uses
143
+ DTLS fingerprints from the SDP rather than a true RFC 5705 keying exporter (a limitation of the
144
+ host's WebRTC stack today), and the secret still sits in your browser's local storage until it
145
+ ages out. Forward secrecy / rekeying, and the separate codehost transport, are future work.
146
+ </p>