Skip to content

feat(rivetkit): redesign sandbox actor module#4425

Merged
NathanFlurry merged 5 commits intomainfrom
sandbox-actor
Mar 16, 2026
Merged

feat(rivetkit): redesign sandbox actor module#4425
NathanFlurry merged 5 commits intomainfrom
sandbox-actor

Conversation

@NathanFlurry
Copy link
Member

Summary

  • redesign the RivetKit sandbox actor module around a dedicated config and actor runtime layout
  • add sandbox provider parity coverage and driver-test-suite wiring for sandbox actors
  • update sandbox and actor input docs to cover the new API and dynamic provider flow

Testing

  • pnpm test sandbox-providers

NathanFlurry and others added 4 commits March 12, 2026 20:21
The loop over c.vars.activeSessionIds never executes because createVars
produces a fresh empty Set on each wake cycle. Vars are ephemeral and
recreated fresh on each wake, so there are no sessions to restart timers
for.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
@railway-app railway-app bot temporarily deployed to rivet-frontend / rivet-pr-4425 March 14, 2026 06:05 Destroyed
@railway-app
Copy link

railway-app bot commented Mar 14, 2026

🚅 Deployed to the rivet-pr-4425 environment in rivet-frontend

Service Status Web Updated (UTC)
frontend-inspector 😴 Sleeping (View Logs) Web Mar 16, 2026 at 6:17 am
frontend-cloud 😴 Sleeping (View Logs) Web Mar 16, 2026 at 6:12 am
website ❌ Build Failed (View Logs) Web Mar 16, 2026 at 6:06 am
mcp-hub ✅ Success (View Logs) Web Mar 16, 2026 at 6:04 am
ladle ❌ Build Failed (View Logs) Web Mar 16, 2026 at 6:03 am

@claude
Copy link

claude bot commented Mar 14, 2026

PR Review: feat(rivetkit): redesign sandbox actor module

This is a well-architected redesign with excellent documentation and clear separation of concerns. The JSDoc in actor/index.ts is particularly thorough and helpful. Here are observations ranging from bugs to minor nits.


Bugs and Correctness

  1. destroy action does not reset providerName (inconsistency with onDestroy)

In actor/index.ts, the destroy action resets sandboxId but not providerName. After a destroy action call, resolveProvider would still pass the providerName mismatch check (since state.providerName is still set), but this is a latent inconsistency that could confuse future readers and behaves differently from onDestroy.

  1. Race condition in destroy action -- double-destroy not fully guarded

Two concurrent calls to destroy can both pass the guard before either sets sandboxDestroyed = true, potentially calling provider.destroy() twice. Consider using a per-wake-cycle flag in vars (which is synchronous / non-async-interleaved) to guard the critical section, or add a destroyInProgress state field.

  1. listSessions subscriptions bypass the live agent check

addSubscribedSession still persists session IDs to state.subscribedSessionIds even if the subscription itself could not be set up. This means on next wake, ensureAgent will try to re-subscribe to sessions retrieved by listSessions even if those sessions were never actively monitored. Consider only persisting sessions that are truly active (created/resumed).


Design Concerns

  1. SandboxActorConfigSchema mutual exclusion: TypeScript type does not enforce the constraint

The Zod schema correctly enforces exactly one of provider | createProvider via .refine(). But the TypeScript type SandboxActorProviderConfig is a discriminated union where provider?: never in the second variant allows neither to be provided at the type level. TypeScript accepts sandboxActor({}) with no compile error, but Zod will reject it at runtime. Consider making both branches use required fields with never on the opposing field.

  1. client.ts -- connectTerminal sends close frame then immediately closes socket

The WebSocket close() call after sendFrame may not guarantee the server processes the JSON control frame before the connection drops. If the server uses the close frame to initiate a graceful process shutdown, this could cause race conditions. Consider waiting for a server acknowledgment frame or relying solely on the standard WebSocket close handshake.

  1. client.ts -- SSE reader does not cancel on abort

In consumeProcessLogSse, the abort signal check happens only at the top of the while loop. reader.read() is not passed the signal, so if the fetch is aborted while waiting on a read, the AbortError will only be caught in the outer catch. This works but means the reader may block indefinitely until a byte arrives in environments where the underlying stream does not auto-close on abort.


Test Coverage

  1. actor.test.ts -- Very limited actor unit test coverage

The single test only covers getSandboxUrl and post-destroy behavior. Missing coverage for sleep/wake reconnection, provider mismatch detection, concurrent destroy calls, the createProvider dynamic callback path, and onSessionEvent / onPermissionRequest hook invocation. The client.test.ts file, by contrast, has thorough coverage. Consider adding more actor-level tests, even with mocked providers.


Minor Nits

  1. session.ts -- clearSessionActiveInMemory is unexported but markSessionActiveInMemory is

This is intentional (internal helper). clearAllActiveSessions (exported) is the public way to bulk-clear, while clearSessionActiveInMemory is intentionally internal. A brief comment to that effect would help future readers.

  1. actor/index.ts -- SANDBOX_AGENT_ACTION_METHODS type cast chain

The double cast (as unknown as) is expected here given the dynamic proxy construction, but a comment explaining why this is safe would help.

  1. db.ts -- Legacy table drop comment

Since these tables were never shipped in a release, consider whether this migration cleanup code can have a TTL (i.e., removed in a future release once all dev databases have been migrated). Keeping dead migration code forever adds noise.


Overall

The architecture is sound and the code is production-quality. The proxy action pattern cleanly wraps the sandbox-agent SDK, the session lifecycle management is well-thought-out, and the client.ts direct-access layer for binary/streaming operations is a good design choice. The main issues to address before merge are the destroy action inconsistency (1), the double-destroy race (2), and the TypeScript type gap (4).

Replace in-house sandbox providers with re-exports from the upstream
sandbox-agent package, which now ships built-in providers. Each provider
is available as a separate subpackage import (e.g. rivetkit/sandbox/docker).

- Upgrade sandbox-agent from 0.3.1 to 0.4.0-rc.1
- Remove custom docker, e2b, daytona provider implementations
- Re-export upstream providers: docker, e2b, daytona, local, vercel, modal, computesdk
- Replace SandboxActorProvider interface with upstream SandboxProvider
- Use SandboxAgent.start() for create+connect lifecycle instead of manual provider.create() + provider.connectAgent()
- Upstream ensureServer() replaces our wake() hook
- Add destroySandbox to action methods (new in 0.4.0-rc.1)
- Adapt SessionPersistDriver to upstream API changes (null → undefined, insertEvent signature)
- Cloudflare provider not supported (architecture incompatible with direct sandbox URL access)
- Update docs with new import paths, provider docs, and custom provider guide
@railway-app railway-app bot temporarily deployed to rivet-frontend / rivet-pr-4425 March 16, 2026 06:03 Destroyed
@NathanFlurry NathanFlurry merged commit 7f41d07 into main Mar 16, 2026
10 of 19 checks passed
@NathanFlurry NathanFlurry deleted the sandbox-actor branch March 16, 2026 20:28
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.

1 participant