Skip to content

Commit 3c43b59

Browse files
committed
fix(gateway): async trajectory export approvals
1 parent 714d894 commit 3c43b59

5 files changed

Lines changed: 75 additions & 8 deletions

File tree

src/agents/bash-tools.exec-host-gateway.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,6 +1069,51 @@ EOF`,
10691069
expect(approvalInput?.outcome?.exitCode).toBe(0);
10701070
});
10711071

1072+
it("uses async agent followups for explicit webchat approval mode", async () => {
1073+
buildExecApprovalFollowupTargetMock.mockImplementation((value) => value);
1074+
resolveExecHostApprovalContextMock.mockReturnValue({
1075+
approvals: { allowlist: [], file: { version: 1, agents: {} } },
1076+
hostSecurity: "allowlist",
1077+
hostAsk: "always",
1078+
askFallback: "deny",
1079+
});
1080+
resolveApprovalDecisionOrUndefinedMock.mockResolvedValue("allow-once");
1081+
createExecApprovalDecisionStateMock.mockReturnValue({
1082+
baseDecision: { timedOut: false },
1083+
approvedByAsk: true,
1084+
deniedReason: null,
1085+
});
1086+
runExecProcessMock.mockResolvedValue({
1087+
session: { id: "sess-1" },
1088+
promise: Promise.resolve({
1089+
status: "completed",
1090+
exitCode: 0,
1091+
timedOut: false,
1092+
aggregated: "done",
1093+
}),
1094+
});
1095+
1096+
const result = await runGatewayAllowlist({
1097+
command: "openclaw sessions export-trajectory --json",
1098+
approvalFollowupMode: "agent",
1099+
sessionId: "approval-session",
1100+
sessionStore: "/tmp/openclaw-sessions.json",
1101+
turnSourceChannel: "webchat",
1102+
});
1103+
1104+
expect(result.pendingResult?.details.status).toBe("approval-pending");
1105+
await vi.waitFor(() => {
1106+
expect(sendExecApprovalFollowupResultMock).toHaveBeenCalledTimes(1);
1107+
});
1108+
expect(requireBuildFollowupTargetInput(0)).toMatchObject({
1109+
direct: false,
1110+
expectedSessionId: "approval-session",
1111+
sessionStore: "/tmp/openclaw-sessions.json",
1112+
});
1113+
expect(requireSentFollowupTarget(0)?.direct).toBe(false);
1114+
expect(requireSentFollowupText(0)).toContain("done");
1115+
});
1116+
10721117
it("waits inline for webchat approval so the exec tool can return real output to the model", async () => {
10731118
resolveApprovalDecisionOrUndefinedMock.mockResolvedValue("allow-once");
10741119
createExecApprovalDecisionStateMock.mockReturnValue({

src/agents/bash-tools.exec-host-gateway.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ function shouldAwaitGatewayApprovalInline(params: {
295295
turnSourceChannel?: string;
296296
approvalFollowupMode?: "agent" | "direct";
297297
}): boolean {
298-
if (params.approvalFollowupMode === "direct") {
298+
if (params.approvalFollowupMode !== undefined) {
299299
return false;
300300
}
301301
return normalizeMessageChannel(params.turnSourceChannel) === INTERNAL_MESSAGE_CHANNEL;

src/auto-reply/reply/commands-export-trajectory.test.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,11 @@ function makeTempDir(): string {
8989

9090
function makeParams(workspaceDir = makeTempDir()): HandleCommandsParams {
9191
return {
92-
cfg: {},
92+
cfg: {
93+
session: {
94+
store: "/tmp/openclaw-sessions.json",
95+
},
96+
},
9397
ctx: {
9498
SessionKey: "agent:main:slash-session",
9599
AccountId: "account-1",
@@ -370,6 +374,9 @@ describe("buildExportTrajectoryCommandReply", () => {
370374
expect(execCall.defaults.security).toBe("allowlist");
371375
expect(execCall.defaults.ask).toBe("always");
372376
expect(execCall.defaults.trigger).toBe("export-trajectory");
377+
expect(execCall.defaults.approvalFollowupMode).toBe("agent");
378+
expect(execCall.defaults.sessionId).toBe("session-1");
379+
expect(execCall.defaults.sessionStore).toBe("/tmp/openclaw-sessions.json");
373380
expect(execCall.defaults.currentChannelId).toBe("bot");
374381
expect(execCall.defaults.accountId).toBe("account-1");
375382
expect(execCall.params.security).toBe("allowlist");

src/auto-reply/reply/commands-export-trajectory.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,10 +244,13 @@ async function requestTrajectoryExportApproval(
244244
trigger: "export-trajectory",
245245
scopeKey: EXPORT_TRAJECTORY_EXEC_SCOPE_KEY,
246246
allowBackground: true,
247+
approvalFollowupMode: "agent",
247248
timeoutSec,
248249
cwd: params.workspaceDir,
249250
agentId,
250251
sessionKey: params.sessionKey,
252+
sessionId: params.sessionEntry?.sessionId,
253+
sessionStore: params.cfg.session?.store,
251254
mainKey: params.cfg.session?.mainKey,
252255
sessionScope: params.cfg.session?.scope,
253256
messageProvider: options.privateApprovalTarget?.channel ?? params.command.channel,

src/gateway/gateway-trajectory-export.live.test.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -229,10 +229,24 @@ function getTrajectoryExportApprovalCommands(entry: TrajectoryExportApprovalEntr
229229
].filter((value): value is string => typeof value === "string" && value.trim().length > 0);
230230
}
231231

232+
function hasTrajectoryExportArgv(argv: string[] | undefined): boolean {
233+
if (!argv) {
234+
return false;
235+
}
236+
return argv.some((arg, index) => arg === "sessions" && argv[index + 1] === "export-trajectory");
237+
}
238+
232239
function isTrajectoryExportApproval(entry: TrajectoryExportApprovalEntry): boolean {
233-
return getTrajectoryExportApprovalCommands(entry).some((command) =>
234-
command.includes("sessions export-trajectory"),
235-
);
240+
if (
241+
hasTrajectoryExportArgv(entry.request?.commandArgv) ||
242+
hasTrajectoryExportArgv(entry.commandArgv)
243+
) {
244+
return true;
245+
}
246+
return getTrajectoryExportApprovalCommands(entry).some((command) => {
247+
const normalized = command.replaceAll(/['"]/gu, "");
248+
return normalized.includes("sessions export-trajectory");
249+
});
236250
}
237251

238252
function summarizeTrajectoryExportApproval(
@@ -401,9 +415,7 @@ async function approveTrajectoryExport(
401415
`expected trajectory export approval id; approvals=${JSON.stringify(lastApprovalSummaries)}`,
402416
);
403417
}
404-
expect(getTrajectoryExportApprovalCommands(approval).join("\n")).toContain(
405-
"sessions export-trajectory",
406-
);
418+
expect(isTrajectoryExportApproval(approval)).toBe(true);
407419
await client.request(
408420
"exec.approval.resolve",
409421
{ id: approval.id, decision: "allow-once" },

0 commit comments

Comments
 (0)