@particle-academy/agent-integrations 0.45.0 → 0.47.0

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 (110) hide show
  1. package/CHANGELOG.md +835 -0
  2. package/LICENSE +21 -0
  3. package/README.md +54 -6
  4. package/dist/bridges/artboard.d.cts +3 -3
  5. package/dist/bridges/artboard.d.ts +3 -3
  6. package/dist/bridges/catalog.d.cts +3 -3
  7. package/dist/bridges/catalog.d.ts +3 -3
  8. package/dist/bridges/charts.d.cts +3 -3
  9. package/dist/bridges/charts.d.ts +3 -3
  10. package/dist/bridges/cms.d.cts +3 -3
  11. package/dist/bridges/cms.d.ts +3 -3
  12. package/dist/bridges/code.d.cts +3 -3
  13. package/dist/bridges/code.d.ts +3 -3
  14. package/dist/bridges/doc.d.cts +3 -3
  15. package/dist/bridges/doc.d.ts +3 -3
  16. package/dist/bridges/features.d.cts +3 -3
  17. package/dist/bridges/features.d.ts +3 -3
  18. package/dist/bridges/files.d.cts +3 -3
  19. package/dist/bridges/files.d.ts +3 -3
  20. package/dist/bridges/flow.d.cts +3 -3
  21. package/dist/bridges/flow.d.ts +3 -3
  22. package/dist/bridges/forms.d.cts +3 -3
  23. package/dist/bridges/forms.d.ts +3 -3
  24. package/dist/bridges/git.d.cts +3 -3
  25. package/dist/bridges/git.d.ts +3 -3
  26. package/dist/bridges/grid.d.cts +3 -3
  27. package/dist/bridges/grid.d.ts +3 -3
  28. package/dist/bridges/map.d.cts +3 -3
  29. package/dist/bridges/map.d.ts +3 -3
  30. package/dist/bridges/navigation.d.cts +127 -0
  31. package/dist/bridges/navigation.d.ts +127 -0
  32. package/dist/bridges/passkeys.d.cts +4 -4
  33. package/dist/bridges/passkeys.d.ts +4 -4
  34. package/dist/bridges/scene.d.cts +3 -3
  35. package/dist/bridges/scene.d.ts +3 -3
  36. package/dist/bridges/screens.d.cts +3 -3
  37. package/dist/bridges/screens.d.ts +3 -3
  38. package/dist/bridges/sheets.d.cts +3 -3
  39. package/dist/bridges/sheets.d.ts +3 -3
  40. package/dist/bridges/slides.d.cts +3 -3
  41. package/dist/bridges/slides.d.ts +3 -3
  42. package/dist/bridges/terminal.d.cts +3 -3
  43. package/dist/bridges/terminal.d.ts +3 -3
  44. package/dist/bridges/tui.d.cts +2 -2
  45. package/dist/bridges/tui.d.ts +2 -2
  46. package/dist/bridges/whiteboard.d.cts +3 -3
  47. package/dist/bridges/whiteboard.d.ts +3 -3
  48. package/dist/bridges-navigation.cjs +391 -0
  49. package/dist/bridges-navigation.cjs.map +1 -0
  50. package/dist/bridges-navigation.js +7 -0
  51. package/dist/bridges-navigation.js.map +1 -0
  52. package/dist/bridges-passkeys.cjs.map +1 -1
  53. package/dist/bridges-passkeys.js +1 -1
  54. package/dist/{chunk-OZBUPO4K.js → chunk-5W34X4EY.js} +263 -58
  55. package/dist/chunk-5W34X4EY.js.map +1 -0
  56. package/dist/{chunk-PWYEW6NL.js → chunk-G6CRHFWU.js} +2 -2
  57. package/dist/{chunk-PWYEW6NL.js.map → chunk-G6CRHFWU.js.map} +1 -1
  58. package/dist/chunk-JGJHPH65.js +271 -0
  59. package/dist/chunk-JGJHPH65.js.map +1 -0
  60. package/dist/dom-navigation/index.d.cts +164 -0
  61. package/dist/dom-navigation/index.d.ts +164 -0
  62. package/dist/dom-navigation/script.d.cts +3 -0
  63. package/dist/dom-navigation/script.d.ts +3 -0
  64. package/dist/dom-navigation-script.cjs +11 -0
  65. package/dist/dom-navigation-script.cjs.map +1 -0
  66. package/dist/dom-navigation-script.js +9 -0
  67. package/dist/dom-navigation-script.js.map +1 -0
  68. package/dist/dom-navigation.cjs +387 -0
  69. package/dist/dom-navigation.cjs.map +1 -0
  70. package/dist/dom-navigation.js +370 -0
  71. package/dist/dom-navigation.js.map +1 -0
  72. package/dist/index.cjs +31 -31
  73. package/dist/index.cjs.map +1 -1
  74. package/dist/index.d.cts +10 -125
  75. package/dist/index.d.ts +10 -125
  76. package/dist/index.js +12 -275
  77. package/dist/index.js.map +1 -1
  78. package/dist/mcp/index.d.cts +5 -5
  79. package/dist/mcp/index.d.ts +5 -5
  80. package/dist/mcp/stdio.d.cts +3 -3
  81. package/dist/mcp/stdio.d.ts +3 -3
  82. package/dist/presence/index.d.cts +1 -1
  83. package/dist/presence/index.d.ts +1 -1
  84. package/dist/relay-server/index.d.cts +110 -26
  85. package/dist/relay-server/index.d.ts +110 -26
  86. package/dist/relay-server-cli.cjs +374 -89
  87. package/dist/relay-server-cli.cjs.map +1 -1
  88. package/dist/relay-server-cli.js +120 -38
  89. package/dist/relay-server-cli.js.map +1 -1
  90. package/dist/relay-server.cjs +255 -51
  91. package/dist/relay-server.cjs.map +1 -1
  92. package/dist/relay-server.js +1 -1
  93. package/dist/{server-BIcA3ZtK.d.cts → server-1y0n61R5.d.cts} +2 -2
  94. package/dist/{server-DnUyeavY.d.ts → server-vq6qVl3T.d.ts} +2 -2
  95. package/dist/sharing/index.d.cts +3 -3
  96. package/dist/sharing/index.d.ts +3 -3
  97. package/dist/sheets-adapter.d.cts +3 -3
  98. package/dist/sheets-adapter.d.ts +3 -3
  99. package/dist/{tool-host-LVBgCN5m.d.cts → tool-host-D67N2a2h.d.cts} +1 -1
  100. package/dist/{tool-host-Wo0pkY83.d.ts → tool-host-LYfdPE8u.d.ts} +1 -1
  101. package/dist/{types-EcElKOlk.d.cts → types-DP5yQZQU.d.cts} +1 -1
  102. package/dist/{types-BxNRKSVd.d.ts → types-VX_9je7w.d.ts} +1 -1
  103. package/dist/{types-C2DtnHIn.d.cts → types-gzIpsUDE.d.cts} +1 -1
  104. package/dist/{types-C2DtnHIn.d.ts → types-gzIpsUDE.d.ts} +1 -1
  105. package/dist/undo/index.d.cts +2 -2
  106. package/dist/undo/index.d.ts +2 -2
  107. package/docs/relay-protocol.md +41 -5
  108. package/docs/relay-server.md +102 -15
  109. package/package.json +41 -3
  110. package/dist/chunk-OZBUPO4K.js.map +0 -1
package/CHANGELOG.md ADDED
@@ -0,0 +1,835 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ > **Pre-1.0:** breaking changes may land in MINOR releases — read the entry, not
9
+ > the version number. This changelog begins at 0.32.0; for earlier history see
10
+ > the git log.
11
+
12
+ ## [Unreleased]
13
+
14
+ ## [0.47.0] - 2026-10-21
15
+
16
+ **What to do — nothing, unless you want to drive a page you don't run in.** One
17
+ type widened (see Changed); implementations of `NavigationBridgeAdapter` keep
18
+ compiling.
19
+
20
+ ### Added
21
+
22
+ - **`./dom-navigation` — the co-browse DOM walker, framework-free.** It lived
23
+ only inside px-ui-sandbox's `CoBrowseProvider`. Now: `createDomNavigationAgent()`
24
+ for code in the page (`describe`, `read`, `setField`, `click`, `submit`,
25
+ `rectFor`, `visit`, `scrollTo`, … plus a serialisable `call(method, args)`),
26
+ and `createRemoteNavigationAdapter({ transport, confirm?, getRevision?, screenId? })`
27
+ for a host process that reaches the page over `executeJavaScript`, a
28
+ MessagePort or anything else carrying `{ call(method, args): Promise<unknown> }`.
29
+ `domNavigationCallScript(method, args)` builds the expression for
30
+ `executeJavaScript`. Results from the page are coerced to the declared shapes
31
+ and malformed entries dropped: the page is hostile input.
32
+ - **`./dom-navigation/script` — `DOM_NAVIGATION_SCRIPT`**, the in-page half as a
33
+ self-contained IIFE string with no module system, for
34
+ `executeJavaScriptInIsolatedWorld`. It installs `globalThis.__fancyDomNavigation`
35
+ (`{ protocol: 1, call }`); re-injecting keeps the existing instance.
36
+ - **`./bridges/navigation`** — `registerNavigationBridge` without the root
37
+ barrel, which is the React UI. A host process no longer has to load React to
38
+ register the bridge.
39
+
40
+ ### Changed
41
+
42
+ - **`NavigationBridgeAdapter` methods may return a Promise**, and the bridge now
43
+ awaits every adapter call. A host driving a page from another process can only
44
+ answer asynchronously; before this, an async `describe()` made `page_describe`
45
+ fail. Sync adapters behave exactly as before. **Only if you CALL adapter
46
+ methods yourself** (rather than implementing them) do you need an `await`,
47
+ because their return types now include `Promise`.
48
+
49
+ ### Fixed
50
+
51
+ - **Native controls with an `id` but no `name` now get the id as their handle.**
52
+ The walker's documented order is `data-co-handle` → `name` → `id`, but
53
+ `button.name` / `a.name` / `input.name` is `""`, not null, so the `??` chain
54
+ never reached the id and those elements got a synthetic `role#index`. Empty
55
+ attributes now count as absent. Handles for such elements change from
56
+ `button#3` to their id; re-run `page_describe`, which an agent does anyway.
57
+ - **Sensitive text no longer leaks through `page_read` or a label.** A
58
+ `<textarea data-private>`'s content is text, so it appeared in `read()` and
59
+ could become the field's own accessible name. Text inside a sensitive element
60
+ is now skipped in both.
61
+
62
+ - **`CHANGELOG.md` is now in the published tarball.** `files` did not whitelist it, so npm never shipped it — and this package puts breaking changes in MINOR releases and tells you in the README to read the entry before taking one. The instruction existed for the author, who has the file, and not for the consumer, who is the only one being instructed. Nothing for you to do; the file simply arrives from this release on.
63
+
64
+ ### Security
65
+
66
+ - `source-map-js` is pinned forward to `^1.2.2` via `overrides`. Versions up to
67
+ 1.2.1 allow an event-loop denial of service through indexed source-map section
68
+ offsets, and it arrives here transitively through the build toolchain.
69
+ **Nothing for a consumer to do, and no runtime change**: an npm package does
70
+ not ship a lockfile, so this governs builds OF this repo, not anything
71
+ installed FROM it. Recorded rather than left silent because the override it
72
+ sits beside — `shell-quote` `^1.9.0`, added for an earlier advisory — was
73
+ carried with no note of why, and had drifted back inside the vulnerable range
74
+ before anyone looked.
75
+
76
+ ## [0.46.0] - 2026-09-13
77
+
78
+ **What to do — for most consumers, nothing.** The Node relay gains a long-poll
79
+ route, a CORS list that works, and a `410` on the one route that still said
80
+ `401`. Check these four cases:
81
+
82
+ 1. **You run `agent-integrations-relay` behind Cloudflare** (or any CDN that
83
+ resets long streams): upgrade the relay, and install
84
+ `@particle-academy/fancy-cf-relay` in the browser app so its receive leg
85
+ polls. Agents on `mcp-relay-client` poll already.
86
+ 2. **Your relay config is invalid** — `--cors` / `CORS_ALLOW_ORIGIN` /
87
+ `corsAllowOrigin` mixing `*` with origins, empty, `null`, or an entry with
88
+ a path. The CLI now exits `2` at startup and `createNodeRelay` throws. Fix the
89
+ value; before, it produced a header no browser accepted.
90
+ 3. **Your client treats `401` from `GET /{session}/events` as "the page
91
+ closed"**: that answer is now `410`. Handle `410` as the end of the session;
92
+ `401` still means a wrong or missing token.
93
+ 4. **Your app imports this package's React entries without React in its own
94
+ dependencies**, relying on npm to install the peer: add `react` and
95
+ `react-dom` (^19) yourself. An app rendering React components has them.
96
+
97
+ ### Added
98
+
99
+ - **Long-poll receive leg on the Node relay: `GET {prefix}/{session}/poll`.**
100
+ Cloudflare's HTTP/3 edge resets long-lived SSE streams, so a Node relay behind
101
+ it had no working receive leg — `/poll` answered `404`, while the PHP relay in
102
+ px-ui-sandbox, `fancy-cf-relay` (browser) and `mcp-relay-client` (agent) all
103
+ speak poll. Same contract as the PHP relay:
104
+ `?token&direction=inbound|outbound&wait=<ms>&subscriber=<id>[&client]` →
105
+ `200 { subscriber, frames: ["<raw frame>", …] }`. `wait` is clamped to
106
+ 0–25000 ms (default 20000) and returns early the moment a frame is queued.
107
+ Frames queue between polls; an aborted poll takes nothing; a poller idle for
108
+ 60 s is dropped (`pollIdleMs`), and an agent's departure is announced to the
109
+ page with `notifications/peer_left`. An outbound poller is announced once, not
110
+ once per poll. Reply scoping by `client` works as on SSE, and a poll cannot
111
+ adopt a streaming subscriber's id. The first three tests mirror px-ui-sandbox's
112
+ `AgentRelayPollTest.php` case for case.
113
+ - `RelayBroker#poll()` and the `PollResult` type, for custom adapters;
114
+ `relay.poll` on `createNodeRelay()` for piecemeal mounting —
115
+ `app.get("/mcp-relay/:s/poll", relay.poll)`.
116
+ - A gone session answers `410 {"error":"session_gone"}` here too.
117
+
118
+ ### Changed
119
+
120
+ - **`--cors` / `corsAllowOrigin` take a real origin list.** The option was
121
+ documented as "comma-separated origins (or `*`)" and sent the string verbatim,
122
+ so `--cors "https://particle.academy,https://www.particle.academy"` produced one
123
+ `Access-Control-Allow-Origin` header that no browser matches. Now a listed
124
+ request `Origin` is echoed back, every response carries `Vary: Origin`, and an
125
+ unlisted origin gets no allow-origin header at all. `*` still sends `*`.
126
+ Entries are normalised to what a browser sends (`HTTPS://Example.com:443/` →
127
+ `https://example.com`), `--cors` may be repeated, and an invalid value fails
128
+ at startup (see "What to do" 2). **Do:** nothing, unless your value was
129
+ invalid.
130
+
131
+ - **Settings precedence is documented and reported: flag, then env var, then
132
+ default.** It was always flag-first — so `CORS_ALLOW_ORIGIN` did nothing
133
+ whenever a start script passed `--cors`, while a deploy README said to set it
134
+ to override the start script. Flag-first is kept (it is what every setting
135
+ does, and what `docker run … --host 127.0.0.1` against the Dockerfile's
136
+ `HOST=0.0.0.0` needs); what changed is that a flag overriding a different env
137
+ var now prints `[relay] WARNING: CORS_ALLOW_ORIGIN=… is ignored: --cors … was
138
+ passed`, and the startup line names the CORS policy's source. An env var set
139
+ to the empty string counts as unset (it used to become an empty header).
140
+ **Do:** nothing; if you see the warning, edit the flag, not the env var.
141
+
142
+ - **`react` and `react-dom` are optional peers.** Headless hosts — a stdio MCP
143
+ server importing `/mcp` and `/mcp/stdio`, a Node relay, a bridge on a server —
144
+ no longer get React installed through this package. Measured on clean
145
+ installs: `agent-integrations` alone is 6 packages with 0.45.0 (react,
146
+ react-dom, scheduler among them) and 3 with 0.46.0, and all 27 subpaths
147
+ outside the React list below import (ESM) and require (CJS) with no React
148
+ present. React stays declared with its range, so a UI consumer on the wrong
149
+ React is still told. (`fancy-flow-mcp-js` still receives React today, from
150
+ `@particle-academy/fancy-flow`'s own required peer; with that one also made
151
+ optional, its tree measured 5 packages, no React, and its stdio server
152
+ answered `initialize` and `tools/list`.) A new test fails if
153
+ any entry outside the React ones (root, `/bridges/tui`, `/sheets-adapter`,
154
+ `/connectors`, `/components/shared-whiteboard`, `/presence`, `/heuristics`,
155
+ `/undo`) starts importing React. **Do:** see "What to do" 4.
156
+
157
+ ### Fixed
158
+
159
+ - **`GET /{session}/events` answers `410 session_gone` for an ended session**,
160
+ as `docs/relay-server.md` promises for every route. 0.43.0 taught the POST
161
+ routes the distinction and left `events` on the boolean check, so an agent
162
+ re-attaching to a closed page was told `401 invalid_token` — the misleading
163
+ answer 0.43.0 existed to remove, on the route a reconnecting client hits
164
+ first. The body stays SSE-shaped: `event: error` / `data: session_gone`.
165
+ `RelayBroker#subscribe()` now returns `reason: "session_gone"` for it too.
166
+ - **An SSE stream open when its session ends is closed.** `unregister`,
167
+ `dropSession` and the TTL reaper dropped subscribers without waking them
168
+ (the reaper did wake them), so a stream stayed open, heartbeating, for a
169
+ session that no longer existed. The client now reconnects and learns `410`.
170
+ - **Frames fanned out in the same tick are all delivered to a streaming
171
+ subscriber.** The resolver handing a frame to a parked generator stayed set
172
+ until the generator resumed a microtask later, so a second frame in the same
173
+ tick called the already-settled resolver after shifting its frame off the
174
+ queue, and that frame was lost.
175
+ - **`allowedOrigins` no longer answers an unlisted origin with
176
+ `Access-Control-Allow-Origin: null`.** Sandboxed iframes and `file://` pages
177
+ send `Origin: null`, so that value admitted exactly them. No header is sent
178
+ instead.
179
+ - **Docs that contradicted the code.** `docs/relay-server.md` gave `--host`'s
180
+ default as `0.0.0.0`; it has been `127.0.0.1` since 0.30.0.
181
+ `docs/relay-protocol.md` (and this changelog's 0.44.0 entry) showed `client`
182
+ on `/sse/{session}` and `/inbound/{session}`, which no relay serves; the routes
183
+ are `/{session}/events` and `/{session}/inbox`. Both docs now cover poll,
184
+ `410` on every route, CORS lists and precedence; the Forge recipe's
185
+ `^0.6.1` range is now `>=0.46.0 <2.0`. The README's "the relay fans tool
186
+ results to all peers" predated 0.44.0's reply scoping.
187
+
188
+ ## [0.45.0] - 2026-09-13
189
+
190
+ **What to do: nothing, unless you want the new pieces.** Every change below is
191
+ additive or only removes a notification nobody could have used. The default
192
+ `initialize` answer is byte-for-byte what 0.44.0 sent.
193
+
194
+ ### Added
195
+
196
+ - **`@particle-academy/agent-integrations/mcp/stdio` — a stdio transport, so a
197
+ headless Node MCP server needs no third-party SDK.** `attachStdio(server)`
198
+ serves newline-delimited JSON-RPC on stdin/stdout, the framing every MCP
199
+ client uses to launch a server as a subprocess; pass `{ input, output }` for
200
+ other streams.
201
+
202
+ It exists because `fancy-flow-mcp-js` was built on `@modelcontextprotocol/sdk`
203
+ for want of one, and that dependency was refused. It is its own subpath, and
204
+ NODE ONLY: browser bundles import `/mcp`, which never reaches it.
205
+
206
+ - A bad line costs that line, not the session: a line that is not JSON, or is
207
+ JSON but not one JSON-RPC message (a number, `null`, a batch), goes to
208
+ `onError` — stderr by default — and nothing is written to stdout for it.
209
+ - Replies still in flight when stdin ends are delivered before it closes, so
210
+ `printf '<request>\n' | server` gets its answer.
211
+ - Input is decoded as bytes, not chunk by chunk, so a multi-byte character
212
+ split across a chunk boundary survives.
213
+
214
+ - **`protocolVersions` on `MicroMcpServer`** — the revisions a server speaks,
215
+ newest first. `initialize` now echoes the client's requested revision when it
216
+ is listed and answers the first entry otherwise, which is the spec's rule.
217
+ **Omit it and nothing changes:** the default is `[MCP_PROTOCOL_VERSION]`, one
218
+ revision answered whatever is asked, exactly as before. An empty list throws
219
+ at construction rather than in a handshake.
220
+
221
+ - **`ToolDefinition` names the spec's optional fields** — `outputSchema`,
222
+ `annotations` (new `ToolAnnotations` type), `execution` (2025-11-25 task
223
+ support), `_meta`, and `$schema` on the input schema. They were always passed
224
+ to `tools/list` verbatim; now they typecheck, and a test pins that they reach
225
+ the client.
226
+
227
+ - **A test that walks the import graphs of `/mcp` and `/mcp/stdio`** and fails
228
+ if either reaches a package (`/mcp`) or anything but a Node built-in
229
+ (`/mcp/stdio`). The root and most subpaths import React by design; one
230
+ careless re-export would break every headless host that chose the subpath to
231
+ avoid it, and nothing else here would notice, because every other test runs
232
+ with React installed.
233
+
234
+ ### Fixed
235
+
236
+ - **`notifications/tools/list_changed` is no longer sent to a transport attached
237
+ AFTER the change.** It went to every transport attached when the notification
238
+ flushed at the end of the tick, so a transport attached in the same tick as a
239
+ registration was told a list it never had was stale. On stdio that was the
240
+ first frame a client received — before it had sent `initialize` — whenever a
241
+ host registered tools and attached in one go, which is the natural way to
242
+ write a stdio server. Changes still coalesce to one notification per tick, and
243
+ a transport attached between two changes still hears about the second.
244
+
245
+ **What to do:** nothing. A transport attached after a change receives the
246
+ current list from `tools/list`, which it has to call anyway.
247
+
248
+ ## [0.44.0] - 2026-08-26
249
+
250
+ ### Security
251
+
252
+ - **A relay session token granted READ ACCESS TO EVERY PARTICIPANT'S TRAFFIC,
253
+ not just the ability to act.** If you run the HTTP relay with more than one
254
+ agent attached to a session, take this release.
255
+
256
+ Two decisions, each defensible alone: `fanOut` pushed every frame to every
257
+ subscriber on that direction, and the session token is bearer authority with
258
+ no per-agent identity. **Together, a second holder of the token passively
259
+ received the results of everyone else's tool calls without making one** — and
260
+ on a live application page those results carry whatever the bridges expose.
261
+
262
+ A share link that grants *action* is something a host can reason about. One
263
+ that grants *surveillance of other participants* is a different offer, and
264
+ nobody chose it — it fell out of the composition.
265
+
266
+ It also contradicted our own spec: `relay-protocol.md` has always said "tool-
267
+ call replies go back on the transport that originated the call". The relay
268
+ never implemented that, so this restores documented behaviour rather than
269
+ inventing new behaviour.
270
+
271
+ **What to do:** send a `client` query parameter on both your subscription and
272
+ your posts, with the same value:
273
+
274
+ ```
275
+ GET {base}/{session}/events?token=…&direction=outbound&client=worker-7
276
+ POST {base}/{session}/inbox?token=…&client=worker-7
277
+ ```
278
+
279
+ *(Corrected in 0.46.0: this example first read `/sse/{session}` and
280
+ `/inbound/{session}`, routes no relay has ever served.)*
281
+
282
+ Replies then reach only that client. **Notifications are unchanged** and still
283
+ broadcast — presence, activity and server-pushed state answer nobody and are
284
+ meant for every attached client.
285
+
286
+ A client sending no label still gets the old broadcast, because narrowing it
287
+ would break existing clients in silence. But a session where **any** subscriber
288
+ identifies itself switches to scoped routing, and an uncorrelated response then
289
+ reaches nobody rather than everyone.
290
+
291
+ **This does not fix the token itself.** Possession is still authority for every
292
+ registered tool: no expiry beyond a 4h idle timeout, no audience binding, no
293
+ per-token tool subset, no way to tell two holders apart. That is a design
294
+ question, and it is now written down rather than merely true.
295
+
296
+ Reported by the Prism harness, composing two answers given separately — each
297
+ piece defensible, the composition the defect, and nothing looking at
298
+ compositions.
299
+
300
+ ## [0.43.0] - 2026-08-26
301
+
302
+ ### Added
303
+
304
+ - **`RelayBroker.check()` — "the session is gone" is now reportable, and
305
+ distinct from "your token is wrong".**
306
+
307
+ A relay session ends when the page closes or its TTL elapses, and that is the
308
+ NORMAL end of a lifecycle rather than an error: the browser is the server, it
309
+ owns the tools and the state, and nothing persists across restarts. So it is
310
+ the one outcome every server-side client has to be able to recognise.
311
+
312
+ It was not reportable. `validate()` returned a bare boolean for three
313
+ different states — no credentials, session gone, wrong token — and the HTTP
314
+ layer mapped all of them to `401 invalid_token`. **An agent holding a
315
+ perfectly good token against a closed page was told its credentials were
316
+ wrong.** Not a missing signal; an actively misleading one, which sends the
317
+ reader to debug an auth path that was never the problem.
318
+
319
+ A gone session now answers **`410 Gone`** with `{"error": "session_gone"}`.
320
+ `410` rather than `404` because the session existed and is now over, which
321
+ separates "this is finished" from "you have the wrong URL".
322
+
323
+ Two deliberate limits: a wrong token on a LIVE session is still
324
+ `invalid_token` (answering otherwise would tell an unauthenticated caller
325
+ which sessions exist), and unregistering an already-gone session now SUCCEEDS
326
+ with `{ok: true, alreadyGone: true}` — the caller wanted it gone and it is
327
+ gone, and reporting an auth failure there makes a clean shutdown look broken
328
+ and a retrying client loop.
329
+
330
+ **Do you have to do anything?** No. `check()` is additive and `validate()`
331
+ keeps its boolean contract, with a test pinning that. If you consume the HTTP
332
+ relay, you may now receive `410` where you previously received `401`, which is
333
+ the point.
334
+
335
+ Documented in [`docs/relay-server.md`](./docs/relay-server.md).
336
+
337
+ Requested by the Prism harness while designing a non-Node relay client: a dead
338
+ session must fail clearly "rather than looking like a page with no tools". The
339
+ reality was one notch worse than the shape they were guarding against.
340
+
341
+ ## [0.42.0] - 2026-08-14
342
+
343
+ Implements the decided co-browse design — **site tools always; page tools while mounted** (#7). The cross-session activity leak, the other half of #6 item 4, shipped separately in 0.40.0.
344
+
345
+ ### Added
346
+
347
+ - **`contributeBridges` on the `useCoBrowseSession` result** — a page contributes bridges for as long as the returned disposer is uncalled:
348
+
349
+ ```tsx
350
+ useEffect(() => contributeBridges((server) =>
351
+ registerArtboardBridge(server, { adapter }).dispose), []);
352
+ ```
353
+
354
+ `extraBridges` already existed and was already called, so the plumbing looked done. It could not express this design, because it fires **exactly once while the server is built**: a page mounting afterwards never gets a chance to contribute, and one that unmounts can never withdraw. Both failures are silent — the agent simply never sees those tools, or keeps ones aimed at a surface that is gone. Contributions are therefore a registry applied when a server appears, re-applied if the session is restarted under a still-mounted page, and withdrawn on unmount.
355
+
356
+ Safe to call before sharing starts. `extraBridges` is unchanged and still works for bridges that live for the whole session.
357
+
358
+ - **`BridgeContributions`** — the registry itself, exported for hosts not using the React hook.
359
+
360
+ ### Changed
361
+
362
+ - **A call to a withdrawn tool now says so.** A dynamic tool surface means an agent calling a tool while the human navigates away is normal traffic, not an edge case, and it previously produced `Unknown tool: x` — indistinguishable from a name that never existed, and the least useful thing to say while a human is watching. A withdrawn tool now reports that the surface it targets is no longer mounted and points at `tools/list`, while a genuinely unknown name still reports `Unknown tool`. Re-registering clears it, so a remounted surface stops apologising.
363
+
364
+ **What you must do:** nothing, unless you match on the exact error string for a tool that was withdrawn rather than never registered.
365
+
366
+ ### Notes
367
+
368
+ - No new notification machinery was needed for withdrawal: bridge disposers already unregister their tools, `ToolRegistry` fires `onToolsChanged`, and `MicroMcpServer` already broadcasts `notifications/tools/list_changed`. Verified before building — note that grepping for `unregisterTool` under-reports this, because bridges use the disposer `registerTool` returns.
369
+ - The **sandbox side is not done**: `CoBrowseProvider` still passes no contributions, so nothing exercises this in the showcase yet.
370
+
371
+ ## [0.41.0] - 2026-08-11
372
+
373
+ ### Added
374
+
375
+ - **`files_create_folder` — the files bridge's first write.** The bridge shipped
376
+ read-only and said so; react-fancy 5.18.0 then gave `FileBrowser` an opt-in
377
+ New Folder button, so a human could create a folder and an agent could not.
378
+
379
+ **Opt-in via the adapter:** no `createFolder` on your `FilesBridgeAdapter`, no
380
+ tool. A host that never wired creation cannot have an agent create anything,
381
+ and the advertised tool list stays honest about what is possible.
382
+
383
+ **Staged by default.** `pendingMode` defaults to `true`, matching the catalog
384
+ bridge's destructive ops — this writes to a filesystem on the strength of a
385
+ model's output, so the safe mode is the default and turning it off is the
386
+ deliberate act. Wire `confirm` for a human prompt, or let the agent
387
+ acknowledge with `confirm: true`.
388
+
389
+ **Names are validated separately from paths.** `assertPathWithinRoot` inspects
390
+ the PARENT; the name is appended afterwards, so `{ parentPath: "/root",
391
+ name: "../.." }` clears the root check and still escapes. Separators, `.` and
392
+ `..` are rejected as names. Exported as `invalidFolderName` if a host wants
393
+ the same rule.
394
+
395
+ An adapter rejection surfaces as a tool error rather than success — the host's
396
+ filesystem is the authority, and reporting its refusal as success would tell
397
+ the agent a folder exists that does not.
398
+
399
+
400
+ ## [0.40.0] - 2026-08-11
401
+
402
+ ### Fixed
403
+
404
+ - **Two concurrent relay sessions leaked each other's activity** (#6, item 4).
405
+
406
+ `attachSseRelay` subscribes to the in-process activity bus and forwarded every
407
+ event it saw. That bus is global to the page, so running the site co-browse
408
+ relay alongside the agent playground's meant each agent received the other's
409
+ navigations and clicks, and the presence overlay attributed them to whoever
410
+ was nearest.
411
+
412
+ - **The relay's own connect/disconnect events carried a hardcoded
413
+ `agentId: "agent"`.** Two sessions' connect events were therefore identical,
414
+ and unfilterable — there was nothing to tell them apart by. They now carry the
415
+ session's configured `agent`, falling back to the old literal.
416
+
417
+ ### Added
418
+
419
+ - **`activityFilter` on `SseRelayOptions` and `useCoBrowseSession`** — a
420
+ predicate deciding which events this relay forwards.
421
+
422
+ Omitted, everything is forwarded, which is the historical behaviour and right
423
+ for the single-session case; **consumers with one session do nothing.** A
424
+ throwing predicate drops that one event and keeps the subscription, because a
425
+ host predicate is host code and a session that goes permanently silent looks
426
+ exactly like an agent that has died.
427
+
428
+ - **`agent` on `SseRelayOptions`**, passed through by `useCoBrowseSession` from
429
+ the identity it already had.
430
+
431
+ ### Notes
432
+
433
+ - This is the standalone half of #6 item 4. The rest — one merged session with
434
+ one tool surface — needs a decision about whether a mounted playground page
435
+ contributes its bridges permanently or only while mounted, and that does not
436
+ belong inside a bug fix. The leak is the part that is visibly wrong today, and
437
+ it is fixed independently of how that question is answered.
438
+
439
+
440
+ ## [0.39.1] — 2026-08-10
441
+
442
+ ### Fixed
443
+
444
+ - **`artboard_set_piece_content` failed on any piece that had no content yet.**
445
+ The undo snapshot cloned `existing.content` through
446
+ `JSON.parse(JSON.stringify(...))`, and `JSON.stringify(undefined)` returns the
447
+ *value* `undefined` rather than a string — so `JSON.parse` threw
448
+ `"undefined" is not valid JSON` and the tool returned an error.
449
+
450
+ Invisible from inside the bridge, because a piece the BRIDGE adds always has
451
+ content (`coerceContent` returns at least `{kind:"node"}`). It only fired on a
452
+ piece the host app built — which is every piece on a board a human made, i.e.
453
+ the normal case for an agent asked to fill something in.
454
+
455
+ **What you must do:** nothing. `clone` now passes `undefined` through, so undo
456
+ can also restore a piece to genuinely having no content.
457
+
458
+ ### Added
459
+
460
+ - **Tests for the ten bridges that had none** — `artboard`, `charts`, `cms`,
461
+ `git`, `map`, `scene`, `screens`, `sheets`, `slides`, `whiteboard`. The suite
462
+ goes 233 → 317.
463
+
464
+ `UNTESTED_BRIDGES` is now **empty**. It stays as a mechanism — an entry is a
465
+ reviewable admission that a bridge ships untested, and a further test fails if
466
+ one goes stale — so it cannot quietly become a parking space.
467
+
468
+ Beyond the defect above, writing them pinned several contracts that were not
469
+ what they looked like from outside: `git` omits `git_reviews_list` /
470
+ `git_checks` entirely rather than advertising tools that fail; `element_move`
471
+ takes FRACTIONAL slide coordinates and clamps to 0..1; `map_add_marker` takes
472
+ flat `lat`/`lng` but stores a nested `position`; `artboard_add_piece` marks
473
+ agent-added pieces `pending`; and `artboard_set_piece_content` silently
474
+ coerces an unrecognised content shape to `{kind:"node"}` while reporting
475
+ success.
476
+
477
+ - **A test that every bridge is reachable by a consumer.** Shipping a bridge
478
+ takes four edits in four files, and the guidance already warned what happens
479
+ when one is missed — *"the bridge lands in source but invisible to consumers,
480
+ exactly how `registerSlidesBridge` sat un-shipped until v0.6.3."* Nothing
481
+ checked it. A bridge's own tests import it by relative path, so they pass
482
+ whether or not the **package** exposes it: green suite, correct source, and a
483
+ feature that does not exist outside this repo.
484
+
485
+ It also asserts the subtler half — that a module eagerly importing an optional
486
+ peer stays **out** of the root barrel. That was a comment, and comments do not
487
+ fail builds. Re-exporting `SharedWhiteboard` would make `fancy-whiteboard`
488
+ mandatory for everyone importing the package root, the same class of breakage
489
+ as `fancy-flow` shipping `@xyflow/react` and making `fancy-screens` impossible
490
+ to co-install. Verified by adding that export and watching the test go red.
491
+
492
+ ### Notes
493
+
494
+ - **`ToolRegistry.callTool` does not validate the `required` list.** It is
495
+ advertised in each tool's schema and never enforced, so a call omitting a
496
+ required argument runs anyway — `chart_update_option` with no `partial` merges
497
+ `{}` and reports "Merged chart option"; `scene_add_object` with no `kind`
498
+ mints an object with the id `undefined_<random>`. A schema-respecting MCP
499
+ client will not do this, but a relay, a hand-rolled client or a model emitting
500
+ malformed arguments will, and this package exists to be driven by exactly
501
+ those. Not changed here: enforcing it turns silent no-ops into errors across
502
+ 24 bridges, which is a deliberate decision rather than a test-writing side
503
+ effect.
504
+
505
+ ## [0.39.0] — 2026-08-09
506
+
507
+ ### Added
508
+
509
+ - **`registerGridBridge`** (`./bridges/grid`) — MCP access to a
510
+ `@particle-academy/fancy-grid` surface: `grid_get`, `grid_sort`,
511
+ `grid_filter`, `grid_select_rows` and `grid_edit_cell`.
512
+
513
+ The grid state types are **mirrored here, not imported**. fancy-grid keeps
514
+ TanStack Table and Virtual as peers precisely so nothing bundles them, and an
515
+ `import type` would still make fancy-grid a build-time dependency of this
516
+ package. The state is four small JSON shapes; the coupling is not worth it —
517
+ the same call the scene bridge makes about fancy-3d's descriptor.
518
+
519
+ Three behaviours worth knowing:
520
+
521
+ - **An unknown column is an ERROR**, not a silent no-op. A sort nothing
522
+ applies looks exactly like a grid that is not sorted, so an agent would have
523
+ no way to tell it failed. Same for selecting a row that is not on the page.
524
+ - **`grid_edit_cell` is omitted entirely on a read-only grid** rather than
525
+ registered and failing at call time. An agent should learn what it can do
526
+ from the tool list, not by trying.
527
+ - **`pendingMode` gates `grid_edit_cell` only.** It is the one tool that
528
+ changes stored DATA rather than view state; sorting a grid is not a
529
+ trust-but-verify action, and gating it would train people to click through
530
+ confirmations.
531
+
532
+ View state goes through the adapter's single `setState`, because that is how
533
+ the grid is controlled — a bridge mutating pieces separately would drift from
534
+ what the component accepts. Every view change is undoable via `agent_undo`.
535
+
536
+
537
+ ## [0.38.0] — 2026-08-09
538
+
539
+ ### Added
540
+
541
+ - **`AgentPanel` is now a tool-call feed.** `AgentActivity` gains optional
542
+ `args`, `result`, `durationMs` and `status` (`"pending" | "ok" | "error"`),
543
+ and a `kind: "tool"` row carrying any of them renders as
544
+ `tool(args) · 142ms` with `→ result` beneath.
545
+
546
+ This is an extension rather than a new `<ToolCallFeed>` because the panel
547
+ already rendered an activity stream with a `"tool"` kind — a sibling
548
+ component would have re-rendered the same rows beside it.
549
+
550
+ `status: "pending"` is what makes it a **stream** rather than a log: a row can
551
+ appear the moment a call starts and be replaced when it settles, instead of
552
+ the feed only ever showing finished work. Status is inferred as `error` from
553
+ `kind: "error"` when not given.
554
+
555
+ Payloads are truncated to ~80 chars on the row. A feed is scanned, not read,
556
+ and an untruncated result pushes every subsequent row off screen — the full
557
+ value stays available on `detail`.
558
+
559
+ Each part carries a handle (`data-fai-args`, `data-fai-result`,
560
+ `data-fai-latency`, plus `data-kind` / `data-status` on the row) so a host can
561
+ restyle or address it.
562
+
563
+ **What you must do:** nothing. Every field is optional, and a row without them
564
+ renders exactly as before — pinned by a test.
565
+
566
+
567
+ ### Added
568
+
569
+ - **A shared-substrate parity suite** (`src/bridges/__tests__/substrate-parity.test.ts`)
570
+ asserting that `fancy-cms-ui` and `fancy-screens` really do read and write the
571
+ same node / tree / op types, run against the **published** packages rather
572
+ than by inspection.
573
+
574
+ The "one document substrate" claim is easy to state and easy to quietly break:
575
+ each package has its own reducer, and two reducers that agree today can
576
+ disagree on one edge — ordering, cascade, a no-op guard — without anything
577
+ failing, because each package's own suite only ever exercises its own. Both
578
+ are devDependencies, so nothing at runtime depends on them.
579
+
580
+ Also covers the other half: `registerDocBridge` driving a document the CMS
581
+ itself created, with `registerCmsBridge` adding two domain ops and nothing
582
+ else — the generic bridge doing the work, which is what "no CMS-specific
583
+ bridge" was supposed to mean.
584
+
585
+
586
+ ## [0.37.0] — 2026-08-07
587
+
588
+ ### Changed
589
+
590
+ - **BREAKING — Node 22 is now declared as the floor.** `engines.node` is `>=22`, where this package previously declared **nothing at all**.
591
+
592
+ Declaring nothing was not the same as supporting old Node: a consumer on 18 installed cleanly and found out at runtime.
593
+
594
+ **What you must do:** on Node 22 or newer, nothing. Note npm only *warns* on an `engines` mismatch while **pnpm fails the install**, so this surfaces differently depending on your package manager. Node 18 is end-of-life and 20 is maintenance-only.
595
+
596
+ - **BREAKING — React 18 is no longer supported.** `peerDependencies.react` / `react-dom` are now `^19.0.0`.
597
+
598
+ **What you must do:** on React 19, nothing. On React 18, stay on the previous release, or upgrade your app to 19 first.
599
+
600
+ React 18 support was a claim nothing tested — every build and test in this package ran against 19, so the 18 half of the old range was never executed. An untested compatibility claim is worse than an absent one, because it reads as support.
601
+
602
+ ### Why
603
+
604
+ These are the kit 0.5 platform floors, applied across every package at once so a consumer never has to resolve a mix. **No API changed, nothing was removed, nothing was renamed** — only what the package requires.
605
+
606
+
607
+ ## [0.36.0] — 2026-08-03
608
+
609
+ ### Added
610
+
611
+ - **`registerScreenDocBridge`** — an agent bridge over the *contents* of one
612
+ rendered `<Screen doc={…}>` (fancy-screens ≥ 0.6), built on
613
+ `registerDocBridge`. Read the nodes in a screen, patch a prop, retext a label,
614
+ reparent a card — with undo, agent activity and staged writes inherited from
615
+ the substrate.
616
+
617
+ This is the other half of `registerScreensBridge`, and the halves are not
618
+ interchangeable: that one moves *between* screens and treats their contents as
619
+ an opaque `config` blob. Until fancy-screens 0.6 there was no way to address
620
+ anything *inside* a screen at all, because `ScreenSchema` nodes had no `id` —
621
+ an agent could emit an entire surface and then touch none of it. That was a
622
+ standing violation of the component contract's stable-handles requirement, on
623
+ the one shipped "agent emits a UI" path.
624
+
625
+ Two things it adds over the raw generic bridge, both about a screen being a
626
+ rendered React tree:
627
+
628
+ - **Positional ids are reported as positional.** fancy-screens mints one for
629
+ any node the author left anonymous; it looks like a handle and is not one,
630
+ because inserting a sibling above it silently repoints it. Every read here
631
+ says which kind of id it returned, and `screen_addressable` lists only the
632
+ durable ones.
633
+ - **`screen_set_text`** retexts an element by the *element's* id, so an agent
634
+ never has to know that literal text is a reserved `#text` child node.
635
+
636
+ Pass `surface` to prefix the tools per screen (`"screen_checkout"`) when more
637
+ than one doc-driven screen is bridged at once.
638
+
639
+ - **`add` accepts an optional `id`** on every doc-derived bridge (`cms_add`,
640
+ `screen_add`, …), so an agent can name the handle it is about to depend on
641
+ instead of taking a minted one. Adding over an existing id is an error rather
642
+ than a silent overwrite.
643
+
644
+ **What you must DO: nothing.** The argument is optional and omitting it
645
+ behaves exactly as before.
646
+
647
+ ### Fixed
648
+
649
+ - **BREAKING (staged writes): a staged `update` / `remove` / `move` / domain op
650
+ now returns its pending id as `pendingId`, not `id`.**
651
+
652
+ These ops report the *node* id they targeted, and that assignment ran after
653
+ the staged result was spread — so it overwrote the pending id with the node
654
+ id. `*_confirm` only accepts a pending id, so three of the four canonical ops
655
+ produced a staged write that could never be confirmed, and the edit was
656
+ silently dropped. Only `add` worked, and only because it does not report a
657
+ separate node id. Every mutation now returns both, under separate keys.
658
+
659
+ **What you must DO — only if you use `stagePolicy`:** read `pendingId` where
660
+ you previously read `id` from a staged result. Anything staging only `add`
661
+ keeps working if it reads `pendingId`; `id` on a staged `add` is now the new
662
+ node's id, which is what it always claimed to be. Hosts that never set a
663
+ `stagePolicy` are unaffected — nothing stages by default.
664
+
665
+ - **A minted node id no longer collides with an existing node.** The counter
666
+ restarts with the bridge, so on a tree loaded from elsewhere `cms-1` could
667
+ already be taken and `add` would overwrite that node. Minting now skips past
668
+ anything present.
669
+
670
+ ## [0.35.0] — 2026-07-31
671
+
672
+ ### Added
673
+
674
+ - **`useCoBrowseSession` now reports whether an agent is actually there** —
675
+ `agentConnected` / `agentCount`, driven by the relay's peer join/leave frames
676
+ (`SseRelayTransport.onPeersChange` / `peerCount()` are public too).
677
+
678
+ The only signal available before this was `relayState`, which describes the
679
+ BROWSER's own channel to the relay and turns `"open"` the instant sharing
680
+ starts. Anything keyed on it therefore announced a driver to a human who had
681
+ not yet handed the link to anybody — and stayed exactly the same when a real
682
+ agent arrived, so it could not report the one event it existed to report.
683
+
684
+ **No action needed** — additive fields on an object you already receive.
685
+
686
+ ### Fixed
687
+
688
+ - **`<CoBrowsePresence>` shows what the agent is doing.** 0.33.0 gave
689
+ `<ShareControls>` the `agentConnected` + `activity` props, but nothing passed
690
+ them, so the site-wide co-browse panel still rendered the paste-this-prompt UI
691
+ (Agent prompt / URL / JSON / cURL) while an agent was connected and driving.
692
+ It now feeds both from the presence stream and opens on the log.
693
+
694
+ Its "Agent is driving" badge was keyed on `relayState === "open"` and is now
695
+ keyed on a real peer, so it no longer reads "Agent is driving" over a session
696
+ nobody has joined.
697
+
698
+ - **The agent cursor no longer appears for an agent that has not acted.**
699
+ `agent_connected` — a lifecycle frame carrying no target — was parking a
700
+ motionless pointer captioned "Agent connected" at the centre of the viewport.
701
+ Short-lived relay clients (one process per MCP call) re-emit it constantly, so
702
+ the cursor could sit there for the whole session having never once moved,
703
+ which reads as a hung agent rather than a present one.
704
+
705
+ `<CoBrowseCursorLayer>` now creates and moves the cursor only on real tool
706
+ traffic, and retires it after `idleAfterMs` (default 15s) of silence. Pass
707
+ `idleAfterMs={0}` for the old always-on behaviour. Connect / disconnect frames
708
+ still update the caption of a cursor that is already on screen; they just
709
+ never conjure one, and never extend its life.
710
+
711
+
712
+ ## [0.34.0] — 2026-07-31
713
+
714
+ ### Added
715
+
716
+ - **`registerPasskeyBridge` — the 21st bridge, over passkey (WebAuthn)
717
+ *management*.** Import from `@particle-academy/agent-integrations/bridges/passkeys`
718
+ or the root barrel. Five tools: `passkey_list`, `passkey_status`,
719
+ `passkey_rename`, `passkey_revoke`, `passkey_begin_enrollment`. Pairs with
720
+ `@particle-academy/fancy-passkeys-ui`'s `PasskeyManager` — the adapter takes
721
+ the same callbacks you already passed the component — but imports nothing, so
722
+ it type-checks and builds with that package absent.
723
+
724
+ **What it deliberately does not do, and never will:** complete a ceremony.
725
+ There is no `passkey_authenticate`, no `passkey_sign_in`, no `passkey_complete`.
726
+ A WebAuthn ceremony needs a user gesture and a biometric or PIN, both of which
727
+ only the human at the keyboard has; a tool that performed one would be a bypass
728
+ of the exact property that makes a passkey better than a password. A test
729
+ asserts the registered tool names against a closed list, so adding one fails CI.
730
+
731
+ Two more things the bridge does rather than documents:
732
+
733
+ - **`passkey_list` re-projects every record onto the eight public summary
734
+ fields.** Hosts hand `list()` their ORM model — the obvious thing to do — and
735
+ that model carries the COSE public key, the user handle and the signature
736
+ counter. A whitelist (not a blacklist) means a backend growing a column
737
+ cannot silently start publishing credential material to every agent in the
738
+ session.
739
+ - **`passkey_revoke` only ever STAGES.** It puts the request in front of the
740
+ human and returns; the confirming click comes from the surface. Unlike
741
+ `features_grant`, there is no `confirm: true` argument and no host hook that
742
+ turns staging off — `additionalProperties: false` means the schema will not
743
+ even accept one. Revoking the last passkey is a lockout, and the response
744
+ says when that is the case so the agent can warn the human first.
745
+
746
+ `passkey_rename` is immediate and undoable via `agent_undo`. Set
747
+ `confirmRename: true` to route it through a host `confirm` hook as well — worth
748
+ it where the label carries trust, since a rogue credential relabelled
749
+ "Avery's iPhone" survives a human's audit of the list.
750
+
751
+ ### Note
752
+
753
+ - `@particle-academy/fancy-passkeys-ui` is **not** listed as a peer or a
754
+ devDependency, because it is not published yet and a dependency on a package
755
+ that 404s is worse than none. The bridge does not import it; the adapter is
756
+ satisfied structurally. The peer entry lands with that package's first release.
757
+
758
+ ## [0.33.0] — 2026-07-30
759
+
760
+ ### Added
761
+
762
+ - **`<ShareControls>` becomes an activity log once an agent connects.** It kept
763
+ showing the paste-this-prompt UI — Agent prompt / URL / JSON / cURL — long
764
+ after the agent had connected and started driving. Dead weight at exactly the
765
+ moment the human needs the opposite, and it made a **connected agent
766
+ indistinguishable from a broken one**: a stalled session looked identical to a
767
+ working one.
768
+
769
+ Two new optional props: `agentConnected` and `activity`. The panel switches to
770
+ the log the FIRST time an agent connects and then leaves the tab alone, so it
771
+ never fights a human who went back to re-copy the URL. Connected-with-nothing-
772
+ yet says so explicitly rather than rendering an empty list.
773
+
774
+ `activity` reuses `AgentActivity` from `AgentPanel` rather than introducing a
775
+ second event shape, so a host already collecting presence events passes the
776
+ same array to both.
777
+
778
+ **No action needed** — both props are optional and the panel behaves exactly as
779
+ before without them.
780
+
781
+ ### Changed
782
+
783
+ - Widened the `fancy-auto-common` and `fancy-doc-commons` requirement from `^0.1.0` to `>=0.1 <2.0`, so a
784
+ sibling minor release is an upgrade and not a resolver conflict. **No action
785
+ needed** — widening a range only adds candidates; the version you have today
786
+ still resolves.
787
+
788
+ A caret on a `0.x` range locks the MINOR, so this pinned a sibling at
789
+ whatever it happened to be on the day it was written, and each sibling
790
+ release then read as a conflict to the resolver rather than an upgrade.
791
+ Nothing here was using an API the newer minors removed — the range was the
792
+ whole problem.
793
+
794
+ ## [0.32.0] — 2026-07-24
795
+
796
+ > This release also cuts work already merged to `main` but unreleased since
797
+ > 0.31.0: a Human+ **Git bridge** (`registerGitBridge`), a **files-bridge
798
+ > ReDoS** hardening, and the CI workflow.
799
+
800
+ ### Added
801
+
802
+ - **Flow bridge trust surface (`registerFlowBridge`).** The bridge now validates
803
+ what an agent builds, closing the drift between it and `<FlowCanvas>`:
804
+ - **Connection validation** — `flow_connect` enforces port-type compatibility
805
+ using fancy-flow's `createConnectionValidator` (the SAME rule the canvas
806
+ applies), so an agent can no longer create an edge the canvas would refuse.
807
+ New `validateConnections?: boolean | ConnectionValidatorOptions` option
808
+ (default `true`); self-loops are blocked. No-ops when fancy-flow (>= 0.18.0)
809
+ isn't importable — falls back to the previous existence-only check.
810
+ - **Config validation** — `flow_add_node` / `flow_update_node` validate a
811
+ node's config against its kind's `configSchema`. New
812
+ `validateConfig?: "reject" | "warn" | "off"` option (default `"reject"`);
813
+ `flow_update_node` validates the merged config BEFORE applying, so a reject
814
+ leaves the graph untouched.
815
+ - **Staging** — `flow_delete_node` and `flow_run` can be gated on a human
816
+ confirm. New `pendingMode?: boolean` option (default **off** — flow authoring
817
+ is high-frequency, unlike a form submit) plus an optional
818
+ `adapter.confirm(request)` hook and a `FlowConfirmRequest` type.
819
+
820
+ ### Fixed
821
+
822
+ - **`flow_list_node_kinds` `category` filter now works.** It was a no-op
823
+ placeholder (`const cat = adapter ? undefined : undefined`); the tool now
824
+ actually filters kinds by the requested category.
825
+
826
+ ### Changed
827
+
828
+ - Raised the `@particle-academy/fancy-flow` peer floor to `>= 0.18.0` (for
829
+ `createConnectionValidator`). It remains an **optional** peer — the bridge
830
+ degrades gracefully when fancy-flow is absent, so this is non-breaking for
831
+ hosts that don't use the flow bridge.
832
+
833
+ **What a consumer must DO:** nothing, unless you use `registerFlowBridge` with
834
+ typed ports or config schemas — then ensure fancy-flow is on 0.18.0+ so
835
+ validation activates (otherwise the bridge silently skips it, as before).