mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-16 15:15:35 +02:00
## Thinking Path > - Paperclip manages work by agents within company boundaries. > - Email tasks can run under the low-trust review preset. > - These tasks must use an isolated workspace and a sandbox. > - The default workspace strategy assumed that the project had a Git repository. > - A project without a configured workspace failed before the agent could start. > - This change gives each such task a private directory and keeps the sandbox requirement. ## Linked Issues or Issue Description **What happened?** An inbound email assigned to a low-trust agent failed with `git_worktree_base_not_git_checkout` when its boundary project had no configured workspace. Setup had accepted the project and sandbox. **Expected behavior** The agent can process email without a repository. Its workspace stays isolated from other tasks and the shared agent home. **Steps to reproduce** 1. Select a low-trust agent with an active sandbox and a project boundary. 2. Leave the project without a configured workspace. 3. Receive an email through AgentMail. 4. Observe that startup fails before provider work starts. **Paperclip version or commit** Reproduced against `5edf55d73`. **Deployment mode** Hosted staging with sandbox execution. Related: #13256 added email tasks. #13636 fixed default isolation for projects without workspaces; the explicit isolation used by low-trust tasks still needed this path. ## What Changed - Select private task directories for low-trust sandbox tasks with no configured workspace or explicit workspace strategy. - Keep each directory scoped to its company and task. Retain files across turns and reassignment and reject symlink paths and mismatched workspace reuse. - Preserve Git validation for configured workspaces and explicit strategies, plus the existing authorization and remote gates for referenced projects. - Add a startup regression and directory isolation tests. Document the supported repository-free path. ## Verification - The startup regression failed before the fix with the same Git validation error. - 260 targeted email, workspace policy, heartbeat, referenced-project and directory tests pass. - Full `pnpm -r typecheck` and `pnpm build` pass on the latest commit. - All CI checks, including the complete sharded test suite and canary dry run, pass on `b4ccd9802b09b2e95499df72d48b4a3906b8c328`. - The final commit also passes the same server shard locally: 60 files, 1,024 passed / 6 skipped tests. The earlier all-groups local run was interrupted during follow-up edits; complete-suite verification comes from CI on the final commit. - Deployed the reviewed commit to staging and independently verified the full serving SHA. Two real Codex runs in Daytona succeeded and finalized the same private company/task workspace. The first wrote a 35-byte marker; the second read the existing file without modifying it and returned the independently verified SHA-256 `ce3bbeb44d07ca6822826d3a5945752a38d30b356d10829f3159a191e5aa92a6`. - Live runtime caveat: Codex reported a nested `bwrap` loopback permission error and used its configured escalated execution inside Daytona. The outer Daytona sandbox remained active for both runs. - The startup regression uses a real database, production trust checks, workspace persistence, sandbox lease acquisition and realization, and a fake provider. It checks reassignment and allows only an authorized referenced project. - The transfer regression runs production archive/sync-back/merge code against distinct filesystem roots: create output in one sandbox, restore it, then read and update it in a fresh sandbox. The provider I/O is emulated; live staging verification is separate. ## Risks - The new default applies only to low-trust sandbox tasks without workspace configuration. Standard agents and explicit Git strategies keep their existing behavior. - Task directories retain work across turns and consume instance storage. The change does not migrate or copy existing shared files. - No database migration or credential changes are required. ## Model Used OpenAI GPT-6 through Codex, with reasoning, repository inspection, code execution, and browser tools. The exact runtime model revision and context window are not exposed in this session. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
49 lines
1.9 KiB
TypeScript
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;
|
|
issueId: string;
|
|
}): Promise<string> {
|
|
// Files belong to the task, so reassignment preserves the same workspace.
|
|
const identities = [input.companyId, 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;
|
|
}
|