From 83a03270b29af155deb3f5175f52b0f596ce9b6f Mon Sep 17 00:00:00 2001 From: Christina Tong Date: Fri, 13 Feb 2026 20:01:43 -0800 Subject: [PATCH] update: validate tags parsing in headless (#958) --- src/headless.ts | 234 +++++++++++++++++++++++++----------------------- 1 file changed, 124 insertions(+), 110 deletions(-) diff --git a/src/headless.ts b/src/headless.ts index a436a3e..c35ec46 100644 --- a/src/headless.ts +++ b/src/headless.ts @@ -157,10 +157,10 @@ export async function handleHeadlessCommand( if (validModes.includes(permissionModeValue)) { permissionMode.setMode( permissionModeValue as - | "default" - | "acceptEdits" - | "bypassPermissions" - | "plan", + | "default" + | "acceptEdits" + | "bypassPermissions" + | "plan", ); } } @@ -227,9 +227,9 @@ export async function handleHeadlessCommand( if (values.resume) { console.error( "Error: --resume is for interactive mode only (opens conversation selector).\n" + - "In headless mode, use:\n" + - " --continue Resume the last session (agent + conversation)\n" + - " --conversation Resume a specific conversation by ID", + "In headless mode, use:\n" + + " --continue Resume the last session (agent + conversation)\n" + + " --conversation Resume a specific conversation by ID", ); process.exit(1); } @@ -269,7 +269,21 @@ export async function handleHeadlessCommand( const fromAfFile = values["from-af"] as string | undefined; const preLoadSkillsRaw = values["pre-load-skills"] as string | undefined; const maxTurnsRaw = values["max-turns"] as string | undefined; - const tags = values["tags"] as string[] | undefined; + const tagsRaw = values["tags"] as string | undefined; + + // Parse and validate base tools + let tags: string[] | undefined + if (tagsRaw !== undefined) { + const trimmed = tagsRaw.trim(); + if (!trimmed || trimmed.toLowerCase() === "none") { + tags = []; + } else { + tags = trimmed + .split(",") + .map((name) => name.trim()) + .filter((name) => name.length > 0); + } + } // Parse and validate max-turns if provided let maxTurns: number | undefined; @@ -444,9 +458,9 @@ export async function handleHeadlessCommand( // - BlockReference: { blockId: string } let memoryBlocks: | Array< - | { label: string; value: string; description?: string } - | { blockId: string } - > + | { label: string; value: string; description?: string } + | { blockId: string } + > | undefined; if (memoryBlocksJson !== undefined) { if (!forceNew) { @@ -710,8 +724,8 @@ export async function handleHeadlessCommand( initBlocks === undefined ? [...ISOLATED_BLOCK_LABELS] : ISOLATED_BLOCK_LABELS.filter((label) => - initBlocks.includes(label as string), - ); + initBlocks.includes(label as string), + ); if (specifiedConversationId) { if (specifiedConversationId === "default") { @@ -908,24 +922,24 @@ export async function handleHeadlessCommand( // Phase 1: Collect decisions for all approvals type Decision = | { - type: "approve"; - approval: { - toolCallId: string; - toolName: string; - toolArgs: string; - }; - reason: string; - matchedRule: string; - } - | { - type: "deny"; - approval: { - toolCallId: string; - toolName: string; - toolArgs: string; - }; - reason: string; + type: "approve"; + approval: { + toolCallId: string; + toolName: string; + toolArgs: string; }; + reason: string; + matchedRule: string; + } + | { + type: "deny"; + approval: { + toolCallId: string; + toolName: string; + toolArgs: string; + }; + reason: string; + }; const { autoAllowed, autoDenied } = await classifyApprovals( pendingApprovals, @@ -1033,7 +1047,7 @@ export async function handleHeadlessCommand( await drainStreamWithResume( approvalStream, createBuffers(agent.id), - () => {}, + () => { }, ); } } @@ -1294,14 +1308,14 @@ ${SYSTEM_REMINDER_CLOSE} uuid: crypto.randomUUID(), ...(errorInfo.error_type && errorInfo.run_id && { - api_error: { - message_type: "error_message", - message: errorInfo.message, - error_type: errorInfo.error_type, - detail: errorInfo.detail, - run_id: errorInfo.run_id, - }, - }), + api_error: { + message_type: "error_message", + message: errorInfo.message, + error_type: errorInfo.error_type, + detail: errorInfo.detail, + run_id: errorInfo.run_id, + }, + }), }; console.log(JSON.stringify(errorEvent)); shouldOutputChunk = false; @@ -1395,7 +1409,7 @@ ${SYSTEM_REMINDER_CLOSE} const result = await drainStreamWithResume( stream, buffers, - () => {}, + () => { }, undefined, undefined, streamJsonHook, @@ -1410,7 +1424,7 @@ ${SYSTEM_REMINDER_CLOSE} const result = await drainStreamWithResume( stream, buffers, - () => {}, // No UI refresh needed in headless mode + () => { }, // No UI refresh needed in headless mode ); stopReason = result.stopReason; approvals = result.approvals || []; @@ -1448,22 +1462,22 @@ ${SYSTEM_REMINDER_CLOSE} // Phase 1: Collect decisions for all approvals type Decision = | { - type: "approve"; - approval: { - toolCallId: string; - toolName: string; - toolArgs: string; - }; - } - | { - type: "deny"; - approval: { - toolCallId: string; - toolName: string; - toolArgs: string; - }; - reason: string; + type: "approve"; + approval: { + toolCallId: string; + toolName: string; + toolArgs: string; }; + } + | { + type: "deny"; + approval: { + toolCallId: string; + toolName: string; + toolArgs: string; + }; + reason: string; + }; const { autoAllowed, autoDenied, needsUserInput } = await classifyApprovals(approvals, { @@ -1648,12 +1662,12 @@ ${SYSTEM_REMINDER_CLOSE} const run = await client.runs.retrieve(lastRunId); const metaError = run.metadata?.error as | { - error_type?: string; - message?: string; - detail?: string; - // Handle nested error structure (error.error) that can occur in some edge cases - error?: { error_type?: string; detail?: string }; - } + error_type?: string; + message?: string; + detail?: string; + // Handle nested error structure (error.error) that can occur in some edge cases + error?: { error_type?: string; detail?: string }; + } | undefined; // Check for llm_error at top level or nested (handles error.error nesting) @@ -1826,7 +1840,7 @@ ${SYSTEM_REMINDER_CLOSE} line.kind === "tool_call" && "resultText" in line && typeof (line as Extract).resultText === - "string" && + "string" && ((line as Extract).resultText ?? "").trim() .length > 0, ) as Extract | undefined; @@ -1971,24 +1985,24 @@ async function runBidirectionalMode( type Decision = | { - type: "approve"; - approval: { - toolCallId: string; - toolName: string; - toolArgs: string; - }; - reason: string; - matchedRule: string; - } - | { - type: "deny"; - approval: { - toolCallId: string; - toolName: string; - toolArgs: string; - }; - reason: string; + type: "approve"; + approval: { + toolCallId: string; + toolName: string; + toolArgs: string; }; + reason: string; + matchedRule: string; + } + | { + type: "deny"; + approval: { + toolCallId: string; + toolName: string; + toolArgs: string; + }; + reason: string; + }; const { autoAllowed, autoDenied } = await classifyApprovals( pendingApprovals, @@ -2064,7 +2078,7 @@ async function runBidirectionalMode( await drainStreamWithResume( approvalStream, createBuffers(agent.id), - () => {}, + () => { }, ); } }; @@ -2466,14 +2480,14 @@ async function runBidirectionalMode( uuid: crypto.randomUUID(), ...(errorInfo.error_type && errorInfo.run_id && { - api_error: { - message_type: "error_message", - message: errorInfo.message, - error_type: errorInfo.error_type, - detail: errorInfo.detail, - run_id: errorInfo.run_id, - }, - }), + api_error: { + message_type: "error_message", + message: errorInfo.message, + error_type: errorInfo.error_type, + detail: errorInfo.detail, + run_id: errorInfo.run_id, + }, + }), }; console.log(JSON.stringify(errorEvent)); return { shouldAccumulate: true }; @@ -2513,7 +2527,7 @@ async function runBidirectionalMode( const result = await drainStreamWithResume( stream, buffers, - () => {}, + () => { }, currentAbortController?.signal, undefined, streamJsonHook, @@ -2547,23 +2561,23 @@ async function runBidirectionalMode( // Check permissions and collect decisions type Decision = | { - type: "approve"; - approval: { - toolCallId: string; - toolName: string; - toolArgs: string; - }; - matchedRule: string; - } - | { - type: "deny"; - approval: { - toolCallId: string; - toolName: string; - toolArgs: string; - }; - reason: string; + type: "approve"; + approval: { + toolCallId: string; + toolName: string; + toolArgs: string; }; + matchedRule: string; + } + | { + type: "deny"; + approval: { + toolCallId: string; + toolName: string; + toolArgs: string; + }; + reason: string; + }; const { autoAllowed, autoDenied, needsUserInput } = await classifyApprovals(approvals, { @@ -2629,9 +2643,9 @@ async function runBidirectionalMode( // update the approval's toolArgs to use it const finalApproval = permResponse.updatedInput ? { - ...ac.approval, - toolArgs: JSON.stringify(permResponse.updatedInput), - } + ...ac.approval, + toolArgs: JSON.stringify(permResponse.updatedInput), + } : ac.approval; decisions.push({ @@ -2708,7 +2722,7 @@ async function runBidirectionalMode( line.kind === "tool_call" && "resultText" in line && typeof (line as Extract).resultText === - "string" && + "string" && ( (line as Extract).resultText ?? "" ).trim().length > 0,