Files
PaperClipAI/server/src/routes/task-workspaces.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

51 lines
3.4 KiB
TypeScript

import { and, eq } from "drizzle-orm";
import { issues, type Db } from "@paperclipai/db";
import { Router, type Request } from "express";
import { prepareWorkspaceRepositorySchema, selectTaskWorkspaceSchema } from "@paperclipai/shared";
import { accessService } from "../services/access.js";
import { executionWorkspaceService } from "../services/execution-workspaces.js";
import { issueReadSqlCondition } from "../services/authorization.js";
import { projectToolContext } from "../services/project-tool-context.js";
import { logActivity } from "../services/activity-log.js";
import { forbidden, notFound } from "../errors.js";
import { assertCompanyAccess, getActorInfo, hasCompanyAccess } from "./authz.js";
import { validate } from "../middleware/validate.js";
export function taskWorkspaceRoutes(db: Db) {
const router = Router();
const workspaces = executionWorkspaceService(db);
async function task(req: Request, write = false) {
const [issue] = await db.select().from(issues).where(and(eq(issues.id, req.params.id as string), await issueReadSqlCondition(db, req.actor)));
if (!issue) throw notFound("Task not found");
if (!hasCompanyAccess(req, issue.companyId)) throw notFound("Task not found");
assertCompanyAccess(req, issue.companyId);
const decision = await accessService(db).decide({ actor: req.actor, action: write ? "issue:mutate" : "issue:read", resource: {
type: "issue", companyId: issue.companyId, issueId: issue.id,
status: issue.status, assigneeAgentId: issue.assigneeAgentId, assigneeUserId: issue.assigneeUserId,
} });
if (!decision.allowed) throw forbidden("Task workspace is outside this actor's authorization boundary");
if (write && req.actor.type === "agent") {
const context = await projectToolContext(db, req.actor, true, "Workspace");
if (context.issue.id !== issue.id) throw forbidden("Workspace selection is scoped to the current task");
}
return issue;
}
router.get("/issues/:id/workspace", async (req, res) => {
const issue = await task(req);
const view = await workspaces.inspectTaskWorkspace(issue.companyId, issue.id, req.actor);
res.json({ ...view, repositories: issue.executionWorkspaceId ? await workspaces.listTaskRepositories(issue.companyId, issue.executionWorkspaceId) : [], capabilities: { prepareRepository: "next_normal_admission", selectWorkspace: "next_normal_admission" }, fileLocations: { taskFiles: "workspace", personalInstructionsAndMemory: "AGENT_HOME", providerHome: "runtime_managed" } });
});
router.put("/issues/:id/workspace", validate(selectTaskWorkspaceSchema), async (req, res) => {
const issue = await task(req, true);
const result = await workspaces.selectTaskWorkspace({ companyId: issue.companyId, issueId: issue.id, actor: req.actor, ...req.body });
const actor = getActorInfo(req);
await logActivity(db, { companyId: issue.companyId, ...actor, action: "issue.workspace.selection_requested", entityType: "issue", entityId: issue.id, details: { selection: req.body.selection, applies: "next_normal_admission" } });
res.json(result);
});
router.post("/issues/:id/workspace/repositories", validate(prepareWorkspaceRepositorySchema), async (req, res) => {
const issue = await task(req, true);
res.json(await workspaces.requestTaskRepository({ companyId: issue.companyId, issueId: issue.id, actor: req.actor, request: req.body }));
});
return router;
}