Hide transcript-only OpenClaw history artifacts#69217
Conversation
…naged identity + workload identity)
# Conflicts: # src/canvas-host/a2ui/.bundle.hash
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0a5c0025d1
ℹ️ 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".
| if (countChannels(activeRegistry) > 0) { | ||
| return activeRegistry; |
There was a problem hiding this comment.
Align channel-registry fallback with version tracking
getActivePluginChannelRegistryFromState now falls back to activeRegistry when the pinned channel registry is empty, but getActivePluginChannelRegistryVersionFromState still keys off whether state.channel.registry is non-null. That can pair an active registry reference with a pinned-registry version, so cache users like src/channels/plugins/registry-loaded.ts (which compare both ref and version) can miss invalidation when activeVersion changes while an empty pinned registry remains present.
Useful? React with 👍 / 👎.
🔒 Aisle Security AnalysisWe found 3 potential security issue(s) in this PR:
1. 🟠 SSRF and bot token exfiltration via unvalidated Teams conversation reference `serviceUrl`
Description
Vulnerable code (token-bearing request built from const baseUrl = serviceUrl.replace(/\/+
$/, "");
const url = `${baseUrl}/v3/conversations/${encodeURIComponent(conversationId)}/activities/${encodeURIComponent(activityId)}`;
const headers: Record<string, string> = {
"User-Agent": buildUserAgent(),
};
if (token) {
headers.Authorization = `Bearer ${token}`;
}
await fetchWithSsrFGuard({ url, init: { method: "DELETE", headers } });RecommendationTreat
Example (sketch): function assertAllowedServiceUrl(serviceUrl: string) {
const u = new URL(serviceUrl);
if (u.protocol !== "https:") throw new Error("Invalid serviceUrl protocol");
const allowed = [/\.trafficmanager\.net$/i, /\.botframework\.com$/i];
if (!allowed.some((re) => re.test(u.hostname))) {
throw new Error("Untrusted serviceUrl host");
}
}
assertAllowedServiceUrl(reference.serviceUrl);Also apply the same validation to 2. 🟡 Debug proxy capture stores sensitive headers and URL query parameters (Discord/Telegram fetch wrappers)
Description
This is sensitive because:
Vulnerable callsite (Discord): captureHttpExchange({
url: resolveRequestUrl(input),
method: init?.method ?? "GET",
requestHeaders: init?.headers as Headers | Record<string, string> | undefined,
requestBody: (init as RequestInit & { body?: BodyInit | null })?.body ?? null,
response,
flowId: randomUUID(),
meta: { subsystem: "discord-rest" },
});Data flow context:
RecommendationRedact secrets before persisting captures (even in debug mode), and avoid storing full query strings by default. Suggested changes:
Example (sanitizing at callsite): function sanitizeHeaders(h?: Headers | Record<string,string>) {
const obj = h instanceof Headers ? Object.fromEntries(h.entries()) : { ...(h ?? {}) };
for (const k of Object.keys(obj)) {
if (["authorization","cookie","set-cookie","x-api-key"].includes(k.toLowerCase())) {
obj[k] = "[REDACTED]";
}
}
return obj;
}
function sanitizeUrl(raw: string) {
const u = new URL(raw);
u.username = "";
u.password = "";
// Option A: drop query entirely
u.search = "";
return u.toString();
}
captureHttpExchange({
url: sanitizeUrl(resolveRequestUrl(input)),
method: init?.method ?? "GET",
requestHeaders: sanitizeHeaders(init?.headers as any),
requestBody: null, // or only persist for non-sensitive content-types
response,
meta: { subsystem: "discord-rest" },
});3. 🟡 Audit/log bypass: gateway-injected assistant transcript entries are filtered out of history APIs/UI
Description
Impact:
Vulnerable behavior (filtering): if (isTranscriptOnlyOpenClawAssistantMessage(message)) {
return true;
}Message classification: return provider === "openclaw" && (model === "delivery-mirror" || model === "gateway-injected");Injection path setting the hidden markers: provider: "openclaw",
model: "gateway-injected",This combination enables transcript entries to be persisted but omitted from history responses. RecommendationDo not fully omit persisted transcript messages from audit/history surfaces based solely on Safer options:
Example: keep entries but tag them if (isTranscriptOnlyOpenClawAssistantMessage(message)) {
next.push({ ...(message as any), __openclaw: { ...(message as any).__openclaw, internal: true } });
continue;
}Analyzed PR: #69217 at commit Last updated on: 2026-04-20T06:32:21Z |
|
Follow-up pushed in What I changed:
Re-run after the fix:
Current result:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a4a2492d64
ℹ️ 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".
| id: "discord-rest", | ||
| label: "Discord REST monitor fetch", | ||
| modulePath: "extensions/discord/src/monitor/rest-fetch.ts", | ||
| modulePath: "extensions/discord/monitor/rest-fetch.ts", |
There was a problem hiding this comment.
Restore real module paths in proxy coverage report
The modulePath values for several extension seams now point to files that do not exist (for example extensions/discord/monitor/rest-fetch.ts, extensions/telegram/fetch.ts, and extensions/mattermost/mattermost/monitor-websocket.ts all dropped /src). This makes openclaw proxy coverage output inaccurate and breaks any workflow that uses modulePath to jump to the implemented seam; these entries should keep repo-real paths like extensions/.../src/....
Useful? React with 👍 / 👎.
|
Closing this as duplicate/superseded by #40716 after the dedupe pass. The useful slice here is the transcript-only OpenClaw assistant artifact filter ( The broader replay/idempotency work remains tracked in #69208 and #66443; this closure is only PR dedupe, not a claim that the umbrella is fixed. |
…ents Adds an optional `audience: 'internal' | 'user-facing'` field to SystemEvent, defaulting to 'user-facing' (full backward-compat). Internal-audience events are wrapped at the consumer integration point (drainFormattedSystemEvents) in the existing INTERNAL_RUNTIME_CONTEXT_BEGIN/END delimiters with the canonical header lines from formatAgentInternalEventsForPrompt — the agent runtime sees the content as runtime context but every user-facing surface strips it via the already-installed stripInternalRuntimeContext consumers (sanitize-user-facing-text.ts, memory-host-sdk session-files, history readers, Control UI extract). This mirrors the producer-side runtime-context delimiter pattern landed in e918e5f (openclaw#71761) and 6e985a4 (webchat runtime context) — no new persistence shape, no consumer-side filter (avoids the audit-bypass smell flagged on openclaw#69217), no display-layer per-surface patch (avoids the rejection pattern from openclaw#69366). Scope is intentionally tight. This primitive is the hidden runtime-context lane only; it is NOT a delivery-routing primitive. Events with a positive user delivery contract (exec completion via notifyOnExit, cron payloads, heartbeat acks) are not migrated — those have heartbeat-driven explicit-relay paths (buildExecEventPrompt / buildCronEventPrompt) plus tactical producer-side skips like bd60df3. Migrated (one caller): - queueCronAwarenessSystemEvent in src/cron/isolated-agent/delivery-dispatch.ts. Main-session reflection of an isolated agent run that already delivered to the user via its own channel (gated on `delivered === true`). Pure awareness, no user delivery contract. Tests cover: default audience, round-trip preservation, audience equality (consumeSystemEventEntries prefix-match respects audience), wrap-on-drain shape with canonical header, mixed user-facing+internal ordering, the user-facing-only and internal-only edge cases, end-to-end strip via stripInternalRuntimeContext, and adversarial delimiter-token escape. Addresses part of openclaw#69492 (system-event-shape consumer leakage). Leaves the user-deliverable noise question (exec completion, cron payloads, heartbeat acks) open for case-by-case follow-ups matching the maintainer's revealed pattern (bd60df3, 3f63ba8). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ents Adds an optional `audience: 'internal' | 'user-facing'` field to SystemEvent, defaulting to 'user-facing' (full backward-compat). Internal-audience events are wrapped at the consumer integration point (drainFormattedSystemEvents) in the existing INTERNAL_RUNTIME_CONTEXT_BEGIN/END delimiters with the canonical header lines from formatAgentInternalEventsForPrompt — the agent runtime sees the content as runtime context but every user-facing surface strips it via the already-installed stripInternalRuntimeContext consumers (sanitize-user-facing-text.ts, memory-host-sdk session-files, history readers, Control UI extract). This mirrors the producer-side runtime-context delimiter pattern landed in e918e5f (openclaw#71761) and 6e985a4 (webchat runtime context) — no new persistence shape, no consumer-side filter (avoids the audit-bypass smell flagged on openclaw#69217), no display-layer per-surface patch (avoids the rejection pattern from openclaw#69366). Scope is intentionally tight. This primitive is the hidden runtime-context lane only; it is NOT a delivery-routing primitive. Events with a positive user delivery contract (exec completion via notifyOnExit, cron payloads, heartbeat acks) are not migrated — those have heartbeat-driven explicit-relay paths (buildExecEventPrompt / buildCronEventPrompt) plus tactical producer-side skips like bd60df3. Migrated (one caller): - queueCronAwarenessSystemEvent in src/cron/isolated-agent/delivery-dispatch.ts. Main-session reflection of an isolated agent run that already delivered to the user via its own channel (gated on `delivered === true`). Pure awareness, no user delivery contract. Tests cover: default audience, round-trip preservation, audience equality (consumeSystemEventEntries prefix-match respects audience), wrap-on-drain shape with canonical header, mixed user-facing+internal ordering, the user-facing-only and internal-only edge cases, end-to-end strip via stripInternalRuntimeContext, and adversarial delimiter-token escape. Addresses part of openclaw#69492 (system-event-shape consumer leakage). Leaves the user-deliverable noise question (exec completion, cron payloads, heartbeat acks) open for case-by-case follow-ups matching the maintainer's revealed pattern (bd60df3, 3f63ba8). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ents Adds an optional `audience: 'internal' | 'user-facing'` field to SystemEvent, defaulting to 'user-facing' (full backward-compat). Internal-audience events are wrapped at the consumer integration point (drainFormattedSystemEvents) in the existing INTERNAL_RUNTIME_CONTEXT_BEGIN/END delimiters with the canonical header lines from formatAgentInternalEventsForPrompt — the agent runtime sees the content as runtime context but every user-facing surface strips it via the already-installed stripInternalRuntimeContext consumers (sanitize-user-facing-text.ts, memory-host-sdk session-files, history readers, Control UI extract). This mirrors the producer-side runtime-context delimiter pattern landed in e918e5f (openclaw#71761) and 6e985a4 (webchat runtime context) — no new persistence shape, no consumer-side filter (avoids the audit-bypass smell flagged on openclaw#69217), no display-layer per-surface patch (avoids the rejection pattern from openclaw#69366). Scope is intentionally tight. This primitive is the hidden runtime-context lane only; it is NOT a delivery-routing primitive. Events with a positive user delivery contract (exec completion via notifyOnExit, cron payloads, heartbeat acks) are not migrated — those have heartbeat-driven explicit-relay paths (buildExecEventPrompt / buildCronEventPrompt) plus tactical producer-side skips like bd60df3. Migrated (one caller): - queueCronAwarenessSystemEvent in src/cron/isolated-agent/delivery-dispatch.ts. Main-session reflection of an isolated agent run that already delivered to the user via its own channel (gated on `delivered === true`). Pure awareness, no user delivery contract. Tests cover: default audience, round-trip preservation, audience equality (consumeSystemEventEntries prefix-match respects audience), wrap-on-drain shape with canonical header, mixed user-facing+internal ordering, the user-facing-only and internal-only edge cases, end-to-end strip via stripInternalRuntimeContext, and adversarial delimiter-token escape. Addresses part of openclaw#69492 (system-event-shape consumer leakage). Leaves the user-deliverable noise question (exec completion, cron payloads, heartbeat acks) open for case-by-case follow-ups matching the maintainer's revealed pattern (bd60df3, 3f63ba8). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ents Adds an optional `audience: 'internal' | 'user-facing'` field to SystemEvent, defaulting to 'user-facing' (full backward-compat). Internal-audience events are wrapped at the consumer integration point (drainFormattedSystemEvents) in the existing INTERNAL_RUNTIME_CONTEXT_BEGIN/END delimiters with the canonical header lines from formatAgentInternalEventsForPrompt — the agent runtime sees the content as runtime context but every user-facing surface strips it via the already-installed stripInternalRuntimeContext consumers (sanitize-user-facing-text.ts, memory-host-sdk session-files, history readers, Control UI extract). This mirrors the producer-side runtime-context delimiter pattern landed in e918e5f (openclaw#71761) and 6e985a4 (webchat runtime context) — no new persistence shape, no consumer-side filter (avoids the audit-bypass smell flagged on openclaw#69217), no display-layer per-surface patch (avoids the rejection pattern from openclaw#69366). Scope is intentionally tight. This primitive is the hidden runtime-context lane only; it is NOT a delivery-routing primitive. Events with a positive user delivery contract (exec completion via notifyOnExit, cron payloads, heartbeat acks) are not migrated — those have heartbeat-driven explicit-relay paths (buildExecEventPrompt / buildCronEventPrompt) plus tactical producer-side skips like bd60df3. Migrated (one caller): - queueCronAwarenessSystemEvent in src/cron/isolated-agent/delivery-dispatch.ts. Main-session reflection of an isolated agent run that already delivered to the user via its own channel (gated on `delivered === true`). Pure awareness, no user delivery contract. Tests cover: default audience, round-trip preservation, audience equality (consumeSystemEventEntries prefix-match respects audience), wrap-on-drain shape with canonical header, mixed user-facing+internal ordering, the user-facing-only and internal-only edge cases, end-to-end strip via stripInternalRuntimeContext, and adversarial delimiter-token escape. Addresses part of openclaw#69492 (system-event-shape consumer leakage). Leaves the user-deliverable noise question (exec completion, cron payloads, heartbeat acks) open for case-by-case follow-ups matching the maintainer's revealed pattern (bd60df3, 3f63ba8). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ents Adds an optional `audience: 'internal' | 'user-facing'` field to SystemEvent, defaulting to 'user-facing' (full backward-compat). Internal-audience events are wrapped at the consumer integration point (drainFormattedSystemEvents) in the existing INTERNAL_RUNTIME_CONTEXT_BEGIN/END delimiters with the canonical header lines from formatAgentInternalEventsForPrompt — the agent runtime sees the content as runtime context but every user-facing surface strips it via the already-installed stripInternalRuntimeContext consumers (sanitize-user-facing-text.ts, memory-host-sdk session-files, history readers, Control UI extract). This mirrors the producer-side runtime-context delimiter pattern landed in e918e5f (openclaw#71761) and 6e985a4 (webchat runtime context) — no new persistence shape, no consumer-side filter (avoids the audit-bypass smell flagged on openclaw#69217), no display-layer per-surface patch (avoids the rejection pattern from openclaw#69366). Scope is intentionally tight. This primitive is the hidden runtime-context lane only; it is NOT a delivery-routing primitive. Events with a positive user delivery contract (exec completion via notifyOnExit, cron payloads, heartbeat acks) are not migrated — those have heartbeat-driven explicit-relay paths (buildExecEventPrompt / buildCronEventPrompt) plus tactical producer-side skips like bd60df3. Migrated (one caller): - queueCronAwarenessSystemEvent in src/cron/isolated-agent/delivery-dispatch.ts. Main-session reflection of an isolated agent run that already delivered to the user via its own channel (gated on `delivered === true`). Pure awareness, no user delivery contract. Tests cover: default audience, round-trip preservation, audience equality (consumeSystemEventEntries prefix-match respects audience), wrap-on-drain shape with canonical header, mixed user-facing+internal ordering, the user-facing-only and internal-only edge cases, end-to-end strip via stripInternalRuntimeContext, and adversarial delimiter-token escape. Addresses part of openclaw#69492 (system-event-shape consumer leakage). Leaves the user-deliverable noise question (exec completion, cron payloads, heartbeat acks) open for case-by-case follow-ups matching the maintainer's revealed pattern (bd60df3, 3f63ba8). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ents Adds an optional `audience: 'internal' | 'user-facing'` field to `SystemEvent`, defaulting to `'user-facing'` (full backward-compat for every existing caller). Internal-audience events are wrapped at the consumer integration point (`drainFormattedSystemEvents` in session-system-events.ts) in the existing `INTERNAL_RUNTIME_CONTEXT_BEGIN/END` delimiters with the canonical header lines from `formatAgentInternalEventsForPrompt`. The agent runtime sees the content as runtime context; user-facing surfaces strip it via the already-installed `stripInternalRuntimeContext` consumers (`sanitize-user-facing-text.ts`, `memory-host-sdk/host/session-files.ts`, `agents/internal-events.ts`, history readers, Control UI extract). Mirrors the producer-side runtime-context delimiter pattern landed in `e918e5f75c` (openclaw#71761) and `6e985a421d` (webchat runtime context). No new persistence shape, no consumer-side filter (avoids the audit-bypass concern flagged on openclaw#69217), no display-layer per-surface patch (avoids the rejection pattern from openclaw#69366). Heartbeat-path additions: `selectGenericSystemEvents` keeps audience: "internal" events queued when the drain is invoked from a heartbeat reply (whose prompt envelope discards `systemEventBlocks`), so the wrapped runtime-context block is not silently consumed before the next regular reply turn that actually carries it through to the model. The heartbeat exec/cron-prompt selectors also skip internal events so they don't get classified as user-facing relay payloads. `compactSystemEvent` bypasses heartbeat-noise filters ("reason periodic", "Read HEARTBEAT.md", "heartbeat poll/wake") for audience: "internal" events — those go through the wrap-on-drain path and never reach a user-facing surface, so filtering them after consumption would silently drop the event (same no-consumer hole class as the exec-shape filter). Migrated one producer (the canonical justification for the lane): - `queueCronAwarenessSystemEvent` in `src/cron/isolated-agent/delivery-dispatch.ts`. This event is queued onto the main session only after the isolated agent run has already delivered to the user via its own channel (gated on `delivered === true` and `shouldQueueCronAwareness`). The main session needs hidden runtime context so the next turn knows the cron landed, not a user-visible `System: ...` bubble. Scope is intentionally tight. This primitive is the hidden runtime-context lane only; it is NOT a delivery-routing primitive. Events with a positive user delivery contract (exec completion via `notifyOnExit`, cron payloads, heartbeat acks) are not migrated — those have heartbeat-driven explicit-relay paths (`buildExecEventPrompt` / `buildCronEventPrompt`) plus tactical producer-side skips like `bd60df3e53`. Marking them internal would suppress delivery on regular reply turns where the model is instructed to keep wrapped content private. Tests cover: default audience, round-trip preservation, audience equality (consumeSystemEventEntries prefix-match respects audience), wrap-on-drain shape with canonical header, mixed user-facing+internal ordering, the user-facing-only and internal-only edge cases, end-to-end strip via stripInternalRuntimeContext, adversarial delimiter-token escape, heartbeat-path audience: "internal" queueing semantics, and exec-shaped internal events bypassing the text-shape filter. Addresses part of openclaw#69492 (system-event-shape consumer leakage; umbrella openclaw#69208 Track B). Leaves the user-deliverable noise question (exec completion, cron payloads, heartbeat acks) open for case-by-case follow-ups matching the maintainer's revealed pattern (`bd60df3e53`, `3f63ba8fd808`). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
This patch stops transcript-only OpenClaw assistant artifacts from leaking back out through user-facing history surfaces.
The concrete issue here is the delivery-mirror and gateway-injected assistant messages that are intentionally written into transcripts for replay, mirroring, or bootstrap purposes, but should not show up as visible conversational history in
chat.history,sessions.get, or the session history HTTP endpoint.This is part of the broader duplicate transcript and replay umbrella tracked in #69208. It is most closely related to the delivery-mirror and consumer leakage family, especially #39469.
Root Cause
OpenClaw already distinguishes some internal transcript-only assistant artifacts in a few paths, but the gateway history readers were still consuming raw transcript messages too directly.
That meant messages with:
provider: "openclaw"model: "delivery-mirror"ormodel: "gateway-injected"could still reappear in history APIs even though they are not real user-visible assistant turns.
What Changed
src/config/sessions/transcript.tsto identify transcript-only OpenClaw assistant artifacts.chat.historysanitization to drop transcript-only OpenClaw assistant artifacts.sessions.getto filter those transcript-only artifacts from returned history.chat.historysessions.getUser Impact
Before this change, some transcript-only artifacts could be surfaced back to users or downstream consumers as if they were real assistant replies.
After this change, those internal mirror and injected transcript entries are preserved where they are needed for transcript mechanics, but they no longer appear in the main history surfaces that users and clients treat as conversational state.
Validation
Passed:
git commitpnpm check:no-conflict-markerspnpm tool-display:checkpnpm check:host-env-policy:swiftpnpm tsgopnpm lintpnpm lint:webhook:no-low-level-body-readpnpm lint:auth:no-pairing-store-grouppnpm lint:auth:pairing-account-scopenode scripts/run-vitest.mjs run src/gateway/server.chat.gateway-server-chat.test.ts src/gateway/server.sessions.gateway-server-sessions-a.test.tsnode scripts/run-vitest.mjs run src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.suppresses-message-end-block-replies-message-tool.test.tsNotes:
src/gateway/sessions-history-http.test.ts, but I did not get a clean standalone run of that file because it is routed through a heavier e2e-scoped Vitest config in this repo.pnpm exec tsc -p tsconfig.json --noEmit --pretty falseattempt previously hit a local Node memory ceiling on this machine, so I am not claiming a full repository typecheck from that command.Scope Notes
This does not attempt to solve the broader replay and idempotency family by itself.
It is a focused fix for one tractable slice of the umbrella issue:
Follow-up work is still needed for the broader replay and duplicate context cluster tracked in #69208.