Skip to content

feat: readonly connections — restrict WebSocket clients from modifying agent state#610

Merged
threepointone merged 10 commits intomainfrom
readonly-state
Feb 8, 2026
Merged

feat: readonly connections — restrict WebSocket clients from modifying agent state#610
threepointone merged 10 commits intomainfrom
readonly-state

Conversation

@threepointone
Copy link
Copy Markdown
Contributor

@threepointone threepointone commented Oct 28, 2025

Closes #255

Summary

Adds a readonly connections API that lets you restrict certain WebSocket clients from modifying agent state while still receiving state updates and calling non-mutating RPC methods.

Server-side API

  • shouldConnectionBeReadonly(connection, ctx) — hook evaluated on connect; return true to mark readonly
  • setConnectionReadonly(connection, readonly?) — toggle readonly status dynamically at any time
  • isConnectionReadonly(connection) — check a connection's current status

Enforcement

Readonly connections are blocked at two levels:

  1. Client-side setState() — the server rejects the message and sends a cf_agent_state_error back
  2. @callable() methods that call this.setState() — the framework throws Error("Connection is readonly") inside setState(), which surfaces as an RPC error to the caller

Non-mutating callables (reads, queries, permission checks) work normally for readonly connections.

Storage

The readonly flag is stored in a namespaced key (_cf_readonly) inside the connection's WebSocket attachment via connection.setState(). This means:

  • Survives hibernation — no extra SQL tables or queries
  • Automatically cleaned up when the connection closes
  • Safe from user code — connection.state and connection.setState() are wrapped to hide and preserve the internal key

Client-side

  • New onStateUpdateError callback on useAgent / AgentClient for handling rejected state writes

What's included

Area Files
Core implementation packages/agents/src/index.ts — readonly hooks, setState() gating, connection wrapping
Client SDK packages/agents/src/client.ts, react.tsxonStateUpdateError callback
Types packages/agents/src/types.tsCF_AGENT_STATE_ERROR message type
Tests packages/agents/src/tests/readonly-connections.test.ts (17 tests)
Test agents packages/agents/src/tests/agents/readonly.ts
Docs docs/readonly-connections.md — user-facing guide with API reference
Design doc design/readonly-connections.md — internal design, tradeoffs, caveats
Playground demo examples/playground/src/demos/core/ReadonlyDemo.tsx + readonly-agent.ts
Changeset .changeset/empty-eels-unite.md

Test plan

  • 17 tests covering: readonly gating (client setState + RPC), dynamic status changes, isConnectionReadonly, state broadcasts to readonly connections, reconnection, connection state wrapping (hidden _cf_readonly, preserved across setState value/callback forms), sequential mutation failures
  • Playground demo with side-by-side editor/viewer panels, dynamic readonly toggle, separate callable/setState buttons, toast notifications for blocked writes
  • npx tsc --noEmit clean
  • npx oxlint clean

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Oct 28, 2025

🦋 Changeset detected

Latest commit: 029a193

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
agents Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Oct 28, 2025

Open in StackBlitz

npm i https://pkg.pr.new/cloudflare/agents@610

commit: 029a193

@claude

This comment was marked as resolved.

agents-git-bot bot pushed a commit to cloudflare/cloudflare-docs that referenced this pull request Nov 25, 2025
Adds comprehensive documentation for the new readonly connections feature introduced in PR #610.

Key additions:
- Server-side methods: shouldConnectionBeReadonly(), setConnectionReadonly(), isConnectionReadonly()
- Client-side API: onStateUpdateError callback for error handling
- Multiple usage examples covering common scenarios (query params, RBAC, admin dashboards, dynamic permissions)
- Implementation details including SQL persistence and hibernation support
- Best practices for authentication, user feedback, and access control

This feature allows restricting certain WebSocket connections from modifying agent state while still allowing them to receive state updates and call RPC methods.

Related PR: cloudflare/agents#610

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@threepointone threepointone marked this pull request as ready for review November 25, 2025 16:48
@threepointone threepointone changed the title wip: Readonly state Readonly state Nov 25, 2025
Introduces readonly connections to restrict certain WebSocket clients from modifying agent state while allowing state updates and RPC calls. Adds server-side methods for managing readonly status, persists status in SQL for hibernation, and client-side error handling via onStateUpdateError. Updates documentation and relevant types, client, and React hook implementations.
Introduces a new TestReadonlyAgent Durable Object and comprehensive tests for readonly connection behavior, including state update restrictions, RPC permissions, persistence, and cleanup. Updates wrangler config to register the new agent for testing.
Improved type safety in readonly-connections.test.ts by introducing explicit message interfaces and type guards, replacing 'any' with specific types in test helpers and assertions. Also updated TestReadonlyAgent to ignore unused connection parameter in shouldConnectionBeReadonly. These changes enhance code reliability and maintainability in the test suite.
agents-git-bot bot pushed a commit to cloudflare/cloudflare-docs that referenced this pull request Nov 27, 2025
Synced from cloudflare/agents PR #610 (cloudflare/agents#610)

Introduces documentation for the readonly connections feature which allows restricting certain WebSocket connections from modifying Agent state while still allowing them to receive state updates and call RPC methods.

Key features documented:
- Server-side methods: shouldConnectionBeReadonly, setConnectionReadonly, isConnectionReadonly
- Client-side API: onStateUpdateError callback
- Usage examples for query parameter based access, role-based access control, admin dashboards, and dynamic permission changes
- Behavior details, best practices, and migration guide
- Implementation details including persistence across hibernation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@whoiskatrin whoiskatrin self-requested a review November 27, 2025 16:56
@threepointone
Copy link
Copy Markdown
Contributor Author

moving to draft, might have a better implementation here

@threepointone threepointone marked this pull request as draft December 4, 2025 15:22
Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	packages/agents/src/ai-types.ts
#	packages/agents/src/client.ts
#	packages/agents/src/index.ts
#	packages/agents/src/tests/worker.ts
#	packages/agents/src/tests/wrangler.jsonc
Replace biome-ignore comments in packages/agents/src/index.ts with oxlint-disable-next-line typescript-eslint(no-explicit-any) to satisfy the linter while allowing variadic args to be passed through. Also update the test import in packages/agents/src/tests/readonly-connections.test.ts from "../ai-types" to "../types" to match the correct module path.
agents-git-bot bot pushed a commit to cloudflare/cloudflare-docs that referenced this pull request Feb 8, 2026
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: cloudflare/agents#610
elithrar added a commit to cloudflare/cloudflare-docs that referenced this pull request Feb 8, 2026
* 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: cloudflare/agents#610

* Split mixed TS+JSX code block to fix build

Co-authored-by: elithrar <elithrar@users.noreply.github.com>

* Fix build: unwrap JSX code blocks from TypeScriptExample

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: <div> 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).

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: elithrar <elithrar@users.noreply.github.com>
Introduce readonly connections to prevent certain WebSocket clients from modifying agent state while still allowing them to receive updates and call non-mutating RPCs. Adds server APIs (shouldConnectionBeReadonly, setConnectionReadonly, isConnectionReadonly), client onStateUpdateError handling, and enforces restrictions in both the client message handler and Agent.setState(). Internally stores the flag in a namespaced connection attachment (_cf_readonly) and wraps connection.state/setState to preserve the flag across hibernation. Includes design doc, user docs, tests, and a playground demo (ReadonlyDemo + readonly-agent). Also updates example manifests and bumps minor dependencies (hono) where applicable.
Introduce a suite of test agent classes under packages/agents/src/tests/agents (callable, email, mcp, oauth, race, readonly, schedule, state, workflow) to cover RPC, streaming, MCP tooling, OAuth flows, state management, schedules, workflows and concurrency/read-only behaviors. Add an agents index that re-exports these test agents and update packages/agents/src/tests/worker.ts to re-export the agents from ./agents, simplify imports, and expose an Env type via import-types to avoid runtime circulars. These changes centralize test helpers and streamline test worker wiring.
@threepointone threepointone changed the title Readonly state feat: readonly connections — restrict WebSocket clients from modifying agent state Feb 8, 2026
agents-git-bot bot pushed a commit to cloudflare/cloudflare-docs that referenced this pull request Feb 8, 2026
Update documentation for the readonly connections feature which restricts
WebSocket clients from modifying agent state while still receiving updates.

Key changes:
- Updated readonly-connections.mdx with new enforcement model (blocks both
  client setState and @callable methods that call setState)
- Updated storage implementation details (connection state wrapping instead
  of SQL tables)
- Added caveats about side effects in callables
- Added link to readonly connections in store-and-sync-state.mdx

Source PR: cloudflare/agents#610

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Introduce onStatePersisted as the new server-side state notification hook and deprecate onStateUpdate. Add internal dispatch that calls onStatePersisted (or the deprecated onStateUpdate), emits a one-time console warning per class when the old hook is used, and throws if a class overrides both hooks. Ensure validateStateChange rejections propagate a CF_AGENT_STATE_ERROR message back to the client. Update docs, examples, tests, and test harness (wrangler) to cover the new hook and error behavior. Add changeset documenting the patch.
@threepointone threepointone marked this pull request as ready for review February 8, 2026 22:09
agents-git-bot bot pushed a commit to cloudflare/cloudflare-docs that referenced this pull request Feb 8, 2026
Updates documentation for readonly connections feature and deprecates
onStateUpdate in favor of onStatePersisted.

Changes:
- Update readonly-connections.mdx with new implementation using connection
  state attachment instead of SQL storage
- Clarify that readonly blocks both client-side setState() and callable
  methods that call this.setState()
- Add enforcement diagram showing how readonly works
- Add caveats section about side effects and limitations
- Rename onStateUpdate to onStatePersisted across all documentation files
- Update API reference tables and examples

Related PR: cloudflare/agents#610

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Rename the server-side persistence hook from `onStatePersisted` to `onStateChanged` across the codebase (docs, READMEs, examples, tests, playground, and package implementation). Update Agent internals to detect and call `onStateChanged`, adjust deprecation/error messages (one-time warning for `onStateUpdate`, and error if both hooks are overridden), and update tests/assertions to match the new name. Also update the changeset metadata to deprecate `onStateUpdate` in favor of `onStateChanged`. (Note: validateStateChange rejection behavior that propagates a `CF_AGENT_STATE_ERROR` message to clients is preserved as documented in the changeset.)
agents-git-bot bot pushed a commit to cloudflare/cloudflare-docs that referenced this pull request Feb 8, 2026
Updates from cloudflare/agents PR #610 (feat: readonly connections):

## Major changes

1. **Readonly connections feature** (readonly-connections.mdx):
   - Updated implementation details: uses connection state wrapping instead of SQL
   - Clarified enforcement for @callable() methods that call setState()
   - Added "What readonly does and does not restrict" table
   - Added caveats section about side effects in callables
   - Removed outdated SQL storage implementation details
   - Updated "How it works" section to reflect connection attachment storage

2. **onStateUpdate → onStateChanged rename**:
   - Renamed server-side hook from onStateUpdate to onStateChanged
   - Updated across all documentation files
   - Client-side onStateUpdate callback remains unchanged

## Updated files
- api-reference/readonly-connections.mdx - major rewrite with corrected implementation
- api-reference/store-and-sync-state.mdx - renamed onStateUpdate to onStateChanged
- api-reference/agents-api.mdx - renamed hook references
- api-reference/client-sdk.mdx - renamed hook references
- concepts/agent-class.mdx - renamed hook references
- getting-started/quick-start.mdx - renamed hook references
- model-context-protocol/mcp-agent-api.mdx - renamed hook references

Related upstream PR: cloudflare/agents#610

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@threepointone
Copy link
Copy Markdown
Contributor Author

ok, I'm feeling good about this, landing it.

@threepointone threepointone merged commit f59f305 into main Feb 8, 2026
5 checks passed
@threepointone threepointone deleted the readonly-state branch February 8, 2026 22:48
@github-actions github-actions bot mentioned this pull request Feb 8, 2026
whoiskatrin added a commit to cloudflare/cloudflare-docs that referenced this pull request Feb 11, 2026
…ebSocket clients from modifying agent state (#28197)

* Sync documentation for PR #610: readonly connections and onStateChanged

Updates from cloudflare/agents PR #610 (feat: readonly connections):

## Major changes

1. **Readonly connections feature** (readonly-connections.mdx):
   - Updated implementation details: uses connection state wrapping instead of SQL
   - Clarified enforcement for @callable() methods that call setState()
   - Added "What readonly does and does not restrict" table
   - Added caveats section about side effects in callables
   - Removed outdated SQL storage implementation details
   - Updated "How it works" section to reflect connection attachment storage

2. **onStateUpdate → onStateChanged rename**:
   - Renamed server-side hook from onStateUpdate to onStateChanged
   - Updated across all documentation files
   - Client-side onStateUpdate callback remains unchanged

## Updated files
- api-reference/readonly-connections.mdx - major rewrite with corrected implementation
- api-reference/store-and-sync-state.mdx - renamed onStateUpdate to onStateChanged
- api-reference/agents-api.mdx - renamed hook references
- api-reference/client-sdk.mdx - renamed hook references
- concepts/agent-class.mdx - renamed hook references
- getting-started/quick-start.mdx - renamed hook references
- model-context-protocol/mcp-agent-api.mdx - renamed hook references

Related upstream PR: cloudflare/agents#610

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* Fix build: wrap @callable() code snippets in class declarations

The Prettier/Babel parser requires decorators to be attached to class
declarations. Three code blocks in readonly-connections.mdx had standalone
@callable() decorators on methods outside of a class body, causing the
build to fail with 'Leading decorators must be attached to a class
declaration'.

* Wrap @callable() snippets in class bodies

Co-authored-by: whoiskatrin <whoiskatrin@users.noreply.github.com>

* Apply suggestions from code review

* Delete package-lock.json

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: opencode-agent[bot] <opencode-agent[bot]@users.noreply.github.com>
Co-authored-by: whoiskatrin <whoiskatrin@users.noreply.github.com>
Co-authored-by: Jun Lee <junlee@cloudflare.com>
Co-authored-by: whoiskatrin <kreznykova@cloudflare.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Make client-initiated direct state change opt-in

2 participants