Skip to content

fix(ui): filter leaked control ui transcript rows#67036

Closed
hansolo949 wants to merge 6 commits into
openclaw:mainfrom
hansolo949:fix/control-ui-leak-filtering
Closed

fix(ui): filter leaked control ui transcript rows#67036
hansolo949 wants to merge 6 commits into
openclaw:mainfrom
hansolo949:fix/control-ui-leak-filtering

Conversation

@hansolo949

Copy link
Copy Markdown
Contributor

Summary

  • Problem: Control UI can render leaked internal transcript rows such as System: / System (untrusted): exec-status lines and sender-envelope noise as normal chat messages.
  • Why it matters: these rows expose internal plumbing in the user-visible transcript and make webchat feel broken and untrustworthy.
  • What changed: added history-side filtering in controllers/chat.ts, added a render-time guard in views/chat.ts, and added regression tests for both layers.
  • What did NOT change (scope boundary): this PR does not change backend event production or session history serialization. It only hardens the UI against known leaked row shapes.

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)

  • Root cause: Control UI trusted some transcript/history rows that were clearly internal-status content and rendered them as ordinary chat messages.
  • Missing detection / guardrail: there was no single defensive filter covering both history ingestion and final render-time grouping.
  • Contributing context (if known): some leaked rows can arrive with inconsistent roles or replay from local chat state, so filtering in only one layer was not enough.

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/controllers/chat.test.ts
    • ui/src/ui/views/chat.test.ts
  • Scenario the test should lock in:
    • leaked System (untrusted): ... Exec completed ... rows are dropped from loaded history
    • leaked sender-metadata + exec-status rows are dropped from loaded history
    • leaked rows are not rendered even if they reach the chat view
  • Why this is the smallest reliable guardrail:
    • the regression is in UI-side message filtering and rendering, and these tests exercise exactly those paths without requiring a full gateway repro
  • Existing test that already covers this (if any):
    • none specific to leaked internal transcript rows
  • If no new test is added, why not:
    • N/A

User-visible / Behavior Changes

  • Control UI no longer shows known leaked internal exec-status transcript rows as visible conversation bubbles.
  • Control UI no longer renders known sender-envelope leak shapes tied to exec status text.

Diagram (if applicable)

Before:
[leaked internal row reaches history/view] -> [rendered as normal chat bubble]

After:
[leaked internal row reaches history/view] -> [filtered at load and render guard] -> [not shown]

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:

Repro + Verification

Environment

  • OS: macOS
  • Runtime/container: local OpenClaw install, web Control UI
  • Model/provider: N/A
  • Integration/channel (if any): webchat / Control UI
  • Relevant config (redacted): default local Control UI flow

Steps

  1. Open Control UI on a session where leaked internal exec/system transcript rows are visible.
  2. Load or refresh chat history.
  3. Observe whether System: / System (untrusted): exec-status rows or sender-envelope noise appear as normal chat bubbles.

Expected

  • internal exec/system leak rows are not shown in the visible chat transcript

Actual

  • before this change, leaked internal rows could appear as normal visible chat content

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

  • Verified scenarios:
    • locally patched Control UI no longer showed leaked System (untrusted) / exec rows after refresh
    • sender-envelope leak shapes were covered in filtering logic
  • Edge cases checked:
    • leaked rows with inconsistent role assignment
    • leaked rows replayed from local recent chat state
  • What you did not verify:
    • full upstream CI/test suite in the source repo
    • every possible backend leak shape outside the known signatures covered here

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)
  • If yes, exact upgrade steps:

Risks and Mitigations

  • Risk:
    • the filter could hide a legitimate user-visible message if it exactly matches a known leak signature
    • Mitigation:
      • signatures are intentionally narrow and tied to known internal leak patterns
      • render-time and load-time tests lock in the current intended behavior

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 69bf4049b1

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread ui/src/ui/views/chat.ts Outdated
Comment on lines +1747 to +1748
if (lower.startsWith("system:") || lower.startsWith("system (untrusted):")) {
return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restrict system-prefix filter to leaked exec rows

This guard now drops any message whose text starts with System: or System (untrusted):, regardless of role or whether it is an internal exec-status leak. That means legitimate user/assistant content like System: reboot checklist... will disappear from the transcript, which is a user-visible data-loss regression for normal chat inputs. The check should be narrowed to the known leaked signature (for example, require the exec-status marker and/or the expected internal role metadata) instead of hiding all System:-prefixed text.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds two-layer filtering to prevent leaked internal transcript rows (e.g., System (untrusted): ... Exec completed ... lines) from rendering as normal chat bubbles in the Control UI. The controller layer (controllers/chat.ts) filters these at history-load time using isLeakedInternalHistoryMessage, and the view layer (views/chat.ts) provides a render-time guard via shouldHideRenderedHistoryMessage to catch leaked rows that arrive with inconsistent roles. Both layers are covered by new regression tests.

Confidence Score: 5/5

Safe to merge; the two-layer filtering correctly handles the leaked-row scenarios described, and all edge cases are covered by new regression tests.

The only finding is a P2 style/guard asymmetry in the view filter — intentional for defence-in-depth but subtly over-broad for user-role messages. No P0/P1 defects were found, so the score stays at 5.

ui/src/ui/views/chat.ts — the shouldHideRenderedHistoryMessage role-guard gap noted above.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: ui/src/ui/views/chat.ts
Line: 1747-1749

Comment:
**View filter can silence legitimate user messages starting with "system:"**

`shouldHideRenderedHistoryMessage` has no role guard for the `"system:"` / `"system (untrusted):"` prefix check. A user message whose text starts with `"system:"` (case-insensitive) — e.g., `"System: please reset the session"` — would survive the controller filter (which correctly gates on `role === "system"`) but then be silently dropped here at render time.

The controller's `isLeakedInternalHistoryMessage` demonstrates the safer pattern: role-gate the prefix check while keeping the `sender (untrusted metadata):` branch role-agnostic for the inconsistent-role scenario. Excluding `"user"` role here would preserve the defence-in-depth intent without the over-filtering risk:

```suggestion
  const msg = message as Record<string, unknown>;
  const role = typeof msg.role === "string" ? msg.role.toLowerCase() : "";
  if (role !== "user" && (lower.startsWith("system:") || lower.startsWith("system (untrusted):"))) {
    return true;
  }
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(ui): filter leaked control ui transc..." | Re-trigger Greptile

Comment thread ui/src/ui/views/chat.ts Outdated
Comment on lines +1747 to +1749
if (lower.startsWith("system:") || lower.startsWith("system (untrusted):")) {
return true;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 View filter can silence legitimate user messages starting with "system:"

shouldHideRenderedHistoryMessage has no role guard for the "system:" / "system (untrusted):" prefix check. A user message whose text starts with "system:" (case-insensitive) — e.g., "System: please reset the session" — would survive the controller filter (which correctly gates on role === "system") but then be silently dropped here at render time.

The controller's isLeakedInternalHistoryMessage demonstrates the safer pattern: role-gate the prefix check while keeping the sender (untrusted metadata): branch role-agnostic for the inconsistent-role scenario. Excluding "user" role here would preserve the defence-in-depth intent without the over-filtering risk:

Suggested change
if (lower.startsWith("system:") || lower.startsWith("system (untrusted):")) {
return true;
}
const msg = message as Record<string, unknown>;
const role = typeof msg.role === "string" ? msg.role.toLowerCase() : "";
if (role !== "user" && (lower.startsWith("system:") || lower.startsWith("system (untrusted):"))) {
return true;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: ui/src/ui/views/chat.ts
Line: 1747-1749

Comment:
**View filter can silence legitimate user messages starting with "system:"**

`shouldHideRenderedHistoryMessage` has no role guard for the `"system:"` / `"system (untrusted):"` prefix check. A user message whose text starts with `"system:"` (case-insensitive) — e.g., `"System: please reset the session"` — would survive the controller filter (which correctly gates on `role === "system"`) but then be silently dropped here at render time.

The controller's `isLeakedInternalHistoryMessage` demonstrates the safer pattern: role-gate the prefix check while keeping the `sender (untrusted metadata):` branch role-agnostic for the inconsistent-role scenario. Excluding `"user"` role here would preserve the defence-in-depth intent without the over-filtering risk:

```suggestion
  const msg = message as Record<string, unknown>;
  const role = typeof msg.role === "string" ? msg.role.toLowerCase() : "";
  if (role !== "user" && (lower.startsWith("system:") || lower.startsWith("system (untrusted):"))) {
    return true;
  }
```

How can I resolve this? If you propose a fix, please make it concise.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7c456a3614

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread ui/src/ui/controllers/chat.ts Outdated
Comment on lines +97 to +99
role === "system" &&
(lower.startsWith("system:") || lower.startsWith("system (untrusted):") || hasExecStatus)
) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Align load-time leak filter with render-time leak guard

The new history filter only removes System:/System (untrusted): rows when role === "system", but the render path hides those prefixes for any role. Since leaked rows are known to arrive with inconsistent roles (the new view test uses role: "assistant"), those entries stay in state.chatMessages after loadChatHistory and can still surface in consumers of raw history such as chat export (ui/src/ui/app.ts:544 uses exportChatMarkdown(this.chatMessages, ...)). This creates a user-visible leak path where hidden rows reappear in exports; the ingestion and render predicates should be consistent (or export should apply the same scrubber).

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1f5d562af0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread ui/src/ui/chat/history-filter.ts Outdated
Comment on lines +45 to +46
if (hasExecStatus && (lower.startsWith("system:") || lower.startsWith("system (untrusted):"))) {
return true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Narrow system-row filter to avoid hiding normal chat text

isLeakedInternalHistoryMessage drops any row that starts with System: whenever the text contains broad tokens like exec failed/exec completed, and that shared predicate is used by history load, render, and export paths. This means a legitimate message such as System: exec failed after deploy, retrying will be removed from the transcript after refresh/export even though it is not leaked internal status content. Fresh evidence compared with the earlier review thread: the added hasLeakedExecStatus gate still matches generic wording and does not require the known leaked timestamp/signature shape.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f6d1450a59

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread ui/src/ui/chat/history-filter.ts Outdated
if (hasLeakedSenderMetadataExecStatus(lower)) {
return true;
}
return lower.startsWith(normalizeLowercaseStringOrEmpty(LEAKED_SESSION_RESET_PROMPT_PREFIX));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Narrow session-reset leak filter to internal prompt shape

The new startsWith check for LEAKED_SESSION_RESET_PROMPT_PREFIX will hide any message that begins with this sentence, regardless of role or whether it is actually an internal leak. In practice, if a user or assistant legitimately quotes that reset instruction text (for debugging, docs discussion, or prompt analysis), the message is silently removed from history/render/export, which is user-visible transcript data loss. This predicate should require additional leak-specific markers (for example the known appended startup-prompt structure) instead of matching only the shared prefix.

Useful? React with 👍 / 👎.

@prtags

prtags Bot commented Apr 23, 2026

Copy link
Copy Markdown

Related work from PRtags group shining-insect-tzpr

Title: Open PR duplicate: Control UI async exec/system transcript leak

Number Title
#67036* fix(ui): filter leaked control ui transcript rows
#68518 fix(ui): filter system event messages from chat transcript (#68508)
#69366 fix(ui): hide async exec system events and heartbeat acks from chat transcript

* This PR

@clawsweeper

clawsweeper Bot commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Closing this as implemented after Codex automated review.

Current main already fixes the observable Control UI transcript leak through the shipped runtime/transcript path rather than this PR's UI-only row filter. Exec/heartbeat wakeups are treated as heartbeat/system-event runs, their prompt body is excluded from the visible transcript prompt, and current chat history/live-chat projection strips heartbeat acknowledgements and internal runtime context. The linked bug is documented as fixed in v2026.4.24.

Best possible solution:

Close this PR and keep the shipped runtime/transcript implementation on main. Any remaining concern about historical leaked rows should be handled as a focused legacy transcript repair or session-history projection follow-up, not by merging this broad UI-only filter.

What I checked:

So I’m closing this as already implemented rather than keeping a duplicate issue open.

Codex Review notes: model gpt-5.5, reasoning high; reviewed against 7c0fdae9b95b; fix evidence: release v2026.4.24, commit cbcfdf62c729.

@clawsweeper clawsweeper Bot closed this Apr 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Internal exec/heartbeat events leak into visible Control UI chat transcript

1 participant