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);