fix(imessage): wire reply attachments through send-rich --file (with feature gate)#79864
Conversation
|
Codex review: needs maintainer review before merge. Summary Reproducibility: yes. by source inspection: current main's iMessage Real behavior proof Next step before merge Security Review detailsBest possible solution: Land the resolver-backed, feature-gated implementation after maintainer review confirms upstream Do we have a high-confidence way to reproduce the issue? Yes, by source inspection: current main's iMessage Is this the best way to solve the issue? Yes, directionally: feature-detecting What I checked:
Likely related people:
Remaining risk / open question:
Codex review notes: model gpt-5.5, reasoning high; reviewed against c198d4f23b4e. |
Codex review on openclaw#79864 flagged a P1/high security finding: the reply path treated media/path/filePath as trusted local paths and passed them straight to imsg send-rich --file, bypassing the existing outbound media resolver (mediaLocalRoots, sandbox, size limits). Extend hydrateAttachmentParamsForAction to gate reply alongside sendAttachment/setGroupIcon/upload-file so the resolver-validated buffer arrives in args before dispatch. Reply skips the caption -> message fallback (it owns its own text/message field). Rewrite the imessage extractor to consume only the hydrated buffer + filename and reject any unhydrated path-shaped param loudly, and drop the kind: "path" branch from sendRichMessage so the runtime can no longer forward a raw host path to imsg --file. Drops the non-canonical mediaUrls plural alias. Tests: cover the post-hydration contract for imessage reply, the bypass rejection for raw paths, and the runner-level reply hydration including the no-caption-fallback invariant.
Lifts coverage of openclaw#79864 from unit-only to integration: drives runMessageAction({action: "reply", ...}) end to end and asserts - a remote-URL path is loaded into args.buffer + filename via the resolver before the channel handler runs - a host path outside mediaLocalRoots rejects in the resolver before the handler runs (handleAction is never called) - reply does not inherit sendAttachment's caption->message fallback, so the channel handler does not see a synthetic caption that would collide with the reply payload This pins the runner-side wiring that the imessage actions test already exercises at the handler boundary.
a98a9c7 to
0e26214
Compare
Codex review on openclaw#79864 flagged a P1/high security finding: the reply path treated media/path/filePath as trusted local paths and passed them straight to imsg send-rich --file, bypassing the existing outbound media resolver (mediaLocalRoots, sandbox, size limits). Extend hydrateAttachmentParamsForAction to gate reply alongside sendAttachment/setGroupIcon/upload-file so the resolver-validated buffer arrives in args before dispatch. Reply skips the caption -> message fallback (it owns its own text/message field). Rewrite the imessage extractor to consume only the hydrated buffer + filename and reject any unhydrated path-shaped param loudly, and drop the kind: "path" branch from sendRichMessage so the runtime can no longer forward a raw host path to imsg --file. Drops the non-canonical mediaUrls plural alias. Tests: cover the post-hydration contract for imessage reply, the bypass rejection for raw paths, and the runner-level reply hydration including the no-caption-fallback invariant.
Lifts coverage of openclaw#79864 from unit-only to integration: drives runMessageAction({action: "reply", ...}) end to end and asserts - a remote-URL path is loaded into args.buffer + filename via the resolver before the channel handler runs - a host path outside mediaLocalRoots rejects in the resolver before the handler runs (handleAction is never called) - reply does not inherit sendAttachment's caption->message fallback, so the channel handler does not see a synthetic caption that would collide with the reply payload This pins the runner-side wiring that the imessage actions test already exercises at the handler boundary.
Codex review on openclaw#79864 flagged a P1/high security finding: the reply path treated media/path/filePath as trusted local paths and passed them straight to imsg send-rich --file, bypassing the existing outbound media resolver (mediaLocalRoots, sandbox, size limits). Extend hydrateAttachmentParamsForAction to gate reply alongside sendAttachment/setGroupIcon/upload-file so the resolver-validated buffer arrives in args before dispatch. Reply skips the caption -> message fallback (it owns its own text/message field). Rewrite the imessage extractor to consume only the hydrated buffer + filename and reject any unhydrated path-shaped param loudly, and drop the kind: "path" branch from sendRichMessage so the runtime can no longer forward a raw host path to imsg --file. Drops the non-canonical mediaUrls plural alias. Tests: cover the post-hydration contract for imessage reply, the bypass rejection for raw paths, and the runner-level reply hydration including the no-caption-fallback invariant.
Lifts coverage of openclaw#79864 from unit-only to integration: drives runMessageAction({action: "reply", ...}) end to end and asserts - a remote-URL path is loaded into args.buffer + filename via the resolver before the channel handler runs - a host path outside mediaLocalRoots rejects in the resolver before the handler runs (handleAction is never called) - reply does not inherit sendAttachment's caption->message fallback, so the channel handler does not see a synthetic caption that would collide with the reply payload This pins the runner-side wiring that the imessage actions test already exercises at the handler boundary.
0e26214 to
41ed575
Compare
Codex review on openclaw#79864 flagged a P1/high security finding: the reply path treated media/path/filePath as trusted local paths and passed them straight to imsg send-rich --file, bypassing the existing outbound media resolver (mediaLocalRoots, sandbox, size limits). Extend hydrateAttachmentParamsForAction to gate reply alongside sendAttachment/setGroupIcon/upload-file so the resolver-validated buffer arrives in args before dispatch. Reply skips the caption -> message fallback (it owns its own text/message field). Rewrite the imessage extractor to consume only the hydrated buffer + filename and reject any unhydrated path-shaped param loudly, and drop the kind: "path" branch from sendRichMessage so the runtime can no longer forward a raw host path to imsg --file. Drops the non-canonical mediaUrls plural alias. Tests: cover the post-hydration contract for imessage reply, the bypass rejection for raw paths, and the runner-level reply hydration including the no-caption-fallback invariant.
Lifts coverage of openclaw#79864 from unit-only to integration: drives runMessageAction({action: "reply", ...}) end to end and asserts - a remote-URL path is loaded into args.buffer + filename via the resolver before the channel handler runs - a host path outside mediaLocalRoots rejects in the resolver before the handler runs (handleAction is never called) - reply does not inherit sendAttachment's caption->message fallback, so the channel handler does not see a synthetic caption that would collide with the reply payload This pins the runner-side wiring that the imessage actions test already exercises at the handler boundary.
bc0a91b to
959fc62
Compare
Codex review on openclaw#79864 flagged a P1/high security finding: the reply path treated media/path/filePath as trusted local paths and passed them straight to imsg send-rich --file, bypassing the existing outbound media resolver (mediaLocalRoots, sandbox, size limits). Extend hydrateAttachmentParamsForAction to gate reply alongside sendAttachment/setGroupIcon/upload-file so the resolver-validated buffer arrives in args before dispatch. Reply skips the caption -> message fallback (it owns its own text/message field). Rewrite the imessage extractor to consume only the hydrated buffer + filename and reject any unhydrated path-shaped param loudly, and drop the kind: "path" branch from sendRichMessage so the runtime can no longer forward a raw host path to imsg --file. Drops the non-canonical mediaUrls plural alias. Tests: cover the post-hydration contract for imessage reply, the bypass rejection for raw paths, and the runner-level reply hydration including the no-caption-fallback invariant.
Lifts coverage of openclaw#79864 from unit-only to integration: drives runMessageAction({action: "reply", ...}) end to end and asserts - a remote-URL path is loaded into args.buffer + filename via the resolver before the channel handler runs - a host path outside mediaLocalRoots rejects in the resolver before the handler runs (handleAction is never called) - reply does not inherit sendAttachment's caption->message fallback, so the channel handler does not see a synthetic caption that would collide with the reply payload This pins the runner-side wiring that the imessage actions test already exercises at the handler boundary.
959fc62 to
5e5cdfe
Compare
|
Merged via squash.
Thanks @omarshahine! |
…feature gate) (openclaw#79864) Merged via squash. Prepared head SHA: 5e5cdfe Co-authored-by: omarshahine <10343873+omarshahine@users.noreply.github.com> Co-authored-by: omarshahine <10343873+omarshahine@users.noreply.github.com> Reviewed-by: @omarshahine
…feature gate) (openclaw#79864) Merged via squash. Prepared head SHA: 5e5cdfe Co-authored-by: omarshahine <10343873+omarshahine@users.noreply.github.com> Co-authored-by: omarshahine <10343873+omarshahine@users.noreply.github.com> Reviewed-by: @omarshahine
…feature gate) (openclaw#79864) Merged via squash. Prepared head SHA: 5e5cdfe Co-authored-by: omarshahine <10343873+omarshahine@users.noreply.github.com> Co-authored-by: omarshahine <10343873+omarshahine@users.noreply.github.com> Reviewed-by: @omarshahine
Summary
Replaces #79822 (closed). Lets
action: "reply"actually deliver an attachment in a single threaded send instead of either silently dropping the file (pre-#79822 behavior) or throwing unconditionally (post-#79822 behavior).The threaded send path used to route through
imsg send-rich --reply-to, which had no--fileflag. The new flag landed in openclaw/imsg#114, so this PR plumbs attachments throughsendRichMessagewhenever the installed imsg build advertises that capability — and falls back to an explicit error on older builds, matching the behavior #79822 shipped as a stop-gap.What changes
extensions/imessage/src/private-api-status.ts— addscliCapabilities.sendRichSupportsAttachment?: booleantoIMessagePrivateApiStatus.extensions/imessage/src/probe.ts— runsimsg send-rich --helponce during the existing private-API probe and grep-checks the output for--file. Result is cached on the status object alongsideselectors/rpcMethods. Failures resolve tofalseso callers fall back to the explicit error rather than silently dropping.extensions/imessage/src/actions.runtime.ts—sendRichMessageaccepts an optionalattachment: { kind: "path"; path } | { kind: "buffer"; buffer; filename }. Path attachments append--file <path>directly; buffer attachments stage to a temp file via the existingwithTempFilehelper before invoking imsg.extensions/imessage/src/actions.ts—replyaction extracts attachment params (filePath/path/media/mediaUrl/fileUrl/ firstmediaUrlsentry / base64buffer+filename), refuseshttp(s)://URLs with a targeted error so we don't silently duplicate the outbound media-resolver layer, and gates the rest oncliCapabilities.sendRichSupportsAttachment === true. On unsupported imsg builds the throw cites [codex] support threaded attachment replies imsg#114 and points callers atupload-file/send.extensions/imessage/src/actions.test.ts— extends the existing reply test scaffolding with five new cases:mediaUrlsentry threads through,buffer+filenamedecodes into a buffer-shaped attachment,sendRichSupportsAttachmentisfalse(covering all six aliases includingbuffer),http(s)URL is rejected with the targeted "threaded send path cannot fetch" error.Why this shape
--helprather than a version pin — backports and pre-release builds still work, and we never need to bump a constant when imsg's release cadence changes.kind: "path"|"buffer"shape keeps temp-file lifecycle in the runtime wherewithTempFilealready lives, instead of leaking that detail into the action handler. Path callers (the common Lobsterimage_generatecron path) get zero-copy delivery.http(s)://rather than fetching is intentional: thesendaction already has a full attachment-resolver pipeline (resolveOutboundAttachmentFromUrl, size limits, content-type negotiation). Re-implementing a subset on the reply path would invite divergence; the targeted error pushes the caller tosendfor that case.Real behavior proof
Behavior addressed: Production agent on macOS calls
messagewith{ action: "reply", channel: "imessage", target: "+1...", messageId: "...", media: "/.../ig_*.png", message: "🦞 Here it is." }. Today (pre-#114, post-#79822) this throws. Once #114 lands, this PR routes the attachment throughimsg send-rich --reply-to ... --file <path>and the recipient sees a single threaded reply with the image attached.Real environment tested: Live OpenClaw gateway on macOS 26.4.0 (Tahoe) M1 —
openclaw 2026.5.6 (71a2042),imsg 0.8.0, IMCore v2 bridge, SIP disabled. iMessage channel is the nativeimessageextension. Same machine that hit the original silent-drop incident from #79822.Exact steps or command run after this patch:
pnpm build && openclaw gateway restart.imsg 0.8.0, before feat: add ATLAS knowledge base integration #114 ships): probe runs against installed imsg, finds no--fileflag inimsg send-rich --help, cachescliCapabilities.sendRichSupportsAttachment: false.After-fix evidence:
Capability probe against the locally installed
imsg 0.8.0(this PR's gating signal):Targeted unit tests against the patched module on a live Mac (28/28 pass; new cases highlighted):
The five new cases, copied from the run with
--reporter verbose:Observed result after fix:
imsg 0.8.0(nosend-rich --file): areplycarrying any of the seven attachment-bearing param aliases throwsiMessage reply with an attachment needs an imsg build that exposes \send-rich --file` ([codex] support threaded attachment replies imsg#114). Upgrade imsg, or use action 'upload-file' (with filePath/filename) or action 'send' (with media) to deliver the file plus a separate 'reply' for any text.` — matching the explicit-error contract from fix(imessage): reject attachments on reply instead of silently dropping them #79822.send-rich --file(probe returnstrue): the samereplyresolves to one bridge call carrying both--reply-toand--file, delivering the threaded reply with the attachment in a single message — verified by the new unit tests assertingruntime.sendRichMessageis called withattachment: { kind: "path", path }(and the buffer-shaped equivalent for base64 inputs).What was not tested: End-to-end with imsg 0.9.x is not yet possible on this host because openclaw/imsg#114 is still open. As soon as it merges and lands in a release, a follow-up will swap the local
imsg 0.8.0for the new build and re-record the trajectory excerpt that today shows the throw — that gap is the only thing standing between this PR and a true post-fix delivery transcript.Best-fix justification
mediaon a reply just stop seeing silent drops without changing prompts.imsg send-rich --helpper probe interval rather than per call.🤖 Generated with Claude Code
Codex review follow-ups (2026-05-09)
Codex review on this PR flagged the reply path for bypassing the existing outbound media resolver (
mediaLocalRoots/ sandbox / size limits). Two follow-up commits address the finding:cf93925b8c fix(imessage,outbound): route reply attachments through media resolverhydrateAttachmentParamsForAction(src/infra/outbound/message-action-params.ts) to gate onreplyalongsidesendAttachment/setGroupIcon/upload-file. Reply runs withallowMessageCaptionFallback: falseso it doesn't synthesize a bogus caption frommessage.extractReplyAttachment(extensions/imessage/src/actions.ts) to consume only the hydratedbuffer+filename. Path-shaped params without a buffer (hydration was skipped) reject loudly — defense in depth against any caller that bypasses the runner.kind: "path"branch fromsendRichMessage(extensions/imessage/src/actions.runtime.ts) so a raw filesystem path can no longer reachimsg --fileregardless of code path.a98a9c7427 test(outbound): cover reply hydration at the runner levelsrc/infra/outbound/message-action-runner.media.test.tsthat driverunMessageAction({action: "reply", ...})end to end and assert: the resolver loadspath/mediaintoargs.bufferbefore the channel handler runs; host paths outsidemediaLocalRootsreject before the handler runs; reply doesn't inherit the caption-fallback.Live proof on dev sandbox (2026-05-09)
Tested against a worktree-built dist (
cf93925/a98a9c7) running on an isolated~/.openclaw-devprofile (port 19001) — same iMessage runtime andimsgbinary as Lobster's prod gateway but with no port or state overlap.Negative path — defense-in-depth bypass guard. The gateway's
message.actionRPC dispatches directly to the channel handler, bypassing the runner's hydration. The new bypass guard fires verbatim:Happy path — full hydrate-then-deliver.
runMessageActioninvoked directly viatsx(exercising the same code the agent message tool runs), against animsgbuild withsend-rich --file:Confirmed in
chat.db(threaded reply with attachment, delivered):thread_originator_guidmatches the original message's GUID;cache_has_attachments=1. The recipient sees a single threaded iMessage reply with the image inline.