Skip to content

fix(media): add shared media MIME validation helpers#76566

Closed
masatohoshino wants to merge 3 commits into
openclaw:mainfrom
masatohoshino:pr/openclaw-media-helper-bundle
Closed

fix(media): add shared media MIME validation helpers#76566
masatohoshino wants to merge 3 commits into
openclaw:mainfrom
masatohoshino:pr/openclaw-media-helper-bundle

Conversation

@masatohoshino

@masatohoshino masatohoshino commented May 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Problem: src/media/ exposes MIME and audio-classification primitives, but there is no shared seam that distinguishes the caller-provided classified kind of a media payload from filename/URL hints, and no shared seam that normalizes a free-form MIME string before it reaches an outbound header. Adapter and core paths inline ad hoc checks that drift over time.
  • Why it matters: a single shared helper surface makes follow-up call-site work small and reviewable, and lets reviewers compare a single trust boundary instead of N inlined regexes.
  • What changed: two helpers added at the end of src/media/mime.ts and re-exported via the existing src/plugin-sdk/media-runtime.ts wildcard barrel: isVerifiedAudioSource(media: { kind?, contentType? }) and sanitizeMediaMime(input, options?). Tests added in src/media/mime.test.ts. Brief CHANGELOG entry added under ### Changes for plugin-author visibility.
  • What did NOT change (scope boundary): no callers added, no runtime behavior change, no API generalization (no expectedPrefix option, no isVerifiedMediaSource(media, kind) shape), no cross-channel adapter changes, no docs.

Supporting context: the helper-only / adopter-PR split direction was previously affirmed by the maintainer in the close note on #68744.

Trust model

isVerifiedAudioSource reads only the caller-provided classified kind field. It does not infer audio from filename or URL hints, and a contentType starting with audio/ is not enough on its own (a remote sender can set the HTTP Content-Type). The contentType field is kept on the parameter shape as a seam for a later sniffed-MIME extension via detectMime, but it is not read today.

sanitizeMediaMime validates a MIME string against the RFC 2045 token character set (broader than RFC 6838's restricted-name, to admit valid vendor types such as application/vnd.x~v1+json), strips ASCII control characters and DEL, lowercases the type/subtype, and by default strips all parameters. preserveCodecsParam: true preserves common single-codec, multi-codec, and quoted codecs=... parameters when present (e.g. audio/ogg; codecs=opus, audio/mp4; codecs=mp4a.40.2,opus, video/mp4; codecs="avc1.42e01e,mp4a.40.2"); it remains the only parameter the helper round-trips, and malformed or injection-shaped values fall through to the strip-all-parameters path.

Non-goals

  • No runtime behavior change. No call site is migrated in this PR.
  • No broad cross-channel sweep. No outbound MIME normalization is applied anywhere yet.
  • No speculative helper options. No expectedPrefix, no isVerifiedMediaSource(media, kind) generalization, no header-level helper.

Follow-up candidates

Adopter PRs (outbound paths and core audio-source classification call sites) will land separately, each small and individually revertable.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Root Cause (if applicable)

N/A — additive helper surface, no behavior fixed.

  • Root cause: N/A
  • Missing detection / guardrail: N/A
  • Contributing context (if known): N/A

Regression Test Plan (if applicable)

N/A — no regression; helpers have no caller in this PR.

  • Coverage level that should have caught this:
    • Unit test (for the helpers themselves)
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: src/media/mime.test.ts
  • Scenario the test should lock in: helper input/output contracts (RFC 2045 token shape, control-character rejection, parameter handling, audio-kind classification trust model)
  • Why this is the smallest reliable guardrail: helpers are pure functions with no I/O; unit tests are sufficient and proportionate
  • Existing test that already covers this (if any): none — these are new helpers
  • If no new test is added, why not: N/A

User-visible / Behavior Changes

None.

Diagram (if applicable)

N/A.

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No
  • If any Yes, explain risk + mitigation: N/A

No runtime security impact; this PR adds security-relevant helper surface only, and nothing calls it.

Repro + Verification

Environment

  • OS: Linux
  • Runtime/container: Node 22+, pnpm
  • Model/provider: N/A
  • Integration/channel (if any): N/A
  • Relevant config (redacted): default

Steps

  1. pnpm install
  2. pnpm check:changed (covers tsgo:core, tsgo:core:test, oxlint, vitest changed lanes)
  3. pnpm test src/media/mime.test.ts
  4. pnpm exec oxfmt --check --threads=1 src/media/mime.ts src/media/mime.test.ts

Expected

  • pnpm check:changed passes; targeted vitest run reports the new helper assertions pass; oxfmt check is clean.

Actual

  • All of the above pass locally.

Evidence

  • Failing test/log before + passing after (new tests pass; no prior coverage to fail)
  • Real-behavior trace — see ## Real behavior proof section below.
  • Screenshot/recording
  • Perf numbers (if relevant)

Real behavior proof

Captured 2026-05-17 against branch pr/openclaw-media-helper-bundle head 9fbe30b0a3 (= base fb83e6a03f + new commit 9fbe30b0a3 fix(plugin-sdk): expose isVerifiedAudioSource and sanitizeMediaMime via media-mime subpath).

Behavior or issue addressed: the two new helpers (isVerifiedAudioSource, sanitizeMediaMime) added in src/media/mime.ts are intended for plugin code to consume through the narrow public alias openclaw/plugin-sdk/media-mime (the same alias that already re-exports detectMime, extensionForMime, getFileExtension, mediaKindFromMime, normalizeMimeType). The original commit on this PR only added the helpers to src/media/mime.ts and did not extend the re-export list, so plugin code could not reach them from the public SDK surface — addressing the Codex P2 finding about the missing SDK export.

Real environment tested: Linux x86_64 local dev workspace (Node 22, pnpm 10.33.2) running against this branch's actual source tree. The proof script resolves the import alias openclaw/plugin-sdk/media-mime through the real plugin-sdk path map (the same resolver bundled plugins use at runtime), and the script is run via tsx — no mocks, no test framework, no stubs.

Exact steps or command run after this patch:

  1. Add the two helpers (isVerifiedAudioSource, sanitizeMediaMime) to the re-export list in src/plugin-sdk/media-mime.ts.
  2. Import them in a standalone script using the public alias openclaw/plugin-sdk/media-mime (scripts/proof-media-helper-narrow-subpath.ts, not committed).
  3. Invoke both helpers against well-formed, malformed/unsafe, and trust-model inputs.
  4. Probe the resolved module's full export shape to confirm the new exports appear next to the existing ones.

Concrete commands run on the rebased branch:

$ node node_modules/.bin/tsx scripts/proof-media-helper-narrow-subpath.ts
$ CI=true pnpm test src/media/mime.test.ts
$ CI=true pnpm check:changed

Evidence after fix:

Terminal transcript from the standalone SDK-subpath probe (actual production helpers resolved via the real plugin-sdk alias and invoked against fixed inputs, no test framework involved):

=== narrow SDK subpath: openclaw/plugin-sdk/media-mime ===
  typeof isVerifiedAudioSource                                 -> "function"
  typeof sanitizeMediaMime                                     -> "function"

=== sanitizeMediaMime — well-formed inputs (preserveCodecsParam=true) ===
  sanitizeMediaMime("Audio/OGG; codecs=opus", {preserveCodecsParam:true}) -> "audio/ogg; codecs=opus"
  sanitizeMediaMime("video/mp4; codecs=avc1.42e01e,mp4a.40.2", {preserveCodecsParam:true}) -> "video/mp4; codecs=avc1.42e01e,mp4a.40.2"
  sanitizeMediaMime("video/mp4; codecs=\"avc1.42e01e,mp4a.40.2\"", {preserveCodecsParam:true}) -> "video/mp4; codecs=\"avc1.42e01e,mp4a.40.2\""

=== sanitizeMediaMime — default (strip all parameters) ===
  sanitizeMediaMime("application/pdf; charset=utf-8; foo=bar") -> "application/pdf"
  sanitizeMediaMime("application/vnd.x~v1+json")               -> "application/vnd.x~v1+json"

=== sanitizeMediaMime — malformed / unsafe inputs (must return null) ===
  sanitizeMediaMime("image/png\r\nX-Injected: 1")              -> null
  sanitizeMediaMime("image/png\x00evil")                       -> null
  sanitizeMediaMime("not-a-mime")                              -> null
  sanitizeMediaMime(null)                                      -> null
  sanitizeMediaMime(undefined)                                 -> null
  sanitizeMediaMime("")                                        -> null

=== isVerifiedAudioSource — trust model ===
  isVerifiedAudioSource({kind:"audio", contentType:"text/plain"}) -> true
  isVerifiedAudioSource({contentType:"audio/ogg"})  // no kind -> reject -> false
  isVerifiedAudioSource({kind:"image", contentType:"audio/ogg"}) -> false
  isVerifiedAudioSource({kind:"audio"})                        -> true
  isVerifiedAudioSource({})                                    -> false

=== import-shape probe ===
  Object.keys(import * from 'openclaw/plugin-sdk/media-mime')  -> ["detectMime","extensionForMime","getFileExtension","isVerifiedAudioSource","mediaKindFromMime","normalizeMimeType","sanitizeMediaMime"]

Observed result after fix:

  • Both helpers resolve from openclaw/plugin-sdk/media-mime (the import-shape probe lists both alongside the previously-exposed detectMime / extensionForMime / getFileExtension / mediaKindFromMime / normalizeMimeType).
  • isVerifiedAudioSource honors the documented trust model: {kind:"audio"} → true; raw {contentType:"audio/ogg"} (no kind) → false; mismatched {kind:"image", contentType:"audio/ogg"} → false; {} → false. URL/extension hints cannot promote a payload to "audio".
  • sanitizeMediaMime strips control characters (CRLF, NUL, DEL) by returning null, accepts RFC 2045 tokens including vendor types like application/vnd.x~v1+json, normalizes case (Audio/OGGaudio/ogg), strips unrelated parameters by default, and preserves codecs=... (single, comma-separated multi-codec, and matched-quote forms) when preserveCodecsParam:true is passed. Malformed inputs and non-MIME strings return null.

Against the prior commit fb83e6a03f the same import statement would fail with a TS / module-resolution error because the two helpers were absent from the re-export list — the public SDK surface had no path to them.

What was not tested:

  • Cross-channel integration with adapters (e.g. WhatsApp, Telegram, Matrix) — this PR is helper-only by design and intentionally has no caller; channel adoption is staged in follow-up PRs (B-4 through B-9 per the PR scope split rule).
  • Runtime behavior of the built dist/ artifact — the proof imports from the alias-resolved source. pnpm check:changed ran tsgo-typecheck-all on the changed surface so the public type shape is validated against consumers.
  • Live OpenClaw gateway end-to-end consumption — no caller exists yet in this PR; that is the boundary the follow-up adapter PRs will exercise.

Human Verification (required)

  • Verified scenarios:
    • isVerifiedAudioSource returns true only for kind === "audio"; explicit negative cases for audio/-prefixed contentType alone, missing kind, and unrelated kinds.
    • sanitizeMediaMime rejects ASCII control characters (\r\n, \n, \t, \x00, \x7f), accepts the RFC 2045 token character set including ~, %, * for valid vendor types, and preserves common single-codec, multi-codec, and quoted codecs=... parameters when preserveCodecsParam: true is passed (e.g. codecs=opus, codecs=mp4a.40.2,opus, codecs="avc1.42e01e,mp4a.40.2"); malformed or injection-shaped values (mismatched quotes, trailing comma, semicolon inside a quoted value) fall through to the strip-all-parameters path.
    • Local pnpm check:changed (core + core test lanes) was run and reported clean.
  • Edge cases checked:
    • contentType: "audio/ogg" alone is not enough to be a verified audio source.
    • application/vnd.example~v1+json is not falsely rejected by the MIME validation regex.
    • audio/ogg; charset=utf-8 is normalized to audio/ogg (parameters stripped by default).
  • What you did not verify: cross-channel integration (intentionally out of scope; helpers have no caller in this PR).

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes (additive surface only)
  • Config/env changes? No
  • Migration needed? No
  • If yes, exact upgrade steps: N/A

Risks and Mitigations

  • Risk: a future caller adopts the helpers in a way that diverges from the trust model documented here.
    • Mitigation: follow-up adopter PRs must include call-site tests proving the intended trust boundary at the actual call point; reviewers can match adopting code against the helper JSDoc.

@clawsweeper

clawsweeper Bot commented May 3, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge.

Summary
The PR adds shared media MIME helper APIs, unit coverage, a focused plugin SDK export, a regenerated SDK API baseline hash, and a changelog entry.

Reproducibility: not applicable. This PR adds helper/API surface and intentionally has no runtime caller. Source inspection and the PR body show the current branch adds unit coverage plus terminal import proof for the new SDK subpath exports.

Real behavior proof
Sufficient (terminal): The PR body includes terminal proof showing both helpers imported through openclaw/plugin-sdk/media-mime and exercised against representative valid, invalid, and trust-model inputs after the export fix.

Next step before merge
No narrow automated repair remains after the baseline update; a maintainer should review the public SDK contract and require normal final gates before merge.

Security
Cleared: The diff adds pure helper code, tests, an SDK export, a generated API hash, and a changelog line with no new dependencies, scripts, permissions, secrets handling, network calls, or runtime callers.

Review details

Best possible solution:

Land the helper-only SDK addition if maintainers approve the API shape, keeping channel/adopter migrations in separate focused PRs with call-site tests.

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

Not applicable: this PR adds helper/API surface and intentionally has no runtime caller. Source inspection and the PR body show the current branch adds unit coverage plus terminal import proof for the new SDK subpath exports.

Is this the best way to solve the issue?

Mostly yes: the helper-only split matches the related maintainer direction, and the latest branch includes the narrow SDK export plus generated API hash update. The final decision is whether this exact public SDK helper shape should be supported.

Acceptance criteria:

  • node scripts/run-vitest.mjs src/media/mime.test.ts
  • pnpm plugin-sdk:api:check
  • pnpm check:changed

What I checked:

  • Current main lacks the helpers: Current main's src/media/mime.ts ends the relevant helper area at kindFromMime, and repository search found no isVerifiedAudioSource or sanitizeMediaMime definitions on main. (src/media/mime.ts:275, 4aa671b71a1a)
  • PR diff adds helper implementation and tests: The head diff adds isVerifiedAudioSource, sanitizeMediaMime, control-character rejection, MIME token validation, codecs parameter handling, and focused tests for the trust model and MIME normalization. (src/media/mime.ts:276, eab493b0c3d2)
  • Public SDK export and baseline are now included: The latest PR file list includes src/plugin-sdk/media-mime.ts exporting both new helpers and docs/.generated/plugin-sdk-api-baseline.sha256 updating the two tracked baseline hashes, which addresses the earlier ClawSweeper P2 finding. (src/plugin-sdk/media-mime.ts:4, eab493b0c3d2)
  • SDK API baseline contract: The SDK API generator hashes the generated JSON and JSONL baseline artifacts into docs/.generated/plugin-sdk-api-baseline.sha256, and package.json exposes plugin-sdk:api:check / plugin-sdk:api:gen as the drift gate for public SDK changes. (src/plugin-sdk/api-baseline.ts:501, 4aa671b71a1a)
  • Related split context: In the close note on fix(whatsapp): respect audioAsVoice flag in outbound delivery #68744, steipete landed the narrower WhatsApp audio-as-voice fix in c2a2a481b2e15a27c8c0d290b17150d1076d78ae and explicitly said new public SDK helper surface and cross-channel MIME hardening should be split and reviewed separately. (c2a2a481b2e1)
  • Real behavior proof supplied: The PR body includes terminal proof from a real local source tree importing both helpers through openclaw/plugin-sdk/media-mime, exercising valid, invalid, and trust-model inputs, and showing the import shape includes the new exports. (eab493b0c3d2)

Likely related people:

  • steipete: Current-main blame on the sampled media and plugin-sdk/media-mime files points to Peter Steinberger's recent commit, and the related WhatsApp PR close note from steipete explicitly split this helper/API surface for separate review. (role: recent area contributor and related split decision owner; confidence: high; commits: 9616aa6e5a85, c2a2a481b2e1; files: src/media/mime.ts, src/plugin-sdk/media-mime.ts, extensions/whatsapp/src/send.ts)
  • bobbyt74: Recent history shows a WhatsApp MIME fallback change touching src/media/mime.ts, its tests, and WhatsApp send paths, which are adjacent to the new helper contracts. (role: adjacent MIME helper contributor; confidence: medium; commits: cae1d9bc6d6c; files: src/media/mime.ts, src/media/mime.test.ts, extensions/whatsapp/src/send.ts)
  • Devin Robison: Recent audioAsVoice/media-path history includes a media reply path fix touching src/media/web-media.ts and related tests, which is adjacent to the caller side that future adopter PRs may use. (role: adjacent media payload contributor; confidence: low; commits: e420468ebd9c; files: src/media/web-media.ts, src/media/web-media.test.ts, src/auto-reply/reply/reply-media-paths.ts)

Remaining risk / open question:

  • The helper names and signatures become public SDK surface, so maintainer API-contract approval is still needed before merge.
  • This PR intentionally has no runtime callers; channel adoption and end-to-end behavior remain follow-up work.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 4aa671b71a1a.

@openclaw-barnacle openclaw-barnacle Bot added triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. proof: supplied External PR includes structured after-fix real behavior proof. and removed triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels May 17, 2026
@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. P3 Low-priority cleanup, docs, polish, ergonomics, or speculative work. impact:security Security boundary, credential, authz, sandbox, or sensitive-data risk. labels May 17, 2026
…ia media-mime subpath

The two new helpers added to src/media/mime.ts in the first commit on this
branch are only consumable from internal core paths. Bundled plugins consume
the narrow subpath openclaw/plugin-sdk/media-mime, which re-exports a fixed
list of MIME helpers from src/media/mime.js but did not include the two new
ones. Without this entry plugins cannot import them from the public SDK
surface — addressing the Codex P2 finding on this PR.
Two helpers added to openclaw/plugin-sdk/media-mime in the prior commit
(isVerifiedAudioSource, sanitizeMediaMime) intentionally extend the public
SDK surface. Update docs/.generated/plugin-sdk-api-baseline.sha256 so the
drift gate (pnpm plugin-sdk:api:check) reflects the new exports.

Addresses Codex P2 finding on PR openclaw#76566.
@masatohoshino masatohoshino force-pushed the pr/openclaw-media-helper-bundle branch from 9fbe30b to eab493b Compare May 17, 2026 11:20
@openclaw-barnacle openclaw-barnacle Bot added the docs Improvements or additions to documentation label May 17, 2026
@masatohoshino

Copy link
Copy Markdown
Contributor Author

Closing this PR to reduce maintainer review load.

This PR adds shared media MIME validation helpers and a plugin-SDK export, but it has no runtime caller in this PR. The intended follow-up adapter migrations have not materialized, and the PR has since accumulated conflict / review-staleness.

Given the current OpenClaw review shape, an unused helper-only surface is not the strongest PR form. I’d rather re-submit this bundled with its first concrete adapter caller if an outbound MIME-validation gap appears in a specific channel.

Nothing is lost here; the code remains available in git history. Closing to reduce P3 shelf noise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Improvements or additions to documentation impact:security Security boundary, credential, authz, sandbox, or sensitive-data risk. P3 Low-priority cleanup, docs, polish, ergonomics, or speculative work. proof: sufficient ClawSweeper judged the real behavior proof convincing. proof: supplied External PR includes structured after-fix real behavior proof. size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant