Skip to content

Commit e20edd7

Browse files
committed
fix(canvas): guard A2UI asset copy roots
1 parent a89e65c commit e20edd7

2 files changed

Lines changed: 41 additions & 5 deletions

File tree

extensions/canvas/scripts/copy-a2ui.mjs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,27 @@ function shouldSkipMissingA2uiAssets(env = process.env) {
2020
return env.OPENCLAW_A2UI_SKIP_MISSING === "1" || Boolean(env.OPENCLAW_SPARSE_PROFILE);
2121
}
2222

23+
function isRelativeWithin(relPath) {
24+
return (
25+
relPath === "" ||
26+
(relPath !== ".." && !relPath.startsWith(`..${path.sep}`) && !path.isAbsolute(relPath))
27+
);
28+
}
29+
30+
function pathsOverlap(leftDir, rightDir) {
31+
const left = path.resolve(leftDir);
32+
const right = path.resolve(rightDir);
33+
return (
34+
isRelativeWithin(path.relative(left, right)) || isRelativeWithin(path.relative(right, left))
35+
);
36+
}
37+
2338
/** Copies A2UI assets, optionally tolerating missing bundles in sparse builds. */
2439
export async function copyA2uiAssets({ srcDir, outDir }) {
40+
if (pathsOverlap(srcDir, outDir)) {
41+
throw new Error("A2UI source and output directories must not overlap.");
42+
}
43+
2544
const skipMissing = shouldSkipMissingA2uiAssets(process.env);
2645
try {
2746
await fs.stat(path.join(srcDir, "index.html"));

extensions/canvas/scripts/copy-a2ui.test.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,17 +37,17 @@ describe("canvas a2ui copy", () => {
3737

3838
it("throws a helpful error when assets are missing", async () => {
3939
await withA2uiFixture(async (dir) => {
40-
await expect(copyA2uiAssets({ srcDir: dir, outDir: path.join(dir, "out") })).rejects.toThrow(
41-
'Run "pnpm canvas:a2ui:bundle"',
42-
);
40+
await expect(
41+
copyA2uiAssets({ srcDir: path.join(dir, "src"), outDir: path.join(dir, "out") }),
42+
).rejects.toThrow('Run "pnpm canvas:a2ui:bundle"');
4343
});
4444
});
4545

4646
it("skips missing assets when OPENCLAW_A2UI_SKIP_MISSING=1", async () => {
4747
await withA2uiFixture(async (dir) => {
4848
process.env.OPENCLAW_A2UI_SKIP_MISSING = "1";
4949
await expect(
50-
copyA2uiAssets({ srcDir: dir, outDir: path.join(dir, "out") }),
50+
copyA2uiAssets({ srcDir: path.join(dir, "src"), outDir: path.join(dir, "out") }),
5151
).resolves.toBeUndefined();
5252
});
5353
});
@@ -56,7 +56,7 @@ describe("canvas a2ui copy", () => {
5656
await withA2uiFixture(async (dir) => {
5757
process.env.OPENCLAW_SPARSE_PROFILE = "core";
5858
await expect(
59-
copyA2uiAssets({ srcDir: dir, outDir: path.join(dir, "out") }),
59+
copyA2uiAssets({ srcDir: path.join(dir, "src"), outDir: path.join(dir, "out") }),
6060
).resolves.toBeUndefined();
6161
});
6262
});
@@ -102,4 +102,21 @@ describe("canvas a2ui copy", () => {
102102
});
103103
});
104104
});
105+
106+
it("rejects overlapping source and output directories before cleaning output", async () => {
107+
await withA2uiFixture(async (dir) => {
108+
const srcDir = path.join(dir, "src");
109+
await fs.mkdir(srcDir, { recursive: true });
110+
await fs.writeFile(path.join(srcDir, "index.html"), "<html></html>", "utf8");
111+
await fs.writeFile(path.join(srcDir, "a2ui.bundle.js"), "console.log(1);", "utf8");
112+
113+
await expect(copyA2uiAssets({ srcDir, outDir: srcDir })).rejects.toThrow("must not overlap");
114+
await expect(fs.readFile(path.join(srcDir, "index.html"), "utf8")).resolves.toBe(
115+
"<html></html>",
116+
);
117+
await expect(copyA2uiAssets({ srcDir, outDir: path.join(srcDir, "dist") })).rejects.toThrow(
118+
"must not overlap",
119+
);
120+
});
121+
});
105122
});

0 commit comments

Comments
 (0)