fix(browser): allow inbound media uploads

Allow the browser upload tool to resolve OpenClaw-managed inbound media refs such as `media://inbound/<id>` and sandbox-relative `media/inbound/<id>` while preserving the existing upload-root path contract.

Keep upload-root files ahead of sandbox-relative inbound fallback, reject nested absolute inbound media files, and validate raw `media://` paths before URL normalization so traversal-shaped refs cannot resolve to direct media ids.

Verification:
- `OPENCLAW_VITEST_MAX_WORKERS=1 node scripts/run-vitest.mjs extensions/browser/src/browser/paths.test.ts --reporter=verbose`
- `OPENCLAW_VITEST_MAX_WORKERS=1 node scripts/run-vitest.mjs extensions/browser/src/browser/paths.test.ts --reporter=dot`
- `OPENCLAW_HEAVY_CHECK_LOCK_SCOPE=worktree node scripts/run-tsgo.mjs -p test/tsconfig/tsconfig.extensions.test.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/extensions-test.tsbuildinfo`
- `pnpm lint --threads=8`
- `.agents/skills/autoreview/scripts/autoreview --mode branch --base origin/main`
- `git diff --check`
- GitHub PR checks on be08e6c8a8: dependency-guard, check-lint, check-test-types, check-additional-extension-bundled, checks-fast-contracts-plugins-a, checks-fast-contracts-plugins-b all passed.

Fixes #83544.

Co-authored-by: Zee Zheng <zheng.zuo0@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Zee Zheng
2026-05-31 06:49:07 +08:00
committed by GitHub
parent d05e4a4bc6
commit 8be581cbf8
16 changed files with 736 additions and 61 deletions

View File

@@ -137,6 +137,17 @@ vi.mock("openclaw/plugin-sdk/runtime-config-snapshot", async () => {
};
});
const pathValidationMocks = vi.hoisted(() => ({
resolveExistingUploadPaths: vi.fn<
(args: {
requestedPaths: string[];
}) => Promise<{ ok: true; paths: string[] } | { ok: false; error: string }>
>(async ({ requestedPaths }) => ({
ok: true as const,
paths: requestedPaths,
})),
}));
const sessionTabRegistryMocks = vi.hoisted(() => ({
touchSessionBrowserTab: vi.fn(),
trackSessionBrowserTab: vi.fn(),
@@ -226,10 +237,7 @@ vi.mock("./browser-tool.runtime.js", () => {
},
readStringParam,
readStringValue,
resolveExistingPathsWithinRoot: vi.fn(async ({ requestedPaths }) => ({
ok: true,
paths: requestedPaths,
})),
resolveExistingUploadPaths: pathValidationMocks.resolveExistingUploadPaths,
resolveNodeIdFromList: (nodes: Array<Record<string, unknown>>, requested: string) => {
const node = nodes.find(
(entry) => entry.nodeId === requested || entry.displayName === requested,
@@ -1634,3 +1642,45 @@ describe("browser tool act stale target recovery", () => {
expect(browserActionsMocks.browserAct).toHaveBeenCalledTimes(1);
});
});
describe("browser tool upload inbound media fallback (#83544)", () => {
beforeEach(resetBrowserToolMocks);
afterEach(() => vi.restoreAllMocks());
it("resolves upload paths before arming the file chooser", async () => {
const inboundPath = "/home/user/.openclaw/media/inbound/report.pdf";
pathValidationMocks.resolveExistingUploadPaths.mockResolvedValue({
ok: true,
paths: [inboundPath],
});
browserActionsMocks.browserArmFileChooser.mockResolvedValue({ ok: true });
const tool = createBrowserTool();
const result = await tool.execute?.("call-upload-1", {
action: "upload",
paths: [inboundPath],
ref: "file-input-1",
});
expect(pathValidationMocks.resolveExistingUploadPaths).toHaveBeenCalledWith({
requestedPaths: [inboundPath],
});
expect(result?.content[0]).toHaveProperty("type", "text");
});
it("rejects files outside both uploads and inbound media directories", async () => {
pathValidationMocks.resolveExistingUploadPaths.mockResolvedValue({
ok: false as const,
error: "path outside allowed directories",
});
const tool = createBrowserTool();
await expect(
tool.execute?.("call-upload-2", {
action: "upload",
paths: ["/etc/passwd"],
ref: "file-input-1",
}),
).rejects.toThrow("path outside allowed directories");
});
});