feat(task): add fuse-overlay isolation backend

This commit is contained in:
DeprecatedLuke
2026-02-23 19:17:40 +00:00
parent 90b2b881fc
commit 86cd66b3d5
8 changed files with 112 additions and 22 deletions
+28 -15
View File
@@ -49,7 +49,9 @@ import {
applyBaseline,
captureBaseline,
captureDeltaPatch,
cleanupFuseOverlay,
cleanupWorktree,
ensureFuseOverlay,
ensureWorktree,
getRepoRoot,
type WorktreeBaseline,
@@ -145,11 +147,11 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
get description(): string {
const disabledAgents = this.session.settings.get("task.disabledAgents") as string[];
const maxConcurrency = this.session.settings.get("task.maxConcurrency");
const isolationEnabled = this.session.settings.get("task.isolation.enabled");
const isolationMode = this.session.settings.get("task.isolation.mode");
return renderDescription(
this.#discoveredAgents,
maxConcurrency,
isolationEnabled,
isolationMode !== "none",
this.session.settings.get("async.enabled"),
disabledAgents,
);
@@ -168,9 +170,9 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
* Create a TaskTool instance with async agent discovery.
*/
static async create(session: ToolSession): Promise<TaskTool> {
const isolationEnabled = session.settings.get("task.isolation.enabled");
const isolationMode = session.settings.get("task.isolation.mode");
const { agents } = await discoverAgents(session.cwd);
return new TaskTool(session, agents, isolationEnabled);
return new TaskTool(session, agents, isolationMode !== "none");
}
async execute(
@@ -422,18 +424,18 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
const startTime = Date.now();
const { agents, projectAgentsDir } = await discoverAgents(this.session.cwd);
const { agent: agentName, context, schema: outputSchema } = params;
const isolationEnabled = this.session.settings.get("task.isolation.enabled");
const isolationMode = this.session.settings.get("task.isolation.mode");
const isolationRequested = "isolated" in params ? params.isolated === true : false;
const isIsolated = isolationEnabled && isolationRequested;
const isIsolated = isolationMode !== "none" && isolationRequested;
const maxConcurrency = this.session.settings.get("task.maxConcurrency");
const taskDepth = this.session.taskDepth ?? 0;
if (!isolationEnabled && "isolated" in params) {
if (isolationMode === "none" && "isolated" in params) {
return {
content: [
{
type: "text",
text: "Task isolation is disabled. Remove the isolated argument to run subagents.",
text: "Task isolation is disabled. Remove the isolated argument or set task.isolation.mode to 'worktree' or 'fuse-overlay'.",
},
],
details: {
@@ -789,16 +791,23 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
}
const taskStart = Date.now();
let worktreeDir: string | undefined;
let isolationDir: string | undefined;
try {
if (!repoRoot || !baseline) {
throw new Error("Isolated task execution not initialized.");
}
worktreeDir = await ensureWorktree(repoRoot, task.id);
await applyBaseline(worktreeDir, baseline);
if (isolationMode === "fuse-overlay") {
isolationDir = await ensureFuseOverlay(repoRoot, task.id);
// Overlay already reflects the full working tree state — no baseline apply needed
} else {
isolationDir = await ensureWorktree(repoRoot, task.id);
await applyBaseline(isolationDir, baseline);
}
const result = await runSubprocess({
cwd: this.session.cwd,
worktree: worktreeDir,
worktree: isolationDir,
agent,
task: task.task,
description: task.description,
@@ -830,7 +839,7 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
preloadedSkills: task.preloadedSkills,
promptTemplates,
});
const patch = await captureDeltaPatch(worktreeDir, baseline);
const patch = await captureDeltaPatch(isolationDir, baseline);
const patchPath = path.join(effectiveArtifactsDir, `${task.id}.patch`);
await Bun.write(patchPath, patch);
return {
@@ -856,8 +865,12 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
error: message,
};
} finally {
if (worktreeDir) {
await cleanupWorktree(worktreeDir);
if (isolationDir) {
if (isolationMode === "fuse-overlay") {
await cleanupFuseOverlay(isolationDir);
} else {
await cleanupWorktree(isolationDir);
}
}
}
};
+1 -1
View File
@@ -77,7 +77,7 @@ const createTaskSchema = (options: { isolationEnabled: boolean }) => {
...properties,
isolated: Type.Optional(
Type.Boolean({
description: "Run in isolated git worktree; returns patches. Use when tasks edit overlapping files.",
description: "Run in isolated environment; returns patches. Use when tasks edit overlapping files.",
}),
),
});
@@ -168,3 +168,59 @@ export async function cleanupWorktree(dir: string): Promise<void> {
await fs.rm(dir, { recursive: true, force: true });
}
}
// ═══════════════════════════════════════════════════════════════════════════
// Fuse-overlay isolation
// ═══════════════════════════════════════════════════════════════════════════
export async function ensureFuseOverlay(baseCwd: string, id: string): Promise<string> {
const repoRoot = await getRepoRoot(baseCwd);
const encodedProject = getEncodedProjectName(repoRoot);
const baseDir = getWorktreeDir(encodedProject, id);
const upperDir = path.join(baseDir, "upper");
const workDir = path.join(baseDir, "work");
const mergedDir = path.join(baseDir, "merged");
// Clean up any stale mount at this path
const fusermount = Bun.which("fusermount3") ?? Bun.which("fusermount");
if (fusermount) {
await $`${fusermount} -u ${mergedDir}`.quiet().nothrow();
}
await fs.rm(baseDir, { recursive: true, force: true });
await fs.mkdir(upperDir, { recursive: true });
await fs.mkdir(workDir, { recursive: true });
await fs.mkdir(mergedDir, { recursive: true });
const binary = Bun.which("fuse-overlayfs");
if (!binary) {
await fs.rm(baseDir, { recursive: true, force: true });
throw new Error(
"fuse-overlayfs not found. Install it (e.g. `apt install fuse-overlayfs` or `pacman -S fuse-overlayfs`) to use fuse-overlay isolation.",
);
}
const result = await $`${binary} -o lowerdir=${repoRoot},upperdir=${upperDir},workdir=${workDir} ${mergedDir}`
.quiet()
.nothrow();
if (result.exitCode !== 0) {
const stderr = result.stderr.toString().trim();
await fs.rm(baseDir, { recursive: true, force: true });
throw new Error(`fuse-overlayfs mount failed (exit ${result.exitCode}): ${stderr}`);
}
return mergedDir;
}
export async function cleanupFuseOverlay(mergedDir: string): Promise<void> {
try {
const fusermount = Bun.which("fusermount3") ?? Bun.which("fusermount");
if (fusermount) {
await $`${fusermount} -u ${mergedDir}`.quiet().nothrow();
}
} finally {
// baseDir is the parent of the merged directory
const baseDir = path.dirname(mergedDir);
await fs.rm(baseDir, { recursive: true, force: true });
}
}