Files
PaperClipAI/server/src/__tests__/project-workspace-root-provenance.test.ts
T
DottaandPaperclip 813e808af4 Remove filesystem history authorization from workspace preparation (#16963)
Remove physical-root source/reader authorization and sticky directory classifications from workspace preparation. Preserve current task/run authority, app-record privacy, credential authorization, containment, and lifecycle/publication integrity.

Validate replacement selections against persisted new inputs and refresh verified checkout ownership so bounded reuse survives earlier workspace relocation. Add real PostgreSQL/Git regressions, a reproducible history-scaling benchmark, and documentation.

Full CI passed all 48 jobs and Greptile scored 5/5 at 3d5da6c259. Five preparation-guard medians improved from 20,512.99 ms to 52.22 ms on the 8,500-workspace fixture; these are not end-to-end startup timings.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-20 09:19:59 -05:00

80 lines
6.5 KiB
TypeScript

import { randomUUID } from "node:crypto";
import { mkdtemp, mkdir, rm, writeFile, realpath } from "node:fs/promises";
import { tmpdir } from "node:os";
import { pathToFileURL } from "node:url";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { eq } from "drizzle-orm";
import { agents, authUsers, companies, companyMemberships, createDb, executionWorkspaceRepositoryAccess,
executionWorkspaces, issues, projects, projectWorkspaces, projectAccessMembers, principalPermissionGrants,
EMBEDDED_POSTGRES_TEST_TIMEOUT_MS } from "@paperclipai/db";
import { projectService } from "../services/projects.js";
import { assertTaskWorkspaceExecutionAccess, assertTaskWorkspaceSourceProjectAccess } from "../services/task-workspace-access.js";
import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "./helpers/embedded-postgres.js";
const support = await getEmbeddedPostgresTestSupport();
(support.supported ? describe : describe.skip)("configured project mutations without filesystem provenance", () => {
let database: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>>;
let db: ReturnType<typeof createDb>;
let root: string;
beforeAll(async () => {
vi.stubEnv("PAPERCLIP_ISSUE_PRIVACY_MODE", "enforce");
database = await startEmbeddedPostgresTestDatabase("paperclip-configured-provenance-");
db = createDb(database.connectionString);
root = await mkdtemp(path.join(tmpdir(), "paperclip-configured-provenance-"));
}, EMBEDDED_POSTGRES_TEST_TIMEOUT_MS);
afterAll(async () => {
vi.unstubAllEnvs();
await database?.cleanup();
if (root) await rm(root, { recursive: true, force: true });
});
it.each((["cwd", "file_url", "absolute_path"] as const).flatMap(sourceKind =>
(["move", "remove", "remove_project", "replace_repositories"] as const).map(mutation => ({ sourceKind, mutation }))))(
"does not retain filesystem ownership after $sourceKind / $mutation", async ({ sourceKind, mutation }) => {
const companyId = randomUUID(), userId = randomUUID(), agentId = randomUUID(), issueId = randomUUID();
const cwd = path.join(root, companyId), privateCwd = path.join(cwd, "private"), unrelatedCwd = `${cwd}-unrelated`;
await mkdir(privateCwd, { recursive: true });
await mkdir(unrelatedCwd);
await writeFile(path.join(privateCwd, "secret.txt"), "Private configured source\n");
await db.insert(companies).values({ id: companyId, name: "Configured provenance", issuePrefix: `P${companyId.slice(0, 6)}` });
await db.insert(authUsers).values({ id: userId, name: "Reader", email: `${userId}@example.test`, emailVerified: true, createdAt: new Date(), updatedAt: new Date() });
await db.insert(agents).values({ id: agentId, companyId, name: "Worker", adapterType: "process", status: "idle" });
await db.insert(companyMemberships).values({ companyId, principalType: "user", principalId: userId, membershipRole: "member", status: "active" });
const [source] = await db.insert(projects).values({ companyId, name: "Private source", visibility: "private",
executionWorkspacePolicy: { authorizationPolicy: { assignmentPolicy: { mode: "protected" } } } }).returning();
const sourceUrl = (cwd: string) => sourceKind === "file_url" ? pathToFileURL(cwd).href : cwd;
const [configured] = await db.insert(projectWorkspaces).values({ companyId, projectId: source.id, name: "Private files",
cwd: sourceKind === "cwd" ? privateCwd : null,
repoUrl: sourceKind === "cwd" ? "https://github.com/example/private" : sourceUrl(privateCwd),
metadata: { githubRepositoryId: "fixture-private" } }).returning();
const [workspace] = await db.insert(executionWorkspaces).values({ companyId, name: "Public parent", cwd, mode: "shared_workspace", strategyType: "task_directory" }).returning();
await db.insert(issues).values({ id: issueId, companyId, title: "Public task", assigneeAgentId: agentId, executionWorkspaceId: workspace.id });
const actor = { type: "board" as const, source: "session" as const, userId, ignoreInstanceAdmin: true };
const access = { companyId, cwd, sourceWorkspaceId: workspace.id, actor };
expect(await db.select().from(executionWorkspaceRepositoryAccess).where(eq(executionWorkspaceRepositoryAccess.companyId, companyId))).toEqual([]);
const service = projectService(db, { captureWorkspaceUpdates: false });
if (mutation === "move") await service.updateWorkspace(source.id, configured.id,
sourceKind === "cwd" ? { cwd: unrelatedCwd } : { repoUrl: sourceUrl(unrelatedCwd) });
else if (mutation === "remove") await service.removeWorkspace(source.id, configured.id);
else if (mutation === "remove_project") await service.remove(source.id);
else await service.replaceRepositories(source.id, []);
expect(await db.select().from(executionWorkspaceRepositoryAccess).where(eq(executionWorkspaceRepositoryAccess.companyId, companyId))).toEqual([]);
await expect(assertTaskWorkspaceExecutionAccess(db, actor, companyId, { issueId, workspaceId: workspace.id })).resolves.toBeDefined();
if (mutation === "remove_project") return;
// The selected app configuration remains private even though neighboring files are not classified.
await expect(assertTaskWorkspaceSourceProjectAccess(db, actor, companyId, source.id)).rejects.toThrow();
await db.insert(projectAccessMembers).values({ companyId, projectId: source.id, subjectType: "user", subjectId: userId });
await expect(assertTaskWorkspaceSourceProjectAccess(db, actor, companyId, source.id)).resolves.toBeUndefined();
});
it("can remove a configured folder that never existed without inventing provenance", async () => {
const companyId = randomUUID();
await db.insert(companies).values({ id: companyId, name: "Missing source", issuePrefix: `M${companyId.slice(0, 6)}` });
const [source] = await db.insert(projects).values({ companyId, name: "Missing private source", visibility: "private" }).returning();
const [configured] = await db.insert(projectWorkspaces).values({ companyId, projectId: source.id, name: "Missing", cwd: path.join(root, companyId, "never-created") }).returning();
await expect(projectService(db, { captureWorkspaceUpdates: false }).removeWorkspace(source.id, configured.id)).resolves.toMatchObject({ id: configured.id });
expect(await db.select().from(executionWorkspaceRepositoryAccess).where(eq(executionWorkspaceRepositoryAccess.companyId, companyId))).toEqual([]);
});
});