agent / server /src /routes /execution-workspaces.ts
GraziePrego's picture
Upload folder using huggingface_hub
b152fd5 verified
import { and, eq } from "drizzle-orm";
import { Router } from "express";
import type { Db } from "@paperclipai/db";
import { issues, projects, projectWorkspaces } from "@paperclipai/db";
import { updateExecutionWorkspaceSchema } from "@paperclipai/shared";
import { validate } from "../middleware/validate.js";
import { executionWorkspaceService, logActivity, workspaceOperationService } from "../services/index.js";
import { mergeExecutionWorkspaceConfig, readExecutionWorkspaceConfig } from "../services/execution-workspaces.js";
import { parseProjectExecutionWorkspacePolicy } from "../services/execution-workspace-policy.js";
import { readProjectWorkspaceRuntimeConfig } from "../services/project-workspace-runtime-config.js";
import {
cleanupExecutionWorkspaceArtifacts,
startRuntimeServicesForWorkspaceControl,
stopRuntimeServicesForExecutionWorkspace,
} from "../services/workspace-runtime.js";
import { assertCompanyAccess, getActorInfo } from "./authz.js";
export function executionWorkspaceRoutes(db: Db) {
const router = Router();
const svc = executionWorkspaceService(db);
const workspaceOperationsSvc = workspaceOperationService(db);
router.get("/companies/:companyId/execution-workspaces", async (req, res) => {
const companyId = req.params.companyId as string;
assertCompanyAccess(req, companyId);
const workspaces = await svc.list(companyId, {
projectId: req.query.projectId as string | undefined,
projectWorkspaceId: req.query.projectWorkspaceId as string | undefined,
issueId: req.query.issueId as string | undefined,
status: req.query.status as string | undefined,
reuseEligible: req.query.reuseEligible === "true",
});
res.json(workspaces);
});
router.get("/execution-workspaces/:id", async (req, res) => {
const id = req.params.id as string;
const workspace = await svc.getById(id);
if (!workspace) {
res.status(404).json({ error: "Execution workspace not found" });
return;
}
assertCompanyAccess(req, workspace.companyId);
res.json(workspace);
});
router.get("/execution-workspaces/:id/close-readiness", async (req, res) => {
const id = req.params.id as string;
const workspace = await svc.getById(id);
if (!workspace) {
res.status(404).json({ error: "Execution workspace not found" });
return;
}
assertCompanyAccess(req, workspace.companyId);
const readiness = await svc.getCloseReadiness(id);
if (!readiness) {
res.status(404).json({ error: "Execution workspace not found" });
return;
}
res.json(readiness);
});
router.get("/execution-workspaces/:id/workspace-operations", async (req, res) => {
const id = req.params.id as string;
const workspace = await svc.getById(id);
if (!workspace) {
res.status(404).json({ error: "Execution workspace not found" });
return;
}
assertCompanyAccess(req, workspace.companyId);
const operations = await workspaceOperationsSvc.listForExecutionWorkspace(id);
res.json(operations);
});
router.post("/execution-workspaces/:id/runtime-services/:action", async (req, res) => {
const id = req.params.id as string;
const action = String(req.params.action ?? "").trim().toLowerCase();
if (action !== "start" && action !== "stop" && action !== "restart") {
res.status(404).json({ error: "Runtime service action not found" });
return;
}
const existing = await svc.getById(id);
if (!existing) {
res.status(404).json({ error: "Execution workspace not found" });
return;
}
assertCompanyAccess(req, existing.companyId);
const workspaceCwd = existing.cwd;
if (!workspaceCwd) {
res.status(422).json({ error: "Execution workspace needs a local path before Paperclip can manage local runtime services" });
return;
}
const projectWorkspace = existing.projectWorkspaceId
? await db
.select({
id: projectWorkspaces.id,
cwd: projectWorkspaces.cwd,
repoUrl: projectWorkspaces.repoUrl,
repoRef: projectWorkspaces.repoRef,
defaultRef: projectWorkspaces.defaultRef,
metadata: projectWorkspaces.metadata,
})
.from(projectWorkspaces)
.where(
and(
eq(projectWorkspaces.id, existing.projectWorkspaceId),
eq(projectWorkspaces.companyId, existing.companyId),
),
)
.then((rows) => rows[0] ?? null)
: null;
const projectWorkspaceRuntime = readProjectWorkspaceRuntimeConfig(
(projectWorkspace?.metadata as Record<string, unknown> | null) ?? null,
)?.workspaceRuntime ?? null;
const effectiveRuntimeConfig = existing.config?.workspaceRuntime ?? projectWorkspaceRuntime ?? null;
if ((action === "start" || action === "restart") && !effectiveRuntimeConfig) {
res.status(422).json({ error: "Execution workspace has no runtime service configuration or inherited project workspace default" });
return;
}
const actor = getActorInfo(req);
const recorder = workspaceOperationsSvc.createRecorder({
companyId: existing.companyId,
executionWorkspaceId: existing.id,
});
let runtimeServiceCount = existing.runtimeServices?.length ?? 0;
const stdout: string[] = [];
const stderr: string[] = [];
const operation = await recorder.recordOperation({
phase: action === "stop" ? "workspace_teardown" : "workspace_provision",
command: `workspace runtime ${action}`,
cwd: existing.cwd,
metadata: {
action,
executionWorkspaceId: existing.id,
},
run: async () => {
const onLog = async (stream: "stdout" | "stderr", chunk: string) => {
if (stream === "stdout") stdout.push(chunk);
else stderr.push(chunk);
};
if (action === "stop" || action === "restart") {
await stopRuntimeServicesForExecutionWorkspace({
db,
executionWorkspaceId: existing.id,
workspaceCwd,
});
}
if (action === "start" || action === "restart") {
const startedServices = await startRuntimeServicesForWorkspaceControl({
db,
actor: {
id: actor.agentId ?? null,
name: actor.actorType === "user" ? "Board" : "Agent",
companyId: existing.companyId,
},
issue: existing.sourceIssueId
? {
id: existing.sourceIssueId,
identifier: null,
title: existing.name,
}
: null,
workspace: {
baseCwd: workspaceCwd,
source: existing.mode === "shared_workspace" ? "project_primary" : "task_session",
projectId: existing.projectId,
workspaceId: existing.projectWorkspaceId,
repoUrl: existing.repoUrl,
repoRef: existing.baseRef,
strategy: existing.strategyType === "git_worktree" ? "git_worktree" : "project_primary",
cwd: workspaceCwd,
branchName: existing.branchName,
worktreePath: existing.strategyType === "git_worktree" ? workspaceCwd : null,
warnings: [],
created: false,
},
executionWorkspaceId: existing.id,
config: { workspaceRuntime: effectiveRuntimeConfig },
adapterEnv: {},
onLog,
});
runtimeServiceCount = startedServices.length;
} else {
runtimeServiceCount = 0;
}
const metadata = mergeExecutionWorkspaceConfig(existing.metadata as Record<string, unknown> | null, {
desiredState: action === "stop" ? "stopped" : "running",
});
await svc.update(existing.id, { metadata });
return {
status: "succeeded",
stdout: stdout.join(""),
stderr: stderr.join(""),
system:
action === "stop"
? "Stopped execution workspace runtime services.\n"
: action === "restart"
? "Restarted execution workspace runtime services.\n"
: "Started execution workspace runtime services.\n",
metadata: {
runtimeServiceCount,
},
};
},
});
const workspace = await svc.getById(id);
if (!workspace) {
res.status(404).json({ error: "Execution workspace not found" });
return;
}
await logActivity(db, {
companyId: existing.companyId,
actorType: actor.actorType,
actorId: actor.actorId,
agentId: actor.agentId,
runId: actor.runId,
action: `execution_workspace.runtime_${action}`,
entityType: "execution_workspace",
entityId: existing.id,
details: {
runtimeServiceCount,
},
});
res.json({
workspace,
operation,
});
});
router.patch("/execution-workspaces/:id", validate(updateExecutionWorkspaceSchema), async (req, res) => {
const id = req.params.id as string;
const existing = await svc.getById(id);
if (!existing) {
res.status(404).json({ error: "Execution workspace not found" });
return;
}
assertCompanyAccess(req, existing.companyId);
const patch: Record<string, unknown> = {
...(req.body.name === undefined ? {} : { name: req.body.name }),
...(req.body.cwd === undefined ? {} : { cwd: req.body.cwd }),
...(req.body.repoUrl === undefined ? {} : { repoUrl: req.body.repoUrl }),
...(req.body.baseRef === undefined ? {} : { baseRef: req.body.baseRef }),
...(req.body.branchName === undefined ? {} : { branchName: req.body.branchName }),
...(req.body.providerRef === undefined ? {} : { providerRef: req.body.providerRef }),
...(req.body.status === undefined ? {} : { status: req.body.status }),
...(req.body.cleanupReason === undefined ? {} : { cleanupReason: req.body.cleanupReason }),
...(req.body.cleanupEligibleAt !== undefined
? { cleanupEligibleAt: req.body.cleanupEligibleAt ? new Date(req.body.cleanupEligibleAt) : null }
: {}),
};
if (req.body.metadata !== undefined || req.body.config !== undefined) {
const requestedMetadata = req.body.metadata === undefined
? (existing.metadata as Record<string, unknown> | null)
: (req.body.metadata as Record<string, unknown> | null);
patch.metadata = req.body.config === undefined
? requestedMetadata
: mergeExecutionWorkspaceConfig(requestedMetadata, req.body.config ?? null);
}
let workspace = existing;
let cleanupWarnings: string[] = [];
const configForCleanup = readExecutionWorkspaceConfig(
((patch.metadata as Record<string, unknown> | null | undefined) ?? (existing.metadata as Record<string, unknown> | null)) ?? null,
);
if (req.body.status === "archived" && existing.status !== "archived") {
const readiness = await svc.getCloseReadiness(existing.id);
if (!readiness) {
res.status(404).json({ error: "Execution workspace not found" });
return;
}
if (readiness.state === "blocked") {
res.status(409).json({
error: readiness.blockingReasons[0] ?? "Execution workspace cannot be closed right now",
closeReadiness: readiness,
});
return;
}
const closedAt = new Date();
const archivedWorkspace = await svc.update(id, {
...patch,
status: "archived",
closedAt,
cleanupReason: null,
});
if (!archivedWorkspace) {
res.status(404).json({ error: "Execution workspace not found" });
return;
}
workspace = archivedWorkspace;
if (existing.mode === "shared_workspace") {
await db
.update(issues)
.set({
executionWorkspaceId: null,
updatedAt: new Date(),
})
.where(
and(
eq(issues.companyId, existing.companyId),
eq(issues.executionWorkspaceId, existing.id),
),
);
}
try {
await stopRuntimeServicesForExecutionWorkspace({
db,
executionWorkspaceId: existing.id,
workspaceCwd: existing.cwd,
});
const projectWorkspace = existing.projectWorkspaceId
? await db
.select({
cwd: projectWorkspaces.cwd,
cleanupCommand: projectWorkspaces.cleanupCommand,
})
.from(projectWorkspaces)
.where(
and(
eq(projectWorkspaces.id, existing.projectWorkspaceId),
eq(projectWorkspaces.companyId, existing.companyId),
),
)
.then((rows) => rows[0] ?? null)
: null;
const projectPolicy = existing.projectId
? await db
.select({
executionWorkspacePolicy: projects.executionWorkspacePolicy,
})
.from(projects)
.where(and(eq(projects.id, existing.projectId), eq(projects.companyId, existing.companyId)))
.then((rows) => parseProjectExecutionWorkspacePolicy(rows[0]?.executionWorkspacePolicy))
: null;
const cleanupResult = await cleanupExecutionWorkspaceArtifacts({
workspace: existing,
projectWorkspace,
teardownCommand: configForCleanup?.teardownCommand ?? projectPolicy?.workspaceStrategy?.teardownCommand ?? null,
cleanupCommand: configForCleanup?.cleanupCommand ?? null,
recorder: workspaceOperationsSvc.createRecorder({
companyId: existing.companyId,
executionWorkspaceId: existing.id,
}),
});
cleanupWarnings = cleanupResult.warnings;
const cleanupPatch: Record<string, unknown> = {
closedAt,
cleanupReason: cleanupWarnings.length > 0 ? cleanupWarnings.join(" | ") : null,
};
if (!cleanupResult.cleaned) {
cleanupPatch.status = "cleanup_failed";
}
if (cleanupResult.warnings.length > 0 || !cleanupResult.cleaned) {
workspace = (await svc.update(id, cleanupPatch)) ?? workspace;
}
} catch (error) {
const failureReason = error instanceof Error ? error.message : String(error);
workspace =
(await svc.update(id, {
status: "cleanup_failed",
closedAt,
cleanupReason: failureReason,
})) ?? workspace;
res.status(500).json({
error: `Failed to archive execution workspace: ${failureReason}`,
});
return;
}
} else {
const updatedWorkspace = await svc.update(id, patch);
if (!updatedWorkspace) {
res.status(404).json({ error: "Execution workspace not found" });
return;
}
workspace = updatedWorkspace;
}
const actor = getActorInfo(req);
await logActivity(db, {
companyId: existing.companyId,
actorType: actor.actorType,
actorId: actor.actorId,
agentId: actor.agentId,
runId: actor.runId,
action: "execution_workspace.updated",
entityType: "execution_workspace",
entityId: workspace.id,
details: {
changedKeys: Object.keys(req.body).sort(),
...(cleanupWarnings.length > 0 ? { cleanupWarnings } : {}),
},
});
res.json(workspace);
});
return router;
}