























@@ -224,6 +224,108 @@ describe("workspace path resolution", () => {
224224});
225225});
226226227+it.runIf(process.platform !== "win32")(
228+"writes through in-workspace symlink parents when workspaceOnly is enabled",
229+async () => {
230+await withTempDir("openclaw-ws-symlink-write-", async (workspaceDir) => {
231+const realDir = path.join(workspaceDir, "oc_system", "memory");
232+const aliasDir = path.join(workspaceDir, "memory");
233+await fs.mkdir(realDir, { recursive: true });
234+await fs.symlink(realDir, aliasDir);
235+236+const cfg: OpenClawConfig = { tools: { fs: { workspaceOnly: true } } };
237+const tools = createOpenClawCodingTools({ workspaceDir, config: cfg });
238+const { writeTool } = expectReadWriteEditTools(tools);
239+240+await writeTool.execute("ws-write-symlink-parent", {
241+path: "memory/2026-05-20.md",
242+content: "remember this\n",
243+});
244+245+await expect(fs.readFile(path.join(realDir, "2026-05-20.md"), "utf8")).resolves.toBe(
246+"remember this\n",
247+);
248+});
249+},
250+);
251+252+it.runIf(process.platform !== "win32")(
253+"edits through in-workspace symlink parents when workspaceOnly is enabled",
254+async () => {
255+await withTempDir("openclaw-ws-symlink-edit-", async (workspaceDir) => {
256+const realDir = path.join(workspaceDir, "oc_system", "memory");
257+const aliasDir = path.join(workspaceDir, "memory");
258+const targetPath = path.join(realDir, "2026-05-20.md");
259+await fs.mkdir(realDir, { recursive: true });
260+await fs.symlink(realDir, aliasDir);
261+await fs.writeFile(targetPath, "old memory\n", "utf8");
262+263+const cfg: OpenClawConfig = { tools: { fs: { workspaceOnly: true } } };
264+const tools = createOpenClawCodingTools({ workspaceDir, config: cfg });
265+const { editTool } = expectReadWriteEditTools(tools);
266+267+await editTool.execute("ws-edit-symlink-parent", {
268+path: "memory/2026-05-20.md",
269+edits: [{ oldText: "old", newText: "new" }],
270+});
271+272+await expect(fs.readFile(targetPath, "utf8")).resolves.toBe("new memory\n");
273+});
274+},
275+);
276+277+it.runIf(process.platform !== "win32")(
278+"rejects writes through symlink parents that resolve outside the workspace",
279+async () => {
280+await withTempDir("openclaw-ws-symlink-escape-", async (rootDir) => {
281+const workspaceDir = path.join(rootDir, "workspace");
282+const outsideDir = path.join(rootDir, "outside");
283+const aliasDir = path.join(workspaceDir, "memory");
284+await fs.mkdir(workspaceDir, { recursive: true });
285+await fs.mkdir(outsideDir, { recursive: true });
286+await fs.symlink(outsideDir, aliasDir);
287+288+const cfg: OpenClawConfig = { tools: { fs: { workspaceOnly: true } } };
289+const tools = createOpenClawCodingTools({ workspaceDir, config: cfg });
290+const { writeTool } = expectReadWriteEditTools(tools);
291+292+await expect(
293+writeTool.execute("ws-write-symlink-escape", {
294+path: "memory/secret.md",
295+content: "pwned\n",
296+}),
297+).rejects.toThrow(/Path escapes workspace root|outside-workspace|sandbox/i);
298+await expect(fs.stat(path.join(outsideDir, "secret.md"))).rejects.toMatchObject({
299+code: "ENOENT",
300+});
301+});
302+},
303+);
304+305+it.runIf(process.platform !== "win32")(
306+"rejects writes to final symlinks when workspaceOnly is enabled",
307+async () => {
308+await withTempDir("openclaw-ws-symlink-leaf-", async (workspaceDir) => {
309+const targetPath = path.join(workspaceDir, "target.md");
310+const linkPath = path.join(workspaceDir, "memory.md");
311+await fs.writeFile(targetPath, "original\n", "utf8");
312+await fs.symlink(targetPath, linkPath);
313+314+const cfg: OpenClawConfig = { tools: { fs: { workspaceOnly: true } } };
315+const tools = createOpenClawCodingTools({ workspaceDir, config: cfg });
316+const { writeTool } = expectReadWriteEditTools(tools);
317+318+await expect(
319+writeTool.execute("ws-write-final-symlink", {
320+path: "memory.md",
321+content: "pwned\n",
322+}),
323+).rejects.toThrow(/symlink|not-file|directory component/i);
324+await expect(fs.readFile(targetPath, "utf8")).resolves.toBe("original\n");
325+});
326+},
327+);
328+227329it("allows workspaceOnly reads for resolved skill roots without allowing other filesystem access", async () => {
228330await withTempDir("openclaw-skill-read-", async (rootDir) => {
229331const workspaceDir = path.join(rootDir, "workspace");
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。