






















@@ -0,0 +1,171 @@
1+import fs from "node:fs/promises";
2+import os from "node:os";
3+import path from "node:path";
4+import { afterEach, describe, expect, it } from "vitest";
5+import { clearConfigCache, clearRuntimeConfigSnapshot } from "../config/config.js";
6+import { clearSessionStoreCacheForTest } from "../config/sessions/store.js";
7+import { ADMIN_SCOPE } from "../gateway/method-scopes.js";
8+import { startGatewayServer } from "../gateway/server.js";
9+import {
10+connectGatewayClient,
11+disconnectGatewayClient,
12+getFreeGatewayPort,
13+} from "../gateway/test-helpers.e2e.js";
14+import { captureEnv } from "../test-utils/env.js";
15+import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
16+import type { ExecApprovalFollowupOutcome } from "./bash-tools.exec-types.js";
17+import { createExecTool } from "./bash-tools.exec.js";
18+19+const TEST_ENV_KEYS = [
20+"HOME",
21+"OPENCLAW_STATE_DIR",
22+"OPENCLAW_CONFIG_PATH",
23+"OPENCLAW_GATEWAY_TOKEN",
24+"OPENCLAW_GATEWAY_PORT",
25+"OPENCLAW_SKIP_CHANNELS",
26+"OPENCLAW_SKIP_GMAIL_WATCHER",
27+"OPENCLAW_SKIP_CRON",
28+"OPENCLAW_SKIP_CANVAS_HOST",
29+"OPENCLAW_SKIP_BROWSER_CONTROL_SERVER",
30+"OPENCLAW_SKIP_PROVIDERS",
31+];
32+33+type Cleanup = () => Promise<void> | void;
34+35+async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
36+let timeout: NodeJS.Timeout | undefined;
37+const timeoutPromise = new Promise<never>((_, reject) => {
38+timeout = setTimeout(() => reject(new Error(`timed out waiting for ${label}`)), timeoutMs);
39+timeout.unref();
40+});
41+try {
42+return await Promise.race([promise, timeoutPromise]);
43+} finally {
44+if (timeout) {
45+clearTimeout(timeout);
46+}
47+}
48+}
49+50+describe("gateway-hosted exec approvals", () => {
51+const cleanup: Cleanup[] = [];
52+53+afterEach(async () => {
54+for (const step of cleanup.splice(0).toReversed()) {
55+await step();
56+}
57+clearRuntimeConfigSnapshot();
58+clearConfigCache();
59+clearSessionStoreCacheForTest();
60+});
61+62+it("lets PI-style gateway tool calls request and wait for approval over separate connections", async () => {
63+const envSnapshot = captureEnv(TEST_ENV_KEYS);
64+cleanup.push(() => envSnapshot.restore());
65+66+const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-exec-approval-e2e-"));
67+cleanup.push(() => fs.rm(tempHome, { recursive: true, force: true, maxRetries: 5 }));
68+69+const stateDir = path.join(tempHome, ".openclaw");
70+const workspaceDir = path.join(tempHome, "workspace");
71+await fs.mkdir(workspaceDir, { recursive: true });
72+73+const port = await getFreeGatewayPort();
74+const token = "exec-approval-e2e-token";
75+const configPath = path.join(stateDir, "openclaw.json");
76+await fs.mkdir(stateDir, { recursive: true });
77+await fs.writeFile(
78+configPath,
79+`${JSON.stringify(
80+ {
81+ gateway: {
82+ port,
83+ auth: { mode: "token", token },
84+ },
85+ tools: {
86+ exec: {
87+ host: "gateway",
88+ security: "allowlist",
89+ ask: "always",
90+ },
91+ },
92+ },
93+ null,
94+ 2,
95+ )}\n`,
96+"utf8",
97+);
98+99+process.env.HOME = tempHome;
100+process.env.OPENCLAW_STATE_DIR = stateDir;
101+process.env.OPENCLAW_CONFIG_PATH = configPath;
102+process.env.OPENCLAW_GATEWAY_TOKEN = token;
103+process.env.OPENCLAW_GATEWAY_PORT = String(port);
104+process.env.OPENCLAW_SKIP_CHANNELS = "1";
105+process.env.OPENCLAW_SKIP_GMAIL_WATCHER = "1";
106+process.env.OPENCLAW_SKIP_CRON = "1";
107+process.env.OPENCLAW_SKIP_CANVAS_HOST = "1";
108+process.env.OPENCLAW_SKIP_BROWSER_CONTROL_SERVER = "1";
109+process.env.OPENCLAW_SKIP_PROVIDERS = "1";
110+clearRuntimeConfigSnapshot();
111+clearConfigCache();
112+clearSessionStoreCacheForTest();
113+114+const server = await startGatewayServer(port, {
115+bind: "loopback",
116+auth: { mode: "token", token },
117+controlUiEnabled: false,
118+});
119+cleanup.push(() => server.close());
120+121+const operator = await connectGatewayClient({
122+url: `ws://127.0.0.1:${port}`,
123+ token,
124+clientName: GATEWAY_CLIENT_NAMES.TEST,
125+clientDisplayName: "approval operator",
126+mode: GATEWAY_CLIENT_MODES.TEST,
127+scopes: [ADMIN_SCOPE],
128+});
129+cleanup.push(() => disconnectGatewayClient(operator));
130+131+let resolveOutcome: (outcome: ExecApprovalFollowupOutcome) => void = () => {};
132+const outcomePromise = new Promise<ExecApprovalFollowupOutcome>((resolve) => {
133+resolveOutcome = resolve;
134+});
135+136+const tool = createExecTool({
137+host: "gateway",
138+security: "allowlist",
139+ask: "always",
140+cwd: workspaceDir,
141+approvalRunningNoticeMs: 0,
142+approvalFollowupMode: "direct",
143+approvalFollowup: ({ outcome }) => {
144+resolveOutcome(outcome);
145+return undefined;
146+},
147+});
148+149+const pending = await tool.execute("exec-approval-e2e", {
150+command: "printf 'smoke\\n'",
151+workdir: workspaceDir,
152+timeout: 5,
153+});
154+155+expect(pending.details.status).toBe("approval-pending");
156+if (pending.details.status !== "approval-pending") {
157+throw new Error("expected approval-pending exec result");
158+}
159+160+await operator.request(
161+"exec.approval.resolve",
162+{ id: pending.details.approvalId, decision: "allow-once" },
163+{ timeoutMs: 10_000 },
164+);
165+166+const outcome = await withTimeout(outcomePromise, 15_000, "approved exec outcome");
167+expect(outcome.status).toBe("completed");
168+expect(outcome.exitCode).toBe(0);
169+expect(outcome.aggregated).toBe("smoke");
170+});
171+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。