You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/content/1.guide/11.client.md
+3-1Lines changed: 3 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -87,9 +87,11 @@ if (!trusted) {
87
87
88
88
### Authenticating with a one-time code
89
89
90
-
The dev server prints a single-use 6-digit code (expires in five minutes, rotates after repeated wrong attempts); `requestTrustWithCode` exchanges it for a persisted node-issued token shared across sibling tabs:
90
+
The dev server prints a single-use 6-digit code (expires in five minutes, rotates after repeated wrong attempts) when an untrusted RPC client asks for one: call `requestAuthCode()` when your auth UI shows, passing `{ reissue: true }` from a "re-issue" button to rotate the code first. `requestTrustWithCode` then exchanges it for a persisted node-issued token shared across sibling tabs:
91
91
92
92
```ts
93
+
awaitrpc.requestAuthCode()
94
+
// … the developer reads the code from the terminal …
/** listened to by panels, emitted by the page script */
55
+
panel: { flash: (message:string) =>void }
46
56
}
47
57
sharedStates: {
48
58
state: { selections:string[] }
@@ -54,37 +64,41 @@ Channel names are namespaced with the devframe id, like RPC ids. Function names
54
64
55
65
## The page script endpoint
56
66
57
-
Functions use the same authoring metadata as `defineRpcFunction` (`type`, Standard-Schema `args`/`returns`, `jsonSerializable`, `handler`), narrowed to the browser. The required `functions` object's keys are the function names, and it implements every function on that endpoint's protocol side. Each handler is contextually typed from its key and the corresponding function in the protocol. `defineChannelFunction` retains the named definition shape for lower-level authoring. Define each side's functions in that side's source files; the shared protocol file carries only types.
67
+
The required `functions` option and optional `events` option declare every incoming name on the endpoint's protocol side; use `{}` for an empty direction. Functions require a `handler`. Events accept an optional `handler`, and `{}` registers an event for runtime subscriptions through `on()`. Handlers are contextually typed from the shared protocol and support Standard-Schema argument validation and `jsonSerializable` metadata. `defineChannelFunction` retains the named definition shape for lower-level authoring.
68
+
69
+
`call()` accepts names from `functions`, including actions returning `void` or `Promise<void>`: callers can await completion and catch errors or timeouts. `emit()`, its deprecated alias `callEvent()`, and `on()` use the names declared in `events`. Function and event names have separate namespaces.
`callEvent` on the pagescript is 1:N: it fans out to every connected panel, and panels that don't implement the function ignore it. Request/response *to* a panel goes through an explicit peer handle: `channel.panels[0].call('flash', '…')`.
101
+
`emit` on the page-script endpoint fans out to every connected panel endpoint. Functions declared under `functions.panel` are called through a specific `pageChannel.panels[0].call()` peer handle.
88
102
89
103
## The panel endpoint
90
104
@@ -94,19 +108,25 @@ import type { MyChannelProtocol } from '../shared/protocol'
The snippets form one channel pair: `pageChannel.emit('flash', …)` invokes `panelChannel.on('flash', …)`. In the other direction, `panelChannel.emit('highlight', …)` invokes the page-script endpoint's `highlight` handler and any matching `pageChannel.on()` listeners. An endpoint never receives its own emission.
129
+
110
130
## Shared state
111
131
112
132
The channel's shared-state layer mirrors [`rpc.sharedState`](/guide/shared-state) (same `SharedState<T>` handle, same accessor), with the page script playing the server's role as rendezvous and authority. Its first `get` of a key must provide the initial value; panels are seeded automatically on connect (including late joiners and re-connects) and converge through syncId-deduplicated patches.
@@ -133,7 +153,7 @@ Every failure mode is a coded `InPageChannelError` (`error.code`) with a message
133
153
The panel endpoint's connection lifecycle is explicit, so a panel renders a useful fallback instead of hanging:
134
154
135
155
-`channel.status` is `connecting` → `connected` → (`connecting` on port loss) → `closed`, with `events.on('status:updated', …)` for reactivity.
136
-
- While `connecting`, `call()` is queued (and still subject to its deadline) and `callEvent()` is buffered (up to `eventBufferLimit`, oldest dropped with a warning); both flush on connect.
156
+
- While `connecting`, `call()` is queued (and still subject to its deadline) and `emit()` is buffered (up to `eventBufferLimit`, oldest dropped with a warning); both flush on connect.
137
157
- A page script may legitimately never appear (the panel opened standalone, the user app not instrumented). Race `whenConnected(timeoutMs)` to show a "load the page script" empty state:
serialize: value=>toRawDeep(value), // applied to every outgoing argument and result
180
+
functions: {},
181
+
events: { flash: {} },
160
182
})
161
183
```
162
184
185
+
These hooks also apply to shared-state subscription snapshots, full-state updates, and patch arrays in both directions. Hooks that restore nested values should traverse objects and arrays, including each patch's `value`.
186
+
163
187
Declaring a function `jsonSerializable: true` additionally enforces strict JSON on its payloads at the receiving endpoint, turning a would-be silent coercion or cryptic `DataCloneError` into a coded error naming the offending path.
164
188
165
189
## Multiple tabs
166
190
167
191
The same app open in two tabs means two page scripts on one origin. Each page script carries a per-tab instance id (persisted in `sessionStorage`), and handshakes are targeted `postMessage`, so a dock panel always pairs with its own tab's page script. A panel can also pin explicitly:
Copy file name to clipboardExpand all lines: docs/content/1.guide/14.security.md
+4-4Lines changed: 4 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -19,14 +19,14 @@ An RPC handler runs with the full privileges of its Node process (filesystem, ch
19
19
20
20
## The pre-trust gate
21
21
22
-
One rule decides what an untrusted connection may call: **a method is reachable before trust iff its name starts with `anonymous:`** (`isAnonymousRpcMethod`, from `devframe/constants`); only the two handshake methods below qualify.
22
+
One rule decides what an untrusted connection may call: **a method is reachable before trust iff its name starts with `anonymous:`** (`isAnonymousRpcMethod`, from `devframe/constants`); only the handshake and code-request methods below qualify.
23
23
24
24
The RPC server binding enforces this: pass `auth: authHandler` (its `.authorize` becomes the gate) or your own `authorize(methodName, session)`. Every other call from an untrusted session throws [`DF0036`](/errors/DF0036). `rpc.call` / `rpc.callOptional` / `rpc.callEvent` hold calls issued during the first handshake and release them once it settles.
25
25
26
26
## Authentication flow
27
27
28
28
1. A fresh RPC client calls `anonymous:devframe:auth` with its stored token (empty on first run); the server returns `{ isTrusted: false }` and the UI prompts for a code.
29
-
2. The dev server shows a 6-digit codein the terminal (`auth.printBanner()` once listening).
29
+
2. The auth UI requests a code (`rpc.requestAuthCode()`, sent automatically when the built-in notice view first shows, or by its "re-issue" button with `{ reissue: true }` to rotate the code first); the dev server prints the 6-digit code, its expiry, and the requesting browser in the terminal. An already-authorized page never triggers a print.
30
30
3. The developer enters it; the browser calls `requestTrustWithCode(code)`.
31
31
4. The server verifies the code, mints a high-entropy bearer token, trusts the session, and returns it.
32
32
5. The browser persists the token and presents it on reconnect (or via a `?devframe_auth_token=` query param the connect-time hook checks first); sibling tabs receive it over the `devframe-auth` channel and become trusted.
@@ -51,11 +51,11 @@ Pass `clientAuthTokens` for CI/shared machines to skip the prompt, or a custom `
51
51
52
52
### Auth methods
53
53
54
-
The two `anonymous:`-prefixed handshake methods re-authenticate a stored token (`anonymous:devframe:auth`) and exchange a one-time code for a token (`anonymous:devframe:auth:exchange`); `devframe:auth:revoke` self-revokes, and the `devframe:auth:revoked` event drops affected RPC clients to untrusted. Wire shapes are in the [Node-Side API reference](/references/node-api#auth-methods).
54
+
The `anonymous:`-prefixed methods re-authenticate a stored token (`anonymous:devframe:auth`), exchange a one-time code for a token (`anonymous:devframe:auth:exchange`), and ask the server to print its code banner (`anonymous:devframe:auth:request-code`); `devframe:auth:revoke` self-revokes, and the `devframe:auth:revoked` event drops affected RPC clients to untrusted. Wire shapes are in the [Node-Side API reference](/references/node-api#auth-methods).
55
55
56
56
Node primitives in `devframe/node/auth` (`getTempAuthCode` / `refreshTempAuthCode`, `exchangeTempAuthCode`, `verifyAuthToken`, `buildOtpAuthUrl`, and `revokeAuthToken`) implement the same flow for a host framework wiring its own gate; signatures are in the [reference](/references/node-api#node-auth-primitives).
57
57
58
-
RPC client methods (`devframe/client`): `requestTrustWithCode(code)`, `requestTrustWithToken(token)`, and `ensureTrusted(timeout?)` / `isTrusted` (the trust gate).
58
+
RPC client methods (`devframe/client`): `requestAuthCode(options?)` (print the code banner; `{ reissue: true }` rotates the code first), `requestTrustWithCode(code)`, `requestTrustWithToken(token)`, and `ensureTrusted(timeout?)` / `isTrusted` (the trust gate).
Copy file name to clipboardExpand all lines: docs/content/1.guide/15.agent-native.md
+26-3Lines changed: 26 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -2,14 +2,14 @@
2
2
title: 'Agent-Native Devframe'
3
3
navigation:
4
4
icon: i-lucide-bot
5
-
description: 'Devframe exposes its browser-side API (RPC functions, resources, shared state) to coding agents over MCP, opt-in per function.'
5
+
description: 'Devframe exposes its API (RPC functions, resources, shared state) to agents, over MCP on the node side and WebMCP on the browser side, opt-in per function.'
6
6
---
7
7
8
-
Devframe exposes its browser-side API (RPC functions, resources, shared state) to coding agents over MCP, opt-in per function.
8
+
Devframe exposes its API (RPC functions, resources, shared state) to agents, over MCP on the node side and [WebMCP](#browser-side-tools-over-webmcp) on the browser side, opt-in per function.
9
9
10
10
## How it works
11
11
12
-
Three pieces: the **`agent` field** on `defineRpcFunction`, **`ctx.agent`** (non-RPC tools + resources), and the **MCP adapter** (`devframe/adapters/mcp`) serving an [MCP](https://modelcontextprotocol.io) server.
12
+
Three pieces: the **`agent` field** on `defineRpcFunction`, **`ctx.agent`** (non-RPC tools + resources), and the **MCP adapter** (`devframe/adapters/mcp`) serving an [MCP](https://modelcontextprotocol.io) server. The same `agent` field on a *client* RPC function surfaces it [over WebMCP](#browser-side-tools-over-webmcp) instead.
13
13
14
14
## Exposing an RPC function
15
15
@@ -137,6 +137,29 @@ In `claude_desktop_config.json`:
137
137
138
138
Restart; tools appear in the drawer, resources as `devframe://resource/<id>` / `devframe://state/<key>` URIs.
139
139
140
+
## Browser-side tools over WebMCP
141
+
142
+
The same `agent` signature works on the browser side: a client RPC function (a function the node side calls on the browser, registered on `rpc.client` or through a scoped `client.scope('my-plugin').rpc.register(...)`) carrying an `agent` field is mirrored onto the page's [WebMCP](https://github.com/webmachinelearning/webmcp) model context (`document.modelContext` / `navigator.modelContext`) as a callable tool, so in-page and browser-integrated agents can drive browser-side functionality directly. Wire names, `arg0`/`arg1`/… input schemas, and safety annotations match the MCP projection above.
143
+
144
+
```ts
145
+
const rpc =awaitconnectDevframe()
146
+
147
+
rpc.client.register({
148
+
name: 'my-plugin:highlight-node',
149
+
type: 'action',
150
+
jsonSerializable: true,
151
+
agent: {
152
+
description: 'Highlight a node in the open inspector view. Use it to point the user at a finding.',
153
+
},
154
+
handler: (id:string) =>highlightNode(id),
155
+
})
156
+
```
157
+
158
+
`connectDevframe()` wires this on its own when the browser provides a model context; `webmcp: false` keeps the browser side off the WebMCP surface. `registerWebMcpTools(collector)` (from `devframe/client`) applies the same projection to a hand-built collector and returns a dispose that unregisters every tool.
159
+
160
+
> [!WARNING]
161
+
> WebMCP is an experimental proposal; `registerWebMcpTools` tracks the current draft (`AbortSignal`-based unregistration) and earlier handle-returning drafts, but the browser API may still change.
162
+
140
163
## Writing descriptions agents act on
141
164
142
165
Describe *when* to use a tool, not just its return:
Exposing a function over MCP requires `jsonSerializable: true`.
209
+
The `agent` field implicitly enables strict JSON serialization because MCP consumes JSON-shaped data. Set `jsonSerializable: true` directly when an RPC-only function also benefits from that contract.
Copy file name to clipboardExpand all lines: docs/content/1.guide/4.shared-state.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -72,7 +72,7 @@ state.mutate((draft) => {
72
72
})
73
73
```
74
74
75
-
Devframe applies the recipe to a draft, emits `updated` (with `SharedStatePatch[]` if enabled), and broadcasts to RPC clients; a `syncIds` set keeps mutations idempotent on replay.
75
+
Devframe applies the recipe to a draft. When Immer returns a new state reference, Devframe emits `updated`and broadcasts it to RPC clients. Explicit replacement objects also notify. With patches enabled, `updated` carries `SharedStatePatch[]`. Sync IDs remain recorded for unchanged recipes, so replays stay idempotent.
Copy file name to clipboardExpand all lines: docs/content/2.adapters/1.initiate.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -129,7 +129,7 @@ Fetch handlers only hand over `Request`s, so the host framework binds the RPC so
129
129
130
130
## Auth
131
131
132
-
The running devframe **gates by default**. The interactive OTP handler wires automatically, printing its code/magic-link banner once the public origin is known, whether from the `origin` option or derived from a request whose own origin is loopback or exactly matches an `allowedOrigins` entry. A non-loopback deployment (behind a proxy, on a LAN, on a public host) sets `origin` explicitly so the magic link resolves to the intended address; a raw inbound `Host` header and forwarded headers are never trusted. Pass `auth: false` for single-user localhost, or a `DevframeAuthHandler` for a custom scheme.
132
+
The running devframe **gates by default**. The interactive OTP handler wires automatically, printing its code/magic-link banner when an untrusted browser client asks for a code (`rpc.requestAuthCode()`); an already-authorized page triggers no print. The magic link's origin comes from the `origin` option, or is derived from a request whose own origin is loopback or exactly matches an `allowedOrigins` entry. A non-loopback deployment (behind a proxy, on a LAN, on a public host) sets `origin` explicitly so the magic link resolves to the intended address; a raw inbound `Host` header and forwarded headers are never trusted. Pass `auth: false` for single-user localhost, or a `DevframeAuthHandler` for a custom scheme.
0 commit comments