mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-17 20:09:05 +02:00
Give tasks durable company-scoped workspace bindings independent of project assignment, channel defaults and authorized repository preparation. Reuse verified input seeds and sparse output recovery while preserving trust, privacy and controller provisioning boundaries.
Full CI and E2E, Superagent, legacy checks and Greptile Apex 5/5 passed on reviewed head 753680b61c. All review findings are resolved; the PR records focused verification and historical acceptance limits.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
52 lines
3.6 KiB
TypeScript
52 lines
3.6 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 { canActorReadExecutionWorkspace, 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 (issue.executionWorkspaceId && !(await canActorReadExecutionWorkspace(db, req.actor, issue.executionWorkspaceId))) throw notFound("Execution workspace not found");
|
|
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;
|
|
}
|