Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 13 additions & 12 deletions docs/hooks/tools.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -600,18 +600,19 @@ If a value is too large for the environment, it may be omitted (not set). Mux al
</details>

<details>
<summary>task (8)</summary>

| Env var | JSON path | Type | Description |
| ---------------------------------- | ------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MUX_TOOL_INPUT_AGENT_ID` | `agentId` | string | — |
| `MUX_TOOL_INPUT_N` | `n` | number | Optional best-of count. Use n when several agents should try the same prompt independently. Mutually exclusive with variants; omit both for a single task. Only use grouped runs for sub-agents without interfering side effects, such as read-only agents like explore. |
| `MUX_TOOL_INPUT_PROMPT` | `prompt` | string | — |
| `MUX_TOOL_INPUT_RUN_IN_BACKGROUND` | `run_in_background` | boolean | — |
| `MUX_TOOL_INPUT_SUBAGENT_TYPE` | `subagent_type` | string | — |
| `MUX_TOOL_INPUT_TITLE` | `title` | string | — |
| `MUX_TOOL_INPUT_VARIANTS_<INDEX>` | `variants[<INDEX>]` | string | Optional labels for sibling runs of the same prompt template. Use variants when the task should be repeated across labeled lanes such as issue numbers, commit windows, or frontend/backend/tests/docs review lanes. Mutually exclusive with n. When provided, Mux launches one sibling per label and substitutes ${variant} in the prompt. |
| `MUX_TOOL_INPUT_VARIANTS_COUNT` | `variants.length` | number | Number of elements in variants (Optional labels for sibling runs of the same prompt template. Use variants when the task should be repeated across labeled lanes such as issue numbers, commit windows, or frontend/backend/tests/docs review lanes. Mutually exclusive with n. When provided, Mux launches one sibling per label and substitutes ${variant} in the prompt.) |
<summary>task (9)</summary>

| Env var | JSON path | Type | Description |
| ---------------------------------- | ------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MUX_TOOL_INPUT_AGENT_ID` | `agentId` | string | — |
| `MUX_TOOL_INPUT_ISOLATION` | `isolation` | enum | Workspace isolation for the sub-agent. "fork" (the default) runs it in an isolated copy of this workspace created from committed state. "none" runs it directly in this workspace's checkout, sharing the working tree (including uncommitted changes) and skipping the fork + init overhead. Use "none" only for read-only analysis (e.g. the explore agent) or when you instruct the sub-agent to avoid editing shared files, since it can otherwise modify the same files concurrently. Omit to fork. |
| `MUX_TOOL_INPUT_N` | `n` | number | Optional best-of count. Use n when several agents should try the same prompt independently. Mutually exclusive with variants; omit both for a single task. Only use grouped runs for sub-agents without interfering side effects, such as read-only agents like explore. |
| `MUX_TOOL_INPUT_PROMPT` | `prompt` | string | — |
| `MUX_TOOL_INPUT_RUN_IN_BACKGROUND` | `run_in_background` | boolean | — |
| `MUX_TOOL_INPUT_SUBAGENT_TYPE` | `subagent_type` | string | — |
| `MUX_TOOL_INPUT_TITLE` | `title` | string | — |
| `MUX_TOOL_INPUT_VARIANTS_<INDEX>` | `variants[<INDEX>]` | string | Optional labels for sibling runs of the same prompt template. Use variants when the task should be repeated across labeled lanes such as issue numbers, commit windows, or frontend/backend/tests/docs review lanes. Mutually exclusive with n. When provided, Mux launches one sibling per label and substitutes ${variant} in the prompt. |
| `MUX_TOOL_INPUT_VARIANTS_COUNT` | `variants.length` | number | Number of elements in variants (Optional labels for sibling runs of the same prompt template. Use variants when the task should be repeated across labeled lanes such as issue numbers, commit windows, or frontend/backend/tests/docs review lanes. Mutually exclusive with n. When provided, Mux launches one sibling per label and substitutes ${variant} in the prompt.) |

</details>

Expand Down
9 changes: 9 additions & 0 deletions src/common/schemas/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,15 @@ export const WorkspaceConfigSchema = z.object({
description:
"Trunk branch used to create/init this agent task workspace (used for restart-safe init on queued tasks).",
}),
taskIsolation: z
.enum(["fork", "none"])
.optional()
.meta({
description:
'Workspace isolation for an agent task. "none" means the task shares its parent workspace\'s ' +
"checkout (no fork): its `path` points at the parent's checkout, init is skipped, and removal " +
'must not delete that shared directory. Absent/"fork" is the isolated default.',
}),
mcp: WorkspaceMCPOverridesSchema.optional().meta({
description:
"LEGACY: Per-workspace MCP overrides (migrated to <workspace>/.mux/mcp.local.jsonc)",
Expand Down
22 changes: 21 additions & 1 deletion src/common/types/runtime.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
import { describe, it, expect } from "@jest/globals";
import { parseRuntimeModeAndHost, buildRuntimeString, CODER_RUNTIME_PLACEHOLDER } from "./runtime";
import {
buildRuntimeString,
CODER_RUNTIME_PLACEHOLDER,
parseRuntimeModeAndHost,
RUNTIME_MODE,
runtimeModeSupportsSharedTaskWorkspace,
} from "./runtime";

describe("runtimeModeSupportsSharedTaskWorkspace", () => {
it("is true only for worktree and ssh (runtimes whose fork creates a separate checkout)", () => {
expect(runtimeModeSupportsSharedTaskWorkspace(RUNTIME_MODE.WORKTREE)).toBe(true);
expect(runtimeModeSupportsSharedTaskWorkspace(RUNTIME_MODE.SSH)).toBe(true);
});

it("is false for local (already shares its dir) and container runtimes", () => {
expect(runtimeModeSupportsSharedTaskWorkspace(RUNTIME_MODE.LOCAL)).toBe(false);
expect(runtimeModeSupportsSharedTaskWorkspace(RUNTIME_MODE.DOCKER)).toBe(false);
expect(runtimeModeSupportsSharedTaskWorkspace(RUNTIME_MODE.DEVCONTAINER)).toBe(false);
expect(runtimeModeSupportsSharedTaskWorkspace(undefined)).toBe(false);
});
});

describe("parseRuntimeModeAndHost", () => {
it("parses SSH mode with host", () => {
Expand Down
25 changes: 25 additions & 0 deletions src/common/types/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,31 @@ export const RUNTIME_MODE = {
DEVCONTAINER: "devcontainer" as const,
} as const;

/**
* Runtime modes whose sub-agent fork creates a *separate* checkout from the parent workspace.
* For these, the task tool may offer `isolation: "none"` to skip the fork and run the sub-agent
* directly in the parent's checkout (shared working tree), avoiding fork + init overhead for
* read-only analysis or when isolation is handled via the prompt.
*
* - `local` already shares the project directory (forking is a no-op), so it never exposes the
* option — the parameter must not even appear in the tool schema for local runtimes.
* - `docker`/`devcontainer` derive their runtime identity (container) from the workspace name in
* the runtime factory, so a differently-named workspace cannot currently resolve the parent's
* container. They are intentionally excluded until that identity override exists.
*/
export const SHARED_TASK_WORKSPACE_RUNTIME_MODES: readonly RuntimeMode[] = [
RUNTIME_MODE.WORKTREE,
Comment thread
ammar-agent marked this conversation as resolved.
RUNTIME_MODE.SSH,
];

/**
* Whether sub-agents on this runtime mode can opt out of forking via `isolation: "none"` and share
* the parent workspace's checkout instead. See {@link SHARED_TASK_WORKSPACE_RUNTIME_MODES}.
*/
export function runtimeModeSupportsSharedTaskWorkspace(mode: RuntimeMode | undefined): boolean {
return mode != null && SHARED_TASK_WORKSPACE_RUNTIME_MODES.includes(mode);
}

/**
* Runtime IDs that can be enabled/disabled in Settings → Runtimes.
* Note: includes "coder" which is a UI-level choice (not a RuntimeMode).
Expand Down
32 changes: 32 additions & 0 deletions src/common/utils/tools/toolDefinitions.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { RUNTIME_MODE } from "@/common/types/runtime";
import {
buildTaskToolAgentArgsSchema,
buildTaskToolDescription,
getAvailableTools,
TaskToolArgsSchema,
Expand Down Expand Up @@ -352,6 +353,37 @@ describe("TOOL_DEFINITIONS", () => {
);
});

describe("task tool isolation parameter", () => {
const validArgs = { agentId: "explore", prompt: "investigate", title: "Investigate" };

it("only advertises isolation on runtimes that can share the parent checkout", () => {
// Worktree/SSH expose `isolation`; the (local) variant strips it so it never reaches the model.
const withIsolation = buildTaskToolAgentArgsSchema({ includeIsolation: true });
const withoutIsolation = buildTaskToolAgentArgsSchema({ includeIsolation: false });

expect(withIsolation.safeParse({ ...validArgs, isolation: "none" }).success).toBe(true);
// .strict() rejects the unknown key outright on the local variant.
expect(withoutIsolation.safeParse({ ...validArgs, isolation: "none" }).success).toBe(false);
// Both variants still accept args that omit isolation entirely.
expect(withoutIsolation.safeParse(validArgs).success).toBe(true);
});

it("rejects unknown isolation modes", () => {
const schema = buildTaskToolAgentArgsSchema({ includeIsolation: true });
expect(schema.safeParse({ ...validArgs, isolation: "fork" }).success).toBe(true);
expect(schema.safeParse({ ...validArgs, isolation: "sandbox" }).success).toBe(false);
});

it("documents the isolation option only for shareable runtimes", () => {
for (const mode of [RUNTIME_MODE.WORKTREE, RUNTIME_MODE.SSH]) {
expect(buildTaskToolDescription(mode)).toContain('isolation: "none"');
}
for (const mode of [RUNTIME_MODE.LOCAL, RUNTIME_MODE.DOCKER, RUNTIME_MODE.DEVCONTAINER]) {
expect(buildTaskToolDescription(mode)).not.toContain('isolation: "none"');
}
});
});

it("accepts ask_user_question headers longer than 12 characters", () => {
const parsed = TOOL_DEFINITIONS.ask_user_question.schema.safeParse({
questions: [
Expand Down
Loading
Loading