























@@ -2,9 +2,9 @@ import { spawnSync } from "node:child_process";
22import fs from "node:fs/promises";
33import path from "node:path";
44import { describe, expect, it } from "vitest";
5-import { buildSandboxFsMounts, resolveSandboxFsPathWithMounts } from "./fs-paths.js";
65import { SANDBOX_PINNED_MUTATION_PYTHON } from "./fs-bridge-mutation-helper.js";
76import { createSandbox, withTempDir } from "./fs-bridge.test-helpers.js";
7+import { buildSandboxFsMounts, resolveSandboxFsPathWithMounts } from "./fs-paths.js";
88import { createRemoteShellSandboxFsBridge } from "./remote-fs-bridge.js";
991010describe("workspace skills bridge mount policy", () => {
@@ -38,12 +38,16 @@ describe("workspace skills bridge mount policy", () => {
3838await withTempDir("openclaw-skills-remote-absent-", async (stateDir) => {
3939const workspaceDir = path.join(stateDir, "workspace");
4040await fs.mkdir(workspaceDir, { recursive: true });
41+const canonicalWorkspaceDir = await fs.realpath(workspaceDir);
41424243const bridge = createRemoteShellSandboxFsBridge({
43-sandbox: createSandbox({ workspaceDir, agentWorkspaceDir: workspaceDir }),
44+sandbox: createSandbox({
45+workspaceDir: canonicalWorkspaceDir,
46+agentWorkspaceDir: canonicalWorkspaceDir,
47+}),
4448runtime: {
45-remoteWorkspaceDir: workspaceDir,
46-remoteAgentWorkspaceDir: workspaceDir,
49+remoteWorkspaceDir: canonicalWorkspaceDir,
50+remoteAgentWorkspaceDir: canonicalWorkspaceDir,
4751runRemoteShellScript: async (command) => {
4852const result = command.script.includes('python3 /dev/fd/3 "$@" 3<<')
4953 ? spawnSync(
@@ -55,11 +59,15 @@ describe("workspace skills bridge mount policy", () => {
5559stdio: ["pipe", "pipe", "pipe"],
5660},
5761)
58- : spawnSync("sh", ["-c", command.script, "openclaw-test", ...(command.args ?? [])], {
59-input: command.stdin,
60-encoding: "buffer",
61-stdio: ["pipe", "pipe", "pipe"],
62-});
62+ : spawnSync(
63+"sh",
64+["-c", command.script, "openclaw-test", ...(command.args ?? [])],
65+{
66+input: command.stdin,
67+encoding: "buffer",
68+stdio: ["pipe", "pipe", "pipe"],
69+},
70+);
6371const stdout = Buffer.isBuffer(result.stdout)
6472 ? result.stdout
6573 : Buffer.from(result.stdout ?? []);
@@ -82,9 +90,79 @@ describe("workspace skills bridge mount policy", () => {
8290});
83918492await bridge.writeFile({ filePath: "skills/new.txt", data: "created" });
85-await expect(fs.readFile(path.join(workspaceDir, "skills", "new.txt"), "utf8")).resolves.toBe(
86-"created",
87-);
93+await expect(
94+fs.readFile(path.join(canonicalWorkspaceDir, "skills", "new.txt"), "utf8"),
95+).resolves.toBe("created");
96+});
97+},
98+);
99+100+it.runIf(process.platform !== "win32")(
101+"rejects remote bridge mkdirp under skill roots from container cwd",
102+async () => {
103+await withTempDir("openclaw-skills-remote-cwd-", async (stateDir) => {
104+const workspaceDir = path.join(stateDir, "workspace");
105+const remoteWorkspaceDir = path.join(stateDir, "remote-workspace");
106+await fs.mkdir(path.join(workspaceDir, "skills", "demo"), { recursive: true });
107+await fs.mkdir(path.join(remoteWorkspaceDir, "skills", "demo"), { recursive: true });
108+const canonicalWorkspaceDir = await fs.realpath(workspaceDir);
109+const canonicalRemoteWorkspaceDir = await fs.realpath(remoteWorkspaceDir);
110+111+const bridge = createRemoteShellSandboxFsBridge({
112+sandbox: createSandbox({
113+workspaceDir: canonicalWorkspaceDir,
114+agentWorkspaceDir: canonicalWorkspaceDir,
115+}),
116+runtime: {
117+remoteWorkspaceDir: canonicalRemoteWorkspaceDir,
118+remoteAgentWorkspaceDir: canonicalRemoteWorkspaceDir,
119+runRemoteShellScript: async (command) => {
120+const result = command.script.includes('python3 /dev/fd/3 "$@" 3<<')
121+ ? spawnSync(
122+"python3",
123+["-c", SANDBOX_PINNED_MUTATION_PYTHON, ...(command.args ?? [])],
124+{
125+input: command.stdin,
126+encoding: "buffer",
127+stdio: ["pipe", "pipe", "pipe"],
128+},
129+)
130+ : spawnSync(
131+"sh",
132+["-c", command.script, "openclaw-test", ...(command.args ?? [])],
133+{
134+input: command.stdin,
135+encoding: "buffer",
136+stdio: ["pipe", "pipe", "pipe"],
137+},
138+);
139+const stdout = Buffer.isBuffer(result.stdout)
140+ ? result.stdout
141+ : Buffer.from(result.stdout ?? []);
142+const stderr = Buffer.isBuffer(result.stderr)
143+ ? result.stderr
144+ : Buffer.from(result.stderr ?? []);
145+const code = result.status ?? (result.signal ? 128 : 1);
146+if (result.error) {
147+throw result.error;
148+}
149+if (code !== 0 && !command.allowFailure) {
150+throw Object.assign(
151+new Error(stderr.toString("utf8").trim() || `shell exited with code ${code}`),
152+{ code, stdout, stderr },
153+);
154+}
155+return { stdout, stderr, code };
156+},
157+},
158+});
159+160+await expect(
161+bridge.mkdirp({ filePath: "skills/demo/generated", cwd: canonicalRemoteWorkspaceDir }),
162+).rejects.toThrow(/read-only/);
163+await expect(
164+fs.stat(path.join(canonicalRemoteWorkspaceDir, "skills", "demo", "generated")),
165+).rejects.toMatchObject({ code: "ENOENT" });
88166});
89167},
90168);
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。