feat(ui): render assistant directives and add embed tag#64104
Conversation
Greptile SummaryThis PR wires up embed rendering for Control UI assistant output: it normalizes
Confidence Score: 3/5Not safe to merge as-is: one security-relevant path traversal and a behavioral regression in gateway stage error isolation both need to be fixed first. Two P1 findings: (1) resolveCanvasHttpPathToLocalPath lacks normalizeCanvasDocumentId validation on the extracted documentId, allowing .. to escape the documents sandbox — reachable from both HTTP and agent-generated MEDIA paths; (2) the removal of per-stage error isolation in runGatewayHttpRequestStages re-introduces the failure mode the old guard was explicitly written to prevent. A P2 covers silent error swallowing in the outer catch. The rest of the feature logic looks correct. src/gateway/canvas-documents.ts (path traversal fix), src/gateway/server-http.ts (stage error isolation and outer catch logging)
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3d8cb6b364
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
3d8cb6b to
8ef1415
Compare
🔒 Aisle Security AnalysisWe found 10 potential security issue(s) in this PR:
1. 🟠 Configurable iframe sandbox 'trusted' mode enables same-origin script execution for assistant-controlled canvas content
DescriptionThe new When this mode is enabled via Control UI bootstrap config, canvas previews are rendered in an This becomes a stored-XSS style escalation if an attacker can influence canvas document HTML (e.g. via tool outputs / assistant-generated HTML bundles written by
Vulnerable code: case "trusted":
return "allow-scripts allow-same-origin";RecommendationAvoid Safer options:
case "trusted":
// still allow scripts, but keep unique origin isolation
return "allow-scripts";
2. 🟠 Hardlink-based local file disclosure via /__openclaw__/assistant-media attachment endpoint
DescriptionThe new An attacker who can create a hardlink inside any allowed media root (e.g., a workspace or state/media directory) pointing to a sensitive file elsewhere on the filesystem can then request the hardlink path via this endpoint and have the gateway stream the target file contents. Why this is exploitable:
Vulnerable code paths:
RecommendationReject hardlinks (and ideally enforce root containment at open time) for this endpoint. Option A (minimal change): add a hardlink rejection check after opening: const opened = await openLocalFileSafely({ filePath: source });
if (opened.stat.nlink > 1) {
await opened.handle.close().catch(() => {});
throw new SafeOpenError("invalid-path", "hardlinked path not allowed");
}Option B (preferred): introduce an Also consider folding the allowlist check into the open operation (e.g., by opening via a root-aware function) to avoid TOCTOU between allowlist verification and open. 3. 🟠 Local file exfiltration via mediaSources-driven expansion of allowed local media roots
Description
Vulnerable code: for (const source of mediaSources ?? []) {
const localPath = resolveLocalMediaPath(source);
...
const parentDir = path.dirname(localPath);
...
appended.push(path.resolve(parentDir));
}Even though filesystem root ( RecommendationDo not widen Safer options:
Example: only allow expansion when the resolved parent is inside the workspace root: export function appendLocalMediaParentRoots(
roots: readonly string[],
mediaSources: readonly string[] | undefined,
workspaceRoot: string,
): string[] {
const appended = Array.from(new Set(roots.map((r) => path.resolve(r))));
for (const source of mediaSources ?? []) {
const localPath = resolveLocalMediaPath(source);
if (!localPath) continue;
const parent = path.resolve(path.dirname(localPath));
const rel = path.relative(workspaceRoot, parent);
if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) continue;
if (!appended.includes(parent)) appended.push(parent);
}
return appended;
}Also consider rejecting 4. 🟠 Script injection via unvalidated URL schemes in generated PDF wrapper (canvas documents)
Description
Vulnerable code: function isPdfPathLike(value: string): boolean {
return /\.pdf(?:[?#].*)?$/i.test(value.trim());
}
function buildPdfWrapper(url: string): string {
const escaped = escapeHtml(url);
return `...<object data="${escaped}" ...><iframe src="${escaped}" ...></iframe>...`;
}RecommendationValidate and restrict the allowed URL schemes before generating the wrapper. For example, only allow function assertSafeExternalPdfUrl(raw: string): string {
const url = new URL(raw);
if (url.protocol !== "http:" && url.protocol !== "https:") {
throw new Error("Unsupported PDF URL scheme");
}
return url.toString();
}
if (entrypoint.type === "url" && input.kind === "document" && isPdfPathLike(entrypoint.value)) {
const safePdfUrl = assertSafeExternalPdfUrl(entrypoint.value);
await fs.writeFile(path.join(rootDir, "index.html"), buildPdfWrapper(safePdfUrl), "utf8");
...
}Additionally consider adding a restrictive CSP to canvas-served HTML wrappers (e.g., disallow 5. 🟠 Canvas host capability token now granted to non-node WS clients and accepted for HTTP auth (bearer token in URL)
DescriptionThe gateway's canvas host protection was weakened from node-only to any connected WS client, and the WS handshake now mints a canvas capability token for any client when This creates a broken-access-control condition:
Vulnerable behavior is visible in the changed code: // ws handshake: capability minted for any client
const canvasCapability = canvasHostUrl ? mintCanvasCapabilityToken() : undefined;
...
canvasHostUrl: scopedCanvasHostUrl,// http auth: capability accepted if any WS client has it
if (canvasCapability && hasAuthorizedWsClientForCanvasCapability(clients, canvasCapability)) {
return { ok: true };
}Impact depends on what the canvas host exposes, but at minimum it allows any authorized browser WS client to obtain a capability URL that bypasses normal HTTP bearer-token auth for the canvas routes, and the capability can be replayed by any party who learns it within its TTL (with sliding renewal). RecommendationRe-establish a strict trust boundary for canvas-host access. Options (choose one consistent with the intended threat model):
// ws handshake
const canvasCapability = role === "node" && canvasHostUrl ? mintCanvasCapabilityToken() : undefined;// http-auth: ensure capability belongs to a node client
if (canvasCapability && hasAuthorizedNodeWsClientForCanvasCapability(clients, canvasCapability)) {
return { ok: true };
}
Also consider removing sliding renewal or additionally binding the capability to the specific WS 6. 🟠 Arbitrary local file copy into web-served canvas documents via path-based entrypoints/assets
Description
and then copies the referenced host file into the canvas document directory. If an attacker (LLM/agent output, plugin, or other untrusted caller) can influence Vulnerable code: const sourcePath = asset.sourcePath.startsWith("~")
? resolveUserPath(asset.sourcePath)
: path.isAbsolute(asset.sourcePath)
? path.resolve(asset.sourcePath)
: path.resolve(workspaceDir, asset.sourcePath);
...
await fs.copyFile(sourcePath, destination);and: const resolvedPath = entrypoint.value.startsWith("~")
? resolveUserPath(entrypoint.value)
: path.isAbsolute(entrypoint.value)
? path.resolve(entrypoint.value)
: path.resolve(workspaceDir, entrypoint.value);
...
await fs.copyFile(resolvedPath, path.join(rootDir, fileName));RecommendationRestrict what host paths can be copied into canvas documents. Recommended approach:
Example hardening (workspace-only): function resolveWorkspaceOnlyPath(workspaceDir: string, inputPath: string): string {
if (inputPath.startsWith("~") || path.isAbsolute(inputPath)) {
throw new Error("absolute paths are not allowed");
}
const candidate = path.resolve(workspaceDir, inputPath);
const workspaceRoot = path.resolve(workspaceDir);
if (!(candidate === workspaceRoot || candidate.startsWith(workspaceRoot + path.sep))) {
throw new Error("path escapes workspace");
}
return candidate;
}
const resolvedPath = resolveWorkspaceOnlyPath(workspaceDir, entrypoint.value);
await fs.copyFile(resolvedPath, path.join(rootDir, path.basename(resolvedPath)));If non-workspace roots are required, replace the workspace check with an allowlisted roots check (e.g., 7. 🟡 Client-side memory growth due to unbounded tool-card expansion state caches
DescriptionThe chat view maintains per-session caches for tool-card expansion state ( In
An attacker-controlled prompt/tool output (or simply a very large/long-running session) that causes many tool cards/messages can make these caches grow indefinitely, degrading UI performance or exhausting memory (availability impact). RecommendationPrune per-session expansion caches to the current message set (or cap their size) so they cannot grow without bound. Example: after collecting for (const key of expanded.keys()) {
if (!currentToolCardIds.has(key)) expanded.delete(key);
}
for (const key of initialized.keys()) {
if (!currentToolCardIds.has(key)) initialized.delete(key);
}Also consider:
8. 🟡 Sensitive gateway auth token exposed via URL query parameter for assistant media
DescriptionThe Control UI generates assistant-media URLs that embed the gateway authentication token as a Why this is a problem:
Vulnerable code (client): const params = new URLSearchParams({ source });
const normalizedToken = authToken?.trim();
if (normalizedToken) {
params.set("token", normalizedToken);
}
return `${normalizedBasePath}/__openclaw__/assistant-media?${params.toString()}`;Vulnerable code (server accepts query token): const url = new URL(urlRaw, "http://localhost");
const token = url.searchParams.get("token")?.trim();
return token || undefined;RecommendationAvoid placing authentication tokens in URLs. Preferred fix:
res.setHeader("Cache-Control", "no-store");
res.setHeader("Pragma", "no-cache");And consider adding: res.setHeader("Referrer-Policy", "no-referrer");Server hardening: stop accepting 9. 🟡 Untrusted tool output can inject canvas iframe previews into assistant messages
DescriptionTool-result messages are scanned for a This preview is derived from tool-controlled output text via Key concerns:
Vulnerable code (lifting the preview from tool output into assistant message content): const preview = extractToolPreview(text, toolName);
...
items[assistantIndex] = {
...item,
message: appendCanvasBlockToAssistantMessage(
item.message as Record<string, unknown>,
liftedCanvasSource.preview,
liftedCanvasSource.text,
),
};RecommendationTreat tool-result output as untrusted and do not allow it to create renderable iframe previews unless the tool is explicitly trusted. Suggested hardening (defense-in-depth):
Example (allowlist check before lifting): const TRUSTED_PREVIEW_TOOLS = new Set(["canvas_render"]);
...
if (!toolName || !TRUSTED_PREVIEW_TOOLS.has(toolName)) return null;
const preview = extractToolPreview(text, toolName);10. 🔵 Unauthenticated Control UI bootstrap leaks host filesystem paths via localMediaPreviewRoots
Description
Vulnerable code: sendJson(res, 200, {
...
localMediaPreviewRoots: [...getDefaultLocalRoots()],
embedSandbox: ...,
});RecommendationAvoid exposing host filesystem paths to unauthenticated clients. Options:
Example (omit paths): sendJson(res, 200, {
...,
// Do not reveal absolute paths
localMediaPreviewRoots: [],
});Or (auth gate): if (opts?.auth) {
const requestAuth = await authorizeGatewayHttpRequestOrReply({ ... });
if (!requestAuth) return true;
}Analyzed PR: #64104 at commit Last updated on: 2026-04-11T03:50:04Z |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8ef1415e94
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: eab40d89a1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3b591566c6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 49319ca986
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Follow-up on the broader security/bot comments:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3288d97428
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e0668ee22d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3556a8f685
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5345fe2754
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c06ee4579a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: abe88a048b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dd8f40a686
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f3bb77bcc7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 554e45db36
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
* Add embed rendering for Control UI assistant output * Add changelog entry for embed rendering * Harden canvas path resolution and stage isolation * Secure assistant media route and preserve UI avatar override * Fix chat media and history regressions * Harden embed iframe URL handling * Fix embed follow-up review regressions * Restore offloaded chat attachment persistence * Harden hook and media routing * Fix embed review follow-ups * feat(ui): add configurable embed sandbox mode * fix(gateway): harden assistant media and auth rotation * fix(gateway): restore websocket pairing handshake flows * fix(gateway): restore ws hello policy details * Restore dropped control UI shell wiring * Fix control UI reconnect cleanup regressions * fix(gateway): restore media root and auth getter compatibility * feat(ui): rename public canvas tag to embed * fix(ui): address remaining media and gateway review issues * fix(ui): address remaining embed and attachment review findings * fix(ui): restore stop control and tool card inputs * fix(ui): address history and attachment review findings * fix(ui): restore prompt contribution wiring * fix(ui): address latest history and directive reviews * fix(ui): forward password auth for assistant media * fix(ui): suppress silent transcript tokens with media * feat(ui): add granular embed sandbox modes * fix(ui): preserve relative media directives in history * docs(ui): document embed sandbox modes * fix(gateway): restrict canvas history hoisting to tool entries * fix(gateway): tighten embed follow-up review fixes * fix(ci): repair merged branch type drift * fix(prompt): restore stable runtime prompt rendering * fix(ui): harden local attachment preview checks * fix(prompt): restore channel-aware approval guidance * fix(gateway): enforce auth rotation and media cleanup * feat(ui): gate external embed urls behind config * fix(ci): repair rebased branch drift * fix(ci): resolve remaining branch check failures
* Add embed rendering for Control UI assistant output * Add changelog entry for embed rendering * Harden canvas path resolution and stage isolation * Secure assistant media route and preserve UI avatar override * Fix chat media and history regressions * Harden embed iframe URL handling * Fix embed follow-up review regressions * Restore offloaded chat attachment persistence * Harden hook and media routing * Fix embed review follow-ups * feat(ui): add configurable embed sandbox mode * fix(gateway): harden assistant media and auth rotation * fix(gateway): restore websocket pairing handshake flows * fix(gateway): restore ws hello policy details * Restore dropped control UI shell wiring * Fix control UI reconnect cleanup regressions * fix(gateway): restore media root and auth getter compatibility * feat(ui): rename public canvas tag to embed * fix(ui): address remaining media and gateway review issues * fix(ui): address remaining embed and attachment review findings * fix(ui): restore stop control and tool card inputs * fix(ui): address history and attachment review findings * fix(ui): restore prompt contribution wiring * fix(ui): address latest history and directive reviews * fix(ui): forward password auth for assistant media * fix(ui): suppress silent transcript tokens with media * feat(ui): add granular embed sandbox modes * fix(ui): preserve relative media directives in history * docs(ui): document embed sandbox modes * fix(gateway): restrict canvas history hoisting to tool entries * fix(gateway): tighten embed follow-up review fixes * fix(ci): repair merged branch type drift * fix(prompt): restore stable runtime prompt rendering * fix(ui): harden local attachment preview checks * fix(prompt): restore channel-aware approval guidance * fix(gateway): enforce auth rotation and media cleanup * feat(ui): gate external embed urls behind config * fix(ci): repair rebased branch drift * fix(ci): resolve remaining branch check failures
* Add embed rendering for Control UI assistant output * Add changelog entry for embed rendering * Harden canvas path resolution and stage isolation * Secure assistant media route and preserve UI avatar override * Fix chat media and history regressions * Harden embed iframe URL handling * Fix embed follow-up review regressions * Restore offloaded chat attachment persistence * Harden hook and media routing * Fix embed review follow-ups * feat(ui): add configurable embed sandbox mode * fix(gateway): harden assistant media and auth rotation * fix(gateway): restore websocket pairing handshake flows * fix(gateway): restore ws hello policy details * Restore dropped control UI shell wiring * Fix control UI reconnect cleanup regressions * fix(gateway): restore media root and auth getter compatibility * feat(ui): rename public canvas tag to embed * fix(ui): address remaining media and gateway review issues * fix(ui): address remaining embed and attachment review findings * fix(ui): restore stop control and tool card inputs * fix(ui): address history and attachment review findings * fix(ui): restore prompt contribution wiring * fix(ui): address latest history and directive reviews * fix(ui): forward password auth for assistant media * fix(ui): suppress silent transcript tokens with media * feat(ui): add granular embed sandbox modes * fix(ui): preserve relative media directives in history * docs(ui): document embed sandbox modes * fix(gateway): restrict canvas history hoisting to tool entries * fix(gateway): tighten embed follow-up review fixes * fix(ci): repair merged branch type drift * fix(prompt): restore stable runtime prompt rendering * fix(ui): harden local attachment preview checks * fix(prompt): restore channel-aware approval guidance * fix(gateway): enforce auth rotation and media cleanup * feat(ui): gate external embed urls behind config * fix(ci): repair rebased branch drift * fix(ci): resolve remaining branch check failures
Summary
MEDIA:lines, reply tags, and[[audio_as_voice]]into structured web UI metadata instead of leaking raw directive text into bubbles.[embed ...]and align the Control UI, gateway history stitching, and web-session prompt guidance around that single name.Change Type
Scope
Linked Issue/PR
Root Cause / Regression History
MEDIA:lines and reply/voice tags into bubbles instead of rendering them as UI metadata.Regression Test Plan
ui/src/ui/chat/message-normalizer.test.tsui/src/ui/views/chat.test.tsui/src/ui/chat/tool-cards.test.tssrc/gateway/control-ui.http.test.tssrc/gateway/server.canvas-auth.test.tssrc/gateway/server.auth.control-ui.test.tssrc/gateway/server.auth.browser-hardening.test.tssrc/media/parse.test.tssrc/media/web-media.test.tssrc/agents/system-prompt.test.tsMEDIA:/ reply / voice directives normalize into structured UI state[embed ...]renders in assistant bubblesUser-visible / Behavior Changes
[embed ...].gateway.controlUi.embedSandbox.Security Impact
powerfulbecause the current hosted embed runtime depends on same-origin behavior.isolatedis available as an explicit hardening mode; changing the default should be a separate product/security decision.Repro + Verification
Environment
/Users/thoffman/openclaw3-ui-origin-mainSteps
MEDIA:lines, reply tags,[[audio_as_voice]], and[embed ...]directives.Expected
[embed ...]renders inline rich content in the assistant bubble.Actual
Evidence
Human Verification
pnpm ui:buildpnpm checkin this environmentReview Conversations
Compatibility / Migration
[embed ...]for this unreleased assistant rich web-render pathMEDIA:directives on their own lines in assistant outputgateway.controlUi.embedSandboxtoisolatedfor stricter iframe isolationRisks and Mitigations
[embed ...]is a hard rename for unreleased prompts/tests still using prior exploratory shortcode names.