mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-26 10:26:26 +01:00
Introduce company-scoped task, agent, user and project files, recoverable trash, object-backed repository checkpoints, and a shared 180-second lifecycle for sandbox execution. Wire file browsing and save status into the owner surfaces. This is an implementation checkpoint; staging acceptance and the complete merge gate remain outstanding.
61 lines
3.8 KiB
TypeScript
61 lines
3.8 KiB
TypeScript
import { and, eq } from "drizzle-orm";
|
|
import { agents, companyMemberships, heartbeatRuns, issues, projects, workFolderRuns, type Db } from "@paperclipai/db";
|
|
import type { WorkFolderOwner } from "@paperclipai/shared";
|
|
import { notFound } from "../errors.js";
|
|
import { authorizationService, type AuthorizationActor, type AuthorizationResource } from "./authorization.js";
|
|
|
|
/** Private-file access never inherits the responsible-user shadow-mode bypass. */
|
|
export async function assertWorkFolderAccess(db: Db, actor: AuthorizationActor, owner: WorkFolderOwner, write: boolean) {
|
|
const deny = () => { throw notFound("Work folder not found"); };
|
|
if (actor.type === "none") deny();
|
|
if (actor.type === "agent" && actor.companyId !== owner.companyId) deny();
|
|
if (actor.type === "board" && actor.source !== "local_implicit" && !actor.companyIds?.includes(owner.companyId)) deny();
|
|
const userId = actor.type === "board" ? actor.userId : actor.onBehalfOfUserId;
|
|
if (actor.source !== "local_implicit" && userId) {
|
|
const [membership] = await db.select().from(companyMemberships).where(and(
|
|
eq(companyMemberships.companyId, owner.companyId), eq(companyMemberships.principalType, "user"),
|
|
eq(companyMemberships.principalId, userId), eq(companyMemberships.status, "active")));
|
|
if (!membership || (write && membership.membershipRole === "viewer")) deny();
|
|
}
|
|
if (owner.scope === "user") {
|
|
if (!userId || userId !== owner.ownerId) deny();
|
|
if (actor.type === "agent") {
|
|
if (!actor.runId || !actor.agentId || actor.source !== "agent_jwt") deny();
|
|
const [run] = await db.select().from(heartbeatRuns).where(and(eq(heartbeatRuns.id, actor.runId!),
|
|
eq(heartbeatRuns.companyId, owner.companyId), eq(heartbeatRuns.agentId, actor.agentId!)));
|
|
const [binding] = await db.select().from(workFolderRuns).where(eq(workFolderRuns.runId, actor.runId!));
|
|
if (!run || run.status !== "running" || run.responsibleUserId !== owner.ownerId
|
|
|| binding?.manifest.responsibleUserId !== owner.ownerId) deny();
|
|
}
|
|
return;
|
|
}
|
|
let resource: AuthorizationResource;
|
|
let action: "issue:read" | "issue:mutate" | "agent:read" | "agent_config:update" | "project:read";
|
|
if (owner.scope === "task") {
|
|
const [issue] = await db.select().from(issues).where(and(eq(issues.id, owner.ownerId), eq(issues.companyId, owner.companyId)));
|
|
if (!issue) return deny();
|
|
resource = { type: "issue", companyId: owner.companyId, issueId: issue.id, projectId: issue.projectId,
|
|
parentIssueId: issue.parentId, assigneeAgentId: issue.assigneeAgentId, assigneeUserId: issue.assigneeUserId,
|
|
originKind: issue.originKind, originId: issue.originId, status: issue.status };
|
|
action = write ? "issue:mutate" : "issue:read";
|
|
} else if (owner.scope === "agent") {
|
|
const [agent] = await db.select().from(agents).where(and(eq(agents.id, owner.ownerId), eq(agents.companyId, owner.companyId)));
|
|
if (!agent) return deny();
|
|
if (write && actor.type === "agent" && actor.agentId !== agent.id) deny();
|
|
resource = { type: "agent", companyId: owner.companyId, agentId: agent.id };
|
|
action = "agent:read";
|
|
} else {
|
|
const [project] = await db.select().from(projects).where(and(eq(projects.id, owner.ownerId), eq(projects.companyId, owner.companyId)));
|
|
if (!project) return deny();
|
|
resource = { type: "project", companyId: owner.companyId, projectId: project.id };
|
|
action = "project:read";
|
|
}
|
|
const authz = authorizationService(db);
|
|
if (!(await authz.decide({ actor, action, resource })).allowed) deny();
|
|
if (actor.type === "agent" && userId) {
|
|
const responsibleActor: AuthorizationActor = { type: "board", source: "session", userId,
|
|
companyIds: [owner.companyId], memberships: actor.onBehalfOfMemberships, ignoreInstanceAdmin: true };
|
|
if (!(await authz.decide({ actor: responsibleActor, action, resource })).allowed) deny();
|
|
}
|
|
}
|