update
This commit is contained in:
1 parent
66158e222e
commit
67c757f481
314 files changed
+37945
-22962
No files matched your search
@@ -0,0 +1,212 @@
|
||||
import { z } from "zod";
|
||||
import log from "electron-log/main.js";
|
||||
import { approvalInputSchema } from "@shared/contracts";
|
||||
import { resolveApprovalDecision, shouldAutoApproveApproval } from "../approval-policy";
|
||||
import type { AgentApiServer } from "./server-core";
|
||||
|
||||
const approvalListQuerySchema = z.object({
|
||||
requestedByAgentId: z.string().uuid().optional(),
|
||||
relatedTaskId: z.string().uuid().optional(),
|
||||
status: z.string().optional(),
|
||||
});
|
||||
|
||||
export function registerApprovalRoutes(server: AgentApiServer) {
|
||||
const db = server.db_;
|
||||
const callbacks = server.callbacks_;
|
||||
const onDomainChanged = server.onDomainChanged_;
|
||||
const onWakeAgent = server.onWakeAgent_;
|
||||
|
||||
server.addRoute("POST", "/api/companies/:companyId/approvals", (_req, res, params, token, body) => {
|
||||
const companyId = params.companyId!;
|
||||
if (companyId !== token.companyId) {
|
||||
server.json(res, 403, { error: "Company mismatch" });
|
||||
return;
|
||||
}
|
||||
const parsed = approvalInputSchema.safeParse({
|
||||
companyId,
|
||||
...(typeof body === "object" && body ? body : {}),
|
||||
requestedByAgentId: token.agentId,
|
||||
});
|
||||
if (!parsed.success) {
|
||||
server.json(res, 400, { error: "Invalid approval payload" });
|
||||
return;
|
||||
}
|
||||
const input = parsed.data;
|
||||
if (server.companyRefError(res, companyId, [
|
||||
{ id: input.relatedTaskId, table: "tasks", label: "related task" },
|
||||
{ id: token.agentId, table: "agents", label: "requesting agent" },
|
||||
{ id: input.relatedAgentId, table: "agents", label: "related agent" },
|
||||
])) {
|
||||
return;
|
||||
}
|
||||
|
||||
let id: string;
|
||||
try {
|
||||
id = db.requestApproval({
|
||||
companyId,
|
||||
type: input.type,
|
||||
relatedTaskId: input.relatedTaskId ?? null,
|
||||
requestedByAgentId: token.agentId,
|
||||
relatedAgentId: input.relatedAgentId ?? null,
|
||||
payloadSummary: input.payloadSummary,
|
||||
impactSummary: input.impactSummary,
|
||||
payloadJson: input.payloadJson ?? null,
|
||||
});
|
||||
} catch (error) {
|
||||
server.json(res, 422, { error: error instanceof Error ? error.message : "Approval could not be created" });
|
||||
return;
|
||||
}
|
||||
|
||||
db.addActivity({
|
||||
companyId,
|
||||
actor: token.agentId,
|
||||
action: "approval.created",
|
||||
entityType: "approval",
|
||||
entityId: id,
|
||||
detail: input.payloadSummary,
|
||||
});
|
||||
|
||||
// Notify thread integration about the new approval
|
||||
callbacks.onApprovalCreated(id, companyId, input.relatedTaskId ?? null, input.type, input.payloadSummary ?? "");
|
||||
|
||||
if (shouldAutoApproveApproval(db, companyId, input.type)) {
|
||||
resolveApprovalDecision({
|
||||
db,
|
||||
handleApprovalResolved: callbacks.onApprovalResolved,
|
||||
companyId,
|
||||
approvalId: id,
|
||||
state: "approved",
|
||||
decisionNote: "Auto-approved by company policy",
|
||||
detail: "Auto-approved by company policy",
|
||||
});
|
||||
log.info(`[auto-approve] Auto-approved approval ${id}: ${input.payloadSummary}`);
|
||||
} else {
|
||||
callbacks.notify({
|
||||
title: "Approval Required",
|
||||
body: input.payloadSummary ?? `${input.type} approval pending`,
|
||||
urgency: "critical",
|
||||
navigation: { section: "approvals", entityId: id },
|
||||
batchKey: "pending_approval",
|
||||
});
|
||||
}
|
||||
|
||||
onDomainChanged();
|
||||
server.json(res, 201, { id });
|
||||
});
|
||||
|
||||
server.addRoute("GET", "/api/companies/:companyId/approvals", (req, res, params, token) => {
|
||||
const companyId = params.companyId!;
|
||||
if (companyId !== token.companyId) {
|
||||
server.json(res, 403, { error: "Company mismatch" });
|
||||
return;
|
||||
}
|
||||
const url = new URL(req.url ?? "/", `http://127.0.0.1:${server.port_}`);
|
||||
const parsedQuery = approvalListQuerySchema.safeParse({
|
||||
requestedByAgentId: url.searchParams.get("requestedByAgentId") ?? undefined,
|
||||
relatedTaskId: url.searchParams.get("relatedTaskId") ?? undefined,
|
||||
status: url.searchParams.get("status") ?? undefined,
|
||||
});
|
||||
if (!parsedQuery.success) {
|
||||
server.json(res, 400, { error: "Invalid approval query" });
|
||||
return;
|
||||
}
|
||||
const statuses = parsedQuery.data.status?.split(",").filter(Boolean) ?? null;
|
||||
const snapshot = db.listSnapshot();
|
||||
const approvals = snapshot.approvals.filter((approval) => {
|
||||
if (approval.companyId !== companyId) return false;
|
||||
if (parsedQuery.data.requestedByAgentId && approval.requestedByAgentId !== parsedQuery.data.requestedByAgentId) return false;
|
||||
if (parsedQuery.data.relatedTaskId && approval.relatedTaskId !== parsedQuery.data.relatedTaskId) return false;
|
||||
if (statuses && !statuses.includes(approval.state)) return false;
|
||||
return true;
|
||||
});
|
||||
server.json(res, 200, approvals);
|
||||
});
|
||||
|
||||
server.addRoute("GET", "/api/approvals/:approvalId", (_req, res, params, token) => {
|
||||
const snapshot = db.listSnapshot();
|
||||
const approval = snapshot.approvals.find((a) => a.id === params.approvalId && a.companyId === token.companyId);
|
||||
if (!approval) {
|
||||
server.json(res, 404, { error: "Approval not found" });
|
||||
return;
|
||||
}
|
||||
server.json(res, 200, approval);
|
||||
});
|
||||
|
||||
server.addRoute("PATCH", "/api/approvals/:approvalId", (_req, res) => {
|
||||
server.json(res, 403, { error: "Approvals must be decided from the desktop board, not from an agent run token" });
|
||||
});
|
||||
|
||||
server.addRoute("GET", "/api/approvals/:approvalId/comments", (_req, res, params, token) => {
|
||||
const approval = db.listSnapshot().approvals.find((a) => a.id === params.approvalId);
|
||||
if (!approval) { server.json(res, 404, { error: "Approval not found" }); return; }
|
||||
if (approval.companyId !== token.companyId) { server.json(res, 403, { error: "Company mismatch" }); return; }
|
||||
const comments = db.listApprovalComments(params.approvalId!, approval.companyId);
|
||||
server.json(res, 200, comments);
|
||||
});
|
||||
|
||||
server.addRoute("POST", "/api/approvals/:approvalId/comments", (_req, res, params, token, body) => {
|
||||
const approval = db.listSnapshot().approvals.find((a) => a.id === params.approvalId);
|
||||
if (!approval) { server.json(res, 404, { error: "Approval not found" }); return; }
|
||||
if (approval.companyId !== token.companyId) { server.json(res, 403, { error: "Company mismatch" }); return; }
|
||||
const data = body as Record<string, unknown>;
|
||||
const commentBody = typeof data.body === "string" ? data.body.trim() : "";
|
||||
if (!commentBody) { server.json(res, 400, { error: "Comment body is required" }); return; }
|
||||
const commentId = db.addApprovalComment({
|
||||
companyId: approval.companyId,
|
||||
approvalId: approval.id,
|
||||
authorAgentId: token.agentId ?? null,
|
||||
body: commentBody,
|
||||
});
|
||||
// Wake mentioned agents in comment
|
||||
const mentionedIds = db.findMentionedAgentIds(approval.companyId, commentBody);
|
||||
for (const mentionedId of mentionedIds) {
|
||||
if (mentionedId !== token.agentId) {
|
||||
onWakeAgent(mentionedId, approval.companyId, "approval_comment_mentioned");
|
||||
}
|
||||
}
|
||||
onDomainChanged();
|
||||
server.json(res, 201, { id: commentId });
|
||||
});
|
||||
|
||||
server.addRoute("POST", "/api/approvals/:approvalId/resubmit", (_req, res, params, token) => {
|
||||
const approval = db.listSnapshot().approvals.find((a) => a.id === params.approvalId);
|
||||
if (!approval) { server.json(res, 404, { error: "Approval not found" }); return; }
|
||||
if (approval.companyId !== token.companyId) { server.json(res, 403, { error: "Company mismatch" }); return; }
|
||||
if (approval.state !== "revision_requested") {
|
||||
server.json(res, 422, { error: "Only revision_requested approvals can be resubmitted" });
|
||||
return;
|
||||
}
|
||||
if (token.agentId !== approval.requestedByAgentId) {
|
||||
server.json(res, 403, { error: "Only the requesting agent can resubmit this approval" });
|
||||
return;
|
||||
}
|
||||
db.resubmitApproval(approval.id, approval.companyId);
|
||||
onDomainChanged();
|
||||
server.json(res, 200, { resubmitted: true });
|
||||
});
|
||||
|
||||
server.addRoute("GET", "/api/approvals/:approvalId/tasks", (_req, res, params, token) => {
|
||||
const approval = db.listSnapshot().approvals.find((a) => a.id === params.approvalId);
|
||||
if (!approval) { server.json(res, 404, { error: "Approval not found" }); return; }
|
||||
if (approval.companyId !== token.companyId) { server.json(res, 403, { error: "Company mismatch" }); return; }
|
||||
const taskIds = db.listTasksForApproval(params.approvalId!);
|
||||
server.json(res, 200, taskIds);
|
||||
});
|
||||
|
||||
server.addRoute("POST", "/api/approvals/:approvalId/tasks", (_req, res, params, token, body) => {
|
||||
const approval = db.listSnapshot().approvals.find((a) => a.id === params.approvalId);
|
||||
if (!approval) { server.json(res, 404, { error: "Approval not found" }); return; }
|
||||
if (approval.companyId !== token.companyId) { server.json(res, 403, { error: "Company mismatch" }); return; }
|
||||
const data = body as Record<string, unknown>;
|
||||
const taskIds = Array.isArray(data.taskIds) ? data.taskIds.filter((id): id is string => typeof id === "string") : [];
|
||||
if (taskIds.length === 0) { server.json(res, 400, { error: "taskIds array is required" }); return; }
|
||||
for (const taskId of taskIds) {
|
||||
if (!db.belongsToCompany("tasks", taskId, token.companyId)) {
|
||||
server.json(res, 400, { error: `Task ${taskId} does not belong to this company` }); return;
|
||||
}
|
||||
}
|
||||
db.linkManyTasksToApproval(params.approvalId!, taskIds, token.companyId, token.agentId ?? null);
|
||||
onDomainChanged();
|
||||
server.json(res, 200, { linked: taskIds.length });
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user