feat(core): support dynamic sandbox resolution via requestContext#16048
Conversation
🦋 Changeset detectedLatest commit: 62fd2fd The changes in this PR will be included in the next version bump. This PR includes changesets to release 23 packages
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 |
|
@intojhanurag is attempting to deploy a commit to the Mastra Team on Vercel. A member of the Team first needs to authorize it. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughWorkspace now accepts a sandbox resolver that returns a per-request WorkspaceSandbox from requestContext. New APIs report/resolve dynamic sandboxes; provider resolution and tools support per-request/async resolution. Resolver-owned sandboxes change lifecycle and are incompatible with mounts and (warn/disable) lsp:true. Docs and tests updated. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
packages/core/src/workspace/tools/__tests__/dynamic-sandbox.test.ts (1)
113-145: ⚡ Quick winAdd one negative test for missing process support.
The new resolver path always registers
get_process_outputandkill_process, and the docs promise a clear runtime error when the resolved sandbox lacksprocesses. A focused test for that case would pin the contract down and guard this branch from regressing into a generic runtime failure.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/core/src/workspace/tools/__tests__/dynamic-sandbox.test.ts` around lines 113 - 145, Add a negative unit test that uses Workspace with a sandbox resolver returning a sandbox object that lacks process support and assert that calling the process tools yields the promised clear runtime error; e.g., create a Workspace whose sandbox resolver returns a minimal stub (not LocalSandbox / missing `processes`), call tools[WORKSPACE_TOOLS.SANDBOX.GET_PROCESS_OUTPUT].execute (and optionally KILL_PROCESS) with a fake pid and the same RequestContext, and expect the call to reject/throw a specific runtime error message indicating "processes not supported" (or the exact message the implementation uses) to ensure the resolver-path registers the tools but fails with a clear error when `processes` is absent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@docs/src/content/en/docs/workspace/sandbox.mdx`:
- Around line 112-117: Update the paragraph that currently says "The resolver
must return a sandbox that is ready to use (already started, or able to handle
calls without explicit startup), and must clean it up when the request
finishes." to avoid tying cleanup to request lifetime; instead state that the
resolver returns a ready-to-use sandbox and that the caller owns its lifecycle
and cleanup timing (e.g., per-request, per-tenant, per-user, or long-lived) so
shared sandboxes aren’t destroyed prematurely; reference workspace.init(),
workspace.destroy(), and the resolver behavior in the surrounding text to make
clear ownership semantics.
In `@packages/core/src/workspace/workspace.ts`:
- Around line 77-79: WorkspaceSandboxResolver can be async so getInstructions()
and getPathContext() must support resolving sandbox-specific data from a
resolver using the request context; update the workspace to add async paths
(e.g., new methods like getInstructionsAsync(requestContext) and
getPathContextAsync(requestContext) or a single
preResolveSandbox(requestContext) helper) that await WorkspaceSandboxResolver
when this._sandbox is a resolver, populate the resolved sandbox onto
this._sandbox (or a cached field) and then return sandbox-specific
instructions/path context; ensure getInstructions() and getPathContext()
delegate to these async methods or call the pre-resolve hook when a
requestContext is present so resolver-backed workspaces can surface sandbox
instructions, working-directory hints, and path context.
---
Nitpick comments:
In `@packages/core/src/workspace/tools/__tests__/dynamic-sandbox.test.ts`:
- Around line 113-145: Add a negative unit test that uses Workspace with a
sandbox resolver returning a sandbox object that lacks process support and
assert that calling the process tools yields the promised clear runtime error;
e.g., create a Workspace whose sandbox resolver returns a minimal stub (not
LocalSandbox / missing `processes`), call
tools[WORKSPACE_TOOLS.SANDBOX.GET_PROCESS_OUTPUT].execute (and optionally
KILL_PROCESS) with a fake pid and the same RequestContext, and expect the call
to reject/throw a specific runtime error message indicating "processes not
supported" (or the exact message the implementation uses) to ensure the
resolver-path registers the tools but fails with a clear error when `processes`
is absent.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 87e570b8-719c-4c26-a98f-9227aac9e4de
📒 Files selected for processing (9)
.changeset/giant-adults-behave.mddocs/src/content/en/docs/workspace/overview.mdxdocs/src/content/en/docs/workspace/sandbox.mdxdocs/src/content/en/reference/workspace/workspace-class.mdxpackages/core/src/workspace/tools/__tests__/dynamic-sandbox.test.tspackages/core/src/workspace/tools/tools.tspackages/core/src/workspace/workspace.test-d.tspackages/core/src/workspace/workspace.test.tspackages/core/src/workspace/workspace.ts
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/core/src/workspace/workspace.ts`:
- Around line 1320-1325: The resolved RequestContext (variable requestContext)
is created and used to resolve filesystem/sandbox but not forwarded to
downstream provider instruction builders; update calls to
getInstructionsForProviders (and the similar call at the other site around the
second block) to pass the resolved requestContext via opts (e.g., call
getInstructionsForProviders(filesystem, sandbox, { ...opts, requestContext }))
so provider getInstructions/getContext hooks receive the concrete request-scoped
context; ensure you apply the same change for the other occurrence that resolves
filesystem/sandbox (the block around lines 1347-1352).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 15e1ef6b-4984-4e55-97d8-4083590fadce
📒 Files selected for processing (9)
docs/src/content/en/docs/workspace/filesystem.mdxdocs/src/content/en/docs/workspace/sandbox.mdxdocs/src/content/en/reference/workspace/workspace-class.mdxpackages/core/src/agent/agent.tspackages/core/src/processors/processors/workspace-instructions.test.tspackages/core/src/processors/processors/workspace-instructions.tspackages/core/src/workspace/tools/__tests__/dynamic-sandbox.test.tspackages/core/src/workspace/workspace.test.tspackages/core/src/workspace/workspace.ts
✅ Files skipped from review due to trivial changes (2)
- docs/src/content/en/docs/workspace/filesystem.mdx
- docs/src/content/en/docs/workspace/sandbox.mdx
🚧 Files skipped from review as they are similar to previous changes (1)
- docs/src/content/en/reference/workspace/workspace-class.mdx
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/core/src/workspace/workspace.ts`:
- Around line 1319-1326: getInstructionsAsync currently resolves sandbox eagerly
(using resolveSandbox) and tools code in
packages/core/src/workspace/tools/tools.ts resolves it again, risking different
sandbox instances per request; change flow so a single per-request resolved
sandbox is shared across getInstructionsAsync and the tool execution path:
resolve the sandbox once (and filesystem if needed) using
resolveSandbox/resolveFilesystem and store/pass that resolved instance through
the request flow (e.g., attach to the RequestContext or return it alongside
getInstructionsForProviders), then update getInstructionsForProviders and the
tools call sites to use the same resolved sandbox instance instead of calling
resolveSandbox again; ensure lifecycle callers (init/destroy) still see
resolver-backed sandboxes so they are managed.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: e364b3c7-bdc6-4195-abdf-13160af08cfc
📒 Files selected for processing (2)
packages/core/src/workspace/workspace.test.tspackages/core/src/workspace/workspace.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/core/src/workspace/workspace.test.ts
ca8d172 to
c5d0316
Compare
a09416c to
a90592e
Compare
a90592e to
2868a9c
Compare
… hooks When getInstructionsAsync/getPathContextAsync synthesize a fresh RequestContext to call resolvers, also pass it to provider.getInstructions(opts) so per-request customization stays consistent in callers that omit opts.requestContext.
resolveFilesystem and resolveSandbox now cache the resolver promise per RequestContext, so a single request triggers one resolver call shared across getInstructionsAsync and every sandbox tool execution. Prevents redundant cloud sandbox spin-ups in multi-tenant deployments.
…paration The durable agent prep path called workspace.getInstructions sync and gated on workspace.filesystem || workspace.sandbox, both of which skip resolver-backed providers. Mirror the WorkspaceInstructionsProcessor change so durable agents also surface dynamic workspace instructions.
Add regression coverage for resolver-backed sandboxes that register process-capable tools but resolve to providers missing processes or executeCommand support. This verifies the runtime errors remain clear for customer misconfiguration cases.
TypeScript catches the class-vs-instance mistake at compile time, so the runtime guard mostly added noise. Removing it and the matching test.
8218d0f to
cf038ad
Compare
emitWorkspaceMetadata calls getInfo with resolveDynamicProviders:false so it never invokes a resolver just to emit metadata. That also dropped accurate metadata for a provider the tool had already resolved for the request, reporting 'dynamic' instead and breaking dynamic-filesystem.test.ts. getInfo now reads the provider off this.filesystem/this.sandbox — during tool execution these are the already-resolved instances on the effective workspace proxy, so metadata stays accurate with no extra resolver call. Adds a regression test for the dynamic-sandbox metadata path.
getPathContextAsync was added alongside getInstructionsAsync but never wired into any caller — it shipped @deprecated and only its own tests used it. getInstructionsAsync already covers the dynamic-provider instruction path. Removing it drops dead public API surface and a duplicated resolution block. The two tests that exercised it now assert per-request resolution through getInstructionsAsync for both roles.
The resolveEffectiveWorkspace refactor moved tools from unconditional
filesystem resolution to declared `targets`. lsp_inspect reads
workspace.filesystem (resolveAbsolutePath) but was registered without a
filesystem target, so on a dynamic-filesystem workspace it silently lost
path resolution and fell back to path.resolve. Declares
`targets: { filesystem: true }` and adds a regression test.
Also drops the now-unused `opts` parameter from getPathContextForProviders,
left vestigial after getPathContextAsync was removed.
- Move 'opt out of resolving dynamic filesystems' from dynamic-sandbox.test.ts to dynamic-filesystem.test.ts — it builds a filesystem-only workspace and asserts filesystem behavior, so it belongs with the filesystem feature. - Extract the duplicated command-only sandbox literal (the two "lacks process support" tests) into a shared makeCommandOnlySandbox factory. No behavior change — test reorganization only.
TylerBarnes
left a comment
There was a problem hiding this comment.
I like the overall shape of this feature — especially keeping tool registration stable for resolver-backed sandboxes instead of changing the tool set per request. I think there are two dynamic-sandbox semantics we should tighten before merge.
1. getInstructionsAsync() should not resolve/provision dynamic sandboxes by default
The biggest concern is that dynamic sandbox resolution now happens while building workspace instructions.
WorkspaceInstructionsProcessor injects workspace instructions into the system message, and for resolver-backed sandboxes that path calls workspace.getInstructionsAsync({ requestContext }). getInstructionsAsync() then calls resolveSandbox({ requestContext }).
That means a sandbox resolver may create/start/provision a sandbox just to build the prompt, even if the model never calls execute_command.
This is especially concerning because resolver-backed sandboxes are caller-owned and skipped by Workspace.init() / Workspace.destroy(). If prompt construction alone can resolve a sandbox, the workspace may trigger external resource allocation without also owning cleanup for that resource. That makes lifecycle expectations harder to reason about.
There’s also a related prompt-cache concern: because these instructions become part of the system message, concrete sandbox instructions can make the prompt request-specific. Different tenants/users/roles may produce different working directories, mount descriptions, or provider instruction text. Tool registration is stable for dynamic sandboxes, which is good, but the system message can still vary per request.
Could dynamic sandbox instructions be configurable separately from dynamic sandbox execution?
For example, the default for resolver-backed sandboxes could avoid calling the resolver and emit stable placeholder text:
Dynamic sandbox configured. Shell commands execute in a request-scoped sandbox resolved at tool execution time.
Then users who need concrete per-request sandbox details in the prompt could opt in explicitly, accepting the eager-provisioning, caller-owned lifecycle, and prompt-cache trade-offs. Something along these lines:
new Workspace({
sandbox: ({ requestContext }) => resolveSandbox(requestContext),
instructions: {
dynamicSandbox: 'placeholder', // default, does not call resolver
// or 'resolve' for current behavior
// or ({ requestContext }) => 'custom instruction text'
},
});The exact API can differ, but I think it’s important to separate “resolve sandbox for tool execution” from “resolve sandbox while constructing system prompt”.
2. Background process continuity should not rely on RequestContext object identity
resolveSandbox() caches resolver results in a WeakMap<RequestContext, Promise<WorkspaceSandbox>>.
That fixes the prompt/tool mismatch only when the exact same RequestContext instance flows through the whole request. But background processes can outlive a single tool call. If execute_command({ background: true }) runs with one RequestContext instance and get_process_output later runs with a different-but-equivalent instance, the resolver may return a different sandbox and lose access to the process.
For process-capable dynamic sandboxes, continuity should be keyed by stable request/session/tenant identity, not by object identity. Otherwise the behavior depends on whether callers preserve the same RequestContext object across follow-up calls, which is easy to get wrong.
Could we add an explicit stable cache-key mechanism for resolver-backed sandboxes that support background processes? For example:
new Workspace({
sandbox: ({ requestContext }) => resolveSandbox(requestContext),
sandboxCacheKey: ({ requestContext }) => requestContext.get('threadId') ?? requestContext.get('tenantId'),
});or make this part of the resolver contract/options somehow. The important part is that execute_command(background: true), get_process_output, and kill_process should resolve to the same sandbox based on stable logical identity, not only the same RequestContext object instance.
Summary
I’m supportive of the feature, but I think we should avoid two footguns before merge:
- prompt construction should not resolve/provision caller-owned dynamic sandboxes by default
- background process continuity should not depend on
RequestContextobject identity
My preferred direction would be:
- keep dynamic sandbox tool registration stable
- keep resolver-backed sandbox lifecycle caller-owned
- use stable placeholder instructions for dynamic sandboxes by default
- add an explicit opt-in/config path for concrete dynamic sandbox instructions
- add a stable cache-key mechanism for process-capable dynamic sandboxes
…eKey Addresses the maintainer review on dynamic sandbox resolution. getInstructionsAsync no longer resolves a resolver-backed sandbox by default. It emits stable placeholder text, so building the prompt never provisions a caller-owned sandbox and the system message stays cache-friendly. Opt into concrete instructions with `instructions.dynamicSandbox: 'resolve'` or a custom function. New `sandboxCacheKey` resolver memoizes resolved sandboxes by a stable logical key (thread/tenant id) instead of RequestContext object identity, so background-process tools reach the same sandbox across follow-up requests that carry a different RequestContext. Updates docs and the changeset; adds tests for the placeholder/resolve/ function modes and the cache-key behaviour.
The dynamic-sandbox docs still claimed workspace instructions resolve the sandbox per request. By default they now emit placeholder text — fix sandbox.mdx and the getInstructionsAsync reference to match. Also tighten the JSDoc and inline comments added for instructions.dynamicSandbox and sandboxCacheKey to match the surrounding code's density.
PR triageLinked issue check skipped for core contributor @intojhanurag. PR complexity score
Applied label: Changed test gateChanged Test Gate is pending. The |
NikAiyer
left a comment
There was a problem hiding this comment.
A few follow-ups worth tightening before merge. Inline comments below.
Test coverage to add alongside the inline items (both fit the dynamic-sandbox.test.ts style):
- Resolver throws on first call → assert second call with the same
RequestContextretries (or asserts the documented locked-in behavior). - Workspace with
sandbox: () => new LocalSandbox(...)and nosandboxCacheKey→ spawn background process → callget_process_outputwith a freshRequestContext→ assert clear error (or success once fixed). The existing test atdynamic-sandbox.test.ts:129proves the API works with an external cache, but the misuse path isn't guarded.
Evict rejected resolver promises from the keyed and per-RequestContext caches so a later call can retry instead of replaying the cached failure. Add hasSandboxResolver()/hasSandboxCacheKey() and use them so background process tools hint at sandboxCacheKey when a PID is missing on a dynamic sandbox without a stable cache key.
…6048) ## Description Workspace `sandbox` now accepts a resolver function `({ requestContext }) => WorkspaceSandbox` in addition to a static instance. This enables per-request sandbox routing from a single Workspace, mirroring the dynamic-filesystem pattern shipped in #13150 and unblocking the multi-tenant use case described in #15664. - Added `WorkspaceSandboxResolver` type and widened `config.sandbox` on `WorkspaceConfig` to accept it. - Added `Workspace.hasSandboxConfig()` and `Workspace.resolveSandbox({ requestContext })` to check availability and resolve per-request. - Constructor rejects passing a class constructor (clear `INVALID_CONFIG` error) and rejects `mounts` + sandbox resolver, since mounts need a concrete sandbox at construction time. - LSP warns and disables when combined with a sandbox resolver, since LSP attaches to the process manager at construction time. - `getInfo()` reports `provider: 'dynamic'` for resolver-backed sandbox without invoking the resolver, so per-tool metadata emission stays cheap. - `init()` and `destroy()` skip lifecycle calls for resolver-backed providers — the caller owns the lifecycle of any sandbox returned by the resolver. - Refactored `resolveEffectiveWorkspace` in `tools/tools.ts` to take a `targets` argument so each tool only resolves the providers it actually uses (filesystem tools never invoke the sandbox resolver and vice versa). - Sandbox tools register from `hasSandboxConfig()`. Dynamic sandbox path registers `execute_command` (with background), `get_process_output`, and `kill_process`; missing capabilities surface as `SandboxFeatureNotSupportedError` at runtime. - Added `dynamic-sandbox.test.ts` (7 tool-level tests) and a new `dynamic sandbox` describe block in `workspace.test.ts` (11 unit tests). All 153 tests pass across affected files; zero typecheck errors in workspace package. - Updated `docs/workspace/sandbox.mdx`, `docs/workspace/overview.mdx`, and `reference/workspace/workspace-class.mdx` with the new option, methods, and tool-registration behavior. ## Related Issue(s) Fixes #15664 ## Type of Change - [ ] Bug fix (non-breaking change that fixes an issue) - [x] New feature (non-breaking change that adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [x] Documentation update - [ ] Code refactoring - [ ] Performance improvement - [ ] Test update ## Checklist - [x] I have made corresponding changes to the documentation (if applicable) - [x] I have added tests that prove my fix is effective or that my feature works - [ ] I have addressed all Coderabbit comments on this PR <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## ELI5 A Workspace can now choose a different execution sandbox for each request by calling a small resolver function that looks at the request context (who/what made the request). This lets one Workspace serve multiple users/roles with different execution environments without creating separate Workspace instances. ## Summary This PR adds dynamic per-request sandbox resolution to Workspace by allowing WorkspaceConfig.sandbox to be either a static WorkspaceSandbox or a resolver function (WorkspaceSandboxResolver). It preserves static behavior, introduces validation and lifecycle semantics for resolver-backed sandboxes, updates tool registration and resolution to support dynamic sandboxes, adds async instruction/path-context APIs, expands tests and types, and updates documentation. ### Core API Changes - New exported type: WorkspaceSandboxResolver: ({ requestContext }) => WorkspaceSandbox | Promise<WorkspaceSandbox>. - WorkspaceConfig.sandbox now accepts WorkspaceSandbox | WorkspaceSandboxResolver. - New Workspace instance methods: - hasSandboxConfig(): boolean - resolveSandbox({ requestContext }): Promise<WorkspaceSandbox | undefined> - getInstructionsAsync(opts?: { requestContext?: RequestContext }): Promise<string> - getPathContextAsync(opts?: { requestContext?: RequestContext }): Promise<PathContext> - When a resolver is configured, workspace.sandbox remains undefined; callers should use hasSandboxConfig()/resolveSandbox(). ### Validation & Constraints - Constructor rejects class constructors passed as sandbox (INVALID_CONFIG). - Providing mounts together with a sandbox resolver throws INVALID_CONFIG (mounts require a concrete sandbox at construction). - lsp: true is warned and disabled when used with a sandbox resolver (LSP requires a concrete sandbox/process manager at construction time). ### Lifecycle Semantics - Resolver-returned sandboxes are caller-owned: Workspace.init() and destroy() do not call start()/destroy() on resolver results; the caller manages those lifecycles. - getInfo() reports provider: 'dynamic' (status: 'pending') for resolver-backed sandboxes without invoking the resolver. ### Tools & Runtime Behavior - resolveEffectiveWorkspace now accepts provider targets so each tool resolves only the providers it needs. - Filesystem tools request { filesystem: true } and do not invoke the sandbox resolver. - Sandbox tools request { sandbox: true } and are registered when hasSandboxConfig() is true (even if workspace.sandbox is not statically set). - For dynamic sandboxes, the workspace registers execute_command (including background support), get_process_output, and kill_process. Missing capabilities on the resolved sandbox surface at runtime as SandboxFeatureNotSupportedError. - Resolver results (filesystem and sandbox) are memoized per RequestContext so a single request shares one resolved provider instance across consumers. - init()/destroy() skip lifecycle calls for resolver-backed providers. ### Async Instructions & Agent Integration - getInstructionsAsync/getPathContextAsync resolve dynamic providers using optional requestContext and forward the synthesized requestContext to provider instruction hooks. - WorkspaceInstructionsProcessor and durable agent preparation prefer the async instruction APIs when present and forward requestContext accordingly. - resolveEffectiveWorkspace/proxying ensures tools see resolved provider instances for the current request. ### Tests & Types - New/updated tests: - packages/core/src/workspace/tools/__tests__/dynamic-sandbox.test.ts (tool-level tests covering resolver behavior, background processes, caching, and errors). - Expanded dynamic sandbox tests in packages/core/src/workspace/workspace.test.ts (constructor validation, lifecycle, LSP interaction, resolver behavior, instructions integration). - Type-level test: packages/core/src/workspace/workspace.test-d.ts verifying resolver typing and method signatures. - Reported: all affected tests (153) pass and the workspace package typechecks with zero errors. ### Documentation - Updated docs: docs/workspace/sandbox.mdx, docs/workspace/overview.mdx, reference/workspace/workspace-class.mdx, and related filesystem/instructions docs to document resolver usage, lifecycle semantics, tool-registration behavior, examples, and constraints. Fixes #15664. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Abhi Aiyer <abhiaiyer91@gmail.com> Co-authored-by: NikAiyer <nick.aiyer@gmail.com>
Description
Workspace
sandboxnow accepts a resolver function({ requestContext }) => WorkspaceSandboxin addition to a static instance. This enables per-request sandbox routing from a single Workspace, mirroring the dynamic-filesystem pattern shipped in #13150 and unblocking the multi-tenant use case described in #15664.WorkspaceSandboxResolvertype and widenedconfig.sandboxonWorkspaceConfigto accept it.Workspace.hasSandboxConfig()andWorkspace.resolveSandbox({ requestContext })to check availability and resolve per-request.INVALID_CONFIGerror) and rejectsmounts+ sandbox resolver, since mounts need a concrete sandbox at construction time.getInfo()reportsprovider: 'dynamic'for resolver-backed sandbox without invoking the resolver, so per-tool metadata emission stays cheap.init()anddestroy()skip lifecycle calls for resolver-backed providers — the caller owns the lifecycle of any sandbox returned by the resolver.resolveEffectiveWorkspaceintools/tools.tsto take atargetsargument so each tool only resolves the providers it actually uses (filesystem tools never invoke the sandbox resolver and vice versa).hasSandboxConfig(). Dynamic sandbox path registersexecute_command(with background),get_process_output, andkill_process; missing capabilities surface asSandboxFeatureNotSupportedErrorat runtime.dynamic-sandbox.test.ts(7 tool-level tests) and a newdynamic sandboxdescribe block inworkspace.test.ts(11 unit tests). All 153 tests pass across affected files; zero typecheck errors in workspace package.docs/workspace/sandbox.mdx,docs/workspace/overview.mdx, andreference/workspace/workspace-class.mdxwith the new option, methods, and tool-registration behavior.Related Issue(s)
Fixes #15664
Type of Change
Checklist
ELI5
A Workspace can now choose a different execution sandbox for each request by calling a small resolver function that looks at the request context (who/what made the request). This lets one Workspace serve multiple users/roles with different execution environments without creating separate Workspace instances.
Summary
This PR adds dynamic per-request sandbox resolution to Workspace by allowing WorkspaceConfig.sandbox to be either a static WorkspaceSandbox or a resolver function (WorkspaceSandboxResolver). It preserves static behavior, introduces validation and lifecycle semantics for resolver-backed sandboxes, updates tool registration and resolution to support dynamic sandboxes, adds async instruction/path-context APIs, expands tests and types, and updates documentation.
Core API Changes
Validation & Constraints
Lifecycle Semantics
Tools & Runtime Behavior
Async Instructions & Agent Integration
Tests & Types
Documentation
Fixes #15664.