Files
PaperClipAI/server/src/services/isolated-task-directory.ts
T
DottaandPaperclip e4495f93b6 fix: isolate repository-free low-trust tasks in private directories
Preserve sandbox containment without requiring Git for tasks whose project has no configured workspace. Keep explicit workspace strategies and reusable task ownership checks intact. Add the failing startup regression and directory isolation tests.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-30 16:15:57 -05:00

49 lines
1.9 KiB
TypeScript

import { lstat, mkdir, realpath } from "node:fs/promises";
import path from "node:path";
import { parseObject } from "../adapters/utils.js";
import { resolvePaperclipInstanceRoot } from "../home-paths.js";
/** A repository-free sandbox task needs isolation, but has no Git base. */
export function shouldUseIsolatedTaskDirectory(input: {
trustPreset: string;
environmentDriver: string | null;
mode: string;
hasProjectWorkspace: boolean;
projectWorkspaceId: string | null;
workspaceStrategies: unknown[];
}): boolean {
return input.trustPreset === "low_trust_review"
&& input.environmentDriver === "sandbox"
&& input.mode === "isolated_workspace"
&& !input.hasProjectWorkspace
&& !input.projectWorkspaceId
// Never discard an explicit repository/branch requirement or setup hook.
&& input.workspaceStrategies.every((value) => Object.keys(parseObject(value)).length === 0);
}
/** Stable across turns, separate from project roots and shared agent homes. */
export async function materializeIsolatedTaskDirectory(input: {
companyId: string;
agentId: string;
issueId: string;
}): Promise<string> {
const identities = [input.companyId, input.agentId, input.issueId];
if (identities.some((value) => !/^[a-zA-Z0-9_-]+$/.test(value))) {
throw new Error("Invalid isolated task workspace identity");
}
const root = resolvePaperclipInstanceRoot();
await mkdir(root, { recursive: true });
let cwd = await realpath(root);
for (const segment of ["isolated-workspaces", ...identities]) {
cwd = path.join(cwd, segment);
await mkdir(cwd, { mode: 0o700 }).catch((error: NodeJS.ErrnoException) => {
if (error.code !== "EEXIST") throw error;
});
const stat = await lstat(cwd);
if (!stat.isDirectory() || stat.isSymbolicLink() || await realpath(cwd) !== cwd) {
throw new Error("Isolated task workspace path is not a private directory");
}
}
return cwd;
}