@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.
- package/CHANGELOG.md +835 -0
- package/LICENSE +21 -0
- package/README.md +54 -6
- package/dist/bridges/artboard.d.cts +3 -3
- package/dist/bridges/artboard.d.ts +3 -3
- package/dist/bridges/catalog.d.cts +3 -3
- package/dist/bridges/catalog.d.ts +3 -3
- package/dist/bridges/charts.d.cts +3 -3
- package/dist/bridges/charts.d.ts +3 -3
- package/dist/bridges/cms.d.cts +3 -3
- package/dist/bridges/cms.d.ts +3 -3
- package/dist/bridges/code.d.cts +3 -3
- package/dist/bridges/code.d.ts +3 -3
- package/dist/bridges/doc.d.cts +3 -3
- package/dist/bridges/doc.d.ts +3 -3
- package/dist/bridges/features.d.cts +3 -3
- package/dist/bridges/features.d.ts +3 -3
- package/dist/bridges/files.d.cts +3 -3
- package/dist/bridges/files.d.ts +3 -3
- package/dist/bridges/flow.d.cts +3 -3
- package/dist/bridges/flow.d.ts +3 -3
- package/dist/bridges/forms.d.cts +3 -3
- package/dist/bridges/forms.d.ts +3 -3
- package/dist/bridges/git.d.cts +3 -3
- package/dist/bridges/git.d.ts +3 -3
- package/dist/bridges/grid.d.cts +3 -3
- package/dist/bridges/grid.d.ts +3 -3
- package/dist/bridges/map.d.cts +3 -3
- package/dist/bridges/map.d.ts +3 -3
- package/dist/bridges/navigation.d.cts +127 -0
- package/dist/bridges/navigation.d.ts +127 -0
- package/dist/bridges/passkeys.d.cts +4 -4
- package/dist/bridges/passkeys.d.ts +4 -4
- package/dist/bridges/scene.d.cts +3 -3
- package/dist/bridges/scene.d.ts +3 -3
- package/dist/bridges/screens.d.cts +3 -3
- package/dist/bridges/screens.d.ts +3 -3
- package/dist/bridges/sheets.d.cts +3 -3
- package/dist/bridges/sheets.d.ts +3 -3
- package/dist/bridges/slides.d.cts +3 -3
- package/dist/bridges/slides.d.ts +3 -3
- package/dist/bridges/terminal.d.cts +3 -3
- package/dist/bridges/terminal.d.ts +3 -3
- package/dist/bridges/tui.d.cts +2 -2
- package/dist/bridges/tui.d.ts +2 -2
- package/dist/bridges/whiteboard.d.cts +3 -3
- package/dist/bridges/whiteboard.d.ts +3 -3
- package/dist/bridges-navigation.cjs +391 -0
- package/dist/bridges-navigation.cjs.map +1 -0
- package/dist/bridges-navigation.js +7 -0
- package/dist/bridges-navigation.js.map +1 -0
- package/dist/bridges-passkeys.cjs.map +1 -1
- package/dist/bridges-passkeys.js +1 -1
- package/dist/{chunk-OZBUPO4K.js → chunk-5W34X4EY.js} +263 -58
- package/dist/chunk-5W34X4EY.js.map +1 -0
- package/dist/{chunk-PWYEW6NL.js → chunk-G6CRHFWU.js} +2 -2
- package/dist/{chunk-PWYEW6NL.js.map → chunk-G6CRHFWU.js.map} +1 -1
- package/dist/chunk-JGJHPH65.js +271 -0
- package/dist/chunk-JGJHPH65.js.map +1 -0
- package/dist/dom-navigation/index.d.cts +164 -0
- package/dist/dom-navigation/index.d.ts +164 -0
- package/dist/dom-navigation/script.d.cts +3 -0
- package/dist/dom-navigation/script.d.ts +3 -0
- package/dist/dom-navigation-script.cjs +11 -0
- package/dist/dom-navigation-script.cjs.map +1 -0
- package/dist/dom-navigation-script.js +9 -0
- package/dist/dom-navigation-script.js.map +1 -0
- package/dist/dom-navigation.cjs +387 -0
- package/dist/dom-navigation.cjs.map +1 -0
- package/dist/dom-navigation.js +370 -0
- package/dist/dom-navigation.js.map +1 -0
- package/dist/index.cjs +31 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -125
- package/dist/index.d.ts +10 -125
- package/dist/index.js +12 -275
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.d.cts +5 -5
- package/dist/mcp/index.d.ts +5 -5
- package/dist/mcp/stdio.d.cts +3 -3
- package/dist/mcp/stdio.d.ts +3 -3
- package/dist/presence/index.d.cts +1 -1
- package/dist/presence/index.d.ts +1 -1
- package/dist/relay-server/index.d.cts +110 -26
- package/dist/relay-server/index.d.ts +110 -26
- package/dist/relay-server-cli.cjs +374 -89
- package/dist/relay-server-cli.cjs.map +1 -1
- package/dist/relay-server-cli.js +120 -38
- package/dist/relay-server-cli.js.map +1 -1
- package/dist/relay-server.cjs +255 -51
- package/dist/relay-server.cjs.map +1 -1
- package/dist/relay-server.js +1 -1
- package/dist/{server-BIcA3ZtK.d.cts → server-1y0n61R5.d.cts} +2 -2
- package/dist/{server-DnUyeavY.d.ts → server-vq6qVl3T.d.ts} +2 -2
- package/dist/sharing/index.d.cts +3 -3
- package/dist/sharing/index.d.ts +3 -3
- package/dist/sheets-adapter.d.cts +3 -3
- package/dist/sheets-adapter.d.ts +3 -3
- package/dist/{tool-host-LVBgCN5m.d.cts → tool-host-D67N2a2h.d.cts} +1 -1
- package/dist/{tool-host-Wo0pkY83.d.ts → tool-host-LYfdPE8u.d.ts} +1 -1
- package/dist/{types-EcElKOlk.d.cts → types-DP5yQZQU.d.cts} +1 -1
- package/dist/{types-BxNRKSVd.d.ts → types-VX_9je7w.d.ts} +1 -1
- package/dist/{types-C2DtnHIn.d.cts → types-gzIpsUDE.d.cts} +1 -1
- package/dist/{types-C2DtnHIn.d.ts → types-gzIpsUDE.d.ts} +1 -1
- package/dist/undo/index.d.cts +2 -2
- package/dist/undo/index.d.ts +2 -2
- package/docs/relay-protocol.md +41 -5
- package/docs/relay-server.md +102 -15
- package/package.json +41 -3
- 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).
|