Skip to content

fix(qqbot): deliver cron auto-TTS voice by trusting OpenClaw temp root#92947

Merged
vincentkoc merged 1 commit into
openclaw:mainfrom
ZengWen-DT:fix/92816-qqbot-cron-tts-delivery
Jun 16, 2026
Merged

fix(qqbot): deliver cron auto-TTS voice by trusting OpenClaw temp root#92947
vincentkoc merged 1 commit into
openclaw:mainfrom
ZengWen-DT:fix/92816-qqbot-cron-tts-delivery

Conversation

@ZengWen-DT

@ZengWen-DT ZengWen-DT commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #92816. When messages.tts.auto = "always", cron deliveries to QQBot are silently lost while the run is still recorded as delivered.

Root cause: cron auto-TTS (speech-core) writes the generated voice file under OpenClaw's preferred temp root (resolvePreferredOpenClawTmpDir, e.g. /tmp/openclaw/tts-*/voice-*.mp3). QQBot is the only channel that root-sandboxes outbound local files, and its gate only trusted the QQ Bot media storage roots (~/.openclaw/media, ~/.openclaw/media/qqbot). So the temp file was rejected, sendMedia returned a no-identity error, the QQ message was lost — but the shared delivery layer still counted it.

The trust check runs at three sites that must agree (a file accepted at one is otherwise rejected at the next):

  • resolveOutboundMediaPath (initial resolve)
  • the sendVoiceFromLocal re-check (the gate that actually blocks the voice send)
  • validateStructuredPayloadLocalPath (structured image/video/file payloads)

This adds one shared resolver, resolveTrustedOutboundMediaPath, that also trusts OpenClaw's permission-hardened temp root — which core already treats as a sanctioned media root in buildMediaLocalRoots (src/media/local-roots.ts) — and routes all three gates through it. The temp root is provisioned 0o700, owner-checked, non-symlink, and never world/group-writable (src/infra/tmp-openclaw-dir.ts), so it is the same trust class as ~/.openclaw/media; resolveLocalPathFromRootsSync realpaths both root and candidate to prevent symlink escape. Out-of-root paths are still rejected.

Relation to #79811 (not a duplicate)

This is the delivery fix: it makes the QQ message actually arrive. #79811 (and the earlier, now-closed #92826) only correct accounting — turning a false "delivered" into an honest "failed". The two are complementary: this PR fixes the message loss the user reported; the accounting fix makes a genuine failure visible. Neither has landed on main, and the media accounting asymmetry there is out of scope here.

Real behavior proof

  • Behavior addressed: A cron auto-TTS voice file written under OpenClaw's preferred temp root now resolves through every QQBot outbound media gate and is delivered to QQ, instead of being rejected as out-of-root and silently dropped.
  • Real environment tested: A live, real QQ Bot on the official API (wss://api.sgroup.qq.com), with the OpenClaw gateway built from this branch (pnpm dev gateway), a real C2C recipient, messages.tts.auto = "always", and the keyless Microsoft Edge TTS provider. Also the local resolver unit suite on Node 24.
  • Exact steps or command run after this patch: Ran a deterministic live gate check against the real QQ API (isolates the gate fix from TTS flakiness), then an end-to-end cron auto-TTS run, then the unit suite:
# 1a) Positive: media staged UNDER the OpenClaw temp root
openclaw message send --channel qqbot --target <c2c-openid> \
  --media /tmp/openclaw/proof/temp-root-voice.mp3 --json
# 1b) Negative control: same bytes from a path OUTSIDE every trusted root
openclaw message send --channel qqbot --target <c2c-openid> \
  --media ~/qq-untrusted-proof/untrusted-voice.mp3 --json
# 2) End-to-end cron auto-TTS (tts.auto=always) to the C2C target
openclaw cron add --channel qqbot --to <c2c-openid> --announce --message "<one English sentence>"
openclaw cron run <job-id>
# 3) Local resolver unit suite
pnpm test extensions/qqbot/src/engine/messaging/trusted-media-path.test.ts
  • Evidence after fix: Positive live send (temp-root media) was delivered with a real platform identity messageId: ROBOT1.0_m-9gIoJjF… (kind: media, conversationId: qqbot:c2c:…). Negative control (untrusted path) was rejected: messageId: "", platformMessageIds: []. The end-to-end cron auto-TTS run wrote a voice file under the temp root (/tmp/openclaw/tts-*/voice-*.mp3) and it arrived on the phone as a QQ voice message (screenshot attached below); no ~/.openclaw/media/outbound copy existed, confirming it was sent straight from the temp root. Unit suite: Test Files 1 passed (1) / Tests 4 passed (4).
  • Observed result after fix: The QQ recipient received the cron auto-TTS voice message. Temp-root media delivers with a real platform message id; media outside the trusted roots is still rejected with no delivery identity, so the gate is not over-broadened (security preserved).
  • Before evidence: With the temp-root trust removed, the resolver suite fails 3/4 (including the end-to-end resolveOutboundMediaPath voice case) while the out-of-root rejection test still passes; on unpatched code the same temp-root file is rejected, sendMedia returns a no-identity error, and the QQ voice is dropped while cron still records the run as delivered.
  • What was not tested: Microsoft Edge TTS synthesis is intermittent from this host (public service, no SLA): it synthesized on the first cron run and fell back to text on later runs. The deterministic message send --media runs above isolate the gate fix from that flakiness and exercise the exact code this PR changes against the live QQ API.
Screenshot_2026-06-14-22-33-16-350_com tencent mo

Tests and validation

  • pnpm test extensions/qqbot — 526 passed (64 files), no regressions from rewiring the three gates.
  • pnpm tsgo:extensions and pnpm tsgo:test:extensions — clean.
  • pnpm exec oxfmt --check + oxlint on all changed files — clean.
  • Live QQ Bot run on the official API: deterministic temp-root vs. untrusted-path message send (delivered vs. rejected) plus an end-to-end cron auto-TTS voice delivered to the device.
  • Multi-agent review (correctness + security + altitude): confirmed the fix is at the right owner boundary (plugin-local; QQBot is the only sandboxing channel), no symlink/TOCTOU escape, memo caches success only. The first iteration was caught missing two sibling gates; this revision consolidates all three.

Risk

User-visible behavior change: Yes (QQBot now delivers framework temp-root media that was previously dropped). Config/migration: No. Security/auth/secrets: No — trust is limited to OpenClaw's already-hardened temp root, consistent with core's existing media allowlist; realpath containment is preserved, and the live negative control confirms out-of-root media is still rejected.

AI-assisted (Claude). Proof above includes a live QQ Bot run on the official API.

@openclaw-barnacle openclaw-barnacle Bot added channel: qqbot size: S triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels Jun 14, 2026
@clawsweeper

clawsweeper Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed June 15, 2026, 2:53 PM ET / 18:53 UTC.

Summary
The PR adds a QQBot trusted outbound media resolver for OpenClaw's hardened temp root, routes the three QQBot local-media gates through it, and adds resolver regression coverage.

PR surface: Source +62, Tests +70. Total +132 across 4 files.

Reproducibility: yes. Source inspection shows current main writes cron TTS audio under the preferred temp root while QQBot's current gates reject that root; the contributor's live proof shows the patched path delivering and an out-of-root control still rejected.

Review metrics: none identified.

Merge readiness
Overall: 🦞 diamond lobster
Proof: 🦞 diamond lobster ✨ media proof bonus
Patch quality: 🦞 diamond lobster
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Next step before merge

  • No automated repair is needed; the remaining action is maintainer review and merge decision for the clean, proof-positive external PR.

Security
Cleared: No concrete security or supply-chain concern was found; the diff adds no dependencies or workflows and limits new trust to OpenClaw's hardened temp root through realpath containment.

Review details

Best possible solution:

Land the scoped QQBot temp-root delivery fix and keep shared empty-identity delivery accounting on the separate accounting PR.

Do we have a high-confidence way to reproduce the issue?

Yes. Source inspection shows current main writes cron TTS audio under the preferred temp root while QQBot's current gates reject that root; the contributor's live proof shows the patched path delivering and an out-of-root control still rejected.

Is this the best way to solve the issue?

Yes. Extending QQBot's plugin-local sandbox to OpenClaw's already-hardened temp root is narrower than copying media, changing plugin APIs, or relying on the separate accounting fix.

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against c40db057da33.

Label changes

Label justifications:

  • P2: The PR fixes a concrete QQBot cron auto-TTS message-loss path with limited channel scope and no evidence of a crash loop, security bypass, or core-wide outage.
  • rating: 🦞 diamond lobster: Overall readiness is 🦞 diamond lobster; proof is 🦞 diamond lobster and patch quality is 🦞 diamond lobster.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (screenshot): The PR body and inspected QQ screenshot provide sufficient after-fix proof: live QQBot temp-root delivery succeeded, an out-of-root control was rejected, and cron auto-TTS voice arrived.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body and inspected QQ screenshot provide sufficient after-fix proof: live QQBot temp-root delivery succeeded, an out-of-root control was rejected, and cron auto-TTS voice arrived.
  • proof: 📸 screenshot: Contributor real behavior proof includes screenshot evidence. The PR body and inspected QQ screenshot provide sufficient after-fix proof: live QQBot temp-root delivery succeeded, an out-of-root control was rejected, and cron auto-TTS voice arrived.
Evidence reviewed

PR surface:

Source +62, Tests +70. Total +132 across 4 files.

View PR surface stats
Area Files Added Removed Net
Source 3 67 5 +62
Tests 1 70 0 +70
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 4 137 5 +132

What I checked:

  • Current main rejects cron TTS temp-root media at QQBot gates: On current main, resolveOutboundMediaPath and the post-wait voice re-check still call resolveQQBotPayloadLocalFilePath, which only trusts OpenClaw media storage roots, not the preferred temp root. (extensions/qqbot/src/engine/messaging/outbound-media-send.ts:142, c40db057da33)
  • Structured payload gate has the same current restriction: Current main's structured payload local-path validation also delegates to resolveQQBotPayloadLocalFilePath, so the sibling gate would reject the same temp-root file unless changed consistently. (extensions/qqbot/src/engine/messaging/reply-dispatcher.ts:205, c40db057da33)
  • Cron TTS writes generated audio under the preferred temp root: Speech-core creates TTS workspaces with rootDir: resolvePreferredOpenClawTmpDir() and returns that generated audio path as the reply payload media URL. (packages/speech-core/src/tts.ts:1346, c40db057da33)
  • Core already treats the temp root as a media root: buildMediaLocalRoots includes the preferred temp directory in the baseline local media root allowlist, so the PR aligns QQBot with an existing core media-root contract. (src/media/local-roots.ts:35, c40db057da33)
  • Temp root is permission-hardened before use: resolvePreferredOpenClawTmpDir rejects unsafe directories, checks ownership/writability, repairs permissions to 0700 when allowed, and falls back to a user-scoped temp root when needed. (src/infra/tmp-openclaw-dir.ts:39, c40db057da33)
  • PR unifies the QQBot trust boundary: The new resolveTrustedOutboundMediaPath first preserves QQBot media-storage trust, then accepts only paths contained by the preferred temp root through resolveLocalPathFromRootsSync. (extensions/qqbot/src/engine/messaging/trusted-media-path.ts:38, 56567160ed1d)

Likely related people:

  • cxyhhhhh: GitHub path history shows the unified QQBot media send architecture and local media handling came through the large QQBot group/media PR. (role: original QQBot media architecture contributor; confidence: high; commits: 5ccf179a34a9; files: extensions/qqbot/src/engine/messaging/outbound-media-send.ts, extensions/qqbot/src/channel.ts)
  • sliverp: Recent QQBot path history includes the OPENCLAW_HOME media-path fix and co-authorship on the QQBot media architecture that this PR extends. (role: QQBot media path contributor; confidence: high; commits: 0d23c3b4e133, 5ccf179a34a9; files: extensions/qqbot/src/engine/utils/platform.ts, extensions/qqbot/src/engine/messaging/outbound-media-send.ts)
  • steipete: History shows recent work extracting fs-safe primitives and repeated touches to QQBot media/local-root surfaces used by this patch. (role: recent media-root and fs-safe area contributor; confidence: medium; commits: 538605ff44d2, a607661a71d8; files: src/media/local-roots.ts, src/infra/fs-safe.ts, extensions/qqbot/src/engine/messaging/outbound-media-send.ts)
  • zhangguiping-xydt: The merged QQBot media-send failure surfacing PR overlaps the no-identity failure path that made the original cron issue look delivered. (role: recent adjacent QQBot media error-propagation contributor; confidence: medium; commits: 650c5cac3326; files: extensions/qqbot/src/channel.ts, extensions/qqbot/src/channel.message-adapter.test.ts)
  • vincentkoc: Recent speech-core history includes the voice model and speech-core extraction work around the TTS path that produces the temp-root audio payload. (role: recent speech-core TTS contributor; confidence: medium; commits: 27b15a19e84c; files: packages/speech-core/src/tts.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added the rating: 🌊 off-meta tidepool PR readiness rating does not apply to this item. label Jun 14, 2026
@openclaw-barnacle openclaw-barnacle Bot added proof: supplied External PR includes structured after-fix real behavior proof. and removed triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels Jun 14, 2026
@ZengWen-DT

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

Updated the Real behavior proof with a live QQ Bot run on the official API (wss://api.sgroup.qq.com), which closes the earlier "no live QQBot run" gap:

  • Positive: media staged under the OpenClaw temp root was delivered with a real platform identity (messageId ROBOT1.0_m-9gIoJjF…).
  • Negative control: the same bytes from a path outside every trusted root were rejected (empty messageId, no platform ids) — the gate is not over-broadened.
  • End-to-end cron with messages.tts.auto="always" produced a temp-root voice file that arrived on a real device as a QQ voice message; no media/outbound copy existed, confirming it was sent straight from the temp root.

This is the delivery fix (message actually arrives) and is complementary to the accounting-only #79811, not a duplicate.

@clawsweeper

clawsweeper Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event repository_dispatch).
Result: the existing ClawSweeper review comment will be edited in place when the review finishes.

Re-review progress:

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. P2 Normal backlog priority with limited blast radius. and removed rating: 🌊 off-meta tidepool PR readiness rating does not apply to this item. labels Jun 14, 2026
@clawsweeper clawsweeper Bot added the proof: 📸 screenshot Contributor real behavior proof includes screenshot evidence. label Jun 15, 2026
QQBot is the only channel that root-sandboxes outbound local files. Its three
gate sites (resolveOutboundMediaPath, the voice send re-check, and
structured-payload validation) only trusted the QQ Bot media storage roots, so
framework-generated scratch media written under OpenClaw's hardened temp root
(e.g. cron auto-TTS voice files from speech-core) was rejected. The send then
returned a no-identity error, the message was silently lost, yet cron still
recorded it as delivered.

Add one shared resolver (resolveTrustedOutboundMediaPath) that also trusts the
preferred OpenClaw temp root — already a sanctioned media root in core
(buildMediaLocalRoots) — and route all three gates through it so the trust set
agrees everywhere. Fixes openclaw#92816.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@vincentkoc
vincentkoc force-pushed the fix/92816-qqbot-cron-tts-delivery branch from 5656716 to 782343b Compare June 16, 2026 18:04
@openclaw-barnacle openclaw-barnacle Bot removed the proof: sufficient ClawSweeper judged the real behavior proof convincing. label Jun 16, 2026
@vincentkoc
vincentkoc merged commit cfdcd5c into openclaw:main Jun 16, 2026
158 of 161 checks passed
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jun 17, 2026
openclaw#92947)

QQBot is the only channel that root-sandboxes outbound local files. Its three
gate sites (resolveOutboundMediaPath, the voice send re-check, and
structured-payload validation) only trusted the QQ Bot media storage roots, so
framework-generated scratch media written under OpenClaw's hardened temp root
(e.g. cron auto-TTS voice files from speech-core) was rejected. The send then
returned a no-identity error, the message was silently lost, yet cron still
recorded it as delivered.

Add one shared resolver (resolveTrustedOutboundMediaPath) that also trusts the
preferred OpenClaw temp root — already a sanctioned media root in core
(buildMediaLocalRoots) — and route all three gates through it so the trust set
agrees everywhere. Fixes openclaw#92816.

Co-authored-by: zengwen <[email protected]>
Co-authored-by: Claude Opus 4.8 <[email protected]>
crh-code pushed a commit to crh-code/openclaw that referenced this pull request Jun 18, 2026
openclaw#92947)

QQBot is the only channel that root-sandboxes outbound local files. Its three
gate sites (resolveOutboundMediaPath, the voice send re-check, and
structured-payload validation) only trusted the QQ Bot media storage roots, so
framework-generated scratch media written under OpenClaw's hardened temp root
(e.g. cron auto-TTS voice files from speech-core) was rejected. The send then
returned a no-identity error, the message was silently lost, yet cron still
recorded it as delivered.

Add one shared resolver (resolveTrustedOutboundMediaPath) that also trusts the
preferred OpenClaw temp root — already a sanctioned media root in core
(buildMediaLocalRoots) — and route all three gates through it so the trust set
agrees everywhere. Fixes openclaw#92816.

Co-authored-by: zengwen <[email protected]>
Co-authored-by: Claude Opus 4.8 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: qqbot P2 Normal backlog priority with limited blast radius. proof: 📸 screenshot Contributor real behavior proof includes screenshot evidence. proof: supplied External PR includes structured after-fix real behavior proof. rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. size: S status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: 当tts设置为always时,cron会投递失败,但是结果会显示已投递

3 participants