Skip to content

fix(media): normalize Windows inbound paths case-insensitively#97630

Merged
vincentkoc merged 5 commits into
openclaw:mainfrom
VectorPeak:codex/windows-inbound-path-case
Jul 1, 2026
Merged

fix(media): normalize Windows inbound paths case-insensitively#97630
vincentkoc merged 5 commits into
openclaw:mainfrom
VectorPeak:codex/windows-inbound-path-case

Conversation

@VectorPeak

@VectorPeak VectorPeak commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Related: #96024

What Problem This Solves

Fixes an issue where Windows users could have a valid inbound media file rejected when the configured media root and the reported file path use different casing for the drive path.

For example, a root configured as:

C:\Users\Alice\Library\Messages\Attachments

could fail to match an actual file path reported as:

c:\users\alice\Library\Messages\Attachments\12\34\ABCDEF\IMG_0001.jpeg

Windows drive paths are normally case-insensitive, but packages/media-core/src/inbound-path-policy.ts normalizes separators and then compares root and candidate path segments strictly. That made the inbound media policy treat the same Windows path as outside the allowed root.

Why This Change Was Made

The fix keeps the existing POSIX behavior unchanged while normalizing only absolute Windows drive paths to a shared casing before segment comparison.

This keeps the change narrow: it only affects paths matching the Windows drive absolute-path form, such as C:/Users/Alice/Media. POSIX paths like /Users/Alice/Media remain case-sensitive.

The affected call chain is:

local media / media-understanding inbound file path
-> media root allow-list check
-> isInboundPathAllowed(...)
-> normalizePosixAbsolutePath(...)
-> strict segment comparison

This is the shared policy used by inbound media path validation, so fixing the normalization point avoids caller-specific Windows workarounds.

User Impact

Windows users can use local media roots even when the configured root and the file path reported by the runtime differ only by casing.

This prevents valid local media files from being rejected as outside the allowed inbound root. Linux and macOS-style POSIX path matching is unchanged.

Evidence

Branch-level Windows OpenClaw policy proof from this branch, run in a checked-out worktree at the PR head commit:

git -C D:\ZXY\Github\openclaw worktree add D:\ZXY\Github\openclaw-97630-quality codex/windows-inbound-path-case
$script | node --import file:///D:/ZXY/Github/openclaw/node_modules/tsx/dist/loader.mjs --input-type=module

The stdin script imports and executes the patched OpenClaw policy from the checked-out PR branch:

import {
  isInboundPathAllowed,
  normalizeInboundPathRoots,
} from "./packages/media-core/src/inbound-path-policy.ts";

const filePath = "D:\\Users\\Alice\\Library\\Messages\\Attachments\\12\\34\\ABCDEF\\IMG_0001.jpeg";
const roots = ["d:/users/*/library/messages/attachments"];

console.log("normalizedRoots:", JSON.stringify(normalizeInboundPathRoots(roots)));
console.log("isInboundPathAllowed:", isInboundPathAllowed({ filePath, roots }));

Edited Windows terminal output, with only the local temp username/path identifier redacted:

OpenClaw inbound path policy Windows proof
command: node --import tsx --input-type=module < proof-script.mjs
repository: openclaw/openclaw
worktree: D:\ZXY\Github\openclaw-97630-quality
branch: codex/windows-inbound-path-case
head: 6a403cdc72cff99f542474035cb1f7f62263d490
node: v24.15.0
powershell: 5.1.26100.8737 Desktop
platform: win32
policy import: ./packages/media-core/src/inbound-path-policy.ts
filePath: D:\Users\Alice\Library\Messages\Attachments\12\34\ABCDEF\IMG_0001.jpeg
roots: ["d:/users/*/library/messages/attachments"]
normalizedRoots: ["d:/users/*/library/messages/attachments"]
isInboundPathAllowed: true

Focused regression coverage was added for a mixed-case Windows drive root and candidate path:

isInboundPathAllowed({
  filePath: "C:\\Users\\Alice\\Library\\Messages\\Attachments\\12\\34\\ABCDEF\\IMG_0001.jpeg",
  roots: ["c:/users/*/library/messages/attachments"],
});

Before this fix, that path pair could be rejected because the segment comparison saw C: and c: as different strings. After this fix, the focused regression test confirms it is accepted.

Windows PowerShell behavior proof from this branch:

$dir = Join-Path $env:TEMP "OpenClawCaseProof-$PID"
New-Item -ItemType Directory -Path $dir | Out-Null
New-Item -ItemType File -Path (Join-Path $dir 'MixedCase.TXT') | Out-Null
$probe = Join-Path $dir 'mixedcase.txt'
[pscustomobject]@{
  Created = Join-Path $dir 'MixedCase.TXT'
  Probe = $probe
  TestPath = Test-Path -LiteralPath $probe
  GetItemName = (Get-Item -LiteralPath $probe).Name
} | Format-List
Remove-Item -LiteralPath $dir -Recurse -Force
Created     : C:\Users\ZXY\AppData\Local\Temp\OpenClawCaseProof-12264\MixedCase.TXT
Probe       : C:\Users\ZXY\AppData\Local\Temp\OpenClawCaseProof-12264\mixedcase.txt
TestPath    : True
GetItemName : mixedcase.txt

SDK-facing contract coverage was added because this helper is also exposed through the public media runtime SDK barrel:

import { isInboundPathAllowed, normalizeInboundPathRoots } from "./media-runtime.js";

The SDK-facing regression test confirms plugin-visible callers get the same Windows-drive case-insensitive behavior through src/plugin-sdk/media-runtime.ts.

Validation:

node scripts/run-vitest.mjs packages/media-core/src/inbound-path-policy.test.ts

Test Files  1 passed (1)
Tests       8 passed (8)
node scripts/run-vitest.mjs src/plugin-sdk/media-runtime.test.ts

Test Files  1 passed (1)
Tests       1 passed (1)

AI assistance was used to prepare this PR.

@openclaw-barnacle openclaw-barnacle Bot added size: XS triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence. labels Jun 29, 2026
@clawsweeper

clawsweeper Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed June 30, 2026, 10:30 PM ET / 02:30 UTC.

Summary
The PR lowercases normalized Windows-drive absolute inbound media paths in media-core and adds media-core plus plugin SDK regression tests.

PR surface: Source +2, Tests +28. Total +30 across 3 files.

Reproducibility: yes. at source level: current main and v2026.6.11 preserve Windows-drive casing before strict segment comparison, and the predecessor PR includes installed-Windows before-fix output. I did not run a Windows package repro in this read-only review.

Review metrics: 1 noteworthy metric.

  • Plugin-visible helper behavior: 1 exported helper behavior changed. The inbound path policy is re-exported through plugin-sdk/media-runtime, so this media-core fix changes public plugin-callable Windows matching semantics.

Root-cause cluster
Relationship: canonical
Canonical: #97630
Summary: This PR is the active proof-updated resubmission of the focused Windows media path normalization fix; the older PR is closed unmerged, and the Microsoft tracker is only an adjacent work queue.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🦞 diamond lobster
Patch quality: 🐚 platinum hermit
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • [P2] Maintainers should explicitly accept the SDK-visible Windows-drive matching semantics, or ask for a short contract note before merge.

Risk before merge

  • [P1] Merging changes plugin-visible Windows-drive path matching semantics because plugin-sdk/media-runtime and @openclaw/media-core/inbound-path-policy export the helper; plugin callers relying on strict case-sensitive Windows-drive segment matching may see different allow-list results.

Maintainer options:

  1. Accept SDK-visible Windows semantics (recommended)
    Treat case-insensitive Windows-drive matching as the intended bug-fix behavior for the exported helper and land after normal maintainer review.
  2. Document the exported contract first
    Add a short SDK/runtime note for Windows-drive path matching if maintainers want the public behavior explicit before merge.
  3. Pause for media or SDK owner signoff
    Hold the PR if maintainers want an owner of the media-core or plugin SDK surface to approve the compatibility change first.

Next step before merge

  • No automated repair is needed; maintainers need to decide whether to accept the SDK-visible compatibility change and then land or request documentation.

Security
Cleared: The diff only changes scoped string normalization in the existing inbound media allow-list policy and adds tests; I found no concrete security or supply-chain concern.

Review details

Best possible solution:

Land the shared media-core normalizer once maintainers accept the SDK-visible Windows-drive case-insensitive semantics.

Do we have a high-confidence way to reproduce the issue?

Yes at source level: current main and v2026.6.11 preserve Windows-drive casing before strict segment comparison, and the predecessor PR includes installed-Windows before-fix output. I did not run a Windows package repro in this read-only review.

Is this the best way to solve the issue?

Yes: the shared media-core normalizer is the best fix location because local media access, sandbox staging, media-understanding, iMessage, and SDK callers share this policy. Caller-specific folding would duplicate the invariant, while making POSIX paths case-insensitive would be too broad.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 816038e97a5d.

Label changes

Label justifications:

  • P2: This is a normal-priority Windows media access bug fix with focused blast radius and no evidence of data loss, security bypass, crash loop, or unusable core runtime.
  • merge-risk: 🚨 compatibility: The shared inbound path helper is exported through plugin SDK and media-core package surfaces, so merging changes plugin-visible Windows-drive path matching semantics.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (terminal): The PR body includes redacted Windows terminal output from the patched branch showing the mixed-case Windows-drive path is accepted, and the later head is a merge-only update of the same implementation.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes redacted Windows terminal output from the patched branch showing the mixed-case Windows-drive path is accepted, and the later head is a merge-only update of the same implementation.
Evidence reviewed

PR surface:

Source +2, Tests +28. Total +30 across 3 files.

View PR surface stats
Area Files Added Removed Net
Source 1 3 1 +2
Tests 2 28 0 +28
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 3 31 1 +30

What I checked:

  • AGENTS policy applied: Read the full root policy and the scoped plugin SDK policy; the SDK public-contract and compatibility guidance applies because the changed helper is re-exported through plugin-sdk/media-runtime. (AGENTS.md:22, 816038e97a5d)
  • Current main still has the reported behavior: On current main, normalizePosixAbsolutePath normalizes separators and trailing slash but returns withoutTrailingSlash unchanged, so Windows-drive root and candidate casing is still compared strictly by segments. (packages/media-core/src/inbound-path-policy.ts:24, 816038e97a5d)
  • Latest release still has the reported behavior: The latest release tag v2026.6.11 contains the same case-preserving return in the inbound path policy, so the fix is not already shipped. (packages/media-core/src/inbound-path-policy.ts:24, e085fa1a3ffd)
  • PR implementation is scoped to Windows-drive absolute paths: At the PR head, only values matching the Windows-drive absolute regex are lowercased after slash/trailing-slash normalization; POSIX absolute paths still return unchanged. (packages/media-core/src/inbound-path-policy.ts:24, df4deaac3b93)
  • Focused regression coverage exists: The PR adds a media-core mixed-case Windows-drive root/candidate test and an SDK barrel test for normalizeInboundPathRoots plus isInboundPathAllowed. (packages/media-core/src/inbound-path-policy.test.ts:50, df4deaac3b93)
  • Shared runtime surface: Local media access, sandbox media staging, media-understanding attachment reads, and iMessage attachment filtering all call the shared inbound path policy, making the shared normalizer the right layer to avoid caller-specific Windows workarounds. (src/media/local-media-access.ts:87, 816038e97a5d)

Likely related people:

  • steipete: Peter Steinberger authored the commit that introduced the inbound attachment root policy and the later commit that added the broad plugin-sdk/media-runtime barrel. (role: introduced behavior and SDK barrel contributor; confidence: high; commits: 1316e5740382, 9ebe38b6e36b; files: src/media/inbound-path-policy.ts, src/auto-reply/reply/stage-sandbox-media.ts, src/plugin-sdk/media-runtime.ts)
  • vincentkoc: Vincent Koc authored the recent media-core/package boundary commit that currently owns the visible packages/media-core/src/inbound-path-policy.ts and src/plugin-sdk/media-runtime.ts files in this checkout. (role: recent area contributor; confidence: medium; commits: 51e0997c2bbd; files: packages/media-core/src/inbound-path-policy.ts, src/plugin-sdk/media-runtime.ts)
  • jacobtomlinson: Jacob Tomlinson authored the ACP attachment-root enforcement work in media-understanding, one of the shared caller paths affected by the inbound path policy semantics. (role: adjacent caller contributor; confidence: medium; commits: 566fb73d9da2; files: src/media-understanding/attachments.cache.ts, src/auto-reply/reply/dispatch-acp.test.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P2 Normal backlog priority with limited blast radius. labels Jun 29, 2026
@openclaw-barnacle openclaw-barnacle Bot removed the triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence. label Jun 29, 2026
@VectorPeak

Copy link
Copy Markdown
Contributor Author

Updated the PR body with branch-level Windows proof that imports this branch's packages/media-core/src/inbound-path-policy.ts and shows isInboundPathAllowed accepts the mixed-case Windows drive root/candidate on platform: win32.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@VectorPeak

Copy link
Copy Markdown
Contributor Author

Updated the PR body with a stronger branch-level Windows proof:

  • created a temporary detached worktree from the PR head commit 23ac1c5c892259955f79cdc358c6d714d8057af0
  • ran on Windows with platform: win32
  • imported ./packages/media-core/src/inbound-path-policy.ts from that checked-out PR branch
  • executed isInboundPathAllowed({ filePath, roots }) for the mixed-case Windows drive candidate/root pair
  • observed isInboundPathAllowed: true

@clawsweeper re-review

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jun 30, 2026
@VectorPeak

Copy link
Copy Markdown
Contributor Author

Added a small SDK-facing guardrail for the plugin-visible part of this change.

New commit: 6a403cdc72cff99f542474035cb1f7f62263d490

What changed:

  • added src/plugin-sdk/media-runtime.test.ts
  • imports isInboundPathAllowed and normalizeInboundPathRoots through the public ./media-runtime.js SDK barrel
  • confirms plugin-visible callers get the same Windows-drive case-insensitive inbound path behavior

Validation:

node scripts/run-vitest.mjs packages/media-core/src/inbound-path-policy.test.ts
Test Files  1 passed (1)
Tests       8 passed (8)
node scripts/run-vitest.mjs src/plugin-sdk/media-runtime.test.ts
Test Files  1 passed (1)
Tests       1 passed (1)

The PR body was also updated with the new HEAD and SDK-facing validation evidence.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

@VectorPeak

Copy link
Copy Markdown
Contributor Author

Exact-head CI is now green for 6a403cdc72cff99f542474035cb1f7f62263d490.

The SDK-facing guardrail is included in that commit via src/plugin-sdk/media-runtime.test.ts, and the PR body now reflects the new HEAD plus both focused validation commands.

@clawsweeper re-review

@vincentkoc vincentkoc self-assigned this Jul 1, 2026
@vincentkoc
vincentkoc merged commit fbceb30 into openclaw:main Jul 1, 2026
95 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: XS status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants