mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-12 22:56:58 +01:00
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>
80 lines
6.5 KiB
TypeScript
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([]);
|
|
});
|
|
});
|