openclaw - ✅(Solved) Fix [Bug]: Local assistant attachments shown as \"Unavailable — Outside allowed folders\" despite correct server config [1 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#67915Fetched 2026-04-17 08:28:57
View on GitHub
Comments
0
Participants
1
Timeline
3
Reactions
0
Participants
Timeline (top)
labeled ×2cross-referenced ×1

Local assistant media attachments (e.g. TTS audio in MEDIA: lines) render as "Unavailable — Outside allowed folders" in Control UI even when the server's localMediaPreviewRoots includes the path and the server-side assistant-media?meta=1 endpoint confirms {\"available\": true}.

Root Cause

But the UI renders: "Unavailable — Outside allowed folders"\n\n\nRoot cause traced to ui/src/ui/chat/grouped-render.ts in resolveAssistantAttachmentAvailability():

Fix Action

Fixed

PR fix notes

PR #67916: fix(ui): don't block local attachments before bootstrap roots load

Description (problem / solution / changelog)

Summary

  • Problem: Local assistant media attachments (TTS audio, images, documents via MEDIA: lines) render as "Unavailable — Outside allowed folders" in Control UI even when the server config and meta endpoint confirm the path is valid.
  • Why it matters: Users cannot play, preview, or download assistant-generated local media in the webchat UI — the primary interactive surface.
  • What changed: Skip the client-side allowed-folder gate when localMediaPreviewRoots is empty (not yet loaded from bootstrap), falling through to the authoritative server-side meta check. Also harden bootstrap config caching on both client and server.
  • What did NOT change (scope boundary): The isLocalAttachmentPreviewAllowed logic itself is untouched — it still blocks paths outside configured roots once roots are populated. The sendJson helper's default Cache-Control for other endpoints (no-cache) is unchanged.

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

  • Closes #67915
  • This PR fixes a bug or regression

Root Cause (if applicable)

  • Root cause: resolveAssistantAttachmentAvailability() in ui/src/ui/chat/grouped-render.ts calls isLocalAttachmentPreviewAllowed(source, localMediaPreviewRoots) as a hard gate before the server-side meta check. When localMediaPreviewRoots is [] (bootstrap config fetch not yet complete), every local path fails this check and returns { status: "unavailable", reason: "Outside allowed folders" } without ever reaching the server.
  • Missing detection / guardrail: No test covers the case where roots are empty (bootstrap still loading) and a valid local attachment is checked. The existing "blocked" test correctly provides non-empty roots with a path outside them.
  • Contributing context (if known): The bootstrap config fetch is async and completes after first render. The allowed-folder check was designed as a client-side optimisation to avoid unnecessary server round-trips, but didn't account for the empty-roots bootstrap window.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: ui/src/ui/views/chat.test.ts
  • Scenario the test should lock in: A local MEDIA: attachment checked against an empty localMediaPreviewRoots array should fall through to the server meta check (status "checking"), not hard-deny as "Outside allowed folders".
  • Why this is the smallest reliable guardrail: The render-level test already exercises resolveAssistantAttachmentAvailability through the full render path and can assert the absence of "Outside allowed folders" when roots are empty.
  • Existing test that already covers this (if any): The existing "blocks local attachments outside preview roots" test covers the populated-roots case. No existing test covers the empty-roots case.
  • If no new test is added, why not: Vitest execution is blocked in the current CI-adjacent pod environment by Error: No such built-in module: node: (Node v24 + Vitest environment incompatibility). The test scenario is documented above for addition once the environment issue is resolved or in CI.

User-visible / Behavior Changes

  • Local assistant attachments that previously appeared as "Unavailable — Outside allowed folders" on page load will now show as "Checking..." briefly, then render correctly once the server-side meta check completes.
  • No config or API changes. No new environment variables.

Diagram (if applicable)

Before:
[page load] -> [attachments render] -> [roots=[]] -> isLocalAttachmentPreviewAllowed() -> false -> "Outside allowed folders" (STUCK)

After:
[page load] -> [attachments render] -> [roots=[]] -> skip client gate -> server meta check -> "available" ✓
                                        [bootstrap loads roots] -> future checks use client gate normally

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No new calls. When roots are empty, attachments that previously hard-denied now fall through to the existing assistant-media?meta=1 server endpoint, which performs the authoritative access check server-side. This is the same endpoint used for all other attachment availability checks.
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: Debian 12 (bookworm) / Linux 6.12.0 (OpenShift pod)
  • Runtime/container: Node v24.14.0, OpenClaw 2026.4.12–2026.4.16
  • Model/provider: Any (bug is in UI rendering path)
  • Integration/channel (if any): Control UI webchat
  • Relevant config (redacted): Default config with TTS enabled producing local media under ~/.openclaw/media/outbound/

Steps

  1. Run OpenClaw with Control UI and TTS enabled.
  2. Trigger an assistant response that includes a MEDIA:/path/to/local/file.mp3 line.
  3. Observe the attachment card in webchat on initial page load.

Expected

  • Attachment renders as playable audio with player controls and download link.

Actual

  • Attachment renders as "Unavailable — Outside allowed folders" with a blocked status card, despite the server confirming {"available": true}.

Evidence

  • Trace/log snippets

Server-side verification (all return expected results):

GET /__openclaw__/assistant-media?source=/home/node/.openclaw/media/outbound/8ad46428-7c81-412e-9df8-e1dbab658b57.mp3&meta=1
→ 200 {"available":true}

GET /__openclaw__/assistant-media?source=/home/node/.openclaw/media/outbound/8ad46428-7c81-412e-9df8-e1dbab658b57.mp3
→ 200 audio/mpeg

GET /__openclaw__/control-ui-config.json
→ 200 { "localMediaPreviewRoots": ["/home/node/.openclaw/media", ...] }

UI shows "Unavailable — Outside allowed folders" because the bootstrap config hadn't loaded when the attachment first rendered.

Human Verification (required)

  • Verified scenarios: Traced the full code path from resolveAssistantAttachmentAvailability through isLocalAttachmentPreviewAllowed with empty vs populated roots. Confirmed server-side endpoints return correct results. Confirmed the sendJson default for other callers is unchanged.
  • Edge cases checked: (1) Empty roots skips client gate → falls through to server check. (2) Populated roots with path outside them → still blocks as before. (3) Non-local sources (URLs) → still return available immediately. (4) sendJson callers other than bootstrap (availability, avatar meta) → unchanged no-cache default.
  • What you did not verify: Full Vitest test suite execution (blocked by Node v24 environment issue in pod). CI should cover this.

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
  • Config/env changes? No
  • Migration needed? No

Risks and Mitigations

  • Risk: During the bootstrap loading window, attachments now make server-side meta requests instead of being client-side denied. This adds a brief round-trip per local attachment on page load.
    • Mitigation: The meta endpoint is lightweight (JSON response, no file I/O beyond stat), the window is sub-second, and this is the same endpoint already used for all attachment checks that pass the client gate. The previous behavior (permanent false denial) is worse than a brief loading state.

Changed files

  • src/gateway/control-ui.http.test.ts (modified, +10/-2)
  • src/gateway/control-ui.ts (modified, +23/-18)
  • ui/src/ui/chat/grouped-render.ts (modified, +7/-1)
  • ui/src/ui/controllers/control-ui-bootstrap.test.ts (modified, +3/-3)
  • ui/src/ui/controllers/control-ui-bootstrap.ts (modified, +1/-0)

Code Example

# Server-side meta endpoint confirms attachment is available:
GET /__openclaw__/assistant-media?source=/home/node/.openclaw/media/outbound/8ad46428-7c81-412e-9df8-e1dbab658b57.mp3&meta=1
200 {\"available\":true}

# Raw media fetch works:
GET /__openclaw__/assistant-media?source=/home/node/.openclaw/media/outbound/8ad46428-7c81-412e-9df8-e1dbab658b57.mp3
200 audio/mpeg (valid bytes)

# Bootstrap config includes the correct root:
GET /__openclaw__/control-ui-config.json
200 { \"localMediaPreviewRoots\": [\"/home/node/.openclaw/media\", ...] }

# But the UI renders: \"UnavailableOutside allowed folders\"\n\n\nRoot cause traced to `ui/src/ui/chat/grouped-render.ts` in `resolveAssistantAttachmentAvailability()`:

// Line 747 — runs BEFORE bootstrap config has loaded (roots still [])
if (!isLocalAttachmentPreviewAllowed(source, localMediaPreviewRoots)) {
  return { status: \"unavailable\", reason: \"Outside allowed folders\", checkedAt: Date.now() };
}


When `localMediaPreviewRoots` is `[]` (bootstrap config fetch not yet complete), `isLocalAttachmentPreviewAllowed()` returns `false` for every local path. This hard-returns before reaching the server-side meta check, which is authoritative and would return `available: true`
RAW_BUFFERClick to expand / collapse

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No

Summary

Local assistant media attachments (e.g. TTS audio in MEDIA: lines) render as "Unavailable — Outside allowed folders" in Control UI even when the server's localMediaPreviewRoots includes the path and the server-side assistant-media?meta=1 endpoint confirms {\"available\": true}.

Steps to reproduce

  1. Run OpenClaw with Control UI enabled and a config that produces local media (e.g. TTS generating .mp3 files under ~/.openclaw/media/outbound/).
  2. Trigger an assistant response that includes a MEDIA:/home/node/.openclaw/media/outbound/<uuid>.mp3 line.\n
  3. Observe the attachment card in the Control UI webchat

Expected behavior

The attachment renders as playable audio with a working player and download link, since the path is inside the server-configured localMediaPreviewRoots and the server-side meta check returns available: true

Actual behavior

The attachment renders as "Unavailable — Outside allowed folders" with a blocked status card. The server-side meta endpoint (/__openclaw__/assistant-media?source=...&meta=1) returns {\"available\": true} and the raw media endpoint returns 200 audio/mpeg, confirming the server considers the path valid

OpenClaw version

2026.4.15

Operating system

Debian 12 (bookworm) on Linux 6.12.0 / OpenShift pod

Install method

docker (OpenShift)

Model

Any (not model-dependent — bug is in the UI rendering path)

Provider / routing chain

UI bug

Additional provider/model setup details

No response

Logs, screenshots, and evidence

# Server-side meta endpoint confirms attachment is available:
GET /__openclaw__/assistant-media?source=/home/node/.openclaw/media/outbound/8ad46428-7c81-412e-9df8-e1dbab658b57.mp3&meta=1
200 {\"available\":true}

# Raw media fetch works:
GET /__openclaw__/assistant-media?source=/home/node/.openclaw/media/outbound/8ad46428-7c81-412e-9df8-e1dbab658b57.mp3
200 audio/mpeg (valid bytes)

# Bootstrap config includes the correct root:
GET /__openclaw__/control-ui-config.json
200 { \"localMediaPreviewRoots\": [\"/home/node/.openclaw/media\", ...] }

# But the UI renders: \"Unavailable — Outside allowed folders\"\n\n\nRoot cause traced to `ui/src/ui/chat/grouped-render.ts` in `resolveAssistantAttachmentAvailability()`:

// Line 747 — runs BEFORE bootstrap config has loaded (roots still [])
if (!isLocalAttachmentPreviewAllowed(source, localMediaPreviewRoots)) {
  return { status: \"unavailable\", reason: \"Outside allowed folders\", checkedAt: Date.now() };
}


When `localMediaPreviewRoots` is `[]` (bootstrap config fetch not yet complete), `isLocalAttachmentPreviewAllowed()` returns `false` for every local path. This hard-returns before reaching the server-side meta check, which is authoritative and would return `available: true`

Impact and severity

  • Affected: All Control UI webchat users viewing assistant responses that include local media attachments (TTS audio, generated images, documents)
  • Severity: Medium (attachments appear broken despite being fully available server-side)
  • Frequency: Deterministic on page load / session start when attachments render before bootstrap config completes
  • Consequence: Local assistant media is inaccessible in the UI until a manual page refresh (and even then, only if refresh timing allows bootstrap to complete before render)

Additional information

The client-side isLocalAttachmentPreviewAllowed check is a UX optimisation to avoid unnecessary server round-trips for paths clearly outside allowed roots. However, it should not hard-deny when roots haven't loaded yet — the server-side meta=1 endpoint should be the fallback authority.

A secondary factor is that the bootstrap config endpoint was served with only Cache-Control: no-cache. While not the primary cause, no-store would be more appropriate for a config endpoint whose values change between sessions."

Additional information

No response

extent analysis

TL;DR

The issue can be fixed by modifying the resolveAssistantAttachmentAvailability() function to wait for the bootstrap config to load before checking localMediaPreviewRoots, or by using the server-side meta check as a fallback when localMediaPreviewRoots is empty.

Guidance

  • Verify that the localMediaPreviewRoots array is populated before checking if a local attachment is allowed, to avoid false negatives.
  • Consider adding a check to see if the bootstrap config has finished loading before rendering attachments, to ensure that localMediaPreviewRoots is up-to-date.
  • If localMediaPreviewRoots is empty, use the server-side meta check as a fallback to determine attachment availability, rather than hard-returning "unavailable".
  • Review the caching configuration for the bootstrap config endpoint to ensure it is set to no-store to prevent stale values from being used.

Example

// Modified resolveAssistantAttachmentAvailability() function
if (localMediaPreviewRoots.length === 0) {
  // Use server-side meta check as fallback if roots haven't loaded yet
  return fetchServerSideMetaCheck(source);
} else {
  return isLocalAttachmentPreviewAllowed(source, localMediaPreviewRoots);
}

Notes

The issue is caused by the client-side isLocalAttachmentPreviewAllowed check running before the bootstrap config has finished loading, resulting in an empty localMediaPreviewRoots array. This can be mitigated by waiting for the config to load or using the server-side meta check as a fallback.

Recommendation

Apply a workaround by modifying the resolveAssistantAttachmentAvailability() function to wait for the bootstrap config to load or use the server-side meta check as a fallback, as this will allow local media attachments to be rendered correctly in the Control UI.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

The attachment renders as playable audio with a working player and download link, since the path is inside the server-configured localMediaPreviewRoots and the server-side meta check returns available: true

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING