From de898c423b95d818b15050143c737cdf1c301398 Mon Sep 17 00:00:00 2001 From: Lucas Czekaj Date: Wed, 21 Jan 2026 18:14:51 -0800 Subject: [PATCH] fix(exec): pass undefined instead of null for optional approval params TypeBox Type.Optional(Type.String()) accepts string|undefined but NOT null. Discord exec was failing with 'resolvedPath must be string' because callers passed null explicitly. Web UI worked because it skipped the approval request. Fixes exec approval validation error in Discord-triggered sessions. --- src/agents/bash-tools.exec.ts | 8 ++-- src/cli/nodes-cli/register.invoke.ts | 4 +- .../server-methods/exec-approval.test.ts | 44 +++++++++++++++++++ 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/agents/bash-tools.exec.ts b/src/agents/bash-tools.exec.ts index 32655f6a9..3d58bc705 100644 --- a/src/agents/bash-tools.exec.ts +++ b/src/agents/bash-tools.exec.ts @@ -896,8 +896,8 @@ export function createExecTool( security: hostSecurity, ask: hostAsk, agentId: defaults?.agentId, - resolvedPath: null, - sessionKey: defaults?.sessionKey ?? null, + resolvedPath: undefined, + sessionKey: defaults?.sessionKey, timeoutMs: DEFAULT_APPROVAL_TIMEOUT_MS, }, )) as { decision?: string } | null; @@ -1060,7 +1060,7 @@ export function createExecTool( const approvalSlug = createApprovalSlug(approvalId); const expiresAtMs = Date.now() + DEFAULT_APPROVAL_TIMEOUT_MS; const contextKey = `exec:${approvalId}`; - const resolvedPath = analysis.segments[0]?.resolution?.resolvedPath ?? null; + const resolvedPath = analysis.segments[0]?.resolution?.resolvedPath; const noticeSeconds = Math.max(1, Math.round(approvalRunningNoticeMs / 1000)); const commandText = params.command; const effectiveTimeout = @@ -1082,7 +1082,7 @@ export function createExecTool( ask: hostAsk, agentId: defaults?.agentId, resolvedPath, - sessionKey: defaults?.sessionKey ?? null, + sessionKey: defaults?.sessionKey, timeoutMs: DEFAULT_APPROVAL_TIMEOUT_MS, }, )) as { decision?: string } | null; diff --git a/src/cli/nodes-cli/register.invoke.ts b/src/cli/nodes-cli/register.invoke.ts index 4cad89315..9a9ff2834 100644 --- a/src/cli/nodes-cli/register.invoke.ts +++ b/src/cli/nodes-cli/register.invoke.ts @@ -267,8 +267,8 @@ export function registerNodesInvokeCommands(nodes: Command) { security: hostSecurity, ask: hostAsk, agentId, - resolvedPath: null, - sessionKey: null, + resolvedPath: undefined, + sessionKey: undefined, timeoutMs: 120_000, })) as { decision?: string } | null; const decision = diff --git a/src/gateway/server-methods/exec-approval.test.ts b/src/gateway/server-methods/exec-approval.test.ts index 4682e6e1a..8142b281f 100644 --- a/src/gateway/server-methods/exec-approval.test.ts +++ b/src/gateway/server-methods/exec-approval.test.ts @@ -1,10 +1,54 @@ import { describe, expect, it, vi } from "vitest"; import { ExecApprovalManager } from "../exec-approval-manager.js"; import { createExecApprovalHandlers } from "./exec-approval.js"; +import { validateExecApprovalRequestParams } from "../protocol/index.js"; const noop = () => {}; describe("exec approval handlers", () => { + describe("ExecApprovalRequestParams validation", () => { + it("accepts request with resolvedPath omitted", () => { + const params = { + command: "echo hi", + cwd: "/tmp", + host: "node", + }; + expect(validateExecApprovalRequestParams(params)).toBe(true); + }); + + it("accepts request with resolvedPath as string", () => { + const params = { + command: "echo hi", + cwd: "/tmp", + host: "node", + resolvedPath: "/usr/bin/echo", + }; + expect(validateExecApprovalRequestParams(params)).toBe(true); + }); + + it("accepts request with resolvedPath as undefined", () => { + const params = { + command: "echo hi", + cwd: "/tmp", + host: "node", + resolvedPath: undefined, + }; + expect(validateExecApprovalRequestParams(params)).toBe(true); + }); + + // This documents the TypeBox/AJV behavior that caused the Discord exec bug: + // Type.Optional(Type.String()) does NOT accept null, only string or undefined. + it("rejects request with resolvedPath as null", () => { + const params = { + command: "echo hi", + cwd: "/tmp", + host: "node", + resolvedPath: null, + }; + expect(validateExecApprovalRequestParams(params)).toBe(false); + }); + }); + it("broadcasts request + resolve", async () => { const manager = new ExecApprovalManager(); const handlers = createExecApprovalHandlers(manager);