This commit is contained in:
ZacharyZhang-NY committed 2026-04-21 12:44:33 -04:00
1 parent 740d0812ee
commit 97033dfabd
68 files changed
+2608 -328

No files matched your search

@@ -14,7 +14,7 @@
*/
import type { AppDatabase } from "@main/database";
import { approvalInputSchema } from "@shared/contracts";
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";
@@ -90,6 +90,25 @@ export function handleFileApprovalToolCall(
}
}
// 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.approvalsMod.requestApproval({
@@ -100,7 +119,7 @@ export function handleFileApprovalToolCall(
relatedAgentId: input.relatedAgentId ?? null,
payloadSummary: input.payloadSummary,
impactSummary: input.impactSummary,
payloadJson: input.payloadJson ?? null,
payloadJson: mergedPayloadJson,
});
} catch (error) {
throw classifyUnknownError(error, {
+23 -2
View File
@@ -1,6 +1,6 @@
import { z } from "zod";
import log from "electron-log/main.js";
import { approvalInputSchema } from "@shared/contracts";
import { approvalInputSchema, mergeApprovalAttachments } from "@shared/contracts";
import { resolveApprovalDecision } from "../approval-policy";
import { shouldAutoApprove } from "../execution-policy";
import { incrementDeprecationCall } from "./deprecation-counter";
@@ -94,6 +94,27 @@ export function registerApprovalRoutes(server: AgentApiServer) {
}
}
// Fold the schema-level documentIds / workProductIds into payloadJson so
// the ApprovalInspector and extractApprovalDocumentReferences can read a
// single source of truth. Legacy callers that stuffed documentIds inside
// payloadJson keep working unchanged. Malformed payloadJson (non-JSON
// or non-object) surfaces as 400 at the API boundary — no silent
// rawPayload stashing, because that hid producer bugs from the operator.
let mergedPayloadJson: string | null;
try {
mergedPayloadJson = mergeApprovalAttachments(
input.payloadJson,
input.documentIds,
input.workProductIds,
);
} catch (error) {
server.json(res, 400, {
error: error instanceof Error ? error.message : "Invalid approval payloadJson",
code: "INVALID_PAYLOAD_JSON",
});
return;
}
let id: string;
try {
id = db.approvalsMod.requestApproval({
@@ -104,7 +125,7 @@ export function registerApprovalRoutes(server: AgentApiServer) {
relatedAgentId: input.relatedAgentId ?? null,
payloadSummary: input.payloadSummary,
impactSummary: input.impactSummary,
payloadJson: input.payloadJson ?? null,
payloadJson: mergedPayloadJson,
});
} catch (error) {
server.json(res, 422, { error: error instanceof Error ? error.message : "Approval could not be created" });
@@ -5,7 +5,9 @@ Body: {
"relatedTaskId": "{taskId — what task is this for}",
"requestedByAgentId": "{{agentId}}",
"payloadSummary": "Clear description of what you need and why",
"impactSummary": "What happens if this is approved vs denied"
"impactSummary": "What happens if this is approved vs denied",
"documentIds": ["{docUuid1}", "{docUuid2}"],
"workProductIds": ["{workProductUuid}"]
}
Approval types:
- secret_access: Need API keys, credentials, or account access
@@ -13,6 +15,30 @@ Approval types:
- approve_ceo_strategy: CEO submitting org strategy for Board review
- dangerous_command: Irreversible action that needs human sign-off
Attaching artifacts:
- `documentIds` — UUIDs of existing company documents. The board inspector
renders each as a clickable row on the first screen of the approval modal,
so reviewers can open and edit inline.
- `workProductIds` — UUIDs of existing work products (files on disk,
external URLs). The inspector surfaces each row with a click-to-open
action that hands off to the operator's default application.
- Prefer these schema-typed arrays over stuffing `{documentIds:[...]}` into
`payloadJson`. Agents using the `file_approval` internal tool pass the
same fields directly as arguments.
Example — deliverable_review that asks the CEO to sign off on a spec and an
exported PDF:
```
{
"type": "deliverable_review",
"relatedTaskId": "8f7a...",
"payloadSummary": "Q2 launch plan ready for CEO review",
"impactSummary": "Unblocks the kickoff email if approved; revision_requested loops back to me.",
"documentIds": ["2c9d...e41a"],
"workProductIds": ["55ab...90ff"]
}
```
### Check Approval Status
GET /api/approvals/{approvalId}
@@ -76,7 +76,19 @@ export const fileApprovalSchema = Type.Object({
payloadJson: Type.Optional(
Type.String({
description:
"Optional JSON string carrying structured approval payload. Must be valid JSON if provided.",
"Optional JSON string carrying type-specific structured payload (hire proposal, social draft, etc.). Must be valid JSON if provided. For attaching existing documents or work products, use documentIds / workProductIds — the board UI renders those directly on the first screen.",
}),
),
documentIds: Type.Optional(
Type.Array(Type.String(), {
description:
"UUIDs of existing company documents to attach to this approval. The board inspector surfaces each attachment on the first screen of the approval modal. Prefer this over jamming {documentIds:[...]} into payloadJson.",
}),
),
workProductIds: Type.Optional(
Type.Array(Type.String(), {
description:
"UUIDs of existing work products (files on disk, external URLs) to attach to this approval. The board inspector renders each row as a clickable link that opens in the operator's default application.",
}),
),
relatedTaskId: Type.Optional(
@@ -104,7 +116,7 @@ export function createFileApprovalTool(rpc: WorkerRpcClient, runId: string): Age
name: FILE_APPROVAL_TOOL_NAME,
label: "file approval",
description:
"File an approval request with the company board. Use for 'approve_ceo_strategy' after filing initial hires, or for any other decision that requires board sign-off. The request is auto-approved when the company is in hands-off mode; otherwise it waits for the human operator.",
"File an approval request with the company board. Use for 'approve_ceo_strategy' after filing initial hires, or for any other decision that requires board sign-off. When the approval references existing documents or work products (e.g. deliverable_review, document_review), pass their UUIDs via documentIds / workProductIds so the board inspector can render them as clickable attachments on the first screen — do not bury IDs inside payloadJson. The request is auto-approved when the company is in hands-off mode; otherwise it waits for the human operator.",
parameters: fileApprovalSchema,
async execute(toolCallId, params, _signal?, _onUpdate?) {
const payload: InvokeInternalToolRpcPayload = {