/** * Main-side handler for the `file_approval` internal tool (C4 part 2/5). * * Mirrors the control flow of `POST /api/companies/:id/approvals` in * `api-server/routes-approvals.ts`: validate → enforce the * `approve_ceo_strategy` ordering gate → request approval → auto- * approve or notify. Both the tool path and the HTTP path funnel * through the same `db.core.approvals.requestApproval` + `shouldAutoApprove` * plumbing, so the board sees identical rows regardless of entry point. * * No fallbacks: malformed input throws UserInputError, DB failures * throw UpstreamError. The worker tool wrapper surfaces the classified * message as a pi tool-error so the model can correct its call. */ import type { AppDatabase } from "@main/database"; import { approvalInputSchema, mergeApprovalAttachments } from "@shared/contracts"; import { resolveApprovalDecision } from "@main/approval-policy"; import { shouldAutoApprove } from "@main/execution-policy"; import { UserInputError, UpstreamError, classifyUnknownError } from "@main/errors"; import type { InvokeInternalToolRpcResult } from "@main/worker/runtime/internal-tools/internal-tool-rpc"; export interface FileApprovalHandlerDeps { db: AppDatabase; onApprovalCreated: (approvalId: string, companyId: string, taskId: string | null, approvalType: string, summary: string) => void; onApprovalResolved: (approvalId: string, companyId: string, decision: string) => void; onDomainChanged: () => void; logger: { info: (...args: unknown[]) => void; warn: (...args: unknown[]) => void; error: (...args: unknown[]) => void; }; } export interface FileApprovalHandlerContext { companyId: string; agentId: string; } export function handleFileApprovalToolCall( deps: FileApprovalHandlerDeps, ctx: FileApprovalHandlerContext, rawArgs: unknown, ): InvokeInternalToolRpcResult { const { db } = deps; const { companyId, agentId: requesterId } = ctx; const argObject = typeof rawArgs === "object" && rawArgs !== null ? (rawArgs as Record) : {}; const parsed = approvalInputSchema.safeParse({ companyId, ...argObject, requestedByAgentId: requesterId, }); if (!parsed.success) { throw new UserInputError({ code: "file_approval.input.invalid", message: `Invalid file_approval payload: ${parsed.error.issues .map((i) => `${i.path.join(".")}: ${i.message}`) .join("; ")}`, context: { companyId, requesterId }, }); } const input = parsed.data; // Mirror the HTTP route's `approve_ceo_strategy` ordering gate: the // strategy approval cannot exist before any hire_agent approvals do, // and cannot be re-filed once it exists. The tool path would // otherwise re-open the exact loop the HTTP gate was written to // close. if (input.type === "approve_ceo_strategy") { const companyHireApprovals = db.core.approvals.listCompanyApprovalsByType(companyId, "hire_agent"); if (companyHireApprovals.length === 0) { throw new UserInputError({ code: "file_approval.strategy.before_hires", message: "approve_ceo_strategy requires at least one hire_agent approval to exist first. File the intended hires, then the strategy.", }); } const existingStrategy = db.core.approvals .listCompanyApprovalsByType(companyId, "approve_ceo_strategy") .find((a) => a.state === "pending" || a.state === "approved"); if (existingStrategy) { throw new UserInputError({ code: "file_approval.strategy.already_filed", message: `approve_ceo_strategy already exists for this company (id ${existingStrategy.id}); do not submit another.`, }); } } // Fold schema-level documentIds / workProductIds into payloadJson so the // desktop inspector has a single source of truth. payloadJson remains the // escape hatch for type-specific structured fields. Malformed payloadJson // (non-JSON or non-object) is a producer bug — surface it to the agent as // UserInputError so the tool call can be corrected, not silently stashed. let mergedPayloadJson: string | null; try { mergedPayloadJson = mergeApprovalAttachments( input.payloadJson, input.documentIds, input.workProductIds, ); } catch (error) { throw new UserInputError({ code: "file_approval.payload_json.invalid", message: error instanceof Error ? error.message : "payloadJson must be a JSON object or null.", }); } let approvalId: string; try { approvalId = db.core.approvals.requestApproval({ companyId, type: input.type, relatedTaskId: input.relatedTaskId ?? null, requestedByAgentId: requesterId, relatedAgentId: input.relatedAgentId ?? null, payloadSummary: input.payloadSummary, impactSummary: input.impactSummary, payloadJson: mergedPayloadJson, }); } catch (error) { throw classifyUnknownError(error, { kind: "upstream", code: "file_approval.create.failed", message: error instanceof Error ? error.message : "Approval could not be created.", context: { companyId, requesterId, type: input.type }, }); } try { db.core.activity.addActivity({ companyId, actor: requesterId, action: "approval.created", entityType: "approval", entityId: approvalId, detail: input.payloadSummary, }); } catch (error) { throw new UpstreamError({ code: "file_approval.activity.failed", message: "Failed to record activity for approval.", cause: error, }); } deps.onApprovalCreated( approvalId, companyId, input.relatedTaskId ?? null, input.type, input.payloadSummary ?? "", ); let autoApproved = false; if (shouldAutoApprove(db, companyId, input.type)) { resolveApprovalDecision({ db, handleApprovalResolved: deps.onApprovalResolved, companyId, approvalId, state: "approved", decisionNote: "Auto-approved by company policy", detail: "Auto-approved by company policy", }); autoApproved = true; deps.logger.info( `[file-approval-handler] Auto-approved approval ${approvalId}: ${input.payloadSummary}`, ); } deps.onDomainChanged(); const summary = autoApproved ? `Filed ${input.type} approval ${approvalId}. Auto-approved by company policy.` : `Filed ${input.type} approval ${approvalId}. Pending board decision.`; return { content: [{ type: "text", text: summary }], details: { approvalId, autoApproved, type: input.type, }, isError: false, }; }