Files
PaperClipAI/server/src/__tests__/file-resources.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

2097 lines
98 KiB
TypeScript

import express from "express";
import { createHash } from "node:crypto";
import type { Request } from "express";
import { execFile } from "node:child_process";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { promisify } from "node:util";
import request from "supertest";
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import { activityLog, agents, companies, companyMemberships, createDb, executionWorkspaceRepositoryAccess, executionWorkspaceRepositoryRoots, executionWorkspaceRepositories, executionWorkspaces, goals, issueAccessGrants, issues, projects, projectAccessMembers, projectWorkspaces, type Db } from "@paperclipai/db";
import { eq } from "drizzle-orm";
import { taskRepositoryRelativePath } from "../services/execution-workspace-repositories.js";
import { errorHandler } from "../middleware/index.js";
import { canActorReadExecutionWorkspace } from "../services/authorization.js";
import {
createFileResourceAvailabilityLimiter,
createFileResourceLimiter,
createFileResourceListLimiter,
fileResourceRoutes,
type WorkspaceFileResourceService,
} from "../routes/file-resources.js";
import {
WORKSPACE_FILE_TEXT_MAX_BYTES,
workspaceFileResourceService,
} from "../services/workspace-file-resources.js";
import {
getEmbeddedPostgresTestSupport,
startEmbeddedPostgresTestDatabase,
} from "./helpers/embedded-postgres.js";
const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
const execFileAsync = promisify(execFile);
let seedGraphSequence = 0;
type TestGraph = {
companyId: string;
otherCompanyId: string;
issueId: string;
otherIssueId: string;
projectId: string;
projectWorkspaceId: string;
targetProjectId: string;
targetProjectWorkspaceId: string;
otherProjectId: string;
otherProjectWorkspaceId: string;
workspaceRoot: string;
targetWorkspaceRoot: string;
executionRoot: string;
};
async function makeWorkspace() {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-file-resources-"));
const projectRoot = path.join(root, "project");
const targetProjectRoot = path.join(root, "target-project");
const executionRoot = path.join(root, "execution");
await fs.mkdir(projectRoot, { recursive: true });
await fs.mkdir(targetProjectRoot, { recursive: true });
await fs.mkdir(executionRoot, { recursive: true });
return { root, projectRoot, targetProjectRoot, executionRoot };
}
async function seedGraph(db: Db, input: {
projectRoot: string;
targetProjectRoot?: string;
executionRoot?: string | null;
projectSourceType?: string;
targetProjectSourceType?: string;
}): Promise<TestGraph> {
seedGraphSequence += 1;
const prefixSuffix = seedGraphSequence.toString(36).toUpperCase().padStart(4, "0");
const suffix = crypto.randomUUID().slice(0, 8);
const companyId = crypto.randomUUID();
const otherCompanyId = crypto.randomUUID();
const goalId = crypto.randomUUID();
const otherGoalId = crypto.randomUUID();
const projectId = crypto.randomUUID();
const targetProjectId = crypto.randomUUID();
const otherProjectId = crypto.randomUUID();
const projectWorkspaceId = crypto.randomUUID();
const targetProjectWorkspaceId = crypto.randomUUID();
const otherProjectWorkspaceId = crypto.randomUUID();
const executionWorkspaceId = crypto.randomUUID();
const issueId = crypto.randomUUID();
const otherIssueId = crypto.randomUUID();
await db.insert(companies).values([
{ id: companyId, name: `Company ${suffix}`, issuePrefix: `F${prefixSuffix}` },
{ id: otherCompanyId, name: `Other ${suffix}`, issuePrefix: `G${prefixSuffix}` },
]);
await db.insert(companyMemberships).values({ companyId, principalType: "user", principalId: "board-user", status: "active", membershipRole: "member" });
await db.insert(goals).values([
{ id: goalId, companyId, title: "Goal", level: "company", status: "active" },
{ id: otherGoalId, companyId: otherCompanyId, title: "Other goal", level: "company", status: "active" },
]);
await db.insert(projects).values([
{ id: projectId, companyId, goalId, name: "Project", status: "in_progress" },
{ id: targetProjectId, companyId, goalId, name: "Target project", status: "in_progress" },
{ id: otherProjectId, companyId: otherCompanyId, goalId: otherGoalId, name: "Other project", status: "in_progress" },
]);
await db.insert(projectWorkspaces).values([
{
id: projectWorkspaceId,
companyId,
projectId,
name: "Primary workspace",
sourceType: input.projectSourceType ?? "local_path",
cwd: input.projectRoot,
isPrimary: true,
},
{
id: otherProjectWorkspaceId,
companyId: otherCompanyId,
projectId: otherProjectId,
name: "Other workspace",
sourceType: "local_path",
cwd: input.projectRoot,
isPrimary: true,
},
{
id: targetProjectWorkspaceId,
companyId,
projectId: targetProjectId,
name: "Target workspace",
sourceType: input.targetProjectSourceType ?? "local_path",
cwd: input.targetProjectRoot ?? input.projectRoot,
isPrimary: true,
},
]);
await db.insert(issues).values([
{
id: issueId,
companyId,
projectId,
goalId,
projectWorkspaceId,
title: "Read a file",
status: "todo",
priority: "medium",
},
{
id: otherIssueId,
companyId: otherCompanyId,
projectId: otherProjectId,
goalId: otherGoalId,
projectWorkspaceId: otherProjectWorkspaceId,
title: "Other issue",
status: "todo",
priority: "medium",
},
]);
await db.insert(executionWorkspaces).values({
id: executionWorkspaceId,
companyId,
projectId,
projectWorkspaceId,
sourceIssueId: issueId,
mode: "isolated_workspace",
strategyType: "git_worktree",
name: "Issue worktree",
status: "active",
cwd: input.executionRoot ?? null,
providerType: input.executionRoot ? "git_worktree" : "remote_managed",
providerRef: input.executionRoot ?? "remote-workspace",
});
await db.update(issues).set({ executionWorkspaceId }).where(eq(issues.id, issueId));
return {
companyId,
otherCompanyId,
issueId,
otherIssueId,
projectId,
projectWorkspaceId,
targetProjectId,
targetProjectWorkspaceId,
otherProjectId,
otherProjectWorkspaceId,
workspaceRoot: input.projectRoot,
targetWorkspaceRoot: input.targetProjectRoot ?? input.projectRoot,
executionRoot: input.executionRoot ?? input.projectRoot,
};
}
function createApp(
db: Db,
actor: Request["actor"],
routeOpts: Parameters<typeof fileResourceRoutes>[1] = {},
) {
const app = express();
app.use(express.json());
app.use((req, _res, next) => {
req.actor = actor;
next();
});
app.use("/api", fileResourceRoutes(db, routeOpts));
app.use(errorHandler);
return app;
}
describeEmbeddedPostgres("workspace file resources", () => {
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
let db: Db;
beforeAll(async () => {
vi.stubEnv("PAPERCLIP_ISSUE_PRIVACY_MODE", "enforce");
tempDb = await startEmbeddedPostgresTestDatabase("paperclip-file-resources-");
db = createDb(tempDb.connectionString);
}, 60_000);
afterAll(async () => {
await tempDb?.cleanup();
vi.unstubAllEnvs();
});
it("withholds private task files and private cross-project targets", async () => {
const { projectRoot, executionRoot, targetProjectRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, executionRoot, targetProjectRoot });
await fs.writeFile(path.join(projectRoot, "secret.txt"), "PRIVATE_FILE", "utf8");
await fs.writeFile(path.join(targetProjectRoot, "secret.txt"), "PRIVATE_TARGET", "utf8");
const app = createApp(db, { type: "board", userId: "board-user", companyIds: [graph.companyId], source: "session", isInstanceAdmin: false });
await db.update(issues).set({ visibility: "private", responsibleUserId: "owner" }).where(eq(issues.id, graph.issueId));
await request(app).get(`/api/issues/${graph.issueId}/file-resources/content`).query({ workspace: "project", path: "secret.txt" }).expect(404);
await db.update(issues).set({ visibility: "open" }).where(eq(issues.id, graph.issueId));
await request(app).get(`/api/issues/${graph.issueId}/file-resources/content`).query({ workspace: "project", path: "secret.txt" }).expect(200);
await db.update(projects).set({ visibility: "private" }).where(eq(projects.id, graph.targetProjectId));
await request(app).get(`/api/issues/${graph.issueId}/file-resources/content`).query({ projectId: graph.targetProjectId, workspaceId: graph.targetProjectWorkspaceId, path: "secret.txt" }).expect(404);
});
it("resolves and reads a project file without exposing absolute paths", async () => {
const { root, projectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, executionRoot });
await fs.writeFile(path.join(projectRoot, "src", "app.ts"), "export const ok = true;\n", { encoding: "utf8" }).catch(async (error) => {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
await fs.mkdir(path.join(projectRoot, "src"), { recursive: true });
await fs.writeFile(path.join(projectRoot, "src", "app.ts"), "export const ok = true;\n", "utf8");
});
const app = createApp(db, {
type: "board",
userId: "board-user",
companyIds: [graph.companyId],
source: "session",
isInstanceAdmin: false,
});
const res = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/content`)
.query({ workspace: "project", path: "src/app.ts" });
expect(res.status).toBe(200);
expect(res.headers["x-content-type-options"]).toBe("nosniff");
expect(res.body.resource.displayPath).toBe("src/app.ts");
expect(JSON.stringify(res.body)).not.toContain(root);
expect(res.body.content.data).toContain("export const ok");
});
it("lists and downloads non-previewable workspace files", async () => {
const { root, projectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, executionRoot });
const relativePath = "artifacts/archive.bin";
const bytes = Buffer.from([0, 1, 2, 3, 4, 255]);
await fs.mkdir(path.join(projectRoot, "artifacts"), { recursive: true });
await fs.writeFile(path.join(projectRoot, relativePath), bytes);
const app = createApp(db, {
type: "board",
userId: "board-user",
companyIds: [graph.companyId],
source: "session",
isInstanceAdmin: false,
});
const listed = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/list`)
.query({ workspace: "project", mode: "all", path: "artifacts" });
expect(listed.status).toBe(200);
expect(listed.body.items).toEqual(expect.arrayContaining([
expect.objectContaining({
kind: "file",
relativePath,
previewKind: "unsupported",
capabilities: { preview: false, download: true, listChildren: false },
}),
]));
expect(JSON.stringify(listed.body)).not.toContain(root);
const downloaded = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/content`)
.query({ workspace: "project", path: relativePath, download: "1" })
.buffer(true)
.parse((res, callback) => {
const chunks: Buffer[] = [];
res.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
res.on("end", () => callback(null, Buffer.concat(chunks)));
});
expect(downloaded.status).toBe(200);
expect(downloaded.headers["content-disposition"]).toBe('attachment; filename="archive.bin"');
expect(downloaded.headers["x-content-type-options"]).toBe("nosniff");
expect(Buffer.compare(downloaded.body as Buffer, bytes)).toBe(0);
});
it("falls back from an execution workspace miss to the project workspace", async () => {
const { projectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, executionRoot });
await fs.writeFile(path.join(projectRoot, "README.md"), "# Project\n", "utf8");
const resolved = await workspaceFileResourceService(db).resolve(graph.issueId, {
path: "README.md",
workspace: "auto",
});
expect(resolved.workspaceKind).toBe("project_workspace");
expect(resolved.displayPath).toBe("README.md");
expect(resolved.capabilities.preview).toBe(true);
});
it.each(["missing", "denied", "symlink loop", "overlong path"])("skips an unused %s configured alternative while keeping explicit reads strict", async alternative => {
const { root, projectRoot, executionRoot, targetProjectRoot } = await makeWorkspace();
try {
const graph = await seedGraph(db, { projectRoot, executionRoot, targetProjectRoot });
await db.update(executionWorkspaces).set({ projectId: null, projectWorkspaceId: null, providerType: "local_fs" }).where(eq(executionWorkspaces.sourceIssueId, graph.issueId));
await fs.writeFile(path.join(executionRoot, "report.txt"), "CURRENT_TASK_REPORT");
if (alternative === "missing" || alternative === "symlink loop") {
await fs.rm(projectRoot, { recursive: true });
if (alternative === "symlink loop") await fs.symlink(projectRoot, projectRoot);
} else if (alternative === "overlong path") {
await db.update(projectWorkspaces).set({ cwd: path.join(projectRoot, "x".repeat(512)) }).where(eq(projectWorkspaces.id, graph.projectWorkspaceId));
} else {
const [privateIssue] = await db.insert(issues).values({ companyId: graph.companyId, title: "Private alternative source", visibility: "private", responsibleUserId: "private-owner" }).returning();
await db.insert(executionWorkspaces).values({ companyId: graph.companyId, sourceIssueId: privateIssue.id, name: "Private alternative", cwd: projectRoot,
mode: "shared_workspace", strategyType: "task_directory", providerType: "local_fs" });
await fs.writeFile(path.join(projectRoot, "report.txt"), "PRIVATE_ALTERNATIVE_BYTES");
}
const app = createApp(db, { type: "board", userId: "board-user", companyIds: [graph.companyId], source: "session", isInstanceAdmin: false });
const base = `/api/issues/${graph.issueId}/file-resources`;
const listed = await request(app).get(`${base}/list`).expect(200);
expect(listed.body.items).toEqual(expect.arrayContaining([expect.objectContaining({ relativePath: "report.txt" })]));
await request(app).get(`${base}/resolve`).query({ path: "report.txt" }).expect(200);
const preview = await request(app).get(`${base}/content`).query({ path: "report.txt" }).expect(200);
expect(preview.body.content.data).toBe("CURRENT_TASK_REPORT");
const download = await request(app).get(`${base}/content`).query({ path: "report.txt", download: true }).expect(200);
expect(download.text).toBe("CURRENT_TASK_REPORT");
const explicit = { projectId: graph.projectId, workspaceId: graph.projectWorkspaceId };
for (const target of [explicit, { workspace: "project" }]) {
await request(app).get(`${base}/list`).query(target).expect(alternative === "denied" ? 200 : 404);
await request(app).get(`${base}/resolve`).query({ ...target, path: "report.txt" }).expect(alternative === "denied" ? 200 : 404);
await request(app).get(`${base}/content`).query({ ...target, path: "report.txt" }).expect(alternative === "denied" ? 200 : 404);
await request(app).get(`${base}/content`).query({ ...target, path: "report.txt", download: true }).expect(alternative === "denied" ? 200 : 404);
}
const batch = await request(app).post(`${base}/availability`).send({ queries: [
{ path: "report.txt" }, { workspace: "execution", path: "report.txt" },
{ workspace: "project", path: "report.txt" }, { ...explicit, path: "report.txt" },
] }).expect(200);
expect(batch.body.results).toEqual([
expect.objectContaining({ openable: true, resource: expect.objectContaining({ workspaceKind: "execution_workspace" }) }),
expect.objectContaining({ openable: true, resource: expect.objectContaining({ workspaceKind: "execution_workspace" }) }),
alternative === "denied" ? expect.objectContaining({ openable: true }) : expect.objectContaining({ openable: false, unavailableReason: "not_found", resource: null }),
alternative === "denied" ? expect.objectContaining({ openable: true }) : expect.objectContaining({ openable: false, unavailableReason: "not_found", resource: null }),
]);
expect(JSON.stringify(batch.body)).not.toContain(projectRoot);
expect(JSON.stringify(batch.body)).not.toContain("PRIVATE_ALTERNATIVE_BYTES");
} finally { await fs.rm(root, { recursive: true, force: true }); }
});
it("reads projectless task files before and after organizational project assignment", async () => {
const { projectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, executionRoot });
await db.update(issues).set({ projectId: null, projectWorkspaceId: null }).where(eq(issues.id, graph.issueId));
await db.update(executionWorkspaces).set({ projectId: null, projectWorkspaceId: null, strategyType: "project_primary", providerType: "local_fs" }).where(eq(executionWorkspaces.sourceIssueId, graph.issueId));
await fs.writeFile(path.join(executionRoot, "report.txt"), "Task-owned report", "utf8");
const app = createApp(db, { type: "board", userId: "board-user", companyIds: [graph.companyId], source: "session", isInstanceAdmin: false });
for (const projectId of [null, graph.targetProjectId]) {
await db.update(issues).set({ projectId }).where(eq(issues.id, graph.issueId));
const result = await request(app).get(`/api/issues/${graph.issueId}/file-resources/content`).query({ workspace: "execution", path: "report.txt" }).expect(200);
expect(result.body.content.data).toBe("Task-owned report");
expect(result.body.resource.workspaceKind).toBe("execution_workspace");
}
});
it("reads task-bound files regardless of historical private sources and leaves legacy ledgers unchanged", async () => {
const { root, projectRoot, executionRoot, targetProjectRoot } = await makeWorkspace();
try {
const graph = await seedGraph(db, { projectRoot, executionRoot, targetProjectRoot });
const canonicalCwd = await fs.realpath(executionRoot);
const rootKey = createHash("sha256").update(canonicalCwd).digest("hex");
const privateIssueId = crypto.randomUUID();
await db.update(issues).set({ projectId: null, projectWorkspaceId: null }).where(eq(issues.id, graph.issueId));
await db.update(executionWorkspaces).set({ projectId: null, projectWorkspaceId: null, providerType: "local_fs" }).where(eq(executionWorkspaces.sourceIssueId, graph.issueId));
await db.update(projectWorkspaces).set({ cwd: executionRoot }).where(eq(projectWorkspaces.id, graph.projectWorkspaceId));
await db.insert(issues).values({ id: privateIssueId, companyId: graph.companyId, title: "Private source task", visibility: "private", responsibleUserId: "private-owner" });
const [source] = await db.insert(executionWorkspaces).values({ companyId: graph.companyId, sourceIssueId: privateIssueId,
name: "Private source workspace", cwd: executionRoot, mode: "shared_workspace", strategyType: "task_directory", providerType: "local_fs" }).returning();
await fs.writeFile(path.join(executionRoot, "secret.txt"), "PRIVATE_ROOT_BYTES");
const [task] = await db.select().from(issues).where(eq(issues.id, graph.issueId));
await db.update(executionWorkspaces).set({ sourceIssueId: privateIssueId, name: "Private original workspace metadata" })
.where(eq(executionWorkspaces.id, task.executionWorkspaceId!));
await db.update(executionWorkspaces).set({ sourceIssueId: null }).where(eq(executionWorkspaces.id, task.executionWorkspaceId!));
expect(await canActorReadExecutionWorkspace(db, { type: "board", userId: "board-user", source: "session", companyIds: [graph.companyId] }, task.executionWorkspaceId!)).toBe(false);
const app = createApp(db, { type: "board", userId: "board-user", companyIds: [graph.companyId], source: "session", isInstanceAdmin: false });
const targets = [{ workspace: "execution" }, { projectId: graph.projectId, workspaceId: graph.projectWorkspaceId }];
const reads = () => targets.flatMap(target => [
request(app).get(`/api/issues/${graph.issueId}/file-resources/list`).query(target),
request(app).get(`/api/issues/${graph.issueId}/file-resources/resolve`).query({ ...target, path: "secret.txt" }),
request(app).get(`/api/issues/${graph.issueId}/file-resources/content`).query({ ...target, path: "secret.txt" }),
request(app).get(`/api/issues/${graph.issueId}/file-resources/content`).query({ ...target, path: "secret.txt", download: true }),
request(app).post(`/api/issues/${graph.issueId}/file-resources/availability`).send({ queries: [{ ...target, path: "secret.txt" }] }),
]);
const allow = async () => {
for (const pending of reads()) {
const response = await pending;
expect(response.status).toBe(200);
if (response.body.kind === "workspace_file_availability") {
expect(response.body.results).toEqual([expect.objectContaining({ openable: true })]);
}
for (const hidden of [source.id, privateIssueId, "Private source workspace", "Private original workspace metadata"]) expect(JSON.stringify(response.body)).not.toContain(hidden);
}
};
await allow();
expect(await db.select().from(executionWorkspaceRepositoryAccess).where(eq(executionWorkspaceRepositoryAccess.companyId, graph.companyId))).toEqual([]);
expect(await db.select().from(executionWorkspaceRepositoryRoots).where(eq(executionWorkspaceRepositoryRoots.companyId, graph.companyId))).toEqual([]);
const [grant] = await db.insert(issueAccessGrants).values({ issueId: privateIssueId, subjectType: "user", subjectId: "board-user", source: "explicit" }).returning();
for (const pending of reads()) expect((await pending).status).toBe(200);
const content = await request(app).get(`/api/issues/${graph.issueId}/file-resources/content`).query({ workspace: "execution", path: "secret.txt" }).expect(200);
expect(content.body.content.data).toBe("PRIVATE_ROOT_BYTES");
await db.insert(executionWorkspaceRepositoryAccess).values({ companyId: graph.companyId, rootKey, canonicalCwd, kind: "source", scopeId: source.id });
const provenance = await db.select().from(executionWorkspaceRepositoryAccess).where(eq(executionWorkspaceRepositoryAccess.companyId, graph.companyId));
await db.update(executionWorkspaces).set({ cwd: targetProjectRoot }).where(eq(executionWorkspaces.id, source.id));
await db.update(issueAccessGrants).set({ revokedAt: new Date() }).where(eq(issueAccessGrants.id, grant.id));
await allow();
await db.update(issueAccessGrants).set({ revokedAt: null }).where(eq(issueAccessGrants.id, grant.id));
await request(app).get(`/api/issues/${graph.issueId}/file-resources/content`).query({ workspace: "execution", path: "secret.txt" }).expect(200);
await db.delete(executionWorkspaces).where(eq(executionWorkspaces.id, source.id));
await allow();
expect(await db.select().from(executionWorkspaceRepositoryAccess).where(eq(executionWorkspaceRepositoryAccess.companyId, graph.companyId))).toEqual(provenance);
expect(await db.select().from(executionWorkspaceRepositoryRoots).where(eq(executionWorkspaceRepositoryRoots.companyId, graph.companyId))).toEqual([]);
} finally { await fs.rm(root, { recursive: true, force: true }); }
});
it("ignores an unrelated private project alias while reading the current task root", async () => {
const { root, projectRoot, executionRoot, targetProjectRoot } = await makeWorkspace();
try {
const graph = await seedGraph(db, { projectRoot, executionRoot, targetProjectRoot });
await db.update(issues).set({ projectId: null, projectWorkspaceId: null }).where(eq(issues.id, graph.issueId));
await db.update(executionWorkspaces).set({ projectId: null, projectWorkspaceId: null, providerType: "local_fs" }).where(eq(executionWorkspaces.sourceIssueId, graph.issueId));
await db.update(projects).set({ visibility: "private" }).where(eq(projects.id, graph.targetProjectId));
await db.update(projectWorkspaces).set({ cwd: executionRoot }).where(eq(projectWorkspaces.id, graph.targetProjectWorkspaceId));
await fs.writeFile(path.join(executionRoot, "private-project.txt"), "PRIVATE_CONFIGURED_PROJECT");
const app = createApp(db, { type: "board", userId: "board-user", companyIds: [graph.companyId], source: "session", isInstanceAdmin: false });
await request(app).get(`/api/issues/${graph.issueId}/file-resources/content`).query({ workspace: "execution", path: "private-project.txt" }).expect(200);
await db.insert(projectAccessMembers).values({ companyId: graph.companyId, projectId: graph.targetProjectId, subjectType: "user", subjectId: "board-user" });
const response = await request(app).get(`/api/issues/${graph.issueId}/file-resources/content`).query({ workspace: "execution", path: "private-project.txt" }).expect(200);
expect(response.body.content.data).toBe("PRIVATE_CONFIGURED_PROJECT");
} finally { await fs.rm(root, { recursive: true, force: true }); }
});
it.each([
["configured", "ancestor"], ["configured", "descendant"],
["execution", "ancestor"], ["execution", "descendant"],
] as const)("allows files in authorized roots despite %s private %s aliases and retained history", async (ownerKind, relation) => {
const { root, projectRoot, targetProjectRoot } = await makeWorkspace();
try {
const ancestor = path.join(root, "work"), descendant = path.join(ancestor, "private");
await fs.mkdir(descendant, { recursive: true });
const publicRoot = relation === "ancestor" ? ancestor : descendant;
const privateRoot = relation === "ancestor" ? descendant : ancestor;
const relativeFile = relation === "ancestor" ? "private/secret.txt" : "secret.txt";
await fs.writeFile(path.join(publicRoot, relativeFile), "PRIVATE_OVERLAPPING_ROOT_BYTES");
const graph = await seedGraph(db, { projectRoot, targetProjectRoot, executionRoot: publicRoot });
await db.update(issues).set({ projectId: null, projectWorkspaceId: null }).where(eq(issues.id, graph.issueId));
await db.update(executionWorkspaces).set({ projectId: null, projectWorkspaceId: null, providerType: "local_fs" }).where(eq(executionWorkspaces.sourceIssueId, graph.issueId));
await db.update(projectWorkspaces).set({ cwd: publicRoot }).where(eq(projectWorkspaces.id, graph.projectWorkspaceId));
let ownerId: string;
let privateIssueId: string | null = null;
if (ownerKind === "configured") {
ownerId = graph.targetProjectId;
await db.update(projects).set({ visibility: "private" }).where(eq(projects.id, ownerId));
await db.update(projectWorkspaces).set({ cwd: privateRoot }).where(eq(projectWorkspaces.id, graph.targetProjectWorkspaceId));
expect(await db.select().from(executionWorkspaces).where(eq(executionWorkspaces.projectId, ownerId))).toEqual([]);
} else {
const [task] = await db.insert(issues).values({ companyId: graph.companyId, title: "Private overlapping task", visibility: "private", responsibleUserId: "private-owner" }).returning();
privateIssueId = task.id;
const [workspace] = await db.insert(executionWorkspaces).values({ companyId: graph.companyId, sourceIssueId: task.id,
name: "Private overlapping workspace", cwd: privateRoot, mode: "shared_workspace", strategyType: "task_directory", providerType: "local_fs" }).returning();
ownerId = workspace.id;
}
const app = createApp(db, { type: "board", userId: "board-user", companyIds: [graph.companyId], source: "session", isInstanceAdmin: false });
const targets = [{ workspace: "execution" }, { projectId: graph.projectId, workspaceId: graph.projectWorkspaceId }];
const readState = async () => ({
access: await db.select().from(executionWorkspaceRepositoryAccess).where(eq(executionWorkspaceRepositoryAccess.companyId, graph.companyId)),
policies: await db.select().from(executionWorkspaceRepositoryRoots).where(eq(executionWorkspaceRepositoryRoots.companyId, graph.companyId)),
});
const allow = async () => {
const before = await readState();
for (const target of targets) {
for (const [endpoint, query] of [
["list", {}], ["resolve", { path: relativeFile }], ["content", { path: relativeFile }], ["content", { path: relativeFile, download: true }],
] as const) {
const response = await request(app).get(`/api/issues/${graph.issueId}/file-resources/${endpoint}`).query({ ...target, ...query }).expect(200);
for (const hidden of [ownerId, "Private overlapping"]) expect(JSON.stringify(response.body)).not.toContain(hidden);
}
const availability = await request(app).post(`/api/issues/${graph.issueId}/file-resources/availability`)
.send({ queries: [{ ...target, path: relativeFile }] }).expect(200);
expect(availability.body.results).toEqual([expect.objectContaining({ openable: true })]);
}
expect(await readState()).toEqual(before);
};
await allow();
const grant = ownerKind === "configured"
? (await db.insert(projectAccessMembers).values({ companyId: graph.companyId, projectId: ownerId, subjectType: "user", subjectId: "board-user" }).returning())[0]!
: (await db.insert(issueAccessGrants).values({ issueId: privateIssueId!, subjectType: "user", subjectId: "board-user", source: "explicit" }).returning())[0]!;
const beforeAllowedReads = await readState();
for (const target of targets) {
const response = await request(app).get(`/api/issues/${graph.issueId}/file-resources/content`).query({ ...target, path: relativeFile }).expect(200);
expect(response.body.content.data).toBe("PRIVATE_OVERLAPPING_ROOT_BYTES");
}
expect(await readState()).toEqual(beforeAllowedReads);
const canonicalCwd = await fs.realpath(privateRoot);
await db.insert(executionWorkspaceRepositoryAccess).values({ companyId: graph.companyId,
rootKey: createHash("sha256").update(canonicalCwd).digest("hex"), canonicalCwd,
kind: ownerKind === "configured" ? "project_source" : "source", scopeId: ownerId });
if (ownerKind === "configured") {
await db.update(projectWorkspaces).set({ cwd: targetProjectRoot }).where(eq(projectWorkspaces.id, graph.targetProjectWorkspaceId));
await db.delete(projectAccessMembers).where(eq(projectAccessMembers.id, grant.id));
} else {
await db.update(executionWorkspaces).set({ cwd: targetProjectRoot }).where(eq(executionWorkspaces.id, ownerId));
await db.update(issueAccessGrants).set({ revokedAt: new Date() }).where(eq(issueAccessGrants.id, grant.id));
}
await allow();
expect(await fs.readFile(path.join(publicRoot, relativeFile), "utf8")).toBe("PRIVATE_OVERLAPPING_ROOT_BYTES");
} finally { await fs.rm(root, { recursive: true, force: true }); }
});
it("keeps sibling string-prefix roots independent without registering file readers", async () => {
const { root, projectRoot, targetProjectRoot } = await makeWorkspace();
try {
const privateRoot = path.join(root, "work"), publicRoot = path.join(root, "worker");
await fs.mkdir(privateRoot); await fs.mkdir(publicRoot);
await fs.writeFile(path.join(publicRoot, "public.txt"), "PUBLIC_SIBLING_BYTES");
const graph = await seedGraph(db, { projectRoot, targetProjectRoot, executionRoot: publicRoot });
await db.update(projects).set({ visibility: "private" }).where(eq(projects.id, graph.targetProjectId));
await db.update(projectWorkspaces).set({ cwd: privateRoot }).where(eq(projectWorkspaces.id, graph.targetProjectWorkspaceId));
const [privateIssue] = await db.insert(issues).values({ companyId: graph.companyId, title: "Private sibling", visibility: "private", responsibleUserId: "private-owner" }).returning();
await db.insert(executionWorkspaces).values({ companyId: graph.companyId, sourceIssueId: privateIssue.id, cwd: privateRoot,
name: "Private sibling", mode: "shared_workspace", strategyType: "task_directory", providerType: "local_fs" });
const app = createApp(db, { type: "board", userId: "board-user", companyIds: [graph.companyId], source: "session", isInstanceAdmin: false });
const target = { workspace: "execution", path: "public.txt" };
const content = await request(app).get(`/api/issues/${graph.issueId}/file-resources/content`).query(target).expect(200);
expect(content.body.content.data).toBe("PUBLIC_SIBLING_BYTES");
await request(app).get(`/api/issues/${graph.issueId}/file-resources/list`).query({ workspace: "execution" }).expect(200);
await request(app).get(`/api/issues/${graph.issueId}/file-resources/content`).query({ ...target, download: true }).expect(200);
const availability = await request(app).post(`/api/issues/${graph.issueId}/file-resources/availability`).send({ queries: [target] }).expect(200);
expect(availability.body.results).toEqual([expect.objectContaining({ openable: true })]);
expect(await db.select().from(executionWorkspaceRepositoryAccess).where(eq(executionWorkspaceRepositoryAccess.companyId, graph.companyId))).toEqual([]);
expect(await db.select().from(executionWorkspaceRepositoryRoots).where(eq(executionWorkspaceRepositoryRoots.companyId, graph.companyId))).toEqual([]);
} finally { await fs.rm(root, { recursive: true, force: true }); }
});
it("does not confuse identical repositories in unrelated available roots with private file provenance", async () => {
const { root, projectRoot, executionRoot, targetProjectRoot } = await makeWorkspace();
try {
const graph = await seedGraph(db, { projectRoot, executionRoot, targetProjectRoot });
await db.update(issues).set({ projectId: null, projectWorkspaceId: null }).where(eq(issues.id, graph.issueId));
const [publicWorkspace] = await db.update(executionWorkspaces).set({ projectId: null, projectWorkspaceId: null, providerType: "local_fs" }).where(eq(executionWorkspaces.sourceIssueId, graph.issueId)).returning();
const [privateIssue] = await db.insert(issues).values({ companyId: graph.companyId, title: "Unrelated private repository", visibility: "private", responsibleUserId: "private-owner" }).returning();
const [privateWorkspace] = await db.insert(executionWorkspaces).values({ companyId: graph.companyId, sourceIssueId: privateIssue.id,
name: "Unrelated source", cwd: targetProjectRoot, mode: "shared_workspace", strategyType: "task_directory", providerType: "local_fs" }).returning();
const identity = "https://github.com/fixture/same-repo.git", relativePath = taskRepositoryRelativePath(identity);
for (const workspace of [publicWorkspace, privateWorkspace]) {
await fs.mkdir(path.join(workspace.cwd!, relativePath), { recursive: true });
await fs.writeFile(path.join(workspace.cwd!, relativePath, "report.txt"), workspace.id === publicWorkspace.id ? "PUBLIC_COPY" : "PRIVATE_COPY");
await db.insert(executionWorkspaceRepositories).values({ companyId: graph.companyId, executionWorkspaceId: workspace.id,
repositoryIdentity: identity, repoUrl: identity, relativePath, requestKey: workspace.id, state: "ready" });
}
const app = createApp(db, { type: "board", userId: "board-user", companyIds: [graph.companyId], source: "session", isInstanceAdmin: false });
const response = await request(app).get(`/api/issues/${graph.issueId}/file-resources/content`).query({ workspace: "execution", path: `${relativePath}/report.txt` }).expect(200);
expect(response.body.content.data).toBe("PUBLIC_COPY");
} finally { await fs.rm(root, { recursive: true, force: true }); }
});
it("reads a selected source independently of task project while retaining source privacy", async () => {
const { projectRoot, targetProjectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, targetProjectRoot, executionRoot });
await fs.writeFile(path.join(projectRoot, "source.txt"), "Selected source", "utf8");
const app = createApp(db, { type: "board", userId: "board-user", companyIds: [graph.companyId], source: "session", isInstanceAdmin: false });
for (const projectId of [null, graph.targetProjectId]) {
await db.update(issues).set({ projectId }).where(eq(issues.id, graph.issueId));
const result = await request(app).get(`/api/issues/${graph.issueId}/file-resources/content`).query({ workspace: "project", path: "source.txt" }).expect(200);
expect(result.body.content.data).toBe("Selected source");
expect(result.body.resource.workspaceId).toBe(graph.projectWorkspaceId);
}
await db.update(projects).set({ visibility: "private" }).where(eq(projects.id, graph.projectId));
await request(app).get(`/api/issues/${graph.issueId}/file-resources/content`).query({ workspace: "project", path: "source.txt" }).expect(404);
});
it("auto-discovers unhinted same-company project files when issue workspaces miss", async () => {
const { projectRoot, targetProjectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, targetProjectRoot, executionRoot });
const targetPath = "docs/reference/skills.md";
await fs.mkdir(path.join(targetProjectRoot, path.dirname(targetPath)), { recursive: true });
await fs.writeFile(path.join(targetProjectRoot, targetPath), "# Skills reference\n", "utf8");
const resolved = await workspaceFileResourceService(db).resolve(graph.issueId, {
path: targetPath,
workspace: "auto",
});
expect(resolved).toMatchObject({
workspaceKind: "project_workspace",
workspaceId: graph.targetProjectWorkspaceId,
projectId: graph.targetProjectId,
projectName: "Target project",
displayPath: `Target project / ${targetPath}`,
previewKind: "text",
});
const content = await workspaceFileResourceService(db).readContent(graph.issueId, {
path: targetPath,
workspace: "auto",
});
expect(content.resource.workspaceId).toBe(graph.targetProjectWorkspaceId);
expect(content.content.data).toContain("# Skills reference");
});
it("resolves explicit same-company cross-project workspace files and logs target details", async () => {
const { root, projectRoot, targetProjectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, targetProjectRoot, executionRoot });
await fs.mkdir(path.join(targetProjectRoot, "docs"), { recursive: true });
await fs.writeFile(path.join(targetProjectRoot, "docs", "README.md"), "# Target project\n", "utf8");
const app = createApp(db, {
type: "board",
userId: "board-user",
companyIds: [graph.companyId],
source: "session",
isInstanceAdmin: false,
});
const res = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/content`)
.query({
projectId: graph.targetProjectId,
workspaceId: graph.targetProjectWorkspaceId,
path: "docs/README.md",
});
expect(res.status).toBe(200);
expect(res.body.resource).toMatchObject({
workspaceKind: "project_workspace",
workspaceId: graph.targetProjectWorkspaceId,
projectId: graph.targetProjectId,
projectName: "Target project",
displayPath: "Target project / docs/README.md",
});
expect(res.body.content.data).toContain("# Target project");
expect(JSON.stringify(res.body)).not.toContain(root);
const rows = await db.select().from(activityLog).where(eq(activityLog.entityId, graph.issueId));
const read = rows.find((row) => row.action === "issue.file_resource_content_read");
expect(read?.details).toMatchObject({
outcome: "success",
workspaceId: graph.targetProjectWorkspaceId,
projectId: graph.targetProjectId,
projectName: "Target project",
displayPath: "Target project / docs/README.md",
});
expect(JSON.stringify(read?.details)).not.toContain(targetProjectRoot);
});
it("reads explicit cross-project git_repo workspaces with a local checkout", async () => {
const { root, projectRoot, targetProjectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, {
projectRoot,
targetProjectRoot,
executionRoot,
targetProjectSourceType: "git_repo",
});
const readmePath = "content-os/cases/active/2026-06-06-pap-10199-bundled-skills/README.md";
await fs.mkdir(path.join(targetProjectRoot, path.dirname(readmePath)), { recursive: true });
await fs.writeFile(path.join(targetProjectRoot, readmePath), "# Bundled skills\n\nRendered from content project.\n", "utf8");
const app = createApp(db, {
type: "board",
userId: "board-user",
companyIds: [graph.companyId],
source: "session",
isInstanceAdmin: false,
});
const res = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/content`)
.query({
projectId: graph.targetProjectId,
workspaceId: graph.targetProjectWorkspaceId,
path: readmePath,
});
expect(res.status).toBe(200);
expect(res.body.resource).toMatchObject({
workspaceKind: "project_workspace",
workspaceId: graph.targetProjectWorkspaceId,
projectId: graph.targetProjectId,
projectName: "Target project",
displayPath: `Target project / ${readmePath}`,
provider: "git_repo",
previewKind: "text",
});
expect(res.body.content.encoding).toBe("utf8");
expect(res.body.content.data).toContain("# Bundled skills");
expect(JSON.stringify(res.body)).not.toContain(root);
});
it("resolves and lists explicit same-company cross-project workspace folders", async () => {
const { root, projectRoot, targetProjectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, targetProjectRoot, executionRoot });
const folderPath = "content-os/cases/active/2026-06-06-pap-10199-bundled-skills/";
await fs.mkdir(path.join(targetProjectRoot, folderPath), { recursive: true });
await fs.writeFile(path.join(targetProjectRoot, folderPath, "README.md"), "# Bundled skills\n", "utf8");
await fs.writeFile(path.join(targetProjectRoot, folderPath, "notes.txt"), "notes\n", "utf8");
await fs.writeFile(path.join(targetProjectRoot, "outside.txt"), "not in focused folder\n", "utf8");
const app = createApp(db, {
type: "board",
userId: "board-user",
companyIds: [graph.companyId],
source: "session",
isInstanceAdmin: false,
});
const resolved = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/resolve`)
.query({
projectId: graph.targetProjectId,
workspaceId: graph.targetProjectWorkspaceId,
path: folderPath,
});
expect(resolved.status).toBe(200);
expect(resolved.body).toMatchObject({
kind: "directory",
workspaceKind: "project_workspace",
workspaceId: graph.targetProjectWorkspaceId,
projectId: graph.targetProjectId,
projectName: "Target project",
displayPath: `Target project / ${folderPath}`,
capabilities: { preview: false, download: false, listChildren: true },
});
expect(JSON.stringify(resolved.body)).not.toContain(root);
const listed = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/list`)
.query({
projectId: graph.targetProjectId,
workspaceId: graph.targetProjectWorkspaceId,
path: folderPath,
mode: "all",
});
expect(listed.status).toBe(200);
expect(listed.body.state).toBe("available");
expect(listed.body.query).toMatchObject({ path: folderPath.slice(0, -1), mode: "all" });
expect(new Set(listed.body.items.map((item: { relativePath: string }) => item.relativePath))).toEqual(new Set([
`${folderPath}README.md`,
`${folderPath}notes.txt`,
]));
expect(JSON.stringify(listed.body)).not.toContain(root);
expect(JSON.stringify(listed.body)).not.toContain("outside.txt");
});
it("auto-discovers unhinted same-company project folders when issue workspaces miss", async () => {
const { root, projectRoot, targetProjectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, targetProjectRoot, executionRoot });
const folderPath = "content-os/cases/active/2026-06-06-pap-10199-bundled-skills/";
await fs.mkdir(path.join(targetProjectRoot, folderPath), { recursive: true });
await fs.writeFile(path.join(targetProjectRoot, folderPath, "README.md"), "# Bundled skills\n", "utf8");
await fs.writeFile(path.join(targetProjectRoot, folderPath, "suggestions.md"), "# Suggestions\n", "utf8");
const app = createApp(db, {
type: "board",
userId: "board-user",
companyIds: [graph.companyId],
source: "session",
isInstanceAdmin: false,
});
const resolved = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/resolve`)
.query({ path: folderPath });
expect(resolved.status).toBe(200);
expect(resolved.body).toMatchObject({
kind: "directory",
workspaceId: graph.targetProjectWorkspaceId,
projectId: graph.targetProjectId,
projectName: "Target project",
displayPath: `Target project / ${folderPath}`,
});
const listed = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/list`)
.query({ path: folderPath });
expect(listed.status).toBe(200);
expect(listed.body.state).toBe("available");
expect(listed.body.workspace).toMatchObject({
workspaceId: graph.targetProjectWorkspaceId,
projectId: graph.targetProjectId,
projectName: "Target project",
});
expect(new Set(listed.body.items.map((item: { relativePath: string }) => item.relativePath))).toEqual(new Set([
`${folderPath}README.md`,
`${folderPath}suggestions.md`,
]));
expect(JSON.stringify(listed.body)).not.toContain(root);
});
it("rejects ambiguous unhinted same-company project file matches", async () => {
const { projectRoot, targetProjectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, targetProjectRoot, executionRoot });
const duplicateProjectId = crypto.randomUUID();
const duplicateWorkspaceId = crypto.randomUUID();
const duplicateRoot = path.join(path.dirname(targetProjectRoot), "duplicate-target");
const targetPath = "docs/reference/skills.md";
await fs.mkdir(path.join(targetProjectRoot, path.dirname(targetPath)), { recursive: true });
await fs.writeFile(path.join(targetProjectRoot, targetPath), "# Target\n", "utf8");
await fs.mkdir(path.join(duplicateRoot, path.dirname(targetPath)), { recursive: true });
await fs.writeFile(path.join(duplicateRoot, targetPath), "# Duplicate\n", "utf8");
await db.insert(projects).values({
id: duplicateProjectId,
companyId: graph.companyId,
name: "Duplicate target",
status: "in_progress",
});
await db.insert(projectWorkspaces).values({
id: duplicateWorkspaceId,
companyId: graph.companyId,
projectId: duplicateProjectId,
name: "Duplicate workspace",
sourceType: "local_path",
cwd: duplicateRoot,
isPrimary: true,
});
await expect(workspaceFileResourceService(db).resolve(graph.issueId, {
path: targetPath,
workspace: "auto",
})).rejects.toMatchObject({
status: 409,
details: { code: "ambiguous_workspace_path" },
});
});
it("denies explicit cross-company project workspaces", async () => {
const { projectRoot, targetProjectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, targetProjectRoot, executionRoot });
const app = createApp(db, {
type: "board",
userId: "board-user",
companyIds: [graph.companyId],
source: "session",
isInstanceAdmin: false,
});
const res = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/resolve`)
.query({
projectId: graph.otherProjectId,
workspaceId: graph.otherProjectWorkspaceId,
path: "README.md",
});
expect(res.status).toBe(403);
expect(res.body?.details?.code).toBe("cross_company_workspace");
const rows = await db.select().from(activityLog).where(eq(activityLog.entityId, graph.issueId));
const denied = rows.find((row) => row.action === "issue.file_resource_resolve_denied");
expect(denied?.details).toMatchObject({
outcome: "denied",
projectId: graph.otherProjectId,
workspaceId: graph.otherProjectWorkspaceId,
denialReason: "cross_company_workspace",
});
});
it("rejects explicit project/workspace mismatches", async () => {
const { projectRoot, targetProjectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, targetProjectRoot, executionRoot });
const app = createApp(db, {
type: "board",
userId: "board-user",
companyIds: [graph.companyId],
source: "session",
isInstanceAdmin: false,
});
const res = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/resolve`)
.query({
projectId: graph.targetProjectId,
workspaceId: graph.projectWorkspaceId,
path: "README.md",
});
expect(res.status).toBe(422);
expect(res.body?.details?.code).toBe("workspace_project_mismatch");
});
it("blocks symlink escapes from explicit cross-project workspaces", async () => {
const { root, projectRoot, targetProjectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, targetProjectRoot, executionRoot });
await fs.writeFile(path.join(root, "outside-target.txt"), "secret\n", "utf8");
await fs.symlink(path.join(root, "outside-target.txt"), path.join(targetProjectRoot, "escape.txt"));
await expect(workspaceFileResourceService(db).readContent(graph.issueId, {
projectId: graph.targetProjectId,
workspaceId: graph.targetProjectWorkspaceId,
path: "escape.txt",
})).rejects.toMatchObject({
status: 403,
details: { code: "outside_workspace" },
});
});
it("blocks symlink directory escapes from explicit cross-project workspaces", async () => {
const { root, projectRoot, targetProjectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, targetProjectRoot, executionRoot });
const outsideDir = path.join(root, "outside-dir");
await fs.mkdir(outsideDir, { recursive: true });
await fs.writeFile(path.join(outsideDir, "secret.txt"), "secret\n", "utf8");
await fs.symlink(outsideDir, path.join(targetProjectRoot, "escape-dir"));
await expect(workspaceFileResourceService(db).resolve(graph.issueId, {
projectId: graph.targetProjectId,
workspaceId: graph.targetProjectWorkspaceId,
path: "escape-dir/",
})).rejects.toMatchObject({
status: 403,
details: { code: "outside_workspace" },
});
});
it("resolves and reads video workspace files as base64 previews", async () => {
const { projectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, executionRoot });
await fs.writeFile(path.join(projectRoot, "demo.mp4"), Buffer.from([0, 0, 0, 24, 102, 116, 121, 112]));
const resolved = await workspaceFileResourceService(db).resolve(graph.issueId, {
path: "demo.mp4",
workspace: "project",
});
expect(resolved.previewKind).toBe("video");
expect(resolved.contentType).toBe("video/mp4");
expect(resolved.capabilities.preview).toBe(true);
const content = await workspaceFileResourceService(db).readContent(graph.issueId, {
path: "demo.mp4",
workspace: "project",
});
expect(content.resource.previewKind).toBe("video");
expect(content.content.encoding).toBe("base64");
expect(content.content.data).toBe(Buffer.from([0, 0, 0, 24, 102, 116, 121, 112]).toString("base64"));
});
it("lists and searches safe file candidates from the preferred execution workspace", async () => {
const { root, projectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, executionRoot });
await fs.mkdir(path.join(executionRoot, "src"), { recursive: true });
await fs.mkdir(path.join(executionRoot, "node_modules", "pkg"), { recursive: true });
await fs.writeFile(path.join(executionRoot, "src", "app.ts"), "export const ok = true;\n", "utf8");
await fs.writeFile(path.join(executionRoot, ".env"), "TOKEN=secret\n", "utf8");
await fs.writeFile(path.join(executionRoot, "node_modules", "pkg", "index.ts"), "export {}\n", "utf8");
await fs.writeFile(path.join(executionRoot, "blob.bin"), Buffer.from([0, 1, 2, 3]));
await fs.writeFile(path.join(projectRoot, "src-project.ts"), "export const project = true;\n", "utf8");
await fs.writeFile(path.join(root, "outside.ts"), "secret\n", "utf8");
await fs.symlink(path.join(root, "outside.ts"), path.join(executionRoot, "escape.ts"));
const app = createApp(db, {
type: "board",
userId: "board-user",
companyIds: [graph.companyId],
source: "session",
isInstanceAdmin: false,
});
const res = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/list`)
.query({ workspace: "auto", q: "src", limit: 10 });
expect(res.status).toBe(200);
expect(res.body.state).toBe("available");
expect(res.body.workspace.workspaceKind).toBe("execution_workspace");
expect(res.body.items.map((item: { displayPath: string }) => item.displayPath)).toEqual(["src/app.ts"]);
expect(JSON.stringify(res.body)).not.toContain(root);
expect(JSON.stringify(res.body)).not.toContain(".env");
expect(JSON.stringify(res.body)).not.toContain("node_modules");
expect(JSON.stringify(res.body)).not.toContain("escape.ts");
expect(JSON.stringify(res.body)).not.toContain("blob.bin");
});
it("enforces default and hard list/search caps", async () => {
const { projectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, executionRoot });
await Promise.all(
Array.from({ length: 30 }, (_, index) =>
fs.writeFile(
path.join(projectRoot, `file-${String(index).padStart(2, "0")}.ts`),
"export {}\n",
"utf8",
),
),
);
const tooDeepDir = path.join(projectRoot, ...Array.from({ length: 21 }, (_, index) => `deep-${index}`));
await fs.mkdir(tooDeepDir, { recursive: true });
await fs.writeFile(path.join(tooDeepDir, "too-deep.ts"), "export const hidden = true;\n", "utf8");
const app = createApp(db, {
type: "board",
userId: "board-user",
companyIds: [graph.companyId],
source: "session",
isInstanceAdmin: false,
});
const defaultLimit = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/list`)
.query({ workspace: "project", mode: "all" });
expect(defaultLimit.status).toBe(200);
expect(defaultLimit.body.query.limit).toBe(25);
expect(defaultLimit.body.items).toHaveLength(25);
expect(defaultLimit.body.truncated).toBe(true);
const tooLargeLimit = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/list`)
.query({ workspace: "project", mode: "all", limit: 101 });
expect(tooLargeLimit.status).toBe(422);
expect(tooLargeLimit.body?.details?.code).toBe("invalid_query");
const tooDeep = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/list`)
.query({ workspace: "project", mode: "all", q: "too-deep", limit: 100 });
expect(tooDeep.status).toBe(200);
expect(tooDeep.body.items).toEqual([]);
expect(tooDeep.body.truncated).toBe(true);
});
it("lists one folder level at a time so deep descendants do not hide selected siblings", async () => {
const { projectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, executionRoot });
await fs.mkdir(path.join(projectRoot, "docs", "deep"), { recursive: true });
await fs.writeFile(path.join(projectRoot, "docs", "selected.md"), "# Selected\n", "utf8");
await Promise.all(
Array.from({ length: 130 }, (_, index) =>
fs.writeFile(
path.join(projectRoot, "docs", "deep", `generated-${String(index).padStart(3, "0")}.md`),
"# Generated\n",
"utf8",
),
),
);
const app = createApp(db, {
type: "board",
userId: "board-user",
companyIds: [graph.companyId],
source: "session",
isInstanceAdmin: false,
});
const parent = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/list`)
.query({ workspace: "project", path: "docs", mode: "all", limit: 100 });
expect(parent.status).toBe(200);
expect(parent.body.query).toMatchObject({ path: "docs", mode: "all", offset: 0 });
expect(parent.body.truncated).toBe(false);
expect(parent.body.items.map((item: { kind: string; relativePath: string }) => `${item.kind}:${item.relativePath}`)).toEqual([
"directory:docs/deep",
"file:docs/selected.md",
]);
const firstDeepPage = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/list`)
.query({ workspace: "project", path: "docs/deep", mode: "all", limit: 100 });
expect(firstDeepPage.status).toBe(200);
expect(firstDeepPage.body.items).toHaveLength(100);
expect(firstDeepPage.body.truncated).toBe(true);
const secondDeepPage = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/list`)
.query({ workspace: "project", path: "docs/deep", mode: "all", limit: 100, offset: 100 });
expect(secondDeepPage.status).toBe(200);
expect(secondDeepPage.body.query.offset).toBe(100);
expect(secondDeepPage.body.items).toHaveLength(30);
expect(secondDeepPage.body.truncated).toBe(false);
});
it("supports recent mode, limit caps, and list activity logging", async () => {
const { projectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, executionRoot });
await fs.writeFile(path.join(projectRoot, "old.ts"), "export const old = true;\n", "utf8");
await fs.writeFile(path.join(projectRoot, "new.ts"), "export const newer = true;\n", "utf8");
const app = createApp(db, {
type: "board",
userId: "board-user",
companyIds: [graph.companyId],
source: "session",
isInstanceAdmin: false,
});
const res = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/list`)
.query({ workspace: "project", mode: "recent", limit: 1 });
expect(res.status).toBe(200);
expect(res.body.items).toHaveLength(1);
expect(res.body.truncated).toBe(true);
const rows = await db.select().from(activityLog).where(eq(activityLog.entityId, graph.issueId));
const listRead = rows.find((row) => row.action === "issue.file_resource_list");
expect(listRead).toBeTruthy();
expect(listRead?.details).toMatchObject({
outcome: "success",
workspaceSelector: "project",
mode: "recent",
resultCount: 1,
truncated: true,
});
expect(JSON.stringify(listRead?.details)).not.toContain(projectRoot);
expect(JSON.stringify(listRead?.details)).not.toContain("old.ts");
expect(JSON.stringify(listRead?.details)).not.toContain("new.ts");
});
it("applies list offsets to search, recent, and changed modes", async () => {
const { projectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, executionRoot });
await execFileAsync("git", ["-C", projectRoot, "init"]);
await Promise.all([
fs.writeFile(path.join(projectRoot, "alpha.ts"), "export const alpha = true;\n", "utf8"),
fs.writeFile(path.join(projectRoot, "beta.ts"), "export const beta = true;\n", "utf8"),
fs.writeFile(path.join(projectRoot, "gamma.ts"), "export const gamma = true;\n", "utf8"),
]);
const older = new Date("2026-01-01T00:00:00.000Z");
const middle = new Date("2026-01-02T00:00:00.000Z");
const newer = new Date("2026-01-03T00:00:00.000Z");
await fs.utimes(path.join(projectRoot, "alpha.ts"), older, older);
await fs.utimes(path.join(projectRoot, "beta.ts"), middle, middle);
await fs.utimes(path.join(projectRoot, "gamma.ts"), newer, newer);
const app = createApp(db, {
type: "board",
userId: "board-user",
companyIds: [graph.companyId],
source: "session",
isInstanceAdmin: false,
});
const searchPage = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/list`)
.query({ workspace: "project", q: ".ts", limit: 1, offset: 1 });
expect(searchPage.status).toBe(200);
expect(searchPage.body.items.map((item: { relativePath: string }) => item.relativePath)).toEqual(["beta.ts"]);
expect(searchPage.body.truncated).toBe(true);
const recentPage = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/list`)
.query({ workspace: "project", mode: "recent", limit: 1, offset: 1 });
expect(recentPage.status).toBe(200);
expect(recentPage.body.items.map((item: { relativePath: string }) => item.relativePath)).toEqual(["beta.ts"]);
expect(recentPage.body.truncated).toBe(true);
const changedPage = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/list`)
.query({ workspace: "project", mode: "changed", limit: 1, offset: 1 });
expect(changedPage.status).toBe(200);
expect(changedPage.body.items.map((item: { relativePath: string }) => item.relativePath)).toEqual(["beta.ts"]);
expect(changedPage.body.truncated).toBe(true);
});
it("rejects overlong list searches and redacts the raw query from denial audit details", async () => {
const { projectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, executionRoot });
const app = createApp(db, {
type: "board",
userId: "board-user",
companyIds: [graph.companyId],
source: "session",
isInstanceAdmin: false,
});
const rawQuery = "é".repeat(65);
const res = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/list`)
.query({ workspace: "project", q: rawQuery });
expect(res.status).toBe(422);
expect(res.body?.details?.code).toBe("invalid_query");
const rows = await db.select().from(activityLog).where(eq(activityLog.entityId, graph.issueId));
const denied = rows.find((row) => row.action === "issue.file_resource_list_denied");
expect(denied?.details).toMatchObject({
outcome: "denied",
workspaceSelector: "project",
mode: "all",
denialReason: "invalid_query",
});
expect(JSON.stringify(denied?.details)).not.toContain(rawQuery);
});
it("rejects control characters in the path without crashing the audit log", async () => {
const { projectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, executionRoot });
const app = createApp(db, {
type: "board",
userId: "board-user",
companyIds: [graph.companyId],
source: "session",
isInstanceAdmin: false,
});
const nullByte = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/content?workspace=project&path=foo%00bar.ts`);
expect(nullByte.status).toBe(422);
expect(nullByte.body?.details?.code).toBe("invalid_path");
const resolveNullByte = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/resolve?workspace=project&path=foo%00bar.ts`);
expect(resolveNullByte.status).toBe(422);
expect(resolveNullByte.body?.details?.code).toBe("invalid_path");
const otherControl = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/content?workspace=project&path=a%0Bb.ts`);
expect(otherControl.status).toBe(422);
expect(otherControl.body?.details?.code).toBe("invalid_path");
});
it("rejects traversal, encoded traversal, home-relative paths, backslash traversal, and double-encoding without double-decoding", async () => {
const { projectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, executionRoot });
await fs.writeFile(path.join(projectRoot, "safe.txt"), "safe\n", "utf8");
const app = createApp(db, {
type: "board",
userId: "board-user",
companyIds: [graph.companyId],
source: "session",
isInstanceAdmin: false,
});
expect((await request(app).get(`/api/issues/${graph.issueId}/file-resources/content?workspace=project&path=..%2Fsecret.txt`)).status).toBe(403);
expect((await request(app).get(`/api/issues/${graph.issueId}/file-resources/content?workspace=project&path=%2e%2e%2Fsecret.txt`)).status).toBe(403);
expect((await request(app).get(`/api/issues/${graph.issueId}/file-resources/content`).query({ workspace: "project", path: "~/secret.txt" })).status).toBe(422);
expect((await request(app).get(`/api/issues/${graph.issueId}/file-resources/content`).query({ workspace: "project", path: "..\\secret.txt" })).status).toBe(422);
const doubleEncoded = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/content?workspace=project&path=%252e%252e%252Fsecret.txt`);
expect(doubleEncoded.status).toBe(404);
});
it("blocks symlink escapes and symlinks to denied sensitive files", async () => {
const { root, projectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, executionRoot });
await fs.writeFile(path.join(root, "outside-secret.txt"), "secret\n", "utf8");
await fs.mkdir(path.join(projectRoot, "safe"), { recursive: true });
await fs.writeFile(path.join(projectRoot, "safe", ".env"), "TOKEN=secret\n", "utf8");
await fs.symlink(path.join(root, "outside-secret.txt"), path.join(projectRoot, "escape.txt"));
await fs.symlink(path.join(projectRoot, "safe", ".env"), path.join(projectRoot, "linked-env"));
const app = createApp(db, {
type: "board",
userId: "board-user",
companyIds: [graph.companyId],
source: "session",
isInstanceAdmin: false,
});
const escape = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/content`)
.query({ workspace: "project", path: "escape.txt" });
const linkedSecret = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/content`)
.query({ workspace: "project", path: "linked-env" });
expect(escape.status).toBe(403);
expect(linkedSecret.status).toBe(403);
});
it("rejects denied paths, non-regular files, oversized text and binary while returning HTML and SVG as text", async () => {
const { projectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, executionRoot });
await fs.mkdir(path.join(projectRoot, ".git"), { recursive: true });
await fs.writeFile(path.join(projectRoot, ".git", "config"), "[core]\n", "utf8");
await fs.mkdir(path.join(projectRoot, "folder"), { recursive: true });
await fs.writeFile(path.join(projectRoot, "big.txt"), Buffer.alloc(WORKSPACE_FILE_TEXT_MAX_BYTES + 1, "a"));
await fs.writeFile(path.join(projectRoot, "blob.bin"), Buffer.from([0, 1, 2, 3]));
await fs.writeFile(path.join(projectRoot, "index.html"), "<script>alert(1)</script>", "utf8");
await fs.writeFile(path.join(projectRoot, "icon.svg"), "<svg></svg>\n", "utf8");
const app = createApp(db, {
type: "board",
userId: "board-user",
companyIds: [graph.companyId],
source: "session",
isInstanceAdmin: false,
});
for (const filePath of [".git/config", "folder", "big.txt", "blob.bin"]) {
const res = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/content`)
.query({ workspace: "project", path: filePath });
expect([403, 422]).toContain(res.status);
}
const svg = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/content`)
.query({ workspace: "project", path: "icon.svg" });
expect(svg.status).toBe(200);
expect(svg.body.resource.previewKind).toBe("text");
expect(svg.body.content.data).toContain("<svg");
const html = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/content`)
.query({ workspace: "project", path: "index.html" });
expect(html.status).toBe(200);
expect(html.headers["content-type"]).toContain("application/json");
expect(html.headers["x-content-type-options"]).toBe("nosniff");
expect(html.body.resource).toMatchObject({ contentType: "text/html", previewKind: "text", capabilities: { preview: true } });
expect(html.body.content).toEqual({ encoding: "utf8", data: "<script>alert(1)</script>" });
const downloaded = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/content`)
.query({ workspace: "project", path: "index.html", download: "1" });
expect(downloaded.status).toBe(200);
expect(downloaded.headers["content-disposition"]).toBe('attachment; filename="index.html"');
});
it("rejects remote workspaces without fetching provider resources", async () => {
const { projectRoot } = await makeWorkspace();
const graph = await seedGraph(db, {
projectRoot,
executionRoot: null,
projectSourceType: "remote_managed",
});
const resolved = await workspaceFileResourceService(db).resolve(graph.issueId, {
path: "README.md",
workspace: "project",
});
expect(resolved.kind).toBe("remote_resource");
expect(resolved.capabilities.preview).toBe(false);
await expect(workspaceFileResourceService(db).readContent(graph.issueId, {
path: "http://169.254.169.254/latest/meta-data/",
workspace: "project",
})).rejects.toMatchObject({ status: 422 });
const app = createApp(db, {
type: "board",
userId: "board-user",
companyIds: [graph.companyId],
source: "session",
isInstanceAdmin: false,
});
const listed = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/list`)
.query({ workspace: "project" });
expect(listed.status).toBe(200);
expect(listed.body.state).toBe("unavailable");
expect(listed.body.unavailableReason).toBe("remote_workspace");
expect(listed.body.items).toEqual([]);
});
it("blocks agents and cross-company board users before content reads", async () => {
const { projectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, executionRoot });
await fs.writeFile(path.join(projectRoot, "README.md"), "# Secret\n", "utf8");
const agentId = crypto.randomUUID();
await db.insert(agents).values({
id: agentId,
companyId: graph.companyId,
name: "File audit agent",
role: "engineer",
adapterType: "process",
adapterConfig: {},
});
const agentApp = createApp(db, {
type: "agent",
agentId,
companyId: graph.companyId,
source: "agent_key",
});
const boardApp = createApp(db, {
type: "board",
userId: "mallory",
companyIds: [graph.otherCompanyId],
source: "session",
isInstanceAdmin: false,
});
expect((await request(agentApp).get(`/api/issues/${graph.issueId}/file-resources/resolve`).query({ path: "README.md" })).status).toBe(403);
expect((await request(boardApp).get(`/api/issues/${graph.issueId}/file-resources/resolve`).query({ path: "README.md" })).status).toBe(404);
expect((await request(agentApp).get(`/api/issues/${graph.issueId}/file-resources/content`).query({ path: "README.md" })).status).toBe(403);
expect((await request(boardApp).get(`/api/issues/${graph.issueId}/file-resources/content`).query({ path: "README.md" })).status).toBe(404);
expect((await request(agentApp).get(`/api/issues/${graph.issueId}/file-resources/list`)).status).toBe(403);
expect((await request(boardApp).get(`/api/issues/${graph.issueId}/file-resources/list`)).status).toBe(404);
const rows = await db.select().from(activityLog).where(eq(activityLog.entityId, graph.issueId));
const listDenials = rows.filter((row) => row.action === "issue.file_resource_list_denied");
const resolveDenials = rows.filter((row) => row.action === "issue.file_resource_resolve_denied");
const contentDenials = rows.filter((row) => row.action === "issue.file_resource_content_denied");
expect(listDenials).toHaveLength(2);
expect(resolveDenials).toHaveLength(2);
expect(contentDenials).toHaveLength(2);
expect(JSON.stringify(listDenials.map((row) => row.details))).not.toContain("README.md");
expect(JSON.stringify(rows.map((row) => row.details))).not.toContain(projectRoot);
});
it("logs successful content reads and denied security-relevant attempts", async () => {
const { projectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, executionRoot });
await fs.writeFile(path.join(projectRoot, "README.md"), "# Project\n", "utf8");
await fs.writeFile(path.join(projectRoot, ".env"), "TOKEN=secret\n", "utf8");
const app = createApp(db, {
type: "board",
userId: "board-user",
companyIds: [graph.companyId],
source: "session",
isInstanceAdmin: false,
});
await request(app).get(`/api/issues/${graph.issueId}/file-resources/content`).query({ workspace: "project", path: "README.md" });
await request(app).get(`/api/issues/${graph.issueId}/file-resources/content`).query({ workspace: "project", path: ".env" });
const rows = await db.select().from(activityLog).where(eq(activityLog.entityId, graph.issueId));
expect(rows.some((row) => row.action === "issue.file_resource_content_read")).toBe(true);
expect(rows.some((row) => row.action === "issue.file_resource_content_denied")).toBe(true);
expect(JSON.stringify(rows.map((row) => row.details))).not.toContain(projectRoot);
});
it("logs successful resolves and resolve/content limiter denials", async () => {
const { projectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, executionRoot });
await fs.writeFile(path.join(projectRoot, "README.md"), "# Visible\n", "utf8");
const app = createApp(
db,
{
type: "board",
userId: "board-user",
companyIds: [graph.companyId],
source: "session",
isInstanceAdmin: false,
},
{
limiter: createFileResourceLimiter({ maxConcurrent: 1, maxRequests: 100, windowMs: 60_000 }),
},
);
const resolved = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/resolve`)
.query({ workspace: "project", path: "README.md" });
expect(resolved.status).toBe(200);
const rowsAfterResolve = await db.select().from(activityLog).where(eq(activityLog.entityId, graph.issueId));
const resolveRead = rowsAfterResolve.find((row) => row.action === "issue.file_resource_resolve");
expect(resolveRead?.details).toMatchObject({
outcome: "success",
workspaceKind: "project_workspace",
displayPath: "README.md",
});
expect(JSON.stringify(resolveRead?.details)).not.toContain(projectRoot);
let releaseSlowResolve: (() => void) | null = null;
let slowResolveStarted: (() => void) | null = null;
const slowResolve = new Promise<void>((resolve) => {
releaseSlowResolve = resolve;
});
const resolveStarted = new Promise<void>((resolve) => {
slowResolveStarted = resolve;
});
const resolveLimitedService: WorkspaceFileResourceService = {
getIssue: vi.fn(async () => ({ companyId: graph.companyId })),
availability: vi.fn(async () => {
throw new Error("not used");
}),
list: vi.fn(async () => {
throw new Error("not used");
}),
resolve: vi.fn(async () => {
slowResolveStarted?.();
await slowResolve;
return {
kind: "file",
provider: "local_fs",
title: "README.md",
displayPath: "README.md",
workspaceLabel: "Workspace",
workspaceKind: "project_workspace",
workspaceId: "11111111-1111-4111-8111-111111111111",
previewKind: "text",
capabilities: { preview: true, download: true, listChildren: false },
};
}),
readContent: vi.fn(async () => {
throw new Error("not used");
}),
prepareDownload: vi.fn(async () => {
throw new Error("not used");
}),
};
const resolveLimitedApp = createApp(
db,
{
type: "board",
userId: "board-user",
companyIds: [graph.companyId],
source: "session",
isInstanceAdmin: false,
},
{
service: resolveLimitedService,
limiter: createFileResourceLimiter({ maxConcurrent: 1, maxRequests: 100, windowMs: 60_000 }),
},
);
const firstResolve = request(resolveLimitedApp)
.get(`/api/issues/${graph.issueId}/file-resources/resolve`)
.query({ path: "README.md" });
const firstResolveResponse = firstResolve.then((res) => res);
await resolveStarted;
const secondResolve = await request(resolveLimitedApp)
.get(`/api/issues/${graph.issueId}/file-resources/resolve`)
.query({ path: "README.md" });
expect(secondResolve.status).toBe(429);
releaseSlowResolve?.();
expect((await firstResolveResponse).status).toBe(200);
let releaseSlowContent: (() => void) | null = null;
let slowContentStarted: (() => void) | null = null;
const slowContent = new Promise<void>((resolve) => {
releaseSlowContent = resolve;
});
const contentStarted = new Promise<void>((resolve) => {
slowContentStarted = resolve;
});
const contentLimitedService: WorkspaceFileResourceService = {
getIssue: vi.fn(async () => ({ companyId: graph.companyId })),
availability: vi.fn(async () => {
throw new Error("not used");
}),
list: vi.fn(async () => {
throw new Error("not used");
}),
resolve: vi.fn(async () => {
throw new Error("not used");
}),
readContent: vi.fn(async () => {
slowContentStarted?.();
await slowContent;
return {
resource: {
kind: "file",
provider: "local_fs",
title: "README.md",
displayPath: "README.md",
workspaceLabel: "Workspace",
workspaceKind: "project_workspace",
workspaceId: "11111111-1111-4111-8111-111111111111",
previewKind: "text",
capabilities: { preview: true, download: true, listChildren: false },
},
content: { encoding: "utf8", data: "# Visible\n" },
};
}),
prepareDownload: vi.fn(async () => {
throw new Error("not used");
}),
};
const contentLimitedApp = createApp(
db,
{
type: "board",
userId: "board-user",
companyIds: [graph.companyId],
source: "session",
isInstanceAdmin: false,
},
{
service: contentLimitedService,
limiter: createFileResourceLimiter({ maxConcurrent: 1, maxRequests: 100, windowMs: 60_000 }),
},
);
const firstContent = request(contentLimitedApp)
.get(`/api/issues/${graph.issueId}/file-resources/content`)
.query({ path: "README.md" });
const firstContentResponse = firstContent.then((res) => res);
await contentStarted;
const secondContent = await request(contentLimitedApp)
.get(`/api/issues/${graph.issueId}/file-resources/content`)
.query({ path: "README.md" });
expect(secondContent.status).toBe(429);
releaseSlowContent?.();
expect((await firstContentResponse).status).toBe(200);
const rowsAfterLimits = await db.select().from(activityLog).where(eq(activityLog.entityId, graph.issueId));
expect(rowsAfterLimits.some((row) => row.action === "issue.file_resource_resolve_denied")).toBe(true);
expect(rowsAfterLimits.some((row) => row.action === "issue.file_resource_content_denied")).toBe(true);
expect(JSON.stringify(rowsAfterLimits.map((row) => row.details))).not.toContain(projectRoot);
});
it("holds download concurrency slots until the file stream completes", async () => {
if (process.platform === "win32") return;
const { projectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, executionRoot });
const fifoPath = path.join(projectRoot, "slow-download.bin");
await execFileAsync("mkfifo", [fifoPath]);
let slowDownloadStarted: (() => void) | null = null;
const downloadStarted = new Promise<void>((resolve) => {
slowDownloadStarted = resolve;
});
const service: WorkspaceFileResourceService = {
getIssue: vi.fn(async () => ({ companyId: graph.companyId })),
availability: vi.fn(async () => {
throw new Error("not used");
}),
list: vi.fn(async () => {
throw new Error("not used");
}),
resolve: vi.fn(async () => {
throw new Error("not used");
}),
readContent: vi.fn(async () => {
throw new Error("not used");
}),
prepareDownload: vi.fn(async () => {
slowDownloadStarted?.();
return {
resource: {
kind: "file",
provider: "local_fs",
title: "slow-download.bin",
displayPath: "slow-download.bin",
workspaceLabel: "Workspace",
workspaceKind: "project_workspace",
workspaceId: "11111111-1111-4111-8111-111111111111",
previewKind: "unsupported",
contentType: "application/octet-stream",
byteSize: null,
capabilities: { preview: false, download: true, listChildren: false },
},
realPath: fifoPath,
};
}),
};
const app = createApp(
db,
{
type: "board",
userId: "board-user",
companyIds: [graph.companyId],
source: "session",
isInstanceAdmin: false,
},
{
service,
limiter: createFileResourceLimiter({ maxConcurrent: 1, maxRequests: 100, windowMs: 60_000 }),
},
);
const firstDownload = request(app)
.get(`/api/issues/${graph.issueId}/file-resources/content`)
.query({ path: "slow-download.bin", download: "1" })
.buffer(true)
.parse((res, callback) => {
const chunks: Buffer[] = [];
res.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
res.on("end", () => callback(null, Buffer.concat(chunks)));
});
const firstDownloadResponse = firstDownload.then((res) => res);
await downloadStarted;
await new Promise((resolve) => setImmediate(resolve));
const secondDownload = await request(app)
.get(`/api/issues/${graph.issueId}/file-resources/content`)
.query({ path: "slow-download.bin", download: "1" });
expect(secondDownload.status).toBe(429);
const writer = await fs.open(fifoPath, "w");
await writer.write(Buffer.from("slow"));
await writer.close();
const first = await firstDownloadResponse;
expect(first.status).toBe(200);
expect(first.headers["content-length"]).toBeUndefined();
expect(first.headers["content-disposition"]).toBe('attachment; filename="slow-download.bin"');
expect(Buffer.compare(first.body as Buffer, Buffer.from("slow"))).toBe(0);
});
it("uses tighter list-specific rate and concurrency limits", async () => {
const { projectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, executionRoot });
let releaseSlowList: (() => void) | null = null;
let slowListStarted: (() => void) | null = null;
const slowList = new Promise<void>((resolve) => {
releaseSlowList = resolve;
});
const listStarted = new Promise<void>((resolve) => {
slowListStarted = resolve;
});
const service: WorkspaceFileResourceService = {
getIssue: vi.fn(async () => ({ companyId: graph.companyId })),
availability: vi.fn(async () => {
throw new Error("not used");
}),
list: vi.fn(async () => {
slowListStarted?.();
await slowList;
return {
kind: "workspace_file_list",
state: "available",
workspace: {
provider: "local_fs",
workspaceLabel: "Workspace",
workspaceKind: "project_workspace",
workspaceId: "11111111-1111-4111-8111-111111111111",
},
query: {
workspace: "auto",
mode: "all",
q: null,
limit: 25,
},
items: [],
scannedCount: 0,
truncated: false,
};
}),
resolve: vi.fn(async () => {
throw new Error("not used");
}),
readContent: vi.fn(async () => {
throw new Error("not used");
}),
prepareDownload: vi.fn(async () => {
throw new Error("not used");
}),
};
const app = createApp(
db,
{
type: "board",
userId: "board-user",
companyIds: [graph.companyId],
source: "session",
isInstanceAdmin: false,
},
{
service,
listLimiter: createFileResourceListLimiter({ maxConcurrent: 1, maxRequests: 2, windowMs: 60_000 }),
},
);
const first = request(app).get(`/api/issues/${graph.issueId}/file-resources/list`);
const firstResponse = first.then((res) => res);
await listStarted;
const second = await request(app).get(`/api/issues/${graph.issueId}/file-resources/list`);
expect(second.status).toBe(429);
releaseSlowList?.();
expect((await firstResponse).status).toBe(200);
const third = await request(app).get(`/api/issues/${graph.issueId}/file-resources/list`);
expect(third.status).toBe(429);
});
it("returns mixed deduplicated availability results with one aggregate audit event", async () => {
const { root, projectRoot, targetProjectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, {
projectRoot,
targetProjectRoot,
executionRoot,
targetProjectSourceType: "remote_managed",
});
await fs.mkdir(path.join(projectRoot, "docs"), { recursive: true });
await fs.writeFile(path.join(projectRoot, "README.md"), "# Visible\n", "utf8");
await fs.writeFile(path.join(projectRoot, "docs", "guide.md"), "# Guide\n", "utf8");
await fs.writeFile(path.join(projectRoot, "archive.bin"), Buffer.from([0, 1, 2, 3]));
await fs.writeFile(path.join(projectRoot, "large.txt"), Buffer.alloc(WORKSPACE_FILE_TEXT_MAX_BYTES + 1, "a"));
await fs.writeFile(path.join(projectRoot, ".env"), "TOKEN=secret\n", "utf8");
await fs.writeFile(path.join(root, "outside-secret.txt"), "outside\n", "utf8");
await fs.symlink(path.join(root, "outside-secret.txt"), path.join(projectRoot, "escape.txt"));
const app = createApp(db, {
type: "board",
userId: "board-user",
companyIds: [graph.companyId],
source: "session",
isInstanceAdmin: false,
});
const response = await request(app)
.post(`/api/issues/${graph.issueId}/file-resources/availability`)
.send({
queries: [
{ workspace: "project", path: "README.md" },
{ workspace: "project", path: "./README.md" },
{ workspace: "project", path: "docs/" },
{ workspace: "project", path: "archive.bin" },
{ workspace: "project", path: "large.txt" },
{ workspace: "project", path: ".env" },
{ workspace: "project", path: "../outside-secret.txt" },
{ workspace: "project", path: path.join(root, "host-secret.txt") },
{ workspace: "project", path: "escape.txt" },
{ workspace: "project", path: "missing.ts" },
{
workspace: "project",
projectId: graph.targetProjectId,
workspaceId: graph.targetProjectWorkspaceId,
path: "remote.txt",
},
],
});
expect(response.status).toBe(200);
expect(response.body.kind).toBe("workspace_file_availability");
expect(response.body.results).toHaveLength(10);
const byPath = new Map(response.body.results.map((result: { query: { path: string } }) => [result.query.path, result]));
expect(byPath.get("README.md")).toMatchObject({ openable: true, resource: { kind: "file" } });
expect(byPath.get("docs/")).toMatchObject({ openable: true, resource: { kind: "directory" } });
expect(byPath.get("archive.bin")).toMatchObject({ openable: false, unavailableReason: "unsupported_content" });
expect(byPath.get("large.txt")).toMatchObject({ openable: false, unavailableReason: "too_large" });
expect(byPath.get(".env")).toMatchObject({ openable: false, unavailableReason: "denied_secret", resource: null });
expect(byPath.get("../outside-secret.txt")).toMatchObject({
openable: false,
unavailableReason: "outside_workspace",
resource: null,
});
expect(byPath.get("host-secret.txt")).toMatchObject({
openable: false,
unavailableReason: "invalid_path",
resource: null,
});
expect(byPath.get("escape.txt")).toMatchObject({
openable: false,
unavailableReason: "outside_workspace",
resource: null,
});
expect(byPath.get("missing.ts")).toMatchObject({ openable: false, unavailableReason: "not_found", resource: null });
expect(byPath.get("remote.txt")).toMatchObject({
openable: false,
unavailableReason: "remote_workspace",
resource: { kind: "remote_resource" },
});
expect(JSON.stringify(response.body)).not.toContain(root);
const rows = await db.select().from(activityLog).where(eq(activityLog.entityId, graph.issueId));
const availabilityRows = rows.filter((row) => row.action === "issue.file_resource_availability");
expect(availabilityRows).toHaveLength(1);
expect(availabilityRows[0]?.details).toMatchObject({
outcome: "success",
requestedCount: 11,
uniqueCount: 10,
openableCount: 2,
unavailableCount: 8,
});
expect(JSON.stringify(availabilityRows[0]?.details)).not.toContain(root);
});
it("reports ambiguous auto-discovery as one unavailable result", async () => {
const { projectRoot, targetProjectRoot, executionRoot, root } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, targetProjectRoot, executionRoot });
const extraRoot = path.join(root, "extra-project");
await fs.mkdir(extraRoot, { recursive: true });
await fs.writeFile(path.join(targetProjectRoot, "shared.md"), "target\n", "utf8");
await fs.writeFile(path.join(extraRoot, "shared.md"), "extra\n", "utf8");
const extraProjectId = crypto.randomUUID();
await db.insert(projects).values({
id: extraProjectId,
companyId: graph.companyId,
name: "Extra project",
status: "in_progress",
});
await db.insert(projectWorkspaces).values({
id: crypto.randomUUID(),
companyId: graph.companyId,
projectId: extraProjectId,
name: "Extra workspace",
sourceType: "local_path",
cwd: extraRoot,
isPrimary: true,
});
const app = createApp(db, {
type: "board",
userId: "board-user",
companyIds: [graph.companyId],
source: "session",
isInstanceAdmin: false,
});
const response = await request(app)
.post(`/api/issues/${graph.issueId}/file-resources/availability`)
.send({ queries: [{ path: "shared.md" }] });
expect(response.status).toBe(200);
expect(response.body.results).toEqual([
{
query: { path: "shared.md", workspace: "auto", projectId: null, workspaceId: null },
openable: false,
unavailableReason: "ambiguous_workspace_path",
resource: null,
},
]);
expect(JSON.stringify(response.body)).not.toContain(root);
});
it("enforces board access, company boundaries, and the 100-query cap", async () => {
const { projectRoot, executionRoot } = await makeWorkspace();
const graph = await seedGraph(db, { projectRoot, executionRoot });
const agentId = crypto.randomUUID();
await db.insert(agents).values({
id: agentId,
companyId: graph.companyId,
name: "Availability audit agent",
role: "engineer",
adapterType: "process",
adapterConfig: {},
});
const agentApp = createApp(db, {
type: "agent",
agentId,
companyId: graph.companyId,
source: "agent_key",
});
const otherCompanyApp = createApp(db, {
type: "board",
userId: "other-board",
companyIds: [graph.otherCompanyId],
source: "session",
isInstanceAdmin: false,
});
const boardApp = createApp(db, {
type: "board",
userId: "board-user",
companyIds: [graph.companyId],
source: "session",
isInstanceAdmin: false,
});
expect((await request(agentApp)
.post(`/api/issues/${graph.issueId}/file-resources/availability`)
.send({ queries: [{ path: "README.md" }] })).status).toBe(403);
expect((await request(otherCompanyApp)
.post(`/api/issues/${graph.issueId}/file-resources/availability`)
.send({ queries: [{ path: "README.md" }] })).status).toBe(404);
expect((await request(boardApp)
.post(`/api/issues/${graph.issueId}/file-resources/availability`)
.send({ queries: Array.from({ length: 101 }, (_, index) => ({ path: `file-${index}.ts` })) })).status).toBe(400);
});
});
describeEmbeddedPostgres("file resource route guards", () => {
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>> | null = null;
let db: Db;
beforeAll(async () => {
tempDb = await startEmbeddedPostgresTestDatabase("paperclip-file-resource-guards-");
db = createDb(tempDb.connectionString);
}, 60_000);
afterAll(async () => {
await tempDb?.cleanup();
});
it("enforces bounded rate and concurrency limits", async () => {
const companyId = crypto.randomUUID();
await db.insert(companies).values({
id: companyId,
name: "Rate limit company",
issuePrefix: "RAT",
});
let releaseSlowRead: (() => void) | null = null;
let slowReadStarted: (() => void) | null = null;
const slowRead = new Promise<void>((resolve) => {
releaseSlowRead = resolve;
});
const readStarted = new Promise<void>((resolve) => {
slowReadStarted = resolve;
});
const service: WorkspaceFileResourceService = {
getIssue: vi.fn(async () => ({ companyId })),
availability: vi.fn(async () => {
throw new Error("not used");
}),
list: vi.fn(async () => {
throw new Error("not used");
}),
resolve: vi.fn(async () => {
slowReadStarted?.();
await slowRead;
return {
kind: "file",
provider: "local_fs",
title: "README.md",
displayPath: "README.md",
workspaceLabel: "Workspace",
workspaceKind: "project_workspace",
workspaceId: "11111111-1111-4111-8111-111111111111",
previewKind: "text",
capabilities: { preview: true, download: true, listChildren: false },
};
}),
readContent: vi.fn(async () => {
throw new Error("not used");
}),
prepareDownload: vi.fn(async () => {
throw new Error("not used");
}),
};
const app = express();
app.use((req, _res, next) => {
req.actor = {
type: "board",
userId: "board-user",
companyIds: [companyId],
source: "session",
isInstanceAdmin: false,
};
next();
});
app.use("/api", fileResourceRoutes(db, {
service,
limiter: createFileResourceLimiter({ maxConcurrent: 1, maxRequests: 2, windowMs: 60_000 }),
}));
app.use(errorHandler);
const first = request(app).get("/api/issues/issue-1/file-resources/resolve").query({ path: "README.md" });
const firstResponse = first.then((res) => res);
await readStarted;
const second = await request(app).get("/api/issues/issue-1/file-resources/resolve").query({ path: "README.md" });
expect(second.status).toBe(429);
releaseSlowRead?.();
expect((await firstResponse).status).toBe(200);
const third = await request(app).get("/api/issues/issue-1/file-resources/resolve").query({ path: "README.md" });
expect(third.status).toBe(429);
});
it("uses a batch-specific availability limiter", async () => {
const companyId = crypto.randomUUID();
await db.insert(companies).values({
id: companyId,
name: "Availability rate limit company",
issuePrefix: "AVL",
});
let releaseSlowAvailability: (() => void) | null = null;
let slowAvailabilityStarted: (() => void) | null = null;
const slowAvailability = new Promise<void>((resolve) => {
releaseSlowAvailability = resolve;
});
const availabilityStarted = new Promise<void>((resolve) => {
slowAvailabilityStarted = resolve;
});
const service: WorkspaceFileResourceService = {
getIssue: vi.fn(async () => ({ companyId })),
availability: vi.fn(async () => {
slowAvailabilityStarted?.();
await slowAvailability;
return { kind: "workspace_file_availability", results: [] };
}),
list: vi.fn(async () => { throw new Error("not used"); }),
resolve: vi.fn(async () => { throw new Error("not used"); }),
readContent: vi.fn(async () => { throw new Error("not used"); }),
prepareDownload: vi.fn(async () => { throw new Error("not used"); }),
};
const app = createApp(
db,
{
type: "board",
userId: "board-user",
companyIds: [companyId],
source: "session",
isInstanceAdmin: false,
},
{
service,
availabilityLimiter: createFileResourceAvailabilityLimiter({
maxConcurrent: 1,
maxRequests: 2,
windowMs: 60_000,
}),
},
);
const firstRequest = request(app)
.post("/api/issues/issue-availability/file-resources/availability")
.send({ queries: [{ path: "README.md" }] });
const firstResponse = firstRequest.then((response) => response);
await availabilityStarted;
expect((await request(app)
.post("/api/issues/issue-availability/file-resources/availability")
.send({ queries: [{ path: "README.md" }] })).status).toBe(429);
releaseSlowAvailability?.();
expect((await firstResponse).status).toBe(200);
expect((await request(app)
.post("/api/issues/issue-availability/file-resources/availability")
.send({ queries: [{ path: "README.md" }] })).status).toBe(429);
});
});