Add workspace operation tracking and fix project properties JSX

This commit is contained in:
Dotta
2026-03-17 09:36:35 -05:00
parent e39ae5a400
commit 4da13984e2
19 changed files with 11537 additions and 30 deletions

View File

@@ -29,6 +29,7 @@ import {
issueService,
logActivity,
secretService,
workspaceOperationService,
} from "../services/index.js";
import { conflict, forbidden, notFound, unprocessable } from "../errors.js";
import { assertBoard, assertCompanyAccess, getActorInfo } from "./authz.js";
@@ -62,6 +63,7 @@ export function agentRoutes(db: Db) {
const heartbeat = heartbeatService(db);
const issueApprovalsSvc = issueApprovalService(db);
const secretsSvc = secretService(db);
const workspaceOperations = workspaceOperationService(db);
const strictSecretsMode = process.env.PAPERCLIP_SECRETS_STRICT_MODE === "true";
function canCreateAgents(agent: { role: string; permissions: Record<string, unknown> | null | undefined }) {
@@ -1560,6 +1562,40 @@ export function agentRoutes(db: Db) {
res.json(result);
});
router.get("/heartbeat-runs/:runId/workspace-operations", async (req, res) => {
const runId = req.params.runId as string;
const run = await heartbeat.getRun(runId);
if (!run) {
res.status(404).json({ error: "Heartbeat run not found" });
return;
}
assertCompanyAccess(req, run.companyId);
const context = asRecord(run.contextSnapshot);
const executionWorkspaceId = asNonEmptyString(context?.executionWorkspaceId);
const operations = await workspaceOperations.listForRun(runId, executionWorkspaceId);
res.json(redactCurrentUserValue(operations));
});
router.get("/workspace-operations/:operationId/log", async (req, res) => {
const operationId = req.params.operationId as string;
const operation = await workspaceOperations.getById(operationId);
if (!operation) {
res.status(404).json({ error: "Workspace operation not found" });
return;
}
assertCompanyAccess(req, operation.companyId);
const offset = Number(req.query.offset ?? 0);
const limitBytes = Number(req.query.limitBytes ?? 256000);
const result = await workspaceOperations.readLog(operationId, {
offset: Number.isFinite(offset) ? offset : 0,
limitBytes: Number.isFinite(limitBytes) ? limitBytes : 256000,
});
res.json(result);
});
router.get("/issues/:issueId/live-runs", async (req, res) => {
const rawId = req.params.issueId as string;
const issueSvc = issueService(db);

View File

@@ -4,7 +4,7 @@ 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 } from "../services/index.js";
import { executionWorkspaceService, logActivity, workspaceOperationService } from "../services/index.js";
import { parseProjectExecutionWorkspacePolicy } from "../services/execution-workspace-policy.js";
import {
cleanupExecutionWorkspaceArtifacts,
@@ -17,6 +17,7 @@ const TERMINAL_ISSUE_STATUSES = new Set(["done", "cancelled"]);
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;
@@ -121,6 +122,10 @@ export function executionWorkspaceRoutes(db: Db) {
workspace: existing,
projectWorkspace,
teardownCommand: projectPolicy?.workspaceStrategy?.teardownCommand ?? null,
recorder: workspaceOperationsSvc.createRecorder({
companyId: existing.companyId,
executionWorkspaceId: existing.id,
}),
});
cleanupWarnings = cleanupResult.warnings;
const cleanupPatch: Record<string, unknown> = {