Skip to content

Re-port upstream security/correctness fixes missed in the v2026.4.24 content-only sync #2764

Description

@alexey-pelykh

Summary

During the most recent content-only upstream sync (OpenClaw 2026.4.222026.4.24), a
revert-fidelity audit compared every kept fork file against the true upstream 2026.4.24 blob and
found a set of upstream fixes that landed in the sync window but are missing from the fork's kept
code paths
. RemoteClaw guts OpenClaw's execution engine but keeps the channel-adapter, gateway,
plugin, and config layers — so these fixes apply to code we still ship and should be re-ported.

None are blocking (the sync PR already merged; the fork's aggressive gutting removed most vulnerable
upstream paths wholesale). Two are medium (advertised/expected security controls that are absent
on live paths); the rest are low correctness/UX drift. Upstream commit hashes below refer to the
openclaw/openclaw history (fetched in this repo as the openclaw remote — git show <hash> works).


Priority 1 — Medium

1. sessions.compact is missing the webchat session-mutation guard

File: src/gateway/server-methods/sessions.ts
Upstream fix: commit 873dce178defix(gateway): block webchat session compaction mutations (PR openclaw#70716)

Upstream added a client-class guard so that webchat clients cannot mutate session state: it
extended rejectWebchatSessionMutation to cover the compact (and restore) actions and called it
at the top of the sessions.compact handler.

In the fork, sessions.patch and sessions.delete do call rejectWebchatSessionMutation
(with client + isWebchatConnect in their handler signatures), but sessions.compact does not:

// src/gateway/server-methods/sessions.ts (fork, current)
"sessions.compact": async ({ params, respond }) => {   // <-- no `client`, no `isWebchatConnect`, no guard
  ...
  const compactTarget = await updateSessionStore(storePath, (store) => { ... }); // mutates session transcript

rejectWebchatSessionMutation's action type is still the pre-fix "patch" | "delete".

Impact: a webchat-class client can invoke sessions.compact to compact/mutate session transcripts
even though the same client class is blocked from patch/delete. Bounded (why this is medium, not
high): sessions.compact requires ADMIN_SCOPE (see src/gateway/method-scopes.ts, the
[ADMIN_SCOPE] group), and the scope layer is still enforced (default-deny), so the exposure is an
admin-scoped webchat connection (e.g. an XSS'd control-UI session), and the impact is bounded to
compaction. It is a defense-in-depth gap, not an unauthenticated bypass.

Fix: give the sessions.compact handler client + isWebchatConnect, extend the
rejectWebchatSessionMutation action union to include "compact", and call the guard before mutating
(mirroring sessions.patch/sessions.delete).

Verify the gap:

git show HEAD:src/gateway/server-methods/sessions.ts | grep -n 'sessions.compact\|rejectWebchatSessionMutation'
# patch/delete call the guard; compact does not.

2. plugins.entries.*.hooks.allowConversationAccess is advertised but not enforced

File: src/plugins/registry.ts
Upstream fix: commit 51f9f94cc36fix(hooks): harden cli transcript loading (PR openclaw#70786)

Upstream added a permission gate: a non-bundled (third-party) plugin may only register
conversation/transcript hooks if it explicitly opts in with
plugins.entries.<id>.hooks.allowConversationAccess=true; otherwise the typed-hook registration is
blocked with a diagnostic. The gate uses an isConversationHookName(hookName) check.

In the fork, registerTypedHook enforces only allowPromptInjection — there is no
isConversationHookName gate anywhere (git grep isConversationHookName returns nothing). Yet the
allowConversationAccess config field is fully plumbed and advertised:

  • src/config/zod-schema.ts (allowConversationAccess: z.boolean().optional())
  • src/config/schema.base.generated.ts, src/config/schema.help.ts, src/config/schema.labels.ts
    ("Allow Conversation Access Hooks")
  • src/plugins/config-normalization-shared.ts

and conversation/transcript hooks exist (src/plugins/hooks.ts — "Allows plugins to analyze completed
conversations", transcript-append hooks), and non-bundled plugins are activatable via the allow-list
(src/plugins/config-activation-shared.ts).

Impact: an operator-allowed third-party plugin gets ungated access to completed conversation
transcripts, and the advertised allowConversationAccess opt-in control is inert — setting it to
false (or relying on the default) does nothing. This is a false-assurance confidentiality/permission
gap. Medium (requires an allow-listed non-bundled plugin, so not high).

Fix: port isConversationHookName and enforce it in registerTypedHook: for record.origin !== "bundled", block conversation-hook registration unless policy?.allowConversationAccess === true
(mirroring the existing allowPromptInjection enforcement).

Verify the gap:

git grep -n isConversationHookName        # (empty — gate absent)
git show HEAD:src/plugins/registry.ts | grep -n 'allowPromptInjection\|allowConversationAccess'

Priority 2 — Low (correctness / UX / privacy drift; re-port opportunistically)

All are live but low-impact. Each row is file — upstream commit (subject) — what's missing:

Privacy (data-minimization):

  • src/auto-reply/reply/groups.tse0f5961e289 (fix: harden group chat prompt metadata) — upstream
    removed the group name and participant list from the agent prompt; the fork's
    buildGroupChatContext still emits You are in the ... group chat "${subject}" and
    Participants: ${members} (both sent to the LLM). Low (the model already receives the group messages).

Channel adapters:

  • extensions/slack/src/monitor/message-handler.ts2b5c719a628 (process thread broadcasts as
    messages
    ) — skip-gate lacks subtype !== "thread_broadcast", so thread_broadcast messages are
    silently skipped.
  • extensions/slack/src/send.tsd399ac74f75 (hash token cache keys) — createSlackDmCacheKey
    uses the raw token as a Map key (upstream hashes it); and 107d2b7a096 (preserve rapid send
    ordering
    ) — no send-queue serialization.
  • extensions/whatsapp/src/channel.ts + src/login-qr.ts245451b6a9c (keep QR login state in
    sync
    ) — QR wait flow not bounded to current QR metadata (stale-QR race).
  • extensions/whatsapp/src/auto-reply/deliver-reply.ts18c98316f7a (canonicalize outbound media
    delivery
    ) — pre-fix media path; no outbound-media-contract canonicalization.
  • extensions/feishu/src/send.tsbb5e278f63b (stabilize topic group session keys) — chatType
    omits topic_group.
  • extensions/slack/src/channel.tse40d7abda9f (preserve real thread anchors); slack
    monitor/media.ts/prepare-content.ts/prepare.tsd1cc54866d6 (file placeholders omit fileId);
    monitor/slash.ts76a4c167f7e (group-DM labelled "channel").

Discord:

  • extensions/discord/src/draft-stream.ts70fd1c91aa8 — streaming preview sends omit
    allowed_mentions: { parse: [] }, so a preview containing @everyone/@here triggers real pings.
  • extensions/discord/src/monitor/reply-delivery.ts3dba3d8b35c — the Discord reply path runs no
    message_sending plugin hooks (they are wired for telegram/slack/deliver.ts).

Gateway (correctness/resource):

  • src/gateway/server-methods/agent.ts8cae2ed645e (agent RPC runs not registered in
    chatAbortControllers, so chat.abort can't stop them) and 934dd5b3a73 (codex session-id routing
    missing !requestedSessionId guard).
  • src/gateway/server-methods/chat.ts1ff07245f33 (chat.send omits addChatRun; a pre-dispatch
    throw leaks the abort-controller entry).
  • src/gateway/call.ts77a1cbd5ffb + d39e34d31f3 (CLI gateway-client teardown uses sync
    client.stop() without stopAndWait).

Infra / cron / control-UI:

  • src/infra/heartbeat-runner.ts9f6cda120d3 (clamp scheduler delay to Node setTimeout cap) — no
    clamp, so a heartbeat.every > ~24.85d crash-loops the gateway (operator misconfig); and
    349749f73de (exec-completion payloads not embedded in the heartbeat prompt).
  • src/cron/isolated-agent/session.tsf0ceb4b68f5 (fresh-cron-session uses the old blocklist
    instead of the allowlist isolation; the primary lastThreadId leak is already handled).
  • ui/src/ui/app-chat.ts + app-gateway.ts8513a14406f (no pendingAbort replay on reconnect);
    ui/src/ui/chat/grouped-render.ts86f8c826e20 (avatar render skips bidi/invisible-control
    sanitization; lit-escaped, so residual is bidi visual-spoof on identity-config only);
    ui/src/ui/controllers/chat.ts1afbfdf451d + 3f63ba8fd80 (history-reload drops optimistic tail;
    no heartbeat/empty-user hiding).
  • src/agents/subagent-orphan-recovery.ts + subagent-registry.ts845040214e1 (interrupted
    subagent-run finalization/stale-sweep absent; note the fork's subagent waits are gateway-mediated with
    a timeoutMs, which likely bounds the parent-hang differently).

CLI:

  • src/commands/agents.commands.delete.ts2cacd2097b5 (agents delete trashes the workspace
    unconditionally; missing the shared-workspace-overlap retention guard — data loss, recoverable via
    trash).
  • src/commands/onboard-helpers.ts8e40bdba902 (handleReset uses load-time CONFIG_PATH/
    CONFIG_DIR instead of call-time resolveConfigPath()/resolveConfigDir()).

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions