





















@@ -18,21 +18,24 @@ const OPENCLAW_OPENSHELL_E2E = process.env.OPENCLAW_E2E_OPENSHELL === "1";
1818const OPENCLAW_OPENSHELL_E2E_TIMEOUT_MS = 12 * 60_000;
1919const OPENCLAW_OPENSHELL_COMMAND =
2020process.env.OPENCLAW_E2E_OPENSHELL_COMMAND?.trim() || "openshell";
21+const OPENCLAW_OPENSHELL_CONFIG_HOME =
22+process.env.OPENCLAW_E2E_OPENSHELL_CONFIG_HOME?.trim() || null;
23+const OPENCLAW_OPENSHELL_HOST_IP = process.env.OPENCLAW_E2E_OPENSHELL_HOST_IP?.trim() || null;
24+const ANSI_ESCAPE_RE = new RegExp(`${String.fromCharCode(0x1b)}\\[[0-?]*[ -/]*[@-~]`, "gu");
21252226const CUSTOM_IMAGE_DOCKERFILE = `FROM python:3.13-slim
23272428RUN apt-get update && apt-get install -y --no-install-recommends \\
25- coreutils \\
26- curl \\
27- findutils \\
28- iproute2 \\
29+ coreutils curl findutils iproute2 nftables \\
2930 && rm -rf /var/lib/apt/lists/*
303131-RUN groupadd -g 1000 sandbox && \\
32- useradd -m -u 1000 -g sandbox sandbox
32+RUN groupadd -g 1000660000 sandbox && \\
33+ useradd -m -u 1000660000 -g sandbox sandbox && \\
34+ install -d -o sandbox -g sandbox /sandbox
33353436RUN echo "openclaw-openshell-e2e" > /opt/openshell-e2e-marker.txt
353738+USER sandbox
3639WORKDIR /sandbox
3740CMD ["sleep", "infinity"]
3841`;
@@ -125,17 +128,55 @@ async function commandAvailable(command: string): Promise<boolean> {
125128}
126129}
127130128-async function openshellGatewayAvailable(command: string): Promise<boolean> {
131+async function activeOpenShellGateway(
132+command: string,
133+env: NodeJS.ProcessEnv = process.env,
134+): Promise<string | null> {
129135try {
130136const result = await runCommand({
131137 command,
132-args: ["gateway", "start", "--help"],
138+args: ["gateway", "list"],
139+ env,
133140allowFailure: true,
134141timeoutMs: 20_000,
135142});
136-return result.code === 0 && `${result.stdout}\n${result.stderr}`.includes("--name");
143+if (result.code !== 0) {
144+return null;
145+}
146+const output = `${result.stdout}\n${result.stderr}`.replace(ANSI_ESCAPE_RE, "");
147+for (const line of output.split(/\r?\n/u)) {
148+const match = line.match(/\*\s+(\S+)/u);
149+if (match) {
150+const info = await runCommand({
151+ command,
152+args: ["gateway", "info", "--gateway", match[1]],
153+ env,
154+allowFailure: true,
155+timeoutMs: 20_000,
156+});
157+const endpoint = `${info.stdout}\n${info.stderr}`
158+.replace(ANSI_ESCAPE_RE, "")
159+.match(/Gateway endpoint:\s+(\S+)/u)?.[1];
160+if (
161+info.code === 0 &&
162+endpoint &&
163+/^(?:https?:\/\/)?(?:127\.0\.0\.1|localhost)(?::\d+)?(?:\/|$)/u.test(endpoint)
164+) {
165+const status = await runCommand({
166+ command,
167+args: ["--gateway", match[1], "sandbox", "list"],
168+ env,
169+allowFailure: true,
170+timeoutMs: 20_000,
171+});
172+return status.code === 0 ? match[1] : null;
173+}
174+return null;
175+}
176+}
177+return null;
137178} catch {
138-return false;
179+return null;
139180}
140181}
141182@@ -153,6 +194,41 @@ async function dockerReady(): Promise<boolean> {
153194}
154195}
155196197+async function resolveOpenShellHostIp(): Promise<string> {
198+if (OPENCLAW_OPENSHELL_HOST_IP) {
199+return OPENCLAW_OPENSHELL_HOST_IP;
200+}
201+const networks = await runCommand({
202+command: "docker",
203+args: ["network", "ls", "--format", "{{.Name}}"],
204+timeoutMs: 20_000,
205+});
206+for (const network of networks.stdout.split(/\r?\n/u).map((value) => value.trim())) {
207+if (!network.startsWith("openshell")) {
208+continue;
209+}
210+const gateway = await runCommand({
211+command: "docker",
212+args: [
213+"network",
214+"inspect",
215+network,
216+"--format",
217+"{{range .IPAM.Config}}{{.Gateway}}{{end}}",
218+],
219+allowFailure: true,
220+timeoutMs: 20_000,
221+});
222+const hostIp = gateway.stdout.trim();
223+if (gateway.code === 0 && hostIp) {
224+return hostIp;
225+}
226+}
227+throw new Error(
228+"OpenShell E2E could not resolve the OpenShell Docker network gateway; set OPENCLAW_E2E_OPENSHELL_HOST_IP",
229+);
230+}
231+156232async function allocatePort(): Promise<number> {
157233return await new Promise((resolve, reject) => {
158234const server = net.createServer();
@@ -285,14 +361,21 @@ HTTPServer(("0.0.0.0", 8000), Handler).serve_forever()
285361throw new Error("docker-backed host policy server did not become ready");
286362}
287363288-function buildOpenShellPolicyYaml(params: { port: number; binaryPath: string }): string {
364+function buildOpenShellPolicyYaml(params: {
365+port: number;
366+binaryPath: string;
367+hostIp: string;
368+}): string {
289369const networkPolicies = ` host_echo:
290370 name: host-echo
291371 endpoints:
292372 - host: host.openshell.internal
293373 port: ${params.port}
374+ protocol: rest
375+ enforcement: enforce
376+ access: full
294377 allowed_ips:
295- - "0.0.0.0/0"
378+ - "${params.hostIp}/32"
296379 binaries:
297380 - path: ${params.binaryPath}`;
298381return `version: 1
@@ -351,13 +434,24 @@ describe("openshell sandbox backend e2e", () => {
351434{ timeout: OPENCLAW_OPENSHELL_E2E_TIMEOUT_MS },
352435async () => {
353436if (!(await dockerReady())) {
354-return;
437+throw new Error("OpenShell E2E requires a working Docker daemon");
355438}
356439if (!(await commandAvailable(OPENCLAW_OPENSHELL_COMMAND))) {
357-return;
440+throw new Error(`OpenShell CLI is unavailable: ${OPENCLAW_OPENSHELL_COMMAND}`);
358441}
359-if (!(await openshellGatewayAvailable(OPENCLAW_OPENSHELL_COMMAND))) {
360-return;
442+if (!OPENCLAW_OPENSHELL_CONFIG_HOME) {
443+throw new Error(
444+"OpenShell E2E requires OPENCLAW_E2E_OPENSHELL_CONFIG_HOME because tests isolate HOME and XDG_CONFIG_HOME",
445+);
446+}
447+const openshellConfigHome = OPENCLAW_OPENSHELL_CONFIG_HOME;
448+const hostIp = await resolveOpenShellHostIp();
449+const gatewayName = await activeOpenShellGateway(OPENCLAW_OPENSHELL_COMMAND, {
450+ ...process.env,
451+XDG_CONFIG_HOME: openshellConfigHome,
452+});
453+if (!gatewayName) {
454+throw new Error("OpenShell E2E requires an active local registered gateway");
361455}
362456363457const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-openshell-e2e-"));
@@ -371,10 +465,8 @@ describe("openshell sandbox backend e2e", () => {
371465const denyPolicyPath = path.join(rootDir, "deny-policy.yaml");
372466const allowPolicyPath = path.join(rootDir, "allow-policy.yaml");
373467const scopeSuffix = `${process.pid}-${Date.now()}`;
374-const gatewayName = `openclaw-e2e-${scopeSuffix}`;
375468const scopeKey = `session:openshell-e2e-deny:${scopeSuffix}`;
376469const allowSandboxName = `openclaw-policy-allow-${scopeSuffix}`;
377-const gatewayPort = await allocatePort();
378470let hostPolicyServer: HostPolicyServer | null | undefined;
379471const sandboxCfg = {
380472mode: "all" as const,
@@ -425,40 +517,37 @@ describe("openshell sandbox backend e2e", () => {
425517}
426518await fs.mkdir(workspaceDir, { recursive: true });
427519await fs.mkdir(dockerfileDir, { recursive: true });
520+const isolatedConfigHome = env.XDG_CONFIG_HOME;
521+if (!isolatedConfigHome) {
522+throw new Error("OpenShell E2E could not create an isolated XDG config home");
523+}
524+await fs.mkdir(isolatedConfigHome, { recursive: true });
525+await fs.cp(
526+path.join(openshellConfigHome, "openshell"),
527+path.join(isolatedConfigHome, "openshell"),
528+{ recursive: true },
529+);
428530await fs.writeFile(path.join(workspaceDir, "seed.txt"), "seed-from-local\n", "utf8");
429531await fs.writeFile(dockerfilePath, CUSTOM_IMAGE_DOCKERFILE, "utf8");
430532await fs.writeFile(
431533denyPolicyPath,
432534buildOpenShellPolicyYaml({
433535port: hostPolicyServer.port,
434536binaryPath: "/usr/bin/false",
537+ hostIp,
435538}),
436539"utf8",
437540);
438541await fs.writeFile(
439542allowPolicyPath,
440543buildOpenShellPolicyYaml({
441544port: hostPolicyServer.port,
442-binaryPath: "/**",
545+binaryPath: "/usr/bin/curl",
546+ hostIp,
443547}),
444548"utf8",
445549);
446550447-await runCommand({
448-command: OPENCLAW_OPENSHELL_COMMAND,
449-args: [
450-"gateway",
451-"start",
452-"--name",
453-gatewayName,
454-"--port",
455-String(gatewayPort),
456-"--recreate",
457-],
458- env,
459-timeoutMs: 8 * 60_000,
460-});
461-462551const execResult = await runBackendExec({
463552 backend,
464553command: "pwd && cat /opt/openshell-e2e-marker.txt && cat seed.txt",
@@ -568,13 +657,6 @@ describe("openshell sandbox backend e2e", () => {
568657allowFailure: true,
569658timeoutMs: 2 * 60_000,
570659});
571-await runCommand({
572-command: OPENCLAW_OPENSHELL_COMMAND,
573-args: ["gateway", "destroy", "--name", gatewayName],
574- env,
575-allowFailure: true,
576-timeoutMs: 3 * 60_000,
577-});
578660await hostPolicyServer?.close().catch(() => {});
579661await fs.rm(rootDir, { recursive: true, force: true });
580662if (previousHome === undefined) {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。