244 lines
9.1 KiB
TypeScript
244 lines
9.1 KiB
TypeScript
import {
|
|
IPC_CHANNELS,
|
|
approvalDecisionSchema,
|
|
approvalInputSchema,
|
|
cancelRunSchema,
|
|
commentInputSchema,
|
|
heartbeatSettingsSchema,
|
|
listCommentsSchema,
|
|
runLogChunkSchema,
|
|
runLogSchema,
|
|
startRunSchema,
|
|
triggerHeartbeatSchema,
|
|
} from "@shared/contracts";
|
|
import { resolveApprovalDecision, shouldAutoApproveApproval } from "./approval-policy";
|
|
import { fail, ok, type RegisterCoreHandlersDependencies } from "./ipc-core-common";
|
|
|
|
export function registerCoreOperationHandlers({
|
|
registerHandle,
|
|
db,
|
|
ensureCompanyExists,
|
|
publishDomainChanged,
|
|
emitEvent,
|
|
wakeAgentIfPossible,
|
|
handleApprovalResolved,
|
|
dispatchAutomation,
|
|
queueTaskRun,
|
|
getWorkerProcess,
|
|
notify,
|
|
}: Pick<
|
|
RegisterCoreHandlersDependencies,
|
|
| "registerHandle"
|
|
| "db"
|
|
| "ensureCompanyExists"
|
|
| "publishDomainChanged"
|
|
| "emitEvent"
|
|
| "wakeAgentIfPossible"
|
|
| "handleApprovalResolved"
|
|
| "dispatchAutomation"
|
|
| "queueTaskRun"
|
|
| "getWorkerProcess"
|
|
| "notify"
|
|
>) {
|
|
registerHandle(IPC_CHANNELS.requestApproval, async (_event, payload) => {
|
|
const parsed = approvalInputSchema.parse(payload);
|
|
const companyError = ensureCompanyExists(parsed.companyId);
|
|
if (companyError) return companyError;
|
|
const invalidTask = parsed.relatedTaskId && !db.belongsToCompany("tasks", parsed.relatedTaskId, parsed.companyId);
|
|
const invalidAgent = parsed.requestedByAgentId && !db.belongsToCompany("agents", parsed.requestedByAgentId, parsed.companyId);
|
|
const invalidRelatedAgent = parsed.relatedAgentId && !db.belongsToCompany("agents", parsed.relatedAgentId, parsed.companyId);
|
|
if (invalidTask || invalidAgent || invalidRelatedAgent) {
|
|
return fail("INVALID_COMPANY_REFERENCE", "The approval references data outside the current company.");
|
|
}
|
|
let id: string;
|
|
try {
|
|
id = db.requestApproval(parsed);
|
|
} catch (error) {
|
|
return fail("APPROVAL_INVALID", error instanceof Error ? error.message : "Approval could not be created.");
|
|
}
|
|
db.addActivity({
|
|
companyId: parsed.companyId,
|
|
actor: parsed.requestedByAgentId ?? "board",
|
|
action: "approval.created",
|
|
entityType: "approval",
|
|
entityId: id,
|
|
detail: parsed.payloadSummary,
|
|
});
|
|
if (shouldAutoApproveApproval(db, parsed.companyId, parsed.type)) {
|
|
resolveApprovalDecision({
|
|
db,
|
|
handleApprovalResolved,
|
|
companyId: parsed.companyId,
|
|
approvalId: id,
|
|
state: "approved",
|
|
decisionNote: "Auto-approved by company policy",
|
|
detail: "Auto-approved by company policy",
|
|
});
|
|
publishDomainChanged();
|
|
return ok(id);
|
|
}
|
|
// Approval was NOT auto-approved — wake decision-makers to review it
|
|
dispatchAutomation("approval_created", {
|
|
companyId: parsed.companyId,
|
|
agentId: parsed.requestedByAgentId ?? undefined,
|
|
taskId: parsed.relatedTaskId ?? undefined,
|
|
});
|
|
notify({
|
|
title: "Approval Required",
|
|
body: parsed.payloadSummary ?? `${parsed.type} approval pending`,
|
|
urgency: "critical",
|
|
navigation: { section: "approvals", entityId: id },
|
|
batchKey: "pending_approval",
|
|
});
|
|
publishDomainChanged();
|
|
return ok(id);
|
|
});
|
|
|
|
registerHandle(IPC_CHANNELS.decideApproval, async (_event, payload) => {
|
|
const parsed = approvalDecisionSchema.parse(payload);
|
|
const companyError = ensureCompanyExists(parsed.companyId);
|
|
if (companyError) return companyError;
|
|
if (!db.belongsToCompany("approvals", parsed.approvalId, parsed.companyId)) {
|
|
return fail("APPROVAL_NOT_FOUND", "Approval not found in the current company.");
|
|
}
|
|
const approval = db.getApprovalRecord(parsed.approvalId);
|
|
if (!approval || approval.companyId !== parsed.companyId) {
|
|
return fail("APPROVAL_NOT_FOUND", "Approval not found in the current company.");
|
|
}
|
|
resolveApprovalDecision({
|
|
db,
|
|
handleApprovalResolved,
|
|
companyId: approval.companyId,
|
|
approvalId: approval.id,
|
|
state: parsed.state,
|
|
decisionNote: parsed.decisionNote,
|
|
detail: parsed.decisionNote,
|
|
});
|
|
publishDomainChanged();
|
|
return ok(true);
|
|
});
|
|
|
|
registerHandle(IPC_CHANNELS.startTaskRun, async (_event, payload) => {
|
|
const parsed = startRunSchema.parse(payload);
|
|
const companyError = ensureCompanyExists(parsed.companyId);
|
|
if (companyError) return companyError;
|
|
if (!db.belongsToCompany("tasks", parsed.taskId, parsed.companyId)) {
|
|
return fail("TASK_NOT_FOUND", "Task not found in the current company.");
|
|
}
|
|
return queueTaskRun(parsed.taskId, "manual", "board");
|
|
});
|
|
|
|
registerHandle(IPC_CHANNELS.cancelRun, (_event, payload) => {
|
|
const parsed = cancelRunSchema.parse(payload);
|
|
const companyError = ensureCompanyExists(parsed.companyId);
|
|
if (companyError) return companyError;
|
|
if (!db.belongsToCompany("runs", parsed.runId, parsed.companyId)) {
|
|
return fail("RUN_NOT_FOUND", "Run not found in the current company.");
|
|
}
|
|
getWorkerProcess()?.postMessage({ type: "cancel-run", runId: parsed.runId });
|
|
return ok(true);
|
|
});
|
|
|
|
registerHandle(IPC_CHANNELS.getRunLog, (_event, payload) => {
|
|
const parsed = runLogSchema.parse(payload);
|
|
const companyError = ensureCompanyExists(parsed.companyId);
|
|
if (companyError) return companyError;
|
|
if (!db.belongsToCompany("runs", parsed.runId, parsed.companyId)) {
|
|
return fail("RUN_NOT_FOUND", "Run not found in the current company.");
|
|
}
|
|
return ok(db.readRunLog(parsed.runId));
|
|
});
|
|
|
|
registerHandle(IPC_CHANNELS.getRunLogChunk, (_event, payload) => {
|
|
const parsed = runLogChunkSchema.parse(payload);
|
|
const companyError = ensureCompanyExists(parsed.companyId);
|
|
if (companyError) return companyError;
|
|
if (!db.belongsToCompany("runs", parsed.runId, parsed.companyId)) {
|
|
return fail("RUN_NOT_FOUND", "Run not found in the current company.");
|
|
}
|
|
return ok(db.readRunLogChunk(parsed.runId, parsed.offset, parsed.limit));
|
|
});
|
|
|
|
registerHandle(IPC_CHANNELS.addComment, async (_event, payload) => {
|
|
const parsed = commentInputSchema.parse(payload);
|
|
const companyError = ensureCompanyExists(parsed.companyId);
|
|
if (companyError) return companyError;
|
|
const comment = db.addComment({
|
|
companyId: parsed.companyId,
|
|
taskId: parsed.taskId,
|
|
authorAgentId: parsed.authorAgentId ?? null,
|
|
authorName: parsed.authorName,
|
|
body: parsed.body,
|
|
});
|
|
db.addActivity({
|
|
companyId: parsed.companyId,
|
|
actor: parsed.authorAgentId ?? "board",
|
|
action: "comment.created",
|
|
entityType: "task",
|
|
entityId: parsed.taskId,
|
|
detail: parsed.body.slice(0, 100),
|
|
});
|
|
emitEvent({ type: "agent-message", taskId: parsed.taskId, comment });
|
|
publishDomainChanged();
|
|
try {
|
|
const task = db.getTask(parsed.taskId);
|
|
if (task.assigneeAgentId) {
|
|
wakeAgentIfPossible(task.assigneeAgentId, task.companyId, "comment");
|
|
}
|
|
dispatchAutomation("comment_posted", {
|
|
companyId: parsed.companyId,
|
|
taskId: parsed.taskId,
|
|
agentId: parsed.authorAgentId ?? undefined,
|
|
});
|
|
} catch {
|
|
// Task may have been deleted between comment creation and wake evaluation.
|
|
}
|
|
return ok(comment.id);
|
|
});
|
|
|
|
registerHandle(IPC_CHANNELS.listComments, async (_event, payload) => {
|
|
const parsed = listCommentsSchema.parse(payload);
|
|
const companyError = ensureCompanyExists(parsed.companyId);
|
|
if (companyError) return companyError;
|
|
if (!db.belongsToCompany("tasks", parsed.taskId, parsed.companyId)) {
|
|
return fail("TASK_NOT_FOUND", "Task not found in the current company.");
|
|
}
|
|
return ok(db.listComments(parsed.taskId));
|
|
});
|
|
|
|
registerHandle(IPC_CHANNELS.setHeartbeat, async (_event, payload) => {
|
|
const parsed = heartbeatSettingsSchema.parse(payload);
|
|
const companyError = ensureCompanyExists(parsed.companyId);
|
|
if (companyError) return companyError;
|
|
if (!db.belongsToCompany("agents", parsed.agentId, parsed.companyId)) {
|
|
return fail("AGENT_NOT_FOUND", "Agent not found in the current company.");
|
|
}
|
|
db.setHeartbeat(parsed.agentId, parsed.enabled, parsed.intervalSec);
|
|
db.addActivity({
|
|
companyId: parsed.companyId,
|
|
actor: "board",
|
|
action: parsed.enabled ? "heartbeat.enabled" : "heartbeat.disabled",
|
|
entityType: "agent",
|
|
entityId: parsed.agentId,
|
|
detail: `Interval: ${parsed.intervalSec}s`,
|
|
});
|
|
emitEvent({ type: "heartbeat-status", agentId: parsed.agentId, enabled: parsed.enabled, nextWakeAt: null });
|
|
publishDomainChanged();
|
|
return ok(true);
|
|
});
|
|
|
|
registerHandle(IPC_CHANNELS.triggerHeartbeat, async (_event, payload) => {
|
|
const parsed = triggerHeartbeatSchema.parse(payload);
|
|
const companyError = ensureCompanyExists(parsed.companyId);
|
|
if (companyError) return companyError;
|
|
if (!db.belongsToCompany("agents", parsed.agentId, parsed.companyId)) {
|
|
return fail("AGENT_NOT_FOUND", "Agent not found in the current company.");
|
|
}
|
|
const runId = wakeAgentIfPossible(parsed.agentId, parsed.companyId, parsed.trigger);
|
|
if (!runId) {
|
|
return fail("HEARTBEAT_SKIPPED", "Agent cannot be woken right now (check connector status, budget, or active runs).");
|
|
}
|
|
return ok(runId);
|
|
});
|
|
}
|