From 5c80f8f2c0ae60f4b75681d40e3c99a4c1210aab Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 8 Feb 2026 16:49:05 +0000 Subject: [PATCH 1/3] Add readonly connections documentation Sync documentation for PR #610 from cloudflare/agents repository. Adds comprehensive documentation for the new readonly connections feature, which allows restricting WebSocket connections from modifying agent state while still allowing them to receive state updates and call RPC methods. Key additions: - Server-side methods: shouldConnectionBeReadonly, setConnectionReadonly, isConnectionReadonly - Client-side API: onStateUpdateError callback - Usage examples for query parameters, RBAC, admin dashboards, and dynamic permissions - Behavior details including state sync and connection cleanup - Best practices for authentication, user feedback, and permission checks - Migration guide for existing agents Source PR: https://github.com/cloudflare/agents/pull/610 --- .../api-reference/readonly-connections.mdx | 525 ++++++++++++++++++ 1 file changed, 525 insertions(+) create mode 100644 src/content/docs/agents/api-reference/readonly-connections.mdx diff --git a/src/content/docs/agents/api-reference/readonly-connections.mdx b/src/content/docs/agents/api-reference/readonly-connections.mdx new file mode 100644 index 00000000000..f31afc54df6 --- /dev/null +++ b/src/content/docs/agents/api-reference/readonly-connections.mdx @@ -0,0 +1,525 @@ +--- +title: Readonly connections +pcx_content_type: concept +sidebar: + order: 8 +--- + +import { TypeScriptExample } from "~/components"; + +Readonly connections allow you to restrict certain WebSocket connections from modifying the agent state while still allowing them to receive state updates and call RPC methods. + +## Overview + +When a connection is marked as readonly: + +- It can **receive** state updates from the server +- It can **call** RPC methods (callable methods on the agent) +- It **cannot** send state updates using `setState()` + +This is useful for scenarios like: + +- **View-only modes**: Users who should only observe but not modify +- **Role-based access**: Restricting state modifications based on user roles +- **Multi-tenant scenarios**: Some tenants have read-only access +- **Audit and monitoring connections**: Observers that should not affect the system + +## Server-side methods + +### shouldConnectionBeReadonly + +An overridable hook that determines if a connection should be marked as readonly when it connects. + + + +```ts +export class MyAgent extends Agent { + shouldConnectionBeReadonly( + connection: Connection, + ctx: ConnectionContext + ): boolean { + // Example: Check query parameters + const url = new URL(ctx.request.url); + return url.searchParams.get("readonly") === "true"; + } +} +``` + + + +### setConnectionReadonly + +Explicitly mark or unmark a connection as readonly. Can be called at any time. + + + +```ts +export class MyAgent extends Agent { + onConnect(connection: Connection, ctx: ConnectionContext) { + // Dynamic logic to determine readonly status + if (userIsViewer) { + this.setConnectionReadonly(connection, true); + } + } + + @callable() + async promoteToEditor(connectionId: string) { + const conn = this.getConnections().find((c) => c.id === connectionId); + if (conn) { + this.setConnectionReadonly(conn, false); + } + } +} +``` + + + +### isConnectionReadonly + +Check if a connection is currently marked as readonly. + + + +```ts +export class MyAgent extends Agent { + @callable() + async checkAccess() { + const { connection } = getCurrentAgent(); + if (connection) { + return { + canEdit: !this.isConnectionReadonly(connection) + }; + } + } +} +``` + + + +## Client-side API + +### onStateUpdateError callback + +Handle errors when a readonly connection attempts to update state. + + + +```ts +// Using AgentClient +const client = new AgentClient({ + agent: "MyAgent", + name: "instance", + onStateUpdateError: (error) => { + console.error("State update failed:", error); + alert("You do not have permission to modify the state"); + } +}); + +// Using React Hook +const agent = useAgent({ + agent: "MyAgent", + name: "instance", + onStateUpdateError: (error) => { + setError(error); + // Show user-friendly message + } +}); +``` + + + +## Examples + +### Query parameter based access + + + +```ts +export class DocumentAgent extends Agent { + shouldConnectionBeReadonly( + connection: Connection, + ctx: ConnectionContext + ): boolean { + const url = new URL(ctx.request.url); + const mode = url.searchParams.get("mode"); + return mode === "view"; + } +} + +// Client connects with readonly mode +const agent = useAgent({ + agent: "DocumentAgent", + name: "doc-123", + query: { mode: "view" }, + onStateUpdateError: (error) => { + toast.error("Document is in view-only mode"); + } +}); +``` + + + +### Role-based access control + + + +```ts +export class CollaborativeAgent extends Agent { + shouldConnectionBeReadonly( + connection: Connection, + ctx: ConnectionContext + ): boolean { + const url = new URL(ctx.request.url); + const role = url.searchParams.get("role"); + return role === "viewer" || role === "guest"; + } + + onConnect(connection: Connection, ctx: ConnectionContext) { + const url = new URL(ctx.request.url); + const userId = url.searchParams.get("userId"); + + console.log( + `User ${userId} connected (readonly: ${this.isConnectionReadonly(connection)})` + ); + } + + @callable() + async upgradeToEditor() { + const { connection } = getCurrentAgent(); + if (!connection) return; + + // Check permissions (pseudo-code) + const canUpgrade = await checkUserPermissions(); + if (canUpgrade) { + this.setConnectionReadonly(connection, false); + return { success: true }; + } + + throw new Error("Insufficient permissions"); + } +} +``` + + + +### Admin dashboard + + + +```ts +export class MonitoringAgent extends Agent { + shouldConnectionBeReadonly( + connection: Connection, + ctx: ConnectionContext + ): boolean { + const url = new URL(ctx.request.url); + // Only admins can modify state + return url.searchParams.get("admin") !== "true"; + } + + onStateUpdate(state: SystemState, source: Connection | "server") { + if (source !== "server") { + // Log who modified the state + console.log(`State modified by connection ${source.id}`); + } + } +} + +// Admin client (can modify) +const adminAgent = useAgent({ + agent: "MonitoringAgent", + name: "system", + query: { admin: "true" } +}); + +// Viewer client (readonly) +const viewerAgent = useAgent({ + agent: "MonitoringAgent", + name: "system", + query: { admin: "false" }, + onStateUpdateError: (error) => { + console.log("Viewer cannot modify state"); + } +}); +``` + + + +### Dynamic permission changes + + + +```ts +export class GameAgent extends Agent { + @callable() + async startSpectatorMode() { + const { connection } = getCurrentAgent(); + if (!connection) return; + + this.setConnectionReadonly(connection, true); + return { mode: "spectator" }; + } + + @callable() + async joinAsPlayer() { + const { connection } = getCurrentAgent(); + if (!connection) return; + + const canJoin = this.state.players.length < 4; + if (canJoin) { + this.setConnectionReadonly(connection, false); + return { mode: "player" }; + } + + throw new Error("Game is full"); + } + + @callable() + async getMyPermissions() { + const { connection } = getCurrentAgent(); + if (!connection) return null; + + return { + canEdit: !this.isConnectionReadonly(connection), + connectionId: connection.id + }; + } +} + +// Client-side React component +function GameComponent() { + const [canEdit, setCanEdit] = useState(false); + + const agent = useAgent({ + agent: "GameAgent", + name: "game-123", + onStateUpdateError: (error) => { + toast.error("Cannot modify game state in spectator mode"); + } + }); + + useEffect(() => { + agent.call("getMyPermissions").then(perms => { + setCanEdit(perms?.canEdit ?? false); + }); + }, [agent]); + + return ( +
+ + + + +
+ {canEdit ? "You can modify the game" : "You are spectating"} +
+
+ ); +} +``` + +
+ +## Behavior details + +### What happens when a readonly connection tries to update state + +1. The connection sends a state update message +2. The server checks if the connection is readonly +3. If readonly, the server sends back an error response: + ```json + { + "type": "cf_agent_state_error", + "error": "Connection is readonly" + } + ``` +4. The client `onStateUpdateError` callback is invoked +5. The state is not updated on the server +6. Other connections are not notified + +### State synchronization + +- Readonly connections still receive state updates from the server +- When state is updated (by server or other connections), readonly connections get the new state +- They cannot initiate state changes themselves + +### RPC methods + +- Readonly connections can call RPC methods (functions marked with `@callable()`) +- It is up to you to implement additional authorization checks within RPC methods if needed + +### Connection cleanup + +- When a connection closes, it is automatically removed from the readonly tracking set +- No memory leaks from disconnected connections + +## Best practices + +### Combine with authentication + + + +```ts +export class SecureAgent extends Agent { + shouldConnectionBeReadonly( + connection: Connection, + ctx: ConnectionContext + ): boolean { + const url = new URL(ctx.request.url); + const token = url.searchParams.get("token"); + + // Verify token and get permissions + const permissions = this.verifyToken(token); + return !permissions.canWrite; + } +} +``` + + + +### Provide clear user feedback + + + +```ts +const agent = useAgent({ + agent: "MyAgent", + name: "instance", + onStateUpdateError: (error) => { + // User-friendly messages + if (error.includes("readonly")) { + showToast("You are in view-only mode. Upgrade to edit."); + } + } +}); +``` + + + +### Check permissions before UI actions + + + +```ts +function EditButton() { + const [canEdit, setCanEdit] = useState(false); + const agent = useAgent({ /* ... */ }); + + useEffect(() => { + agent.call("checkPermissions").then(perms => { + setCanEdit(perms.canEdit); + }); + }, []); + + return ( + + ); +} +``` + + + +### Log access attempts + + + +```ts +export class AuditedAgent extends Agent { + onStateUpdate(state: State, source: Connection | "server") { + if (source !== "server") { + this.audit({ + action: "state_update", + connectionId: source.id, + readonly: this.isConnectionReadonly(source), + timestamp: Date.now() + }); + } + } +} +``` + + + +## Migration guide + +If you have existing agents and want to add readonly connection support: + +1. **Server-side**: No breaking changes. The feature is opt-in. +2. **Client-side**: Add `onStateUpdateError` handlers where needed. + + + +```ts +// Before +const agent = useAgent({ + agent: "MyAgent", + name: "instance" +}); + +// After (with error handling) +const agent = useAgent({ + agent: "MyAgent", + name: "instance", + onStateUpdateError: (error) => { + console.error("State update error:", error); + } +}); +``` + + + +## How it works + +### Persistence across hibernation + +Readonly connection status is automatically persisted to the agent SQL storage, which means: + +- **Survives hibernation** - When an agent hibernates and wakes up, readonly connections maintain their status +- **No memory leaks** - Connections are automatically cleaned up when they close +- **Performance optimized** - Uses in-memory cache with SQL fallback + +The implementation uses a two-tier approach: + +1. **In-memory Set** for fast lookups during active operation +2. **SQL table** (`cf_agents_readonly_connections`) for persistence across hibernation + +When checking if a connection is readonly: + +1. First checks the in-memory cache (fast) +2. If not found, queries SQL storage (handles post-hibernation case) +3. Populates cache if found in storage + +### Storage details + +The readonly status is stored in a dedicated table: + +```sql +CREATE TABLE cf_agents_readonly_connections ( + connection_id TEXT PRIMARY KEY NOT NULL, + created_at INTEGER DEFAULT (unixepoch()) +) +``` + +All CRUD operations automatically sync both in-memory and persistent storage. + +## Limitations + +- Readonly status only applies to state updates using `setState()` +- RPC methods can still be called (implement your own checks if needed) + +## Related resources + +- [Store and sync state](/agents/api-reference/store-and-sync-state/) +- [WebSockets](/agents/api-reference/websockets/) From deacc862b50a5bdade7db2130cb3720c72d9b549 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sun, 8 Feb 2026 17:15:35 +0000 Subject: [PATCH 2/3] Split mixed TS+JSX code block to fix build Co-authored-by: elithrar --- .../docs/agents/api-reference/readonly-connections.mdx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/content/docs/agents/api-reference/readonly-connections.mdx b/src/content/docs/agents/api-reference/readonly-connections.mdx index f31afc54df6..b58408bac6b 100644 --- a/src/content/docs/agents/api-reference/readonly-connections.mdx +++ b/src/content/docs/agents/api-reference/readonly-connections.mdx @@ -285,8 +285,15 @@ export class GameAgent extends Agent { }; } } +``` + + + +Client-side React component: + + -// Client-side React component +```tsx function GameComponent() { const [canEdit, setCanEdit] = useState(false); From dc21af5f4034ab926e563990dcb1b11eaebd3e87 Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" Date: Sun, 8 Feb 2026 17:53:27 +0000 Subject: [PATCH 3/3] Fix build: unwrap JSX code blocks from TypeScriptExample MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TypeScriptExample uses ts-blank-space to strip TypeScript types, but ts-blank-space doesn't understand JSX syntax — it misinterprets JSX tags as generics, 'as' in JSX text as type assertions, and '!' as non-null assertions. This caused: 1. GameComponent block:
tag blanked out entirely, breaking Prettier's babel parser with 'Adjacent JSX elements' error 2. EditButton block: !canEdit silently corrupted to canEdit in the generated JavaScript tab Fix by removing the TypeScriptExample wrapper from these two JSX blocks, using plain fenced tsx code blocks instead (matching the pattern used in quick-start.mdx and guides/webhooks.mdx). --- .../api-reference/readonly-connections.mdx | 473 +++++++++--------- 1 file changed, 229 insertions(+), 244 deletions(-) diff --git a/src/content/docs/agents/api-reference/readonly-connections.mdx b/src/content/docs/agents/api-reference/readonly-connections.mdx index b58408bac6b..c40f18ce795 100644 --- a/src/content/docs/agents/api-reference/readonly-connections.mdx +++ b/src/content/docs/agents/api-reference/readonly-connections.mdx @@ -34,14 +34,14 @@ An overridable hook that determines if a connection should be marked as readonly ```ts export class MyAgent extends Agent { - shouldConnectionBeReadonly( - connection: Connection, - ctx: ConnectionContext - ): boolean { - // Example: Check query parameters - const url = new URL(ctx.request.url); - return url.searchParams.get("readonly") === "true"; - } + shouldConnectionBeReadonly( + connection: Connection, + ctx: ConnectionContext, + ): boolean { + // Example: Check query parameters + const url = new URL(ctx.request.url); + return url.searchParams.get("readonly") === "true"; + } } ``` @@ -55,20 +55,20 @@ Explicitly mark or unmark a connection as readonly. Can be called at any time. ```ts export class MyAgent extends Agent { - onConnect(connection: Connection, ctx: ConnectionContext) { - // Dynamic logic to determine readonly status - if (userIsViewer) { - this.setConnectionReadonly(connection, true); - } - } - - @callable() - async promoteToEditor(connectionId: string) { - const conn = this.getConnections().find((c) => c.id === connectionId); - if (conn) { - this.setConnectionReadonly(conn, false); - } - } + onConnect(connection: Connection, ctx: ConnectionContext) { + // Dynamic logic to determine readonly status + if (userIsViewer) { + this.setConnectionReadonly(connection, true); + } + } + + @callable() + async promoteToEditor(connectionId: string) { + const conn = this.getConnections().find((c) => c.id === connectionId); + if (conn) { + this.setConnectionReadonly(conn, false); + } + } } ``` @@ -82,15 +82,15 @@ Check if a connection is currently marked as readonly. ```ts export class MyAgent extends Agent { - @callable() - async checkAccess() { - const { connection } = getCurrentAgent(); - if (connection) { - return { - canEdit: !this.isConnectionReadonly(connection) - }; - } - } + @callable() + async checkAccess() { + const { connection } = getCurrentAgent(); + if (connection) { + return { + canEdit: !this.isConnectionReadonly(connection), + }; + } + } } ``` @@ -107,22 +107,22 @@ Handle errors when a readonly connection attempts to update state. ```ts // Using AgentClient const client = new AgentClient({ - agent: "MyAgent", - name: "instance", - onStateUpdateError: (error) => { - console.error("State update failed:", error); - alert("You do not have permission to modify the state"); - } + agent: "MyAgent", + name: "instance", + onStateUpdateError: (error) => { + console.error("State update failed:", error); + alert("You do not have permission to modify the state"); + }, }); // Using React Hook const agent = useAgent({ - agent: "MyAgent", - name: "instance", - onStateUpdateError: (error) => { - setError(error); - // Show user-friendly message - } + agent: "MyAgent", + name: "instance", + onStateUpdateError: (error) => { + setError(error); + // Show user-friendly message + }, }); ``` @@ -136,24 +136,24 @@ const agent = useAgent({ ```ts export class DocumentAgent extends Agent { - shouldConnectionBeReadonly( - connection: Connection, - ctx: ConnectionContext - ): boolean { - const url = new URL(ctx.request.url); - const mode = url.searchParams.get("mode"); - return mode === "view"; - } + shouldConnectionBeReadonly( + connection: Connection, + ctx: ConnectionContext, + ): boolean { + const url = new URL(ctx.request.url); + const mode = url.searchParams.get("mode"); + return mode === "view"; + } } // Client connects with readonly mode const agent = useAgent({ - agent: "DocumentAgent", - name: "doc-123", - query: { mode: "view" }, - onStateUpdateError: (error) => { - toast.error("Document is in view-only mode"); - } + agent: "DocumentAgent", + name: "doc-123", + query: { mode: "view" }, + onStateUpdateError: (error) => { + toast.error("Document is in view-only mode"); + }, }); ``` @@ -165,38 +165,38 @@ const agent = useAgent({ ```ts export class CollaborativeAgent extends Agent { - shouldConnectionBeReadonly( - connection: Connection, - ctx: ConnectionContext - ): boolean { - const url = new URL(ctx.request.url); - const role = url.searchParams.get("role"); - return role === "viewer" || role === "guest"; - } - - onConnect(connection: Connection, ctx: ConnectionContext) { - const url = new URL(ctx.request.url); - const userId = url.searchParams.get("userId"); - - console.log( - `User ${userId} connected (readonly: ${this.isConnectionReadonly(connection)})` - ); - } - - @callable() - async upgradeToEditor() { - const { connection } = getCurrentAgent(); - if (!connection) return; - - // Check permissions (pseudo-code) - const canUpgrade = await checkUserPermissions(); - if (canUpgrade) { - this.setConnectionReadonly(connection, false); - return { success: true }; - } - - throw new Error("Insufficient permissions"); - } + shouldConnectionBeReadonly( + connection: Connection, + ctx: ConnectionContext, + ): boolean { + const url = new URL(ctx.request.url); + const role = url.searchParams.get("role"); + return role === "viewer" || role === "guest"; + } + + onConnect(connection: Connection, ctx: ConnectionContext) { + const url = new URL(ctx.request.url); + const userId = url.searchParams.get("userId"); + + console.log( + `User ${userId} connected (readonly: ${this.isConnectionReadonly(connection)})`, + ); + } + + @callable() + async upgradeToEditor() { + const { connection } = getCurrentAgent(); + if (!connection) return; + + // Check permissions (pseudo-code) + const canUpgrade = await checkUserPermissions(); + if (canUpgrade) { + this.setConnectionReadonly(connection, false); + return { success: true }; + } + + throw new Error("Insufficient permissions"); + } } ``` @@ -208,38 +208,38 @@ export class CollaborativeAgent extends Agent { ```ts export class MonitoringAgent extends Agent { - shouldConnectionBeReadonly( - connection: Connection, - ctx: ConnectionContext - ): boolean { - const url = new URL(ctx.request.url); - // Only admins can modify state - return url.searchParams.get("admin") !== "true"; - } - - onStateUpdate(state: SystemState, source: Connection | "server") { - if (source !== "server") { - // Log who modified the state - console.log(`State modified by connection ${source.id}`); - } - } + shouldConnectionBeReadonly( + connection: Connection, + ctx: ConnectionContext, + ): boolean { + const url = new URL(ctx.request.url); + // Only admins can modify state + return url.searchParams.get("admin") !== "true"; + } + + onStateUpdate(state: SystemState, source: Connection | "server") { + if (source !== "server") { + // Log who modified the state + console.log(`State modified by connection ${source.id}`); + } + } } // Admin client (can modify) const adminAgent = useAgent({ - agent: "MonitoringAgent", - name: "system", - query: { admin: "true" } + agent: "MonitoringAgent", + name: "system", + query: { admin: "true" }, }); // Viewer client (readonly) const viewerAgent = useAgent({ - agent: "MonitoringAgent", - name: "system", - query: { admin: "false" }, - onStateUpdateError: (error) => { - console.log("Viewer cannot modify state"); - } + agent: "MonitoringAgent", + name: "system", + query: { admin: "false" }, + onStateUpdateError: (error) => { + console.log("Viewer cannot modify state"); + }, }); ``` @@ -251,39 +251,39 @@ const viewerAgent = useAgent({ ```ts export class GameAgent extends Agent { - @callable() - async startSpectatorMode() { - const { connection } = getCurrentAgent(); - if (!connection) return; - - this.setConnectionReadonly(connection, true); - return { mode: "spectator" }; - } - - @callable() - async joinAsPlayer() { - const { connection } = getCurrentAgent(); - if (!connection) return; - - const canJoin = this.state.players.length < 4; - if (canJoin) { - this.setConnectionReadonly(connection, false); - return { mode: "player" }; - } - - throw new Error("Game is full"); - } - - @callable() - async getMyPermissions() { - const { connection } = getCurrentAgent(); - if (!connection) return null; - - return { - canEdit: !this.isConnectionReadonly(connection), - connectionId: connection.id - }; - } + @callable() + async startSpectatorMode() { + const { connection } = getCurrentAgent(); + if (!connection) return; + + this.setConnectionReadonly(connection, true); + return { mode: "spectator" }; + } + + @callable() + async joinAsPlayer() { + const { connection } = getCurrentAgent(); + if (!connection) return; + + const canJoin = this.state.players.length < 4; + if (canJoin) { + this.setConnectionReadonly(connection, false); + return { mode: "player" }; + } + + throw new Error("Game is full"); + } + + @callable() + async getMyPermissions() { + const { connection } = getCurrentAgent(); + if (!connection) return null; + + return { + canEdit: !this.isConnectionReadonly(connection), + connectionId: connection.id, + }; + } } ``` @@ -291,52 +291,43 @@ export class GameAgent extends Agent { Client-side React component: - - ```tsx function GameComponent() { - const [canEdit, setCanEdit] = useState(false); - - const agent = useAgent({ - agent: "GameAgent", - name: "game-123", - onStateUpdateError: (error) => { - toast.error("Cannot modify game state in spectator mode"); - } - }); - - useEffect(() => { - agent.call("getMyPermissions").then(perms => { - setCanEdit(perms?.canEdit ?? false); - }); - }, [agent]); - - return ( -
- - - - -
- {canEdit ? "You can modify the game" : "You are spectating"} -
-
- ); + const [canEdit, setCanEdit] = useState(false); + + const agent = useAgent({ + agent: "GameAgent", + name: "game-123", + onStateUpdateError: (error) => { + toast.error("Cannot modify game state in spectator mode"); + }, + }); + + useEffect(() => { + agent.call("getMyPermissions").then((perms) => { + setCanEdit(perms?.canEdit ?? false); + }); + }, [agent]); + + return ( +
+ + + + +
{canEdit ? "You can modify the game" : "You are spectating"}
+
+ ); } ``` -
- ## Behavior details ### What happens when a readonly connection tries to update state @@ -346,8 +337,8 @@ function GameComponent() { 3. If readonly, the server sends back an error response: ```json { - "type": "cf_agent_state_error", - "error": "Connection is readonly" + "type": "cf_agent_state_error", + "error": "Connection is readonly" } ``` 4. The client `onStateUpdateError` callback is invoked @@ -378,17 +369,17 @@ function GameComponent() { ```ts export class SecureAgent extends Agent { - shouldConnectionBeReadonly( - connection: Connection, - ctx: ConnectionContext - ): boolean { - const url = new URL(ctx.request.url); - const token = url.searchParams.get("token"); - - // Verify token and get permissions - const permissions = this.verifyToken(token); - return !permissions.canWrite; - } + shouldConnectionBeReadonly( + connection: Connection, + ctx: ConnectionContext, + ): boolean { + const url = new URL(ctx.request.url); + const token = url.searchParams.get("token"); + + // Verify token and get permissions + const permissions = this.verifyToken(token); + return !permissions.canWrite; + } } ``` @@ -400,14 +391,14 @@ export class SecureAgent extends Agent { ```ts const agent = useAgent({ - agent: "MyAgent", - name: "instance", - onStateUpdateError: (error) => { - // User-friendly messages - if (error.includes("readonly")) { - showToast("You are in view-only mode. Upgrade to edit."); - } - } + agent: "MyAgent", + name: "instance", + onStateUpdateError: (error) => { + // User-friendly messages + if (error.includes("readonly")) { + showToast("You are in view-only mode. Upgrade to edit."); + } + }, }); ``` @@ -415,45 +406,39 @@ const agent = useAgent({ ### Check permissions before UI actions - - -```ts +```tsx function EditButton() { - const [canEdit, setCanEdit] = useState(false); - const agent = useAgent({ /* ... */ }); - - useEffect(() => { - agent.call("checkPermissions").then(perms => { - setCanEdit(perms.canEdit); - }); - }, []); - - return ( - - ); + const [canEdit, setCanEdit] = useState(false); + const agent = useAgent({ + /* ... */ + }); + + useEffect(() => { + agent.call("checkPermissions").then((perms) => { + setCanEdit(perms.canEdit); + }); + }, []); + + return ; } ``` - - ### Log access attempts ```ts export class AuditedAgent extends Agent { - onStateUpdate(state: State, source: Connection | "server") { - if (source !== "server") { - this.audit({ - action: "state_update", - connectionId: source.id, - readonly: this.isConnectionReadonly(source), - timestamp: Date.now() - }); - } - } + onStateUpdate(state: State, source: Connection | "server") { + if (source !== "server") { + this.audit({ + action: "state_update", + connectionId: source.id, + readonly: this.isConnectionReadonly(source), + timestamp: Date.now(), + }); + } + } } ``` @@ -471,17 +456,17 @@ If you have existing agents and want to add readonly connection support: ```ts // Before const agent = useAgent({ - agent: "MyAgent", - name: "instance" + agent: "MyAgent", + name: "instance", }); // After (with error handling) const agent = useAgent({ - agent: "MyAgent", - name: "instance", - onStateUpdateError: (error) => { - console.error("State update error:", error); - } + agent: "MyAgent", + name: "instance", + onStateUpdateError: (error) => { + console.error("State update error:", error); + }, }); ```