Upgrading & Migration
A consolidated guide for upgrading the workspace stack. Round 1 through round 7 hardened the workspace surface with several breaking-shaped changes — most are silent for greenfield apps but matter when you upgrade an in-flight deployment.
Looking for what changed when? See the per-package
CHANGELOG.mdfiles. This page is the operator-facing companion: when something would break across an upgrade, what to do about it.
Cross-package migration (v6 → v7 stateless suspension). This page covers workspace-stack changes WITHIN v7. For the v6 → v7 stateless-suspension transition that deleted the legacy
JSAgentExecutor.runLoopand reshaped how runtimes drive the executor, see the cross-package walkthrough atdocs/upgrade-guides/v6-to-v7-stateless-suspension.md. The most workspace-relevant takeaway is captured in pitfall 9 below: custom executors that previously relied on the legacy runLoop's direct registry publish must now thread apublishWorkspaceRegistrycallback orgetWorkspaceRegistry(sessionId)/GET /workspacewill silently returnundefined/ 404 for every active session.
Per-version compatibility matrix
The table below summarises the workspace-relevant breaking-shape changes by round. Each entry links to a "common pitfall" section below if migration action is required.
| Round | Cluster | Change | Affects |
|---|---|---|---|
| Round 2 | C | Capability invariant assertion at session start (config.capabilities ⊆ ref.capabilities, declared modules present on returned workspace) | Provider authors whose open() returns a workspace lacking a declared module. See pitfall 1. |
| Round 2 | C | Tool-name collision detection (workspace_ prefix is reserved unconditionally) | Agent code defining tools with the reserved prefix. See pitfall 2. |
| Round 2 | D / Round 3 A / Round 4 A | writeFile size guard (sandbox + filestore) | Custom tools writing very large blobs without chunking. See pitfall 3. |
| Round 4 | A1 | Local-bash passEnv defaults to a minimal allowlist (was: forward-everything) | Local-bash agents that depended on OPENAI_API_KEY (or any host secret) being visible to the LLM-driven shell. See pitfall 4. |
| Round 4 | A | Sandbox id set without shareAcrossSessions: true is rejected at open() | Agents that relied on the silent (and dangerous) cross-session sharing. See pitfall 5. |
| Round 4 | A | RunOptions renamed to ShellRunOptions (every shell-module method); related BaseRefPayloadSchema tightened (strict() + explicit allowlist) | Direct importers of the renamed type; persisted refs containing extra unknown fields. See pitfall 6. |
| Round 4 | A | Branch-from-checkpoint nulls the inherited workspaceRef (was: cloned source-session ref causing silent cross-session corruption on stateful providers) | Branched sessions that relied on inheriting source workspace state. Use Snapshotter.snapshot() + restore() instead. See pitfall 7. |
| Round 4 | D | WorkspaceRef.schemaVersion field added; refs without schemaVersion are treated as v1 | Refs persisted by older code resolve unchanged within ±1 of the current schema version. Beyond that range, providers throw a clear error. See pitfall 8. |
| Round 4 | C | Optional Logger, WorkspaceMetrics, AgentHooks.onWorkspace*, registry.describe(), registry.reset() all added with defaults preserving prior behavior | Operators integrating monitoring. No migration; pure additions. |
| 4.0.0 | — | Cloudflare Sandbox classes moved to @helix-agents/runtime-cloudflare/sandbox subpath | Sandbox-using consumers update import paths. Filestore-only consumers no longer pull @cloudflare/sandbox types into their type graph. See migration to 4.0.0. |
| Round 5 | A5 | Cross-session workspace ownership tightened (sandbox id + filestore namespace require explicit shareAcrossSessions opt-in beyond what round-4 A6 covered) | Same shape as round-4 A6. See pitfall 5. |
| Round 5 | A8 | Workspace byte caps (maxFileSizeMb enforced at every fs entrypoint, not just writeFile) | Custom tools reading large blobs through ws.fs!.readFile(). See pitfall 3 (extended). |
| Round 5 | A9 | Boundary tags on WorkspaceRef payloads (provider id stamped on every ref for cross-provider tamper detection) | Persisted refs from pre-A9 code still resolve unchanged via the schemaVersion N±1 contract. No migration. |
| Round 5 | B2 | maxGlobalConcurrentOpens provider option | Operators sharing a CF Sandbox/Filestore provider across many sessions. Pure addition; default Infinity. |
| Round 5 | B4 | resetAfterMs registry option (auto-reset of 'failed' entries after cooldown) | Operators wanting auto-recovery from transient provider outages. Pure addition; default disabled. |
| Round 5 | B5 | RateLimitedLogger wraps security warns in tool-injection.ts, CloudflareSandboxShell, SubprocessShell | Audit log volume drops materially under tight-loop LLM misuse. No migration; default behavior. |
| Round 5 | D6 | JSAgentExecutor.getWorkspaceRegistry(sessionId) accessor | Operators wiring /healthz introspection. Pure addition. |
| Round 5 | D9 | Provider factory function shape (workspaceProviders: (env, ctx) => Map<...>) on Cloudflare DO createAgentServer | New CF DO consumers use the function form; pre-D9 consumers passing a Map directly continue to work. |
| Round 5 | D11–D15 | Cost notes + capacity docs across provider pages | Documentation; no code change. |
| Round 6 | S2 | RE2 opt-in regex engine via regexEngine: detectRegexEngine() (eliminates ReDoS class) | Operators handling adversarial input. Pure addition; default keeps V8 + heuristic detector. See Workspaces Security → Regex engine. |
| Round 6 | S3 | O_NOFOLLOW on local-bash leaf reads/writes (allowLeafSymlinks: false default) | Local-bash agents that depended on following leaf symlinks. Set allowLeafSymlinks: true to restore prior behavior (discouraged). |
| Round 6 | S4 | sessionId + userId on every audit-log payload | Operators correlating audit lines to sessions. No migration; pure addition. |
| Round 7 | — | Snapshotter.list() + Snapshotter.delete() and corresponding auto-injected tools; capabilities.snapshot.maxListResults ceiling (default 100) | Operators on long-running sessions can prune snapshots; default cap protects context window. Pure addition. |
| Round 7 | — | GET /workspace?sessionId=X HTTP route on @helix-agents/agent-server (gated by authenticate hook with operation tag 'workspace') | Operators wiring centralized SRE tooling. Pure addition. |
| v7 | — | Stateless suspension redesign (legacy JSAgentExecutor.runLoop deleted; runtimes drive a stateless run-loop primitive) | Custom executors forked from the legacy runLoop need to thread publishWorkspaceRegistry to keep operator introspection working. See pitfall 9. |
| v8 | — | Single workspace per agent: workspaces map → singular workspace; tools renamed workspace__<name>__<op> → workspace_<op>; route /workspaces → /workspace; inheritWorkspaces → inheritWorkspace | All workspace-using agents, custom tools that name workspaces, and operators querying the HTTP route. See single-workspace migration. |
| Sub-project A | — | @cloudflare/sandbox peer-dep bumped 0.8.11 → 0.10.3 (EXACT-pinned, not a range) | Sandbox-using consumers MUST npm install @cloudflare/sandbox@0.10.3 (or matching) before upgrading. Filestore-only / no-workspace consumers unaffected (peer is optional). Co-deploy: bump the consumer's pin in the same release. |
| Sub-project A | — | FileSystem.watch() now implemented by the Cloudflare Sandbox provider (was previously unimplemented across all providers) | Custom tools calling ws.fs!.watch?.() get a real event stream on the sandbox provider. Pure addition; other providers still omit watch. See FileSystem module → watch. |
| Sub-project A | — | bucketMounts (R2 / S3-compatible) provider config on cloudflare-sandbox — mount buckets as live directories at open() | Operators wanting bucket-backed sandbox directories. Pure addition; default no mounts. See Cloudflare Sandbox → Bucket mounts. |
| Sub-project B | — | New script capability + cloudflare-dynamic-worker provider (ephemeral V8-isolate JS runner; network off by default) | Agents wanting sandboxed script execution. Pure addition, opt-in. See script module and Cloudflare Dynamic Worker. |
| Sub-project B | — | cloudflare-sandbox dual-tier: pass a Worker-Loader loader provider option to add the script tier alongside the container modules | Sandbox consumers wanting an in-isolate script runner in addition to the container. Pure addition, opt-in (no loader ⇒ no script tier). |
| Local Sandbox | — | New @helix-agents/workspace-local-sandbox provider (kind: 'local-sandbox'): kernel-isolated POSIX fs + shell via seatbelt (macOS) / bwrap (Linux); fails closed when no backend is available; network off by default (egress blocked unless network: 'allow') | Agents wanting an OS-level boundary on a developer machine. Pure addition / opt-in — no existing agent changes. Windows / no-backend hosts must use local-bash (trusted input) or a container host sandbox. See Local Sandbox. |
| Local Sandbox | — | New shared @helix-agents/workspace-posix-core published package — the POSIX subprocess-shell / tmpdir-filesystem / ref-validation plumbing, factored out of local-bash so local-sandbox can reuse it. local-bash is refactored to import it; behavior unchanged | Existing local-bash consumers: no API or behavior change. Consumers installing @helix-agents/workspace-local-bash now transitively pull @helix-agents/workspace-posix-core (a new transitive dependency). Pure addition. |
| Docker | — | New @helix-agents/workspace-docker provider (kind: 'docker'): container-isolated POSIX fs + shell. The agent's files live in a host tmpdir bind-mounted into a Docker container (via dockerode); shell commands run inside the container. Hardened by default (cap-drop ALL, no-new-privileges, read-only rootfs, non-root uid, pids limit); network off by default; fails closed when the Docker daemon is unavailable. resolve() recreates a fresh container over the persisted tmpdir | Agents wanting a reproducible, cgroup-limited container boundary (stronger + more uniform than the host-kernel local-sandbox) at the cost of a Docker daemon dependency. Pure addition / opt-in — no existing agent changes. Requires dockerode (a new dependency of the new package). See Docker. |
| Docker | — | @helix-agents/workspace-posix-core minor bump: the SubprocessShell allowlist/metacharacter/env-denylist audit orchestration is extracted into a new exported ShellGuard class so local-bash, local-sandbox, and the new docker shell share one audited implementation. SubprocessShell behavior is unchanged (it delegates to ShellGuard) | Existing posix-core / local-bash consumers: no behavior change. New ShellGuard export available for custom shell providers that want the same audited guards. Pure addition. |
v8 single-workspace migration
v8 collapses the multi-named-workspace model to one workspace per agent. The change is mechanical but touches several surfaces. Apply each that affects you:
Agent config. Replace the
workspacesmap with the singularworkspacefield. Drop the workspace name; there is only one.typescript// Before defineAgent({ workspaces: { notes: { provider: { kind: 'in-memory' }, capabilities: { fs: true } } }, }); // After defineAgent({ workspace: { provider: { kind: 'in-memory' }, capabilities: { fs: true } }, });Tool names. Auto-injected tools lose the per-workspace name segment and the double underscore:
workspace__<name>__<op>→workspace_<op>(e.g.workspace__notes__write_file→workspace_write_file). Update system prompts, frontend tool-call renderers, and any assertions that match on the old names. The reserved-prefix check now rejects custom tools whose name starts withworkspace_(single underscore).Sub-agent inheritance.
inheritWorkspaces→inheritWorkspaceon bothcreateSubAgentTool(...)options andPersistentAgentConfigentries. An inheriting child must NOT also declare its ownworkspace.agent-server HTTP route.
GET /workspaces?sessionId=X→GET /workspace?sessionId=X. The response shape changes from{ workspaces: EntrySnapshot[] }to{ workspace: EntrySnapshot | null }. Theauthenticatehook operation tag changes from'workspaces'to'workspace'— update any tag-based authorization logic.Registry / state APIs (custom providers and executors).
registry.get(name)→registry.get()(no-arg);registry.names()is removed;registry.describe()returns a singleEntrySnapshot | undefinedinstead of an array;registry.reset(name)/registry.swapRef(name, ref)→reset()/swapRef(ref).WorkspaceRegistryDepsfieldsconfigs/initialRefsbecomeconfig/initialRef; the persistence callback ispersistRef(ref). Session state stores the ref under singularSessionState.workspaceRef.
In-flight session compatibility
Refs persisted by an OLD version of the framework continue to resolve under a NEW version, subject to the schema-version contract documented in round-4 cluster D:
- N forward / N-1 backward. A provider may resolve refs whose
schemaVersionis the current version OR the previous version. Refs with noschemaVersionfield are treated as v1 (the original implicit version). - Beyond the window. Resolution throws
WorkspaceFailedErrorwith a clear message naming the observed version vs the supported window. The session does NOT silently corrupt; the agent surfaces an error and operators can roll back the deploy. - The provider's choice. The version is set by the provider when minting a fresh ref in
open(), and validated by the same provider onresolve(). Cross-provider semantics don't apply — refs are provider-scoped.
In practice this means: deploy NEW code, in-flight sessions resume cleanly because their persisted (OLD-shape) refs are still in the supported window. After a few session lifecycles, all live refs are NEW-shape and the OLD-shape support bound becomes irrelevant.
Rollback procedure
When a workspace-related deployment causes problems and you need to roll back:
- Always-safe path. Roll back code only — do NOT touch persisted state stores or workspace data.
- Refs persisted by the NEW code resolve cleanly under the OLD code IF they fall within the OLD code's supported version window.
- The window is N±1; if you rolled back across more than one schema version (rare — schema versions don't bump every release), see "data risk" below.
- Data risk path. If you rolled back across multiple schema versions, the OLD code may throw
WorkspaceFailedErroron resume because the persisted refs carry a schemaVersion the OLD code doesn't know.- Operator response: identify affected sessions via
registry.describe()(state: 'failed',lastErrorcontainingschemaVersion) and consider re-creating them OR rolling forward to the new version that understands the schema.
- Operator response: identify affected sessions via
- Provider-specific durability.
- In-memory: Workspaces are process-local. Rolling back the process discards all live workspace data. There is no cross-version concern.
- Local-bash: Tmpdirs persist across rollbacks (
/tmp/helix-ws-*survives a code redeploy). Sessions resume cleanly. - Cloudflare Filestore: SQLite data persists in the agent's DO. Rolling back code does not touch the data; the data is provider-scoped and the namespace transformation is deterministic.
- Cloudflare Sandbox: The sandbox container persists in the Sandbox DO independently of the agent DO. Rolling back code does not touch the container; resolve reattaches via the same sandbox ID.
Blue/green deployment recipe
When you need to deploy a new workspace version without disrupting in-flight sessions:
- Deploy NEW code in parallel. Both versions are running simultaneously against the SAME persisted state stores.
- Route NEW sessions to NEW. All
execute()calls hit the new code path (e.g. via a feature-flag or a router). - Drain OLD. Existing sessions on OLD finish naturally (some seconds to hours depending on agent shape). Track via state-store session-status counts.
- Cut over fully. Shut down OLD when its session count reaches zero.
The schema-version contract (N±1) is the safety net: even if a session bounces between OLD and NEW during the transition (e.g. resumes on the other replica), the ref payload is understood by both as long as you have not jumped more than one schema version per deploy.
For Cloudflare Workers + Durable Objects specifically: the platform's own deploy model gives you implicit blue/green via the gradual rollout — DOs hibernate on OLD code and wake on NEW code seamlessly when the rollout is complete. The schema-version contract handles the brief window where mixed code is reading mixed refs.
Common upgrade pitfalls
Pitfall 1: Capability invariant failures
Symptom. New code throws WorkspaceFailedError at session start with a message like workspace: declared capability 'snapshot' but provider returned a workspace with capabilities { fs, shell, code }.
Cause. Round-2 cluster C added an unconditional invariant assertion: the framework verifies that every declared capability is present on the returned workspace. Pre-fix, a capability mismatch was silent — the LLM saw workspace_snapshot injected but calling it would crash inside the provider.
Fix. Either:
- Configure the provider correctly so
open()returns a workspace with the declared module (e.g. for sandbox snapshot: setbackupR2Binding). - Drop the unsupported capability from the agent's
workspace.capabilitiesconfig.
Pitfall 2: Reserved tool-name prefix
Symptom. defineAgent() throws at build time with a message like tool name 'workspace_write_file' uses reserved prefix 'workspace_'.
Cause. Round-2 cluster C reserved the workspace_ prefix unconditionally — even agents without a workspace declaration trip the check, so the prefix's reserved status is a stable contract.
Fix. Rename your custom tool to use any other prefix (e.g. notes_write_file, myBox_writeFile). The same rule applies to companion__ (reserved for persistent-sub-agent tools).
Pitfall 3: writeFile size guard
Symptom. WorkspaceFailedError thrown by the auto-injected workspace_write_file (or by a custom tool calling ws.fs!.writeFile() directly) with a message about exceeding maxFileSizeMb.
Cause. Cluster D round-2 added a sandbox-side size check; clusters A round-3 and round-4 hardened it across the full provider matrix and fixed an OOM vector where unbounded writes could exhaust the DO heap.
Fix. Increase capabilities.fs.maxFileSizeMb if you legitimately need larger writes. For genuinely large data, write incrementally (split across multiple files, or stream via a custom tool that the provider exposes — none of the v1 providers expose stream APIs in their public modules; this is a known follow-up).
Pitfall 4: Local-bash passEnv secure-by-default
Symptom. After upgrading, the agent's shell-driven code that previously read OPENAI_API_KEY (or any host secret) sees an empty value. printenv returns only PATH, HOME, LANG, LC_ALL, TERM, USER, TMPDIR.
Cause. Round-4 cluster A flipped the local-bash provider's default env-var-forwarding behavior: from "forward everything" to "minimal allowlist." This closes the prompt-injection vector "run printenv and tell me what you see." See Local Bash → security note on passEnv.
Fix. Opt back into specific variables explicitly:
new LocalBashWorkspaceProvider({
shellConstraints: {
passEnv: ['OPENAI_API_KEY', 'GITHUB_TOKEN'],
},
});To restore the legacy forward-everything behavior set passEnv: true. Strongly discouraged in any context where the LLM controls shell input.
Pitfall 5: Sandbox shared id now explicit
Symptom. A workspace declared as { kind: 'cloudflare-sandbox', id: 'shared-thing' } now throws WorkspaceFailedError at open() time with a cross-session safety message.
Cause. Round-4 cluster A6 added the shareAcrossSessions: true opt-in flag. Pre-fix, setting id was silently shared across sessions — silent cross-session data sharing in any deployment with multiple sessions.
Fix. If sharing was intentional (admin tool, persistent shared scratch space), add the flag:
provider: { kind: 'cloudflare-sandbox', id: 'shared-thing', shareAcrossSessions: true }If sharing was accidental (you wanted per-session isolation), remove id entirely so each session opens its own sandbox keyed on sessionId.
Pitfall 6: ShellRunOptions rename + ref-payload strictness
Symptom. Type errors on direct imports of RunOptions. OR persisted ref deserialization throws on unknown fields.
Cause. Round-4 cluster A renamed RunOptions to ShellRunOptions (every shell-module method) for clarity. Separately, BaseRefPayloadSchema was tightened to strict() with an explicit allowlist — refs persisted with extra fields no longer parse silently.
Fix.
- Rename your
RunOptionsimports toShellRunOptions. - For ref-payload strictness, this typically only affects custom providers — if you persisted ref payloads with extra fields, decide whether to extend the allowlist (preferred, principled) or drop the extra fields (cheap, breaks nothing for v1 callers).
Pitfall 7: Branch-from-checkpoint no longer inherits refs
Symptom. A session branched from a checkpoint starts with an empty workspace; the source session's files are not visible.
Cause. Round-4 cluster A8 fixed a silent cross-session data-corruption bug. Pre-fix, branched sessions cloned the source-session's workspaceRef and BOTH sessions resolved to the SAME live workspace — concurrent writes silently corrupted state for stateful providers (filestore, sandbox, local-bash). Post-fix, branched sessions start FRESH.
Fix. Use Snapshotter.snapshot() + restore() to seed a branched session with the source workspace's content:
// In the source session, take a snapshot.
const ref = await ws.snapshot!.snapshot();
// Hand the SnapshotRef to the branched session, restore there.
const branchedWs = await ws.snapshot!.restore(ref);This produces a new workspace identity (no shared backing store) initialised from the snapshot. See the Snapshotter module for full semantics.
Pitfall 8: Schema version drift
Symptom. Provider throws WorkspaceFailedError: ref schemaVersion N is outside supported range [M, M+1] on resume.
Cause. Round-4 cluster D introduced an explicit schemaVersion field on every persisted ref payload. Providers support N±1 — refs across more than one version delta are explicitly unsupported to keep the migration logic small and audited.
Fix.
- For an upgrade: roll the deploy forward to a version that handles the persisted schemaVersion.
- For a rollback: see rollback procedure above. If the rollback skips multiple schema versions, identify affected sessions via
registry.describe()and either re-create them or roll forward.
Pitfall 9: Custom executors must wire publishWorkspaceRegistry
Symptom. JSAgentExecutor.getWorkspaceRegistry(sessionId) returns undefined for every active session — even sessions you can see streaming chunks from. GET /workspace?sessionId=X returns 404 universally. Forks of the executor that internally drive runLoop lose operator introspection silently.
Cause. The v6 → v7 stateless-suspension redesign deleted the legacy JSAgentExecutor.runLoop. Pre-v7, that legacy runLoop populated JSAgentExecutor.activeWorkspaceRegistries directly — operator introspection (getWorkspaceRegistry(sessionId) / GET /workspace) read off that map. Post-v7, the stateless runLoop (packages/runtime-js/src/run-loop.ts) is decoupled from any specific executor: it accepts an optional publishWorkspaceRegistry?: (registry | undefined) => void callback on RunLoopInput. The built-in JSAgentExecutor wires this callback to populate / clear its activeWorkspaceRegistries map (packages/runtime-js/src/js-agent-executor.ts:3259-3267). Custom executors that fork from JSAgentExecutor.runLoop (or build directly on runLoop primitives) must thread the callback themselves, or operator introspection silently breaks even though the run is healthy.
Fix.
// In your custom executor:
async runStep(...) {
// ... your existing wiring ...
const result = await runLoop({
// ... existing fields ...
publishWorkspaceRegistry: (registry) => {
if (registry) {
this.activeWorkspaceRegistries.set(sessionId, registry);
} else {
this.activeWorkspaceRegistries.delete(sessionId);
}
},
});
// ... rest of your loop ...
}runLoop invokes the callback with the registry once per owned-default-registry construction and again with undefined in its finally when the registry is closed. Inherited registries (sub-agents that opt into inheritWorkspace) are NOT re-published — the parent's runLoop has already published under the parent's sessionId.
This is the only workspace-relevant change required by the v7 stateless-suspension transition. If you ONLY use the built-in JSAgentExecutor or createAgentServer (Cloudflare DO), there is nothing to do — both wire the callback automatically.
References.
- Cross-package v6 → v7 upgrade guide for the broader migration.
- Workspace runbook —
/healthzversion-drift trap for the operator-facing symptom.
Migrating to @helix-agents/runtime-cloudflare 4.0.0
Breaking change. Cloudflare Sandbox workspace classes moved from the main package barrel to a ./sandbox subpath export. Filestore-only consumers and consumers without any workspace are unaffected at runtime; sandbox-using consumers update their import paths.
What changed
The following exports moved from @helix-agents/runtime-cloudflare to @helix-agents/runtime-cloudflare/sandbox:
CloudflareSandboxWorkspaceProviderCloudflareSandboxWorkspaceCloudflareSandboxFileSystemCloudflareSandboxShellCloudflareSandboxSnapshottercreateCloudflareSandboxCodeInterpretertype CloudflareSandboxWorkspaceConfigtype CloudflareSandboxWorkspaceProviderOptionstype CloudflareSandboxWorkspaceModules
Filestore exports (CloudflareFileStoreWorkspaceProvider, CloudflareFileStoreWorkspace, CloudflareFileStoreFileSystem, CloudflareFileStoreWorkspaceConfig) remain in the main barrel and are unchanged.
Migration
Update sandbox imports:
- import {
- CloudflareSandboxWorkspaceProvider,
- CloudflareSandboxWorkspace,
- createCloudflareSandboxCodeInterpreter,
- type CloudflareSandboxWorkspaceConfig,
- } from '@helix-agents/runtime-cloudflare';
+ import {
+ CloudflareSandboxWorkspaceProvider,
+ CloudflareSandboxWorkspace,
+ createCloudflareSandboxCodeInterpreter,
+ type CloudflareSandboxWorkspaceConfig,
+ } from '@helix-agents/runtime-cloudflare/sandbox';Filestore imports stay the same:
import { CloudflareFileStoreWorkspaceProvider } from '@helix-agents/runtime-cloudflare';Mixed consumers (using both providers) take both imports — it's fine to use both subpaths in the same file.
Why
The sandbox classes import types from @cloudflare/sandbox. When those classes were re-exported through the main barrel, TypeScript's type resolution transitively required @cloudflare/sandbox to be installed for ANY consumer of @helix-agents/runtime-cloudflare — even consumers using only the filestore provider, or consumers using the runtime with no workspace at all. The peerDependenciesMeta.optional: true marker only governs npm/pnpm install behavior; the TypeScript compiler does not honor it during type resolution.
The fix is to isolate sandbox-coupled types to a dedicated subpath, scoping their type graph to consumers who explicitly opt in by importing from /sandbox. After the move, dist/index.d.ts contains zero @cloudflare/sandbox references; only dist/sandbox.d.ts does. The peerDependenciesMeta.optional: true flag remains correct as well, because the peer is still optional at install time.
A regression test (packages/runtime-cloudflare/src/__tests__/peer-dep-isolation.test.ts) asserts the invariant on every build.
See also
- Per-version per-package CHANGELOG entries for full per-release notes.
- Workspaces overview — operations section for the metrics + hooks + healthz surfaces.
- Workspaces runbook for incident-response procedures.
- Building a Provider for provider-author concerns (schemaVersion contract, ref-payload allowlist).