diff --git a/extensions/canvas/src/host/a2ui.ts b/extensions/canvas/src/host/a2ui.ts index 9ecd8ffd4abb..35aaad1c30de 100644 --- a/extensions/canvas/src/host/a2ui.ts +++ b/extensions/canvas/src/host/a2ui.ts @@ -146,23 +146,6 @@ async function handleA2uiHttpRequestWithRootResolver( } } -/** Creates an HTTP handler for a specific hosted A2UI asset root. */ -export function createA2uiHttpRequestHandler(params: { - rootDir: string; - liveReload?: boolean; -}): (req: IncomingMessage, res: ServerResponse) => Promise { - let rootRealPromise: Promise | null = null; - return async (req, res) => { - rootRealPromise ??= fs.realpath(params.rootDir); - return await handleA2uiHttpRequestWithRootResolver( - req, - res, - async () => await rootRealPromise, - { liveReload: params.liveReload }, - ); - }; -} - /** Handles one HTTP request for the hosted A2UI asset surface. */ export async function handleA2uiHttpRequest( req: IncomingMessage, diff --git a/extensions/canvas/src/host/server.test.ts b/extensions/canvas/src/host/server.test.ts index cd23809d88ea..74dcc35aec2d 100644 --- a/extensions/canvas/src/host/server.test.ts +++ b/extensions/canvas/src/host/server.test.ts @@ -121,14 +121,13 @@ async function captureHandlerResponse( } async function captureA2uiFixtureResponse( - rootDir: string, url: string, method = "GET", liveReload = true, ): Promise { - const { createA2uiHttpRequestHandler } = await import("./a2ui.js"); + const { handleA2uiHttpRequest } = await import("./a2ui.js"); return await captureHttpResponse( - createA2uiHttpRequestHandler({ rootDir, liveReload }), + async (req, res) => await handleA2uiHttpRequest(req, res, { liveReload }), url, method, ); @@ -661,59 +660,64 @@ describe("canvas host", () => { }); it("serves A2UI scaffold and blocks traversal/symlink escapes", async () => { - const a2uiRoot = await createCaseDir(); - const nestedAssetDir = path.join(a2uiRoot, "assets", "demo"); + const fixtureEntryDir = await createCaseDir(); + const a2uiRoot = path.join(fixtureEntryDir, "a2ui"); + const nestedAssetDir = path.join( + a2uiRoot, + `test-assets-${Date.now()}-${Math.random().toString(16).slice(2)}`, + ); + const nestedAssetUrlPath = path.basename(nestedAssetDir); + const bundlePath = path.join(a2uiRoot, "a2ui.bundle.js"); const linkName = `test-link-${Date.now()}-${Math.random().toString(16).slice(2)}.txt`; const linkPath = path.join(a2uiRoot, linkName); - - await fs.mkdir(nestedAssetDir, { recursive: true }); - await fs.writeFile( - path.join(a2uiRoot, "index.html"), - ` -`, - "utf8", - ); - await fs.writeFile(path.join(a2uiRoot, "a2ui.bundle.js"), "window.openclawA2UI = {};", "utf8"); - await fs.writeFile(path.join(nestedAssetDir, "sample.txt"), "nested asset", "utf8"); - await fs.symlink(path.join(process.cwd(), "package.json"), linkPath); + const originalArgv = [...process.argv]; try { - const res = await captureA2uiFixtureResponse(a2uiRoot, `${A2UI_PATH}/`); + process.argv[1] = path.join(fixtureEntryDir, "openclaw.mjs"); + await fs.mkdir(nestedAssetDir, { recursive: true }); + await fs.writeFile( + path.join(a2uiRoot, "index.html"), + ` +`, + "utf8", + ); + await fs.writeFile(bundlePath, "window.openclawA2UI = {};", "utf8"); + await fs.writeFile(path.join(nestedAssetDir, "sample.txt"), "nested asset", "utf8"); + await fs.symlink(path.join(process.cwd(), "package.json"), linkPath); + + const res = await captureA2uiFixtureResponse(`${A2UI_PATH}/`); const html = res.body; expect(res.status).toBe(200); expect(html).toContain("openclaw-a2ui-host"); expect(html).toContain("openclawCanvasA2UIAction"); - const noReloadRes = await captureA2uiFixtureResponse(a2uiRoot, `${A2UI_PATH}/`, "GET", false); + const noReloadRes = await captureA2uiFixtureResponse(`${A2UI_PATH}/`, "GET", false); expect(noReloadRes.body).toContain("openclawCanvasA2UIAction"); expect(noReloadRes.body).not.toContain(CANVAS_WS_PATH); - const bundleRes = await captureA2uiFixtureResponse(a2uiRoot, `${A2UI_PATH}/a2ui.bundle.js`); + const bundleRes = await captureA2uiFixtureResponse(`${A2UI_PATH}/a2ui.bundle.js`); const js = bundleRes.body; expect(bundleRes.status).toBe(200); expect(js).toContain("openclawA2UI"); const assetRes = await captureA2uiFixtureResponse( - a2uiRoot, - `${A2UI_PATH}/assets/demo/sample.txt`, + `${A2UI_PATH}/${nestedAssetUrlPath}/sample.txt`, ); expect(assetRes.status).toBe(200); expect(assetRes.headers["content-type"]).toBe("text/plain"); expect(assetRes.body).toBe("nested asset"); - const traversalRes = await captureA2uiFixtureResponse( - a2uiRoot, - `${A2UI_PATH}/%2e%2e%2fpackage.json`, - ); + const traversalRes = await captureA2uiFixtureResponse(`${A2UI_PATH}/%2e%2e%2fpackage.json`); expect(traversalRes.status).toBe(404); expect(traversalRes.body).toBe("not found"); - const malformedRes = await captureA2uiFixtureResponse(a2uiRoot, `${A2UI_PATH}/%E0%A4%A`); + const malformedRes = await captureA2uiFixtureResponse(`${A2UI_PATH}/%E0%A4%A`); expect(malformedRes.status).toBe(404); expect(malformedRes.body).toBe("not found"); - const symlinkRes = await captureA2uiFixtureResponse(a2uiRoot, `${A2UI_PATH}/${linkName}`); + const symlinkRes = await captureA2uiFixtureResponse(`${A2UI_PATH}/${linkName}`); expect(symlinkRes.status).toBe(404); expect(symlinkRes.body).toBe("not found"); } finally { + process.argv.splice(0, process.argv.length, ...originalArgv); await fs.rm(linkPath, { force: true }); } }); diff --git a/scripts/deadcode-exports.baseline.mjs b/scripts/deadcode-exports.baseline.mjs index 5f75ab3a98b3..24b15607f2f3 100644 --- a/scripts/deadcode-exports.baseline.mjs +++ b/scripts/deadcode-exports.baseline.mjs @@ -2,7 +2,6 @@ // New entries fail CI. After deleting dead code, run `pnpm deadcode:exports:update`. // Do not add entries to avoid fixing new findings. export const KNIP_UNUSED_EXPORT_BASELINE = [ - "extensions/canvas/src/host/a2ui.ts: createA2uiHttpRequestHandler", "extensions/codex/src/session-catalog.ts: CODEX_TERMINAL_RESUME_COMMAND", "extensions/codex/src/session-upstream-activity.ts: checkCodexUpstreamActivity (upstream)", "extensions/codex/src/session-upstream-activity.ts: classifyCodexUpstreamTurns",