mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 17:33:51 +02:00
Remove physical-root source/reader authorization and sticky directory classifications from workspace preparation. Preserve current task/run authority, app-record privacy, credential authorization, containment, and lifecycle/publication integrity.
Validate replacement selections against persisted new inputs and refresh verified checkout ownership so bounded reuse survives earlier workspace relocation. Add real PostgreSQL/Git regressions, a reproducible history-scaling benchmark, and documentation.
Full CI passed all 48 jobs and Greptile scored 5/5 at 3d5da6c259. Five preparation-guard medians improved from 20,512.99 ms to 52.22 ms on the 8,500-workspace fixture; these are not end-to-end startup timings.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
51 lines
3.4 KiB
TypeScript
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;
|
|
}
|