























@@ -6,6 +6,7 @@ import "./test-helpers/fast-coding-tools.js";
66import "./test-helpers/fast-openclaw-tools.js";
77import type { OpenClawConfig } from "../config/config.js";
88import { createOpenClawCodingTools } from "./pi-tools.js";
9+import { createCanonicalFixtureSkill } from "./skills.test-helpers.js";
910import { createHostSandboxFsBridge } from "./test-helpers/host-sandbox-fs-bridge.js";
1011import { expectReadWriteEditTools, getTextContent } from "./test-helpers/pi-tools-fs-helpers.js";
1112import { createPiToolsSandboxContext } from "./test-helpers/pi-tools-sandbox-context.js";
@@ -222,6 +223,110 @@ describe("workspace path resolution", () => {
222223}
223224});
224225});
226+227+it("allows workspaceOnly reads for resolved skill roots without allowing other filesystem access", async () => {
228+await withTempDir("openclaw-skill-read-", async (rootDir) => {
229+const workspaceDir = path.join(rootDir, "workspace");
230+const skillDir = path.join(rootDir, "global-skills", "demo");
231+const siblingDir = path.join(rootDir, "global-skills", "other");
232+await fs.mkdir(workspaceDir, { recursive: true });
233+await fs.mkdir(skillDir, { recursive: true });
234+await fs.mkdir(siblingDir, { recursive: true });
235+const skillFile = path.join(skillDir, "SKILL.md");
236+const guideFile = path.join(skillDir, "guide.md");
237+const siblingFile = path.join(siblingDir, "SKILL.md");
238+const outsideFile = path.join(rootDir, "outside.txt");
239+await fs.writeFile(skillFile, "# Demo skill\noriginal skill\n", "utf8");
240+await fs.writeFile(guideFile, "skill guide", "utf8");
241+await fs.writeFile(siblingFile, "sibling skill", "utf8");
242+await fs.writeFile(outsideFile, "outside secret", "utf8");
243+244+const cfg: OpenClawConfig = { tools: { fs: { workspaceOnly: true } } };
245+const tools = createOpenClawCodingTools({
246+ workspaceDir,
247+config: cfg,
248+skillsSnapshot: {
249+prompt: "",
250+skills: [{ name: "demo" }],
251+resolvedSkills: [
252+createCanonicalFixtureSkill({
253+name: "demo",
254+description: "Demo skill",
255+filePath: skillFile,
256+baseDir: skillDir,
257+source: "test",
258+}),
259+],
260+},
261+});
262+const { readTool, writeTool, editTool } = expectReadWriteEditTools(tools);
263+264+expect(getTextContent(await readTool.execute("read-skill", { path: skillFile }))).toContain(
265+"original skill",
266+);
267+expect(
268+getTextContent(await readTool.execute("read-skill-guide", { path: guideFile })),
269+).toContain("skill guide");
270+await expect(readTool.execute("read-sibling", { path: siblingFile })).rejects.toThrow(
271+/Path escapes sandbox root/i,
272+);
273+await expect(readTool.execute("read-outside", { path: outsideFile })).rejects.toThrow(
274+/Path escapes sandbox root/i,
275+);
276+await expect(
277+writeTool.execute("write-skill", { path: skillFile, content: "overwritten" }),
278+).rejects.toThrow(/Path escapes sandbox root|outside-workspace/i);
279+await expect(
280+editTool.execute("edit-skill", {
281+path: skillFile,
282+edits: [{ oldText: "original", newText: "edited" }],
283+}),
284+).rejects.toThrow(/Path escapes sandbox root|outside-workspace/i);
285+expect(await fs.readFile(skillFile, "utf8")).toContain("original skill");
286+});
287+});
288+289+it("rejects symlink escapes inside resolved skill roots", async () => {
290+if (process.platform === "win32") {
291+return;
292+}
293+await withTempDir("openclaw-skill-read-symlink-", async (rootDir) => {
294+const workspaceDir = path.join(rootDir, "workspace");
295+const skillDir = path.join(rootDir, "global-skills", "demo");
296+await fs.mkdir(workspaceDir, { recursive: true });
297+await fs.mkdir(skillDir, { recursive: true });
298+const skillFile = path.join(skillDir, "SKILL.md");
299+const outsideFile = path.join(rootDir, "outside.txt");
300+const linkPath = path.join(skillDir, "outside-link.txt");
301+await fs.writeFile(skillFile, "# Demo skill\n", "utf8");
302+await fs.writeFile(outsideFile, "outside secret", "utf8");
303+await fs.symlink(outsideFile, linkPath);
304+305+const cfg: OpenClawConfig = { tools: { fs: { workspaceOnly: true } } };
306+const tools = createOpenClawCodingTools({
307+ workspaceDir,
308+config: cfg,
309+skillsSnapshot: {
310+prompt: "",
311+skills: [{ name: "demo" }],
312+resolvedSkills: [
313+createCanonicalFixtureSkill({
314+name: "demo",
315+description: "Demo skill",
316+filePath: skillFile,
317+baseDir: skillDir,
318+source: "test",
319+}),
320+],
321+},
322+});
323+const { readTool } = expectReadWriteEditTools(tools);
324+325+await expect(readTool.execute("read-skill-symlink", { path: linkPath })).rejects.toThrow(
326+/symlink|sandbox|outside|escape/i,
327+);
328+});
329+});
225330});
226331227332describe("sandboxed workspace paths", () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。