Skip to content

Commit ee1bb10

Browse files
committed
feat: dynamic actors
1 parent 8b3a33e commit ee1bb10

File tree

214 files changed

+13363
-1633
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

214 files changed

+13363
-1633
lines changed

CLAUDE.md

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,23 @@ git commit -m "chore(my-pkg): foo bar"
104104
- Prefer the Tokio-shaped APIs from `antiox` for concurrency needs. For example, use `antiox/sync/mpsc` for `tx` and `rx` channels, `antiox/task` for spawning tasks, and the matching sync and time modules as needed.
105105
- Treat `antiox` as the default choice for any TypeScript concurrency work because it mirrors Rust and Tokio APIs used elsewhere in the codebase.
106106

107+
### RivetKit Type Build Troubleshooting
108+
- If `rivetkit` type or DTS builds fail with missing `@rivetkit/*` declarations, run `pnpm build -F rivetkit` from repo root (Turbo build path) before changing TypeScript `paths`.
109+
- Do not add temporary `@rivetkit/*` path aliases in `rivetkit-typescript/packages/rivetkit/tsconfig.json` to work around stale or missing built declarations.
110+
111+
### RivetKit Driver Registry Variants
112+
- Keep `rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/registry.ts` as the canonical type anchor for fixtures and test typing.
113+
- Run driver runtime suites through `registry-static.ts` and `registry-dynamic.ts` instead of executing `registry.ts` directly.
114+
- Load static fixture actors with dynamic ESM `import()` from the `fixtures/driver-test-suite/actors/` directory.
115+
- Skip dynamic registry parity only for the explicit nested dynamic harness gate or missing secure-exec dist, and still treat full static and dynamic compatibility as the target for all normal driver suites.
116+
107117
### SQLite Package
108118
- Use `@rivetkit/sqlite` for SQLite WebAssembly support.
109119
- Do not use the legacy upstream package directly. `@rivetkit/sqlite` is the maintained fork used in this repository and is sourced from `rivet-dev/wa-sqlite`.
110120
- The native SQLite addon (`@rivetkit/sqlite-native`) statically links SQLite via `libsqlite3-sys` with the `bundled` feature. The bundled SQLite version must match the version used by `@rivetkit/sqlite` (WASM). When upgrading either, upgrade both.
111121

112122
### RivetKit Package Resolutions
113-
The root `/package.json` contains `resolutions` that map RivetKit packages to their local workspace versions:
123+
- The root `/package.json` contains `resolutions` that map RivetKit packages to local workspace versions:
114124

115125
```json
116126
{
@@ -123,7 +133,7 @@ The root `/package.json` contains `resolutions` that map RivetKit packages to th
123133
}
124134
```
125135

126-
When adding RivetKit dependencies to examples in `/examples/`, use `*` as the version. The root resolutions will automatically resolve these to the local workspace packages:
136+
- Use `*` as the dependency version when adding RivetKit packages to `/examples/`, because root resolutions map them to local workspace packages:
127137

128138
```json
129139
{
@@ -134,7 +144,19 @@ When adding RivetKit dependencies to examples in `/examples/`, use `*` as the ve
134144
}
135145
```
136146

137-
If you need to add a new `@rivetkit/*` package that isn't already in the root resolutions, add it to the `resolutions` object in `/package.json` with `"workspace:*"` as the value. Internal packages like `@rivetkit/workflow-engine` should be re-exported from `rivetkit` subpaths (e.g., `rivetkit/workflow`) rather than added as direct dependencies.
147+
- Add new internal `@rivetkit/*` packages to root `resolutions` with `"workspace:*"` if missing, and prefer re-exporting internal packages (for example `@rivetkit/workflow-engine`) from `rivetkit` subpaths like `rivetkit/workflow` instead of direct dependencies.
148+
149+
### Dynamic Import Pattern
150+
- For runtime-only dependencies, use dynamic loading so bundlers do not eagerly include them.
151+
- Build the module specifier from string parts (for example with `["pkg", "name"].join("-")` or `["@scope", "pkg"].join("/")`) instead of a single string literal.
152+
- Prefer this pattern for modules like `@rivetkit/sqlite-vfs`, `sandboxed-node`, and `isolated-vm`.
153+
- If loading by resolved file path, resolve first and then import via `pathToFileURL(...).href`.
154+
155+
### Fail-By-Default Runtime Behavior
156+
- Avoid silent no-ops for required runtime behavior.
157+
- Do not use optional chaining for required lifecycle and bridge operations (for example sleep, destroy, alarm dispatch, ack, and websocket dispatch paths).
158+
- If a capability is required, validate it and throw an explicit error with actionable context instead of returning early.
159+
- Optional chaining is acceptable only for best-effort diagnostics and cleanup paths (for example logging hooks and dispose/release cleanup).
138160

139161
### Rust Dependencies
140162

@@ -154,29 +176,29 @@ If you need to add a new `@rivetkit/*` package that isn't already in the root re
154176

155177
### Docs (`website/src/content/docs/**/*.mdx`)
156178

157-
Required frontmatter fields:
179+
- Required frontmatter fields:
158180

159181
- `title` (string)
160182
- `description` (string)
161183
- `skill` (boolean)
162184

163185
### Blog + Changelog (`website/src/content/posts/**/page.mdx`)
164186

165-
Required frontmatter fields:
187+
- Required frontmatter fields:
166188

167189
- `title` (string)
168190
- `description` (string)
169191
- `author` (enum: `nathan-flurry`, `nicholas-kissel`, `forest-anderson`)
170192
- `published` (date string)
171193
- `category` (enum: `changelog`, `monthly-update`, `launch-week`, `technical`, `guide`, `frogs`)
172194

173-
Optional frontmatter fields:
195+
- Optional frontmatter fields:
174196

175197
- `keywords` (string array)
176198

177199
## Examples
178200

179-
All example READMEs in `/examples/` should follow the format defined in `.claude/resources/EXAMPLE_TEMPLATE.md`.
201+
- All example READMEs in `/examples/` should follow the format defined in `.claude/resources/EXAMPLE_TEMPLATE.md`.
180202

181203
## Agent Working Directory
182204

@@ -188,11 +210,12 @@ All agent working files live in `.agent/` at the repo root.
188210
- **Notes**: `.agent/notes/` -- general notes and tracking.
189211

190212
When the user asks to track something in a note, store it in `.agent/notes/` by default. When something is identified as "do later", add it to `.agent/todo/`. Design documents and interface specs go in `.agent/specs/`.
213+
- When the user asks to update any `CLAUDE.md`, add one-line bullet points only, or add a new section containing one-line bullet points.
191214

192215
## Architecture
193216

194217
### Monorepo Structure
195-
This is a Rust workspace-based monorepo for Rivet. Key packages and components:
218+
- This is a Rust workspace-based monorepo for Rivet with the following key packages and components:
196219

197220
- **Core Engine** (`packages/core/engine/`) - Main orchestration service that coordinates all operations
198221
- **Workflow Engine** (`packages/common/gasoline/`) - Handles complex multi-step operations with reliability and observability
@@ -208,7 +231,7 @@ This is a Rust workspace-based monorepo for Rivet. Key packages and components:
208231
- Custom error system at `packages/common/error/`
209232
- Uses derive macros with struct-based error definitions
210233

211-
To use custom errors:
234+
- Use this pattern for custom errors:
212235

213236
```rust
214237
use rivet_error::*;
@@ -237,13 +260,13 @@ let error = AuthInvalidToken.build();
237260
let error_with_meta = ApiRateLimited { limit: 100, reset_at: 1234567890 }.build();
238261
```
239262

240-
Key points:
263+
- Key points:
241264
- Use `#[derive(RivetError)]` on struct definitions
242265
- Use `#[error(group, code, description)]` or `#[error(group, code, description, formatted_message)]` attribute
243266
- Group errors by module/domain (e.g., "auth", "actor", "namespace")
244267
- Add `Serialize, Deserialize` derives for errors with metadata fields
245268
- Always return anyhow errors from failable functions
246-
- For example: `fn foo() -> Result<i64> { /* ... */ }`
269+
- For example: `fn foo() -> Result<i64> { /* ... */ }`
247270
- Do not glob import (`::*`) from anyhow. Instead, import individual types and traits
248271
- Prefer anyhow's `.context()` over `anyhow!` macro
249272

@@ -283,7 +306,7 @@ Key points:
283306

284307
## Naming Conventions
285308

286-
Data structures often include:
309+
- Data structures often include:
287310

288311
- `id` (uuid)
289312
- `name` (machine-readable name, must be valid DNS subdomain, convention is using kebab case)
@@ -320,6 +343,7 @@ Data structures often include:
320343
- **Never use `vi.mock`, `jest.mock`, or module-level mocking.** Write tests against real infrastructure (Docker containers, real databases, real filesystems). For LLM calls, use `@copilotkit/llmock` to run a mock LLM server. For protocol-level test doubles (e.g., ACP adapters), write hand-written scripts that run as real processes. If you need callback tracking, `vi.fn()` for simple callbacks is acceptable.
321344
- When running tests, always pipe the test to a file in /tmp/ then grep it in a second step. You can grep test logs multiple times to search for different log lines.
322345
- For RivetKit TypeScript tests, run from `rivetkit-typescript/packages/rivetkit` and use `pnpm test <filter>` with `-t` to narrow to specific suites. For example: `pnpm test driver-file-system -t ".*Actor KV.*"`.
346+
- When RivetKit tests need a local engine instance, start the RocksDB engine in the background with `./scripts/run/engine-rocksdb.sh >/tmp/rivet-engine-startup.log 2>&1 &`.
323347
- For frontend testing, use the `agent-browser` skill to interact with and test web UIs in examples. This allows automated browser-based testing of frontend applications.
324348
- If you modify frontend UI, automatically use the Agent Browser CLI to take updated screenshots and post them to the PR with a short comment before wrapping up the task.
325349

@@ -332,7 +356,7 @@ Data structures often include:
332356
- When talking about "Rivet Actors" make sure to capitalize "Rivet Actor" as a proper noun and lowercase "actor" as a generic noun
333357

334358
### Documentation Sync
335-
When making changes to the engine or RivetKit, ensure the corresponding documentation is updated:
359+
- Ensure corresponding documentation is updated when making engine or RivetKit changes:
336360
- **Limits changes** (e.g., max message sizes, timeouts): Update `website/src/content/docs/actors/limits.mdx`
337361
- **Config changes** (e.g., new config options in `engine/packages/config/`): Update `website/src/content/docs/self-hosting/configuration.mdx`
338362
- **RivetKit config changes** (e.g., `rivetkit-typescript/packages/rivetkit/src/registry/config/index.ts` or `rivetkit-typescript/packages/rivetkit/src/actor/config.ts`): Update `website/src/content/docs/actors/limits.mdx` if they affect limits/timeouts
@@ -359,8 +383,8 @@ When making changes to the engine or RivetKit, ensure the corresponding document
359383

360384
#### Common Vercel Example Errors
361385

362-
After regenerating Vercel examples, you may see type check errors like:
386+
- You may see type-check errors like the following after regenerating Vercel examples:
363387
```
364388
error TS2688: Cannot find type definition file for 'vite/client'.
365389
```
366-
with warnings about `node_modules missing`. This happens because the regenerated examples need their dependencies reinstalled. Fix by running `pnpm install` before running type checks.
390+
- You may also see `node_modules missing` warnings; fix this by running `pnpm install` before type checks because regenerated examples need dependencies reinstalled.
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Dynamic Actors Architecture
2+
3+
## Overview
4+
5+
Dynamic actors let a registry entry resolve actor source code at actor start time.
6+
7+
Dynamic actors are represented by `dynamicActor({ load, auth?, options? })`
8+
and still participate in normal registry routing and actor lifecycle.
9+
10+
Driver parity is verified by running the same driver test suites against two
11+
fixture registries:
12+
13+
- `fixtures/driver-test-suite/registry-static.ts`
14+
- `fixtures/driver-test-suite/registry-dynamic.ts`
15+
16+
Both registries are built from `fixtures/driver-test-suite/actors/` to keep
17+
actor behavior consistent between static and dynamic execution.
18+
19+
## Main Components
20+
21+
- Host runtime manager:
22+
`rivetkit-typescript/packages/rivetkit/src/dynamic/isolate-runtime.ts`
23+
Creates and owns one `NodeProcess` isolate per dynamic actor instance.
24+
- Isolate bootstrap runtime:
25+
`rivetkit-typescript/packages/rivetkit/dynamic-isolate-runtime/src/index.cts`
26+
Runs inside the isolate, parses registry config via
27+
`RegistryConfigSchema.parse`, and exports envelope handlers.
28+
- Runtime bridge:
29+
`rivetkit-typescript/packages/rivetkit/src/dynamic/runtime-bridge.ts`
30+
Shared envelope and callback payload types for host and isolate.
31+
- Driver integration:
32+
`drivers/file-system/global-state.ts` and `drivers/engine/actor-driver.ts`
33+
Branch on definition type, construct dynamic runtime, and proxy fetch and websocket traffic.
34+
35+
## Lifecycle
36+
37+
1. Driver resolves actor definition from registry.
38+
2. If definition is dynamic, driver creates `DynamicActorIsolateRuntime`.
39+
3. Runtime calls loader and gets `{ source, sourceFormat?, nodeProcess? }`.
40+
4. Runtime writes source into actor runtime dir:
41+
- `sourceFormat: "esm-js"` -> `dynamic-source.mjs` (written unchanged)
42+
- `sourceFormat: "commonjs-js"` -> `dynamic-source.cjs` (written unchanged)
43+
- default `sourceFormat: "typescript"` -> transpiled to `dynamic-source.cjs`
44+
5. Runtime writes isolate bootstrap entry into actor runtime dir.
45+
6. Runtime builds a locked down sandbox driver and creates `NodeProcess`.
46+
7. Runtime injects host bridge refs and bootstrap config into isolate globals.
47+
8. Runtime loads bootstrap module and captures exported envelope refs.
48+
49+
Before HTTP and WebSocket traffic is forwarded into the isolate, the host
50+
runtime may run an optional dynamic auth hook. The auth hook receives dynamic
51+
actor metadata, the incoming `Request`, and decoded connection params. Throwing
52+
from auth rejects the request before actor dispatch. HTTP requests return
53+
standard RivetKit error responses and WebSockets close with the derived
54+
`group.code` reason.
55+
56+
Dynamic actors also expose an internal `PUT /dynamic/reload` control endpoint.
57+
Drivers intercept this request before isolate dispatch, mark the actor for
58+
sleep, and return `200`. The next request wakes the actor through the normal
59+
start path, which calls the dynamic loader again and picks up fresh source.
60+
61+
Note: isolate bootstrap does not construct `Registry` at runtime. Constructing
62+
`Registry` would auto-start runtime preparation on next tick in non-test mode
63+
and pull default drivers that are not needed for dynamic actor execution.
64+
65+
## Bridge Contract
66+
67+
Host to isolate calls:
68+
69+
- `dynamicFetchEnvelope`
70+
- `dynamicOpenWebSocketEnvelope`
71+
- `dynamicWebSocketSendEnvelope`
72+
- `dynamicWebSocketCloseEnvelope`
73+
- `dynamicDispatchAlarmEnvelope`
74+
- `dynamicStopEnvelope`
75+
- `dynamicGetHibernatingWebSocketsEnvelope`
76+
- `dynamicDisposeEnvelope`
77+
78+
Isolate to host callbacks:
79+
80+
- KV: `kvBatchPut`, `kvBatchGet`, `kvBatchDelete`, `kvListPrefix`
81+
- Lifecycle: `setAlarm`, `startSleep`, `startDestroy`
82+
- Networking: `dispatch` for websocket events
83+
- Runner ack path: `ackHibernatableWebSocketMessage`
84+
- Inline client bridge: `clientCall`
85+
86+
Binary payloads are normalized to `ArrayBuffer` at the host and isolate boundary.
87+
88+
## Security Model
89+
90+
- Each dynamic actor runs in its own sandboxed `NodeProcess`.
91+
- Sandbox permissions deny network and child process access.
92+
- Filesystem access is restricted to dynamic runtime root and read only `node_modules` paths.
93+
- Environment is explicitly injected by host config for the isolate process.
94+
95+
## Module Access Projection
96+
97+
Dynamic actors use secure-exec `moduleAccess` projection to expose a
98+
read-only `/root/node_modules` view into host dependencies (allow-listing
99+
`rivetkit` and transitive packages). We no longer stage a temporary
100+
`node_modules` tree for runtime bootstrap.
101+
102+
## Driver Test Skip Gate
103+
104+
The dynamic registry variant in driver tests has a narrow skip gate for two
105+
cases only:
106+
107+
- secure-exec dist is not available on the local machine
108+
- nested dynamic harness mode is explicitly enabled for tests
109+
110+
This gate is only to avoid invalid test harness setups. Static and dynamic
111+
behavior parity remains the expected target for normal driver test execution.

0 commit comments

Comments
 (0)