From e074147ddde15432b7283eed35fe9e3035482f55 Mon Sep 17 00:00:00 2001 From: Dallin Romney Date: Tue, 18 Aug 2026 01:00:20 -0700 Subject: [PATCH] fix(commands): preserve approval reviewer custody --- src/auto-reply/reply/commands-diagnostics.test.ts | 6 +++++- .../reply/commands-export-trajectory.test-support.ts | 2 ++ src/auto-reply/reply/commands-private-route.ts | 8 ++++++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/auto-reply/reply/commands-diagnostics.test.ts b/src/auto-reply/reply/commands-diagnostics.test.ts index 021d81c799b6..762393da45ae 100644 --- a/src/auto-reply/reply/commands-diagnostics.test.ts +++ b/src/auto-reply/reply/commands-diagnostics.test.ts @@ -48,6 +48,7 @@ type ExecCall = { type ExecDefaults = { accountId?: string; + approvalReviewerDeviceId?: string; approvalFollowup?: () => Promise; approvalFollowupMode?: string; approvalFollowupText?: string; @@ -302,7 +303,9 @@ afterEach(() => { describe("diagnostics command", () => { it("requests Gateway diagnostics approval without a duplicate pending chat reply", async () => { const { execCalls, handleDiagnosticsCommand } = createDiagnosticsHandlerForTest(); - const result = await handleDiagnosticsCommand(buildDiagnosticsParams("/diagnostics"), true); + const params = buildDiagnosticsParams("/diagnostics"); + params.ctx.ApprovalReviewerDeviceId = "device-diagnostics-reviewer"; + const result = await handleDiagnosticsCommand(params, true); expect(result?.shouldContinue).toBe(false); expect(result?.reply).toBeUndefined(); @@ -312,6 +315,7 @@ describe("diagnostics command", () => { expect(execCall.defaults.security).toBe("allowlist"); expect(execCall.defaults.ask).toBe("always"); expect(execCall.defaults.trigger).toBe("diagnostics"); + expect(execCall.defaults.approvalReviewerDeviceId).toBe("device-diagnostics-reviewer"); expect(execCall.defaults.approvalFollowupMode).toBe("direct"); expect(execCall.defaults.approvalWarningText).toContain( "Diagnostics can include sensitive local logs and host-level runtime metadata.", diff --git a/src/auto-reply/reply/commands-export-trajectory.test-support.ts b/src/auto-reply/reply/commands-export-trajectory.test-support.ts index 25dbf3ce88eb..69b9413c5afb 100644 --- a/src/auto-reply/reply/commands-export-trajectory.test-support.ts +++ b/src/auto-reply/reply/commands-export-trajectory.test-support.ts @@ -131,6 +131,7 @@ describe("buildExportTrajectoryCommandReply", () => { it("requests per-run exec approval for trajectory exports", async () => { const { execCalls, deps } = createExecDeps(); const params = makeParams(); + params.ctx.ApprovalReviewerDeviceId = "device-trajectory-reviewer"; const reply = await buildExportTrajectoryCommandReply(params, deps); @@ -151,6 +152,7 @@ describe("buildExportTrajectoryCommandReply", () => { expect(execCall.defaults.sessionStore).toBe("/tmp/openclaw-sessions.json"); expect(execCall.defaults.currentChannelId).toBe("bot"); expect(execCall.defaults.accountId).toBe("account-1"); + expect(execCall.defaults.approvalReviewerDeviceId).toBe("device-trajectory-reviewer"); expect(execCall.params.security).toBe("allowlist"); expect(execCall.params.ask).toBe("always"); expect(execCall.params.background).toBe(true); diff --git a/src/auto-reply/reply/commands-private-route.ts b/src/auto-reply/reply/commands-private-route.ts index 6365e656042f..f6def9871d11 100644 --- a/src/auto-reply/reply/commands-private-route.ts +++ b/src/auto-reply/reply/commands-private-route.ts @@ -133,8 +133,8 @@ export function readCommandDeliveryTarget(params: HandleCommandsParams): string /** * Resolves where an exec approval prompt for a command should be delivered: * the private owner-DM target when one was resolved, else the originating - * command surface. Keeps the fallback ternaries in one place so private and - * origin routing cannot drift between command handlers. + * command surface. The originating reviewer device stays separate from a + * private delivery target so command handlers cannot drop approval custody. */ export function resolveCommandExecApprovalRoute(params: { commandParams: HandleCommandsParams; @@ -144,6 +144,7 @@ export function resolveCommandExecApprovalRoute(params: { currentChannelId: string | undefined; currentThreadTs: string | undefined; accountId: string | undefined; + approvalReviewerDeviceId: string | undefined; } { const target = params.privateApprovalTarget; return { @@ -157,6 +158,9 @@ export function resolveCommandExecApprovalRoute(params: { accountId: target ? (target.accountId ?? undefined) : (params.commandParams.ctx.AccountId ?? undefined), + approvalReviewerDeviceId: normalizeOptionalString( + params.commandParams.ctx.ApprovalReviewerDeviceId, + ), }; }