Skip to content

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.md files. 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.runLoop and reshaped how runtimes drive the executor, see the cross-package walkthrough at docs/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 a publishWorkspaceRegistry callback or getWorkspaceRegistry(sessionId) / GET /workspace will silently return undefined / 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.

RoundClusterChangeAffects
Round 2CCapability 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 2CTool-name collision detection (workspace_ prefix is reserved unconditionally)Agent code defining tools with the reserved prefix. See pitfall 2.
Round 2D / Round 3 A / Round 4 AwriteFile size guard (sandbox + filestore)Custom tools writing very large blobs without chunking. See pitfall 3.
Round 4A1Local-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 4ASandbox id set without shareAcrossSessions: true is rejected at open()Agents that relied on the silent (and dangerous) cross-session sharing. See pitfall 5.
Round 4ARunOptions 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 4ABranch-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 4DWorkspaceRef.schemaVersion field added; refs without schemaVersion are treated as v1Refs 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 4COptional Logger, WorkspaceMetrics, AgentHooks.onWorkspace*, registry.describe(), registry.reset() all added with defaults preserving prior behaviorOperators integrating monitoring. No migration; pure additions.
4.0.0Cloudflare Sandbox classes moved to @helix-agents/runtime-cloudflare/sandbox subpathSandbox-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 5A5Cross-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 5A8Workspace 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 5A9Boundary 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 5B2maxGlobalConcurrentOpens provider optionOperators sharing a CF Sandbox/Filestore provider across many sessions. Pure addition; default Infinity.
Round 5B4resetAfterMs registry option (auto-reset of 'failed' entries after cooldown)Operators wanting auto-recovery from transient provider outages. Pure addition; default disabled.
Round 5B5RateLimitedLogger wraps security warns in tool-injection.ts, CloudflareSandboxShell, SubprocessShellAudit log volume drops materially under tight-loop LLM misuse. No migration; default behavior.
Round 5D6JSAgentExecutor.getWorkspaceRegistry(sessionId) accessorOperators wiring /healthz introspection. Pure addition.
Round 5D9Provider factory function shape (workspaceProviders: (env, ctx) => Map<...>) on Cloudflare DO createAgentServerNew CF DO consumers use the function form; pre-D9 consumers passing a Map directly continue to work.
Round 5D11–D15Cost notes + capacity docs across provider pagesDocumentation; no code change.
Round 6S2RE2 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 6S3O_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 6S4sessionId + userId on every audit-log payloadOperators correlating audit lines to sessions. No migration; pure addition.
Round 7Snapshotter.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 7GET /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.
v7Stateless 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.
v8Single workspace per agent: workspaces map → singular workspace; tools renamed workspace__<name>__<op>workspace_<op>; route /workspaces/workspace; inheritWorkspacesinheritWorkspaceAll 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.110.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 AFileSystem.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 AbucketMounts (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 BNew 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 Bcloudflare-sandbox dual-tier: pass a Worker-Loader loader provider option to add the script tier alongside the container modulesSandbox consumers wanting an in-isolate script runner in addition to the container. Pure addition, opt-in (no loader ⇒ no script tier).
Local SandboxNew @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 SandboxNew 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 unchangedExisting 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.
DockerNew @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 tmpdirAgents 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:

  1. Agent config. Replace the workspaces map with the singular workspace field. 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 } },
    });
  2. 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_fileworkspace_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 with workspace_ (single underscore).

  3. Sub-agent inheritance. inheritWorkspacesinheritWorkspace on both createSubAgentTool(...) options and PersistentAgentConfig entries. An inheriting child must NOT also declare its own workspace.

  4. agent-server HTTP route. GET /workspaces?sessionId=XGET /workspace?sessionId=X. The response shape changes from { workspaces: EntrySnapshot[] } to { workspace: EntrySnapshot | null }. The authenticate hook operation tag changes from 'workspaces' to 'workspace' — update any tag-based authorization logic.

  5. Registry / state APIs (custom providers and executors). registry.get(name)registry.get() (no-arg); registry.names() is removed; registry.describe() returns a single EntrySnapshot | undefined instead of an array; registry.reset(name) / registry.swapRef(name, ref)reset() / swapRef(ref). WorkspaceRegistryDeps fields configs/initialRefs become config/initialRef; the persistence callback is persistRef(ref). Session state stores the ref under singular SessionState.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 schemaVersion is the current version OR the previous version. Refs with no schemaVersion field are treated as v1 (the original implicit version).
  • Beyond the window. Resolution throws WorkspaceFailedError with 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 on resolve(). 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:

  1. 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.
  2. Data risk path. If you rolled back across multiple schema versions, the OLD code may throw WorkspaceFailedError on resume because the persisted refs carry a schemaVersion the OLD code doesn't know.
    • Operator response: identify affected sessions via registry.describe() (state: 'failed', lastError containing schemaVersion) and consider re-creating them OR rolling forward to the new version that understands the schema.
  3. 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:

  1. Deploy NEW code in parallel. Both versions are running simultaneously against the SAME persisted state stores.
  2. Route NEW sessions to NEW. All execute() calls hit the new code path (e.g. via a feature-flag or a router).
  3. Drain OLD. Existing sessions on OLD finish naturally (some seconds to hours depending on agent shape). Track via state-store session-status counts.
  4. 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: set backupR2Binding).
  • Drop the unsupported capability from the agent's workspace.capabilities config.

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:

typescript
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:

typescript
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 RunOptions imports to ShellRunOptions.
  • 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:

typescript
// 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.

typescript
// 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.

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:

  • CloudflareSandboxWorkspaceProvider
  • CloudflareSandboxWorkspace
  • CloudflareSandboxFileSystem
  • CloudflareSandboxShell
  • CloudflareSandboxSnapshotter
  • createCloudflareSandboxCodeInterpreter
  • type CloudflareSandboxWorkspaceConfig
  • type CloudflareSandboxWorkspaceProviderOptions
  • type CloudflareSandboxWorkspaceModules

Filestore exports (CloudflareFileStoreWorkspaceProvider, CloudflareFileStoreWorkspace, CloudflareFileStoreFileSystem, CloudflareFileStoreWorkspaceConfig) remain in the main barrel and are unchanged.

Migration

Update sandbox imports:

diff
- 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:

typescript
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

Released under the MIT License.