Skip to content

feat(core): support dynamic sandbox resolution via requestContext#16048

Merged
intojhanurag merged 24 commits into
mastra-ai:mainfrom
intojhanurag:feat/dynamic-sandbox-resolver
Jun 4, 2026
Merged

feat(core): support dynamic sandbox resolution via requestContext#16048
intojhanurag merged 24 commits into
mastra-ai:mainfrom
intojhanurag:feat/dynamic-sandbox-resolver

Conversation

@intojhanurag

@intojhanurag intojhanurag commented May 1, 2026

Copy link
Copy Markdown
Contributor

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)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Code refactoring
  • Performance improvement
  • Test update

Checklist

  • I have made corresponding changes to the documentation (if applicable)
  • I have added tests that prove my fix is effective or that my feature works
  • I have addressed all Coderabbit comments on this PR

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.
  • WorkspaceConfig.sandbox now accepts WorkspaceSandbox | WorkspaceSandboxResolver.
  • New Workspace instance methods:
    • hasSandboxConfig(): boolean
    • resolveSandbox({ requestContext }): Promise<WorkspaceSandbox | undefined>
    • getInstructionsAsync(opts?: { requestContext?: RequestContext }): Promise
    • getPathContextAsync(opts?: { requestContext?: RequestContext }): Promise
  • 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.

@changeset-bot

changeset-bot Bot commented May 1, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 62fd2fd

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 23 packages
Name Type
@mastra/core Minor
mastracode Patch
@mastra/mcp-docs-server Patch
@internal/playground Patch
@mastra/client-js Patch
@mastra/opencode Patch
@mastra/longmemeval Patch
mastra Patch
@mastra/deployer-cloud Minor
@mastra/deployer-vercel Patch
@mastra/playground-ui Patch
@mastra/react Patch
@mastra/server Minor
@mastra/deployer Minor
create-mastra Patch
@mastra/express Patch
@mastra/fastify Patch
@mastra/hono Patch
@mastra/koa Patch
@mastra/nestjs Patch
@mastra/deployer-cloudflare Patch
@mastra/deployer-netlify Patch
@mastra/temporal Patch

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

@vercel

vercel Bot commented May 1, 2026

Copy link
Copy Markdown

@intojhanurag is attempting to deploy a commit to the Mastra Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added the Documentation Improvements or additions to documentation label May 1, 2026
@coderabbitai

coderabbitai Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Workspace 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

Cohort / File(s) Summary
Release Notes & Docs
/.changeset/giant-adults-behave.md, docs/src/content/en/docs/workspace/overview.mdx, docs/src/content/en/docs/workspace/sandbox.mdx, docs/src/content/en/reference/workspace/workspace-class.mdx
Adds release note and expanded docs describing sandbox resolver usage, lifecycle ownership, incompatibilities with mounts/lsp, dynamic instruction/path-context resolution, and guidance for multi-tenant patterns.
Core Workspace Implementation
packages/core/src/workspace/workspace.ts
Adds WorkspaceSandboxResolver, allows sandbox to be a resolver or instance, rejects class constructors, forbids mounts with a resolver, warns/disables lsp:true, memoizes per-request resolutions, and adds hasSandboxConfig(), resolveSandbox(), getInstructionsAsync(), getPathContextAsync().
Tooling & Provider Resolution
packages/core/src/workspace/tools/tools.ts
Generalizes resolveEffectiveWorkspace to accept provider targets (filesystem/sandbox), returns proxy with resolved providers, updates wrapTool/wrapWithReadTracker to request targets, and registers sandbox tools for dynamic sandbox-configured workspaces.
Agent & Processor Integration
packages/core/src/agent/agent.ts, packages/core/src/processors/processors/workspace-instructions.ts, packages/core/src/agent/durable/preparation.ts
Agent and durable/processor code now check hasFilesystemConfig()/hasSandboxConfig() when present and prefer/await getInstructionsAsync (forwarding requestContext) versus sync APIs.
Tests (unit & type)
packages/core/src/workspace/tools/__tests__/dynamic-sandbox.test.ts, packages/core/src/workspace/workspace.test.ts, packages/core/src/workspace/workspace.test-d.ts, packages/core/src/processors/processors/workspace-instructions.test.ts
Adds tests covering dynamic sandbox/tool registration, per-request sandbox selection and caching, background-process follow-up semantics, capability error paths, config validation (mounts/LSP), async instruction flow, and type-level resolver signatures.
Docs (filesystem note)
docs/src/content/en/docs/workspace/filesystem.mdx
Clarifies when dynamic filesystem resolution occurs and that instructions/path-context use the same requestContext.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • NikAiyer
  • CalebBarnes
  • abhiaiyer91
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately describes the main feature added: dynamic sandbox resolution via requestContext, which is the primary objective of this PR.
Linked Issues check ✅ Passed The PR fully implements the objectives from issue #15664: enables Workspace.sandbox as a resolver function accepting requestContext, adds hasSandboxConfig() and resolveSandbox() APIs, validates configuration constraints, handles lifecycle semantics, and integrates with tooling and documentation.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing dynamic sandbox resolution and its integration points (processor, agent preparation, tools, documentation); no unrelated or out-of-scope modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/core/src/workspace/tools/__tests__/dynamic-sandbox.test.ts (1)

113-145: ⚡ Quick win

Add one negative test for missing process support.

The new resolver path always registers get_process_output and kill_process, and the docs promise a clear runtime error when the resolved sandbox lacks processes. 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

📥 Commits

Reviewing files that changed from the base of the PR and between 9efa13f and 87d6793.

📒 Files selected for processing (9)
  • .changeset/giant-adults-behave.md
  • docs/src/content/en/docs/workspace/overview.mdx
  • docs/src/content/en/docs/workspace/sandbox.mdx
  • docs/src/content/en/reference/workspace/workspace-class.mdx
  • packages/core/src/workspace/tools/__tests__/dynamic-sandbox.test.ts
  • packages/core/src/workspace/tools/tools.ts
  • packages/core/src/workspace/workspace.test-d.ts
  • packages/core/src/workspace/workspace.test.ts
  • packages/core/src/workspace/workspace.ts

Comment thread docs/src/content/en/docs/workspace/sandbox.mdx
Comment thread packages/core/src/workspace/workspace.ts
@intojhanurag intojhanurag marked this pull request as draft May 1, 2026 11:29
@intojhanurag intojhanurag marked this pull request as ready for review May 1, 2026 17:03

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 87d6793 and e350e6a.

📒 Files selected for processing (9)
  • docs/src/content/en/docs/workspace/filesystem.mdx
  • docs/src/content/en/docs/workspace/sandbox.mdx
  • docs/src/content/en/reference/workspace/workspace-class.mdx
  • packages/core/src/agent/agent.ts
  • packages/core/src/processors/processors/workspace-instructions.test.ts
  • packages/core/src/processors/processors/workspace-instructions.ts
  • packages/core/src/workspace/tools/__tests__/dynamic-sandbox.test.ts
  • packages/core/src/workspace/workspace.test.ts
  • packages/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

Comment thread packages/core/src/workspace/workspace.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between e350e6a and d9260a0.

📒 Files selected for processing (2)
  • packages/core/src/workspace/workspace.test.ts
  • packages/core/src/workspace/workspace.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/workspace/workspace.test.ts

Comment thread packages/core/src/workspace/workspace.ts Outdated
@intojhanurag intojhanurag force-pushed the feat/dynamic-sandbox-resolver branch from ca8d172 to c5d0316 Compare May 1, 2026 18:24
Comment thread packages/core/src/workspace/workspace.ts Outdated
@intojhanurag intojhanurag force-pushed the feat/dynamic-sandbox-resolver branch from a09416c to a90592e Compare May 8, 2026 14:09
@intojhanurag intojhanurag force-pushed the feat/dynamic-sandbox-resolver branch from a90592e to 2868a9c Compare May 18, 2026 19:55
… 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.
@intojhanurag intojhanurag force-pushed the feat/dynamic-sandbox-resolver branch from 8218d0f to cf038ad Compare May 19, 2026 14:41
intojhanurag and others added 5 commits May 19, 2026 21:51
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 TylerBarnes left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 RequestContext object 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.
intojhanurag and others added 2 commits May 20, 2026 00:43
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.
@dane-ai-mastra dane-ai-mastra Bot added the complexity: critical Critical-complexity PR label May 27, 2026
@dane-ai-mastra

dane-ai-mastra Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

PR triage

Linked issue check skipped for core contributor @intojhanurag.


PR complexity score

Factor Value Score impact
Files changed 19 +38
Lines changed 1604 +60
Author merged PRs 190 -20
Test files changed Yes -10
Final score 68

Applied label: complexity: critical


Changed test gate

Changed Test Gate is pending. The Changed Test Gate / changed-tests check will update the test label when it completes.

@NikAiyer NikAiyer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 RequestContext retries (or asserts the documented locked-in behavior).
  • Workspace with sandbox: () => new LocalSandbox(...) and no sandboxCacheKey → spawn background process → call get_process_output with a fresh RequestContext → assert clear error (or success once fixed). The existing test at dynamic-sandbox.test.ts:129 proves the API works with an external cache, but the misuse path isn't guarded.

Comment thread packages/core/src/workspace/workspace.ts
Comment thread packages/core/src/workspace/tools/tools.ts
Comment thread packages/core/src/workspace/workspace.ts
intojhanurag and others added 2 commits June 3, 2026 13:21
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.
@TylerBarnes TylerBarnes dismissed their stale review June 3, 2026 21:02

Nik approved, all good!

@intojhanurag intojhanurag merged commit f82cc72 into mastra-ai:main Jun 4, 2026
87 of 90 checks passed
@intojhanurag intojhanurag deleted the feat/dynamic-sandbox-resolver branch June 4, 2026 03:14
devin-ai-integration Bot pushed a commit that referenced this pull request Jun 4, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

complexity: critical Critical-complexity PR Documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Support dynamic sandbox resolution via requestContext in Workspace

5 participants