Skip to content

Commit 8be581c

Browse files
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 be08e6c: 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>
1 parent d05e4a4 commit 8be581c

16 files changed

Lines changed: 736 additions & 61 deletions

docs/cli/browser.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ File + dialog helpers:
205205
206206
```bash
207207
openclaw browser upload /tmp/openclaw/uploads/file.pdf --ref <ref>
208+
openclaw browser upload media://inbound/file.pdf --ref <ref>
208209
openclaw browser waitfordownload
209210
openclaw browser download <ref> report.pdf
210211
openclaw browser dialog --accept
@@ -215,6 +216,10 @@ Managed Chrome profiles save ordinary click-triggered downloads into the OpenCla
215216
downloads directory (`/tmp/openclaw/downloads` by default, or the configured temp
216217
root). Use `waitfordownload` or `download` when the agent needs to wait for a
217218
specific file and return its path; those explicit waiters own the next download.
219+
Uploads accept files from the OpenClaw temp uploads root and OpenClaw-managed
220+
inbound media, including `media://inbound/<id>` and sandbox-relative
221+
`media/inbound/<id>` references. Nested media refs, traversal, and arbitrary
222+
local paths remain rejected.
218223
When an action opens a modal dialog, the action response returns
219224
`blockedByDialog` with `browserState.dialogs.pending`; pass `--dialog-id` to
220225
answer it directly. Dialogs handled outside OpenClaw appear under

docs/tools/browser-control.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ openclaw browser select 9 OptionA OptionB
191191
openclaw browser download e12 report.pdf
192192
openclaw browser waitfordownload report.pdf
193193
openclaw browser upload /tmp/openclaw/uploads/file.pdf
194+
openclaw browser upload media://inbound/file.pdf
194195
openclaw browser fill --fields '[{"ref":"1","type":"text","value":"Ada"}]'
195196
openclaw browser dialog --accept
196197
openclaw browser dialog --dismiss --dialog-id d1
@@ -232,7 +233,12 @@ Notes:
232233
233234
- `upload` and `dialog` are **arming** calls; run them before the click/press that triggers the chooser/dialog. If an action opens a modal, the action response includes `blockedByDialog` and `browserState.dialogs.pending`; pass that `dialogId` to respond directly. Dialogs handled outside OpenClaw appear under `browserState.dialogs.recent`.
234235
- `click`/`type`/etc require a `ref` from `snapshot` (numeric `12`, role ref `e12`, or actionable ARIA ref `ax12`). CSS selectors are intentionally not supported for actions. Use `click-coords` when the visible viewport position is the only reliable target.
235-
- Download, trace, and upload paths are constrained to OpenClaw temp roots: `/tmp/openclaw{,/downloads,/uploads}` (fallback: `${os.tmpdir()}/openclaw/...`).
236+
- Download and trace paths are constrained to OpenClaw temp roots: `/tmp/openclaw{,/downloads}` (fallback: `${os.tmpdir()}/openclaw/...`).
237+
- `upload` accepts files from the OpenClaw temp uploads root and
238+
OpenClaw-managed inbound media. Managed inbound media can be referenced as
239+
`media://inbound/<id>`, sandbox-relative `media/inbound/<id>`, or a resolved
240+
path inside the managed inbound media directory. Nested media refs,
241+
traversal, symlinks, hardlinks, and arbitrary local paths are still rejected.
236242
- `upload` can also set file inputs directly via `--input-ref` or `--element`.
237243
238244
Stable tab ids and labels survive Chromium raw-target replacement when OpenClaw

extensions/browser/src/browser-runtime.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,11 @@ export {
5353
resolveGoogleChromeExecutableForPlatform,
5454
} from "./browser/chrome.executables.js";
5555
export { redactCdpUrl } from "./browser/cdp.helpers.js";
56-
export { DEFAULT_UPLOAD_DIR, resolveExistingPathsWithinRoot } from "./browser/paths.js";
56+
export {
57+
DEFAULT_UPLOAD_DIR,
58+
resolveExistingPathsWithinRoot,
59+
resolveExistingUploadPaths,
60+
} from "./browser/paths.js";
5761
export { getBrowserProfileCapabilities } from "./browser/profile-capabilities.js";
5862
export { applyBrowserProxyPaths, persistBrowserProxyFiles } from "./browser/proxy-files.js";
5963
export {

extensions/browser/src/browser-tool.runtime.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export {
4848
} from "./browser/client.js";
4949
export { resolveBrowserConfig, resolveProfile } from "./browser/config.js";
5050
export { DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "./browser/constants.js";
51-
export { DEFAULT_UPLOAD_DIR, resolveExistingPathsWithinRoot } from "./browser/paths.js";
51+
export { resolveExistingUploadPaths } from "./browser/paths.js";
5252
export { getBrowserProfileCapabilities } from "./browser/profile-capabilities.js";
5353
export { applyBrowserProxyPaths, persistBrowserProxyFiles } from "./browser/proxy-files.js";
5454
export {

extensions/browser/src/browser-tool.test.ts

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,17 @@ vi.mock("openclaw/plugin-sdk/runtime-config-snapshot", async () => {
137137
};
138138
});
139139

140+
const pathValidationMocks = vi.hoisted(() => ({
141+
resolveExistingUploadPaths: vi.fn<
142+
(args: {
143+
requestedPaths: string[];
144+
}) => Promise<{ ok: true; paths: string[] } | { ok: false; error: string }>
145+
>(async ({ requestedPaths }) => ({
146+
ok: true as const,
147+
paths: requestedPaths,
148+
})),
149+
}));
150+
140151
const sessionTabRegistryMocks = vi.hoisted(() => ({
141152
touchSessionBrowserTab: vi.fn(),
142153
trackSessionBrowserTab: vi.fn(),
@@ -226,10 +237,7 @@ vi.mock("./browser-tool.runtime.js", () => {
226237
},
227238
readStringParam,
228239
readStringValue,
229-
resolveExistingPathsWithinRoot: vi.fn(async ({ requestedPaths }) => ({
230-
ok: true,
231-
paths: requestedPaths,
232-
})),
240+
resolveExistingUploadPaths: pathValidationMocks.resolveExistingUploadPaths,
233241
resolveNodeIdFromList: (nodes: Array<Record<string, unknown>>, requested: string) => {
234242
const node = nodes.find(
235243
(entry) => entry.nodeId === requested || entry.displayName === requested,
@@ -1634,3 +1642,45 @@ describe("browser tool act stale target recovery", () => {
16341642
expect(browserActionsMocks.browserAct).toHaveBeenCalledTimes(1);
16351643
});
16361644
});
1645+
1646+
describe("browser tool upload inbound media fallback (#83544)", () => {
1647+
beforeEach(resetBrowserToolMocks);
1648+
afterEach(() => vi.restoreAllMocks());
1649+
1650+
it("resolves upload paths before arming the file chooser", async () => {
1651+
const inboundPath = "/home/user/.openclaw/media/inbound/report.pdf";
1652+
pathValidationMocks.resolveExistingUploadPaths.mockResolvedValue({
1653+
ok: true,
1654+
paths: [inboundPath],
1655+
});
1656+
browserActionsMocks.browserArmFileChooser.mockResolvedValue({ ok: true });
1657+
1658+
const tool = createBrowserTool();
1659+
const result = await tool.execute?.("call-upload-1", {
1660+
action: "upload",
1661+
paths: [inboundPath],
1662+
ref: "file-input-1",
1663+
});
1664+
1665+
expect(pathValidationMocks.resolveExistingUploadPaths).toHaveBeenCalledWith({
1666+
requestedPaths: [inboundPath],
1667+
});
1668+
expect(result?.content[0]).toHaveProperty("type", "text");
1669+
});
1670+
1671+
it("rejects files outside both uploads and inbound media directories", async () => {
1672+
pathValidationMocks.resolveExistingUploadPaths.mockResolvedValue({
1673+
ok: false as const,
1674+
error: "path outside allowed directories",
1675+
});
1676+
1677+
const tool = createBrowserTool();
1678+
await expect(
1679+
tool.execute?.("call-upload-2", {
1680+
action: "upload",
1681+
paths: ["/etc/passwd"],
1682+
ref: "file-input-1",
1683+
}),
1684+
).rejects.toThrow("path outside allowed directories");
1685+
});
1686+
});

extensions/browser/src/browser-tool.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
import {
99
type AnyAgentTool,
1010
type NodeListNode,
11-
DEFAULT_UPLOAD_DIR,
1211
BrowserToolSchema,
1312
applyBrowserProxyPaths,
1413
browserAct,
@@ -37,8 +36,8 @@ import {
3736
readStringParam,
3837
readStringValue,
3938
resolveBrowserConfig,
39+
resolveExistingUploadPaths,
4040
resolveRuntimeImageSanitization,
41-
resolveExistingPathsWithinRoot,
4241
resolveNodeIdFromList,
4342
resolveProfile,
4443
selectDefaultNodeFromList,
@@ -821,15 +820,11 @@ export function createBrowserTool(opts?: {
821820
if (paths.length === 0) {
822821
throw new Error("paths required");
823822
}
824-
const uploadPathsResult = await resolveExistingPathsWithinRoot({
825-
rootDir: DEFAULT_UPLOAD_DIR,
826-
requestedPaths: paths,
827-
scopeLabel: `uploads directory (${DEFAULT_UPLOAD_DIR})`,
828-
});
829-
if (!uploadPathsResult.ok) {
830-
throw new Error(uploadPathsResult.error);
823+
const resolvedResult = await resolveExistingUploadPaths({ requestedPaths: paths });
824+
if (!resolvedResult.ok) {
825+
throw new Error(resolvedResult.error);
831826
}
832-
const normalizedPaths = uploadPathsResult.paths;
827+
const normalizedPaths = resolvedResult.paths;
833828
const ref = readStringParam(params, "ref");
834829
const inputRef = readStringParam(params, "inputRef");
835830
const element = readStringParam(params, "element");

0 commit comments

Comments
 (0)