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