feat(imessage): private-API support via imsg JSON-RPC [AI-assisted]#78317
feat(imessage): private-API support via imsg JSON-RPC [AI-assisted]#78317omarshahine merged 20 commits intoopenclaw:mainfrom
Conversation
|
Codex review: needs changes before merge. Summary Reproducibility: yes. at source level. Current main lacks the new iMessage action surface and has the chat_id-only echo-scope behavior, while the PR findings are visible by comparing the latest branch code to the shared upload-file contract and upstream imsg RPC docs. Real behavior proof Next step before merge Security Review findings
Review detailsBest possible solution: Keep the implementation in the iMessage plugin, repair the shared action and formatting contracts, narrow the cache threat-model wording, then have maintainers decide whether to merge the private-API direction. Do we have a high-confidence way to reproduce the issue? Yes at source level. Current main lacks the new iMessage action surface and has the chat_id-only echo-scope behavior, while the PR findings are visible by comparing the latest branch code to the shared upload-file contract and upstream imsg RPC docs. Is this the best way to solve the issue? No as-is. The plugin-owned boundary is the right direction, but the branch should support path-based upload-file inputs, avoid unsupported formatting on RPC send, update labeler coverage, and tighten the cache threat-model wording before maintainer review. Full review comments:
Overall correctness: patch is incorrect Security concerns:
Acceptance criteria:
What I checked:
Likely related people:
Remaining risk / open question:
Codex review notes: model gpt-5.5, reasoning high; reviewed against 6cfb08680e71. |
14eff53 to
cc5e482
Compare
|
Update: rebased onto current New commit:
No code, no behavior, no edits to Local re-verification on the rebased tree:
|
|
Review follow-ups landed (4 commits). Summary:
Fix: adds Tests cover: rejection of inbound short id under
Local validation
Out of scope (filed separately)
Re-review progress:
|
Real behavior proof — live macOS Sequoia 15.x, imsg 0.6.0Captured 2026-05-06 from the actively-running gateway (PID 47600 at capture time, dist built from this branch's tip prior to the four follow-up commits — those land on next rebuild). All identifiers below redacted. imsg bridge capabilityPer-method capability gating is doing its job: Cache file modes (the security hardening, on disk)
Reply cache content shape (one entry, redacted){
"accountId": "default",
"shortId": "25",
"messageId": "<GUID>",
"chatGuid": "<REDACTED>",
"chatIdentifier": "<REDACTED>",
"chatId": "<int>",
"timestamp": 1778049001439
}13 live entries, allocated Recent log evidence — production code paths firingThe warn-once one-time message we coded in After upgrading to The bluebubbles plugin reply-context cache shows live test traffic from earlier in the cutover: (That message was the canary used to verify the Test summary (this branch)Pre-existing red, scoped proof
That's the substantive evidence. The actively-running gateway has been driving this code on a real Mac with a real Re-review progress:
|
9210be0 to
e1e60c1
Compare
|
Follow-up commit Lobster (this PR's bundled-iMessage live-test rig) had been silently dropping every group message since the BlueBubbles → bundled-iMessage migration. Diagnosed today: the bundled iMessage plugin runs two separate group allowlist gates back-to-back, and only the first one ( The drop's only signal is This commit documents the failure mode in two places:
Filed #78749 to raise the underlying log severity / surface the misconfiguration at gateway start so the next operator who hits this isn't debugging blind. The docs land first because the bug fix is a separate change that needs its own review. Re-review progress:
|
9b48f82 to
37832b5
Compare
37832b5 to
6ccd21b
Compare
d716c79 to
e2305ce
Compare
721183a to
f3df4d4
Compare
Wires the bundled iMessage plugin to drive imsg over JSON-RPC instead of treating it as a passive AppleScript shim. The plugin now reaches the private-API surface that imsg exposes through its dylib bridge, while remaining safe against older imsg builds that lack newer methods. Capability gating - probe.ts reads the rpc_methods array from imsg status --json and exposes imessageRpcSupportsMethod so each consumer can feature-detect per method instead of pinning a CLI version. Older imsg builds (no rpc_methods) are treated as unsupported for newly-named methods only. Outbound action surface - New actions.ts plus actions.runtime.ts give the plugin a full action set: react, edit, unsend, reply, sendWithEffect, renameGroup, setGroupIcon, addParticipant, removeParticipant, leaveGroup, sendAttachment. Private-API actions throw a clear error when the bridge is unavailable rather than silently degrading to AppleScript. - send.ts gains private-API send paths and unifies how outbound media is staged. Inbound dispatch - monitor-provider switches from createReplyDispatcher to createReplyDispatcherWithTyping and feeds it the typingCallbacks the SDK pipeline produces, so a typing bubble is shown to the sender while the agent generates and stops cleanly on idle. - Each accepted inbound chat is marked read before dispatch when the bridge is up and sendReadReceipts is not explicitly disabled, so users see "Read" instead of "Delivered". - New chat.ts module hosts the typing/read/markUnread/create/delete plus group rename/setIcon/addParticipant/removeParticipant/leave helpers, all routed through the persistent imsg rpc client. Resilience - probe.ts no longer pins a permanently-false private-API status: the cache only short-circuits on confirmed availability, and a lazy probe fires on first action when readiness flips. monitor-provider re-runs the probe after watch.subscribe so the cached status reflects what actually came up. - Reply caching, reflection-guarding, and persisted echo dedupe were tuned to keep the gateway from talking to itself when imsg surfaces both sides of a conversation through chat.db (full set of regression tests added). Schema - IMessageAccountSchemaBase gains optional per-action toggles and a sendReadReceipts boolean. Bundled channel config metadata regenerated. Docs - docs/channels/imessage.md updated with the private-API setup steps, capability discovery, and the relationship between the plugin and steipete/imsg. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Outbound iMessage `send` writes echo-cache scope keys keyed on whichever target shape the caller used: `chat_id:N`, `chat_guid:<guid>`, `chat_identifier:<id>`, or `imessage:<handle>` (see resolveOutboundEchoScope in send.ts). Inbound echo detection only built the chat_id-flavored scope for groups (and `imessage:<sender>` for DMs), so a send addressed by chat_guid would never match the chat_id-only inbound lookup, the agent would re-process its own message as fresh input, and command loops became reachable. Mirror every persistable scope shape on the inbound side and probe all candidate scopes inside hasIMessageEchoMatch. Existing chat_id-only flows are unaffected (single scope, single probe). Adds regression tests covering chat_guid-scoped, chat_identifier-scoped, and chat_id- scoped echoes plus a non-match guard against unrelated chat_guid entries.
reply-cache.jsonl maps gateway-allocated short-ids to message guids. With default mode 0644 + parent dir 0755, a hostile same-UID process on a multi-user host could (a) read the file to enumerate active conversation guids or (b) inject lines so a future shortId resolution returned an attacker-chosen guid — letting the agent react/edit/unsend a message it never saw. Clamp file to 0600 and dir to 0700 on every write/append, plus chmod existing entries from older gateway versions.
Three Unreleased entries: feature note for the imsg JSON-RPC private-API surface, fix for chat_guid/chat_identifier echo dedupe, and security fix for reply-cache.jsonl owner-only permissions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Auto-fixes from `pnpm lint:extensions --fix` plus three manual cleanups: drop the unused `readMessageId` helper (superseded by `readMessageIdWithChatFallback`), drop the stale `WORD` regex constant, and drop the unused `vi` import in `monitor-reply-cache.test.ts`. No behavior change — every reformatted block was already curly-equivalent or already collapsed a redundant `=== true` / `!== true` to direct boolean use. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New docs/channels/imessage-from-bluebubbles.md covers: - when migration makes sense (and when to stay on BlueBubbles) - before-you-start prerequisites (imsg install, private API probe, config snapshot) - full BB to bundled-iMessage config-key translation table - step-by-step dry-run + cutover + verification flow - action parity matrix - pairing/session/ACP-binding migration notes - dual-running caveat (BlueBubbles preferOver still wins) - rollback steps Cross-linked from docs/channels/imessage.md Related section. Registered in docs/docs.json sidebar nav alongside the other channel pages, and added "iMessage" + "Coming from BlueBubbles" to docs/.i18n/glossary.zh-CN.json so docs:check-i18n-glossary passes. No code changes, no behavior changes, no edits to extensions/bluebubbles. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sent-echoes.jsonl carries scope keys, outbound message text, and messageIds. With default mode 0644 + parent dir 0755, a hostile same-UID process on a multi-user host could (a) read the file to enumerate active conversations and outbound content, or (b) inject lines so a future inbound dedupe call wrongly suppresses a legitimate inbound message. Mirrors the reply-cache.jsonl hardening from the prior security commit: clamp file to 0600 and dir to 0700 on every write, plus chmod existing entries written by older gateway versions. Adds regression tests for fresh-write and clamp-on-upgrade paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
IMessageAccountConfig (src/config/types.imessage.ts:82) declares probeTimeoutMs?: number, and actions.ts / probe.ts both read it as account.config.probeTimeoutMs. But the field was missing from IMessageAccountSchemaBase, so any value in user config was silently stripped at zod parse time and the runtime always fell back to DEFAULT_IMESSAGE_PROBE_TIMEOUT_MS. Adds the field between mediaMaxMb and textChunkLimit (matching the placement in the TS type), regenerates bundled-channel-config-metadata, and adds a CHANGELOG ### Fixes entry. Also documents the sent-echoes.jsonl 0600/0700 hardening alongside the existing security entries. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reply cache was previously inbound-only — every entry came from monitor/inbound-processing.ts and represented a message the gateway received from another participant. The edit and unsend actions resolved agent-supplied message ids through this cache and dispatched the bridge call against any guid that came back, with chat-scope verification but no sender check. That left a permission boundary gap: an agent authorized for group chat G could call edit/unsend with a short id pointing at a message a human participant sent in G. Messages.app enforces sender-only at the OS level so the bridge would likely fail, but failing earlier in the plugin produces a clean error and avoids dispatching a guaranteed-to- fail bridge call. Changes: - Add `isFromMe?: boolean` to IMessageReplyCacheEntry. Persists through the JSONL round-trip; missing values from older gateway versions are treated as the safe default (not-from-me). - monitor/inbound-processing.ts: existing call now records `isFromMe: false`. - send.ts: new outbound call after a successful imsg send records `isFromMe: true` keyed by the resolved chat target. - monitor-reply-cache.ts:resolveIMessageMessageId gains an optional `requireFromMe` flag that throws a clear "not one this agent sent" error when the cached entry is `isFromMe: false` (or absent). - actions.ts: edit and unsend handlers pass `requireFromMe: true`. React, reply, sendWithEffect, sendAttachment, and group ops are unchanged — they don't have the same retroactive-mutation concern. Adds regression tests covering: rejection of inbound short id under requireFromMe, allow-self short id, rejection of uncached full guid, rejection of legacy persisted entries with no isFromMe field. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related changes to the iMessage state-cache plumbing: 1. persisted-echo-cache.ts: switch the hot path from full file rewrite on every send to appendFileSync, with periodic compaction triggered only when the on-disk file grows past 2x MAX_PERSISTED_ECHO_ENTRIES or holds entries beyond the TTL window. Group-chat bursts that fired 5+ outbound messages back-to-back used to write the entire 256-entry file 5+ times; now they're 5 line-appends plus one rewrite when compaction is due. Mirror stays in sync via direct in-memory append plus the existing mtime-based invalidation on reads. 2. monitor-reply-cache.ts: appendPersistedEntry was conditionally chmod'ing only on file creation, leaving older 0644 files from pre-hardening gateway versions unchanged. Always clamp now — appendFileSync's `mode` parameter only applies on creation so the chmod is the only path to tighten an existing file. chmod is microseconds; doing it every append keeps the security guarantee monotonic instead of conditional on creation order. Adds a clamp-from-pre-existing-0644 regression test for the reply-cache hardening and parallels the same test for sent-echoes from the prior security commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bundled iMessage plugin runs two separate group allowlist gates
back-to-back, and both must pass for a group message to reach the
agent:
1. Sender / chat-target allowlist via channels.imessage.groupAllowFrom.
2. Group registry via channels.imessage.groups. With
groupPolicy: "allowlist", this gate requires either a
groups: { "*": { ... } } wildcard entry (sets allowAll=true) or an
explicit per-chat_id entry.
If gate 2 has nothing in it, every group message is dropped and the
rejection logs only at verbose/debug level
(extensions/imessage/src/monitor/inbound-processing.ts:336-340). At the
default info log level the drops are silent. DMs continue to work
because they take a different code path.
This is the most common BlueBubbles -> bundled-iMessage migration
failure: BB config typically carries
groups: { "*": { "requireMention": true } }, which sets allowAll=true
in the registry resolver. Operators copying the migration translation
table copy groupAllowFrom and groupPolicy but skip the groups block
because it looks like an unrelated mention setting.
- docs/channels/imessage-from-bluebubbles.md: new "Group registry
footgun" section between the config-translation table and step-by-
step. Cutover example config now includes the groups block. New
group-specific verification step in the cutover flow (DM success
doesn't prove groups are routing).
- docs/channels/imessage.md: Warning callout in the Group policy tab
documenting the dual-gate behavior and how to debug a suspected
silent drop with OPENCLAW_LOG_LEVEL=debug.
- docs/.i18n/glossary.zh-CN.json: BlueBubbles, Pairing, Channel Routing
glossary entries so docs:check-i18n-glossary passes.
Tracker for raising the drop's log severity so it isn't silent at info:
openclaw#78749
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…effect aliases Two issues caught by `codex review --base origin/main` against the PR: 1. resolveIMessageMessageId was reading the in-memory short-id maps without hydrating from disk first. rememberIMessageReplyCache hydrates on its own write path, but a resolve call that fired before any remember (the natural post-restart sequence: agent gets an inbound message with a MessageSid that was issued and persisted before the restart, then tries to react/reply/edit/unsend) would miss the persisted JSONL and throw "no longer available". Add a hydrateFromDiskOnce() call at the top of the resolver. New regression test simulates the post-restart sequence (issue, _reset, restore JSONL, resolve) and asserts the short id resolves. 2. effectIdFromParam advertised five screen-effect aliases — echo, happybirthday, shootingstar, sparkles, spotlight — in its error message, but those weren't in the alias map. Agents directed by our own error message to "Use one of: ... echo, happybirthday, ..." were getting "unknown effect" thrown back. Add the missing aliases pointing at the canonical com.apple.messages.effect.CK*Effect identifiers (and the dashed "happy-birthday" / "shooting-star" variants for forgiving input). New parametrized regression test covers all five aliases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… docs, add catchup gap
Three corrections to the iMessage / BlueBubbles channel docs that
land together because they're the same factual fix:
1. Both channels require disabling SIP, not just BlueBubbles.
The previous framing claimed bundled iMessage reached the advanced
action surface "without requiring SIP disabled". That was wrong.
imsg's README is explicit: "Advanced features such as read, typing,
launch, bridge-backed rich send, message mutation, and chat
management are opt-in. They require SIP to be disabled and a helper
dylib to be injected into Messages.app. imsg launch refuses to
inject when SIP is enabled." The injection technique is a port of
the BlueBubbles private-API surface into imsg's own dylib, so the
underlying Apple constraint (no third-party access to internal
IMCore functions with SIP on) applies identically to both channels.
- imessage.md: drops the "does not require disabling SIP" claim
from the side-by-side comparison Note. Adds a "**Both channels
require disabling SIP**" paragraph spelling out the parity in
prerequisites.
- imessage-from-bluebubbles.md: drops the "you can re-enable SIP"
reason for migrating, replaces with a "What does NOT change with
this migration" section that lists SIP, FDA, Automation
permission, and macOS-version caveats as constants between the
two channels.
- bluebubbles.md: reverts the "Enabling the BlueBubbles Private
API" section that I added earlier in this PR. Per maintainer
direction, that content belongs with the iMessage plugin docs
(we're documenting the operational requirements once, in the
channel that actually needs them written down here, since the
BlueBubbles upstream guide already covers the BB side
authoritatively).
2. New "Enabling the imsg private API" section in imessage.md.
Sits between "Requirements and permissions (macOS)" and "Access
control and routing". Covers the basic-mode vs Private-API-mode
split, the macOS-version-specific SIP-disable flow (defers to the
BlueBubbles upstream guide as the canonical step-by-step since the
SIP procedure itself is identical), the imsg launch injection
step, the OpenClaw-side verification flow, and a "When you can't
disable SIP" subsection covering basic-mode fallback options.
3. Catchup gap noted as a missing-vs-BlueBubbles parity item.
Bundled iMessage does not yet deliver inbound messages that
arrived while the gateway was down — imsg watch resumes from
current chat.db state. BlueBubbles handles this through webhook
replay + history fetch. This is the most operationally
significant parity gap for production deployments and the docs
should be honest about it. Tracked at openclaw#78649.
- imessage.md: new "Known gaps where bundled iMessage is not yet
at parity with BlueBubbles" subsection in the parity Note.
- imessage-from-bluebubbles.md: catchup row added to the action
parity matrix with operational guidance ("if your deployment is
sensitive to that, stay on BlueBubbles until openclaw#78649 lands").
Same-sender DM coalescing is also flagged in both places as a
not-yet-ported BlueBubbles feature.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…I-assisted] Apple's Messages app splits a single composition (`<command> <URL>`, attachment + caption) into multiple chat.db rows ~0.8-2.0 s apart. Without coalescing, the agent sees the command alone on turn 1 (often replies "send me the URL") and the URL on turn 2 with the command context lost. Same root cause BlueBubbles already fixes via coalesceSameSenderDms; the imsg plugin reads the same chat.db so it inherits the same problem. Wires the existing schema flag end-to-end: - monitor-provider: opt-in DM-only key, group instant-dispatch gate, merge helper on flush, 2500 ms default debounce window when flag enabled with no explicit messages.inbound.byChannel.imessage override - coalesce.ts: combineIMessagePayloads helper mirroring BB's caps and semantics (4000 char text cap, 20 attachment cap, 10 entry cap with first+latest preserved, GUID tracking, reply-context preference). Mirrors BB on purpose so a future SDK lift is mechanical. - types.imessage.ts: matching TS field with doc comment Group chats keep instant per-message dispatch so multi-user turn structure is preserved. From-me messages stay on the cache-only path. Attachments are now preserved on merge instead of nulled (the legacy onFlush dropped them). Also fixes a Mintlify rendering bug in docs/channels/imessage.md where unindented code fences inside <Accordion> blocks were collapsing the AccordionGroup into one paragraph. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
lint:tmp:no-random-messaging blocked CI on this branch — withTempFile was reaching for node:os tmpdir(). Swap to resolvePreferredOpenClawTmpDir from openclaw/plugin-sdk/temp-path, matching the discord/zalo pattern.
f3df4d4 to
b7d336b
Compare
|
Merged via squash.
Thanks @omarshahine! |
…penclaw#78317) Merged via squash. Prepared head SHA: b7d336b Co-authored-by: omarshahine <10343873+omarshahine@users.noreply.github.com> Co-authored-by: omarshahine <10343873+omarshahine@users.noreply.github.com> Reviewed-by: @omarshahine
…penclaw#78317) Merged via squash. Prepared head SHA: b7d336b Co-authored-by: omarshahine <10343873+omarshahine@users.noreply.github.com> Co-authored-by: omarshahine <10343873+omarshahine@users.noreply.github.com> Reviewed-by: @omarshahine
Summary
imessageplugin was a passive AppleScript shim. Tapbacks, threaded replies, edits, unsend, expressive effects, attachments, and group management were not reachable, even on Macs already runningsteipete/imsg. Inbound chats showed "Delivered" with no typing indicator, and outbound messages addressed bychat_guidcould re-feed the agent its own reply through the chat.db echo path.imsginstall already exposes the private-API surface over JSON-RPC. Wiring the plugin to it gets iMessage to BlueBubbles-shaped action parity, removes a class of self-reflection loops, and locks down the on-disk reply cache so a hostile same-UID process cannot enumerate or inject conversation guids.actions.ts/actions.runtime.ts/chat.tsgive the pluginreact,edit,unsend,reply,sendWithEffect,renameGroup,setGroupIcon,addParticipant,removeParticipant,leaveGroup,sendAttachmentthrough the imsg RPC bridge. Bridge-unavailable throws an explicit error instead of silently degrading.probe.tsreadsrpc_methodsfromimsg status --jsonand exposesimessageRpcSupportsMethodso each consumer feature-detects per method instead of pinning a CLI version. Probe cache no longer pins a permanently-false private-API status; lazy probe re-fires on first action when readiness flips.createReplyDispatcherWithTypingand marks chats read before dispatch when the bridge is up andsendReadReceiptsis not explicitly disabled.chat_id:N,chat_guid:<guid>,chat_identifier:<id>,imessage:<handle>) on the inbound side, closing the chat_guid-only self-reflection loop.reply-cache.jsonlis clamped to0600(parent dir0700) on every write/append and chmod'd on existing entries from older gateway versions.IMessageAccountSchemaBasegains optional per-action toggles andsendReadReceipts.IMessageAccountConfigtype, zod schema, regenerated bundled-channel-config-metadata, and docs are aligned.docs/channels/imessage.mdrewritten for private-API setup, capability detection, action reference, and read-receipt/typing behavior.docs/channels/index.mdrepositions the iMessage entry from "(legacy)" to its current capabilities.extensions/bluebubbles/**— BlueBubbles'preferOver: ["imessage"]is left intact. No edits tosrc/config/plugin-auto-enable.*. No new gateway/auth surface. No new network endpoints — RPC is stdio JSON-RPC over the existingimsgchild process.Change Type
Scope
IMessageAccountSchemaBase,pluginInspectorblock)Linked Issue/PR
replyaction and the persistent imsg RPC client).chat_guid-addressed sends bypass the chat_id-only inbound lookup).sendWithEffectfor iMessage screen effects; rich text formatting is not addressed here).Root Cause (for the two bug fixes)
sendwrites echo-cache scope keys keyed on whichever target shape the caller used (chat_id,chat_guid,chat_identifier,imessage:<handle>), but inbound echo detection only built thechat_id-flavored scope for groups. A send addressed bychat_guidwould never match thechat_id-only inbound lookup, so the agent re-processed its own message as fresh input and command loops became reachable.0644file +0755dir let any same-UID process on a multi-user host enumerate active conversation guids or inject lines so a future shortId resolution returned an attacker-chosen guid — letting the agent react/edit/unsend a message it never saw.chat_idecho scopes; no fs-mode assertion coveredreply-cache.jsonl.Regression Test Plan
extensions/imessage/src/monitor/inbound-processing.test.ts— chat_guid / chat_identifier / chat_id echo matches + non-match guardextensions/imessage/src/monitor-reply-cache.test.ts— 0600/0700 enforcement on write, append, and pre-existing filesextensions/imessage/src/actions.test.ts+actions.runtime.test.ts— bridge-unavailable error path, capability gating, action dispatchUser-visible / Behavior Changes
imsg launchis running and the private API probe succeeds.channels.imessage.sendReadReceipts: false.channels.imessage.actions.{reactions,edit,unsend,reply,sendWithEffect,renameGroup,setGroupIcon,addParticipant,removeParticipant,leaveGroup,sendAttachment}toggles are now respected.extensions/imessage/package.jsonaddspluginInspectormetadata +compat.pluginApi: ">=2026.5.3"+build.openclawVersion.Diagram
Security Impact
imsgchild process; no new fs/network surface.imsgchild.react,edit,unsend,reply,sendWithEffect,renameGroup,setGroupIcon,addParticipant,removeParticipant,leaveGroup,sendAttachment). Mitigation: every private-API action gates onimessageRpcSupportsMethodplus cached probe status; bridge-unavailable throws a clear error rather than silently degrading.reply-cache.jsonlclamped to0600and parent dir to0700on every write/append and on pre-existing files. Reduces blast radius on multi-user hosts.Repro + Verification
Environment
imsgfromsteipete/tap/imsgimsg rpcchannels.imessage.actions.*toggled true,imsg launchrunningSteps
imsg launch && openclaw channels status --probe→privateApi.available: trueimsg launchkilled, verify each private-API action throws "bridge unavailable" instead of silently no-oping.chat_guid:— verify the inbound echo suppression catches it (no self-loop).stat -f "%p %u %g" ~/.openclaw/state/imessage/reply-cache.jsonl→100600; parent dir40700.Expected / Actual
Evidence
monitor-reply-cache.test.ts,monitor/inbound-processing.test.ts,actions.test.ts,actions.runtime.test.ts,markdown-format.test.ts,probe.test.ts,monitor/persisted-echo-cache.ts(cache-bound tests),test-plugin.test.ts. Local:pnpm test:extension imessage→ 250/250.pnpm tsgo:extensions,pnpm tsgo:core,pnpm config:channels:check,pnpm test:contracts:channels,pnpm exec oxfmt --checkall pass on this branch.Human Verification
steipete/imsg: every action above (sends, attachments, effects, replies, tapbacks, edit, unsend, group rename/icon/participant management, leave). Verifiedimsg launchkilled → bridge-unavailable error path. Verifiedreply-cache.jsonlmode after fresh write and after upgrade from a 0644 file.rpc_methods(treated unsupported only for newly-named methods); private-API probe flips from false → true afterimsg launchbetween gateway start and first action.setGroupIconagainst pre-private-API macOS versions (gracefully gated).Review Conversations
Compatibility / Migration
preferOver: ["imessage"]is preserved, so anyone running both BlueBubbles and iMessage continues to see BlueBubbles win the auto-enable race.channels.imessage.actions.*toggles,channels.imessage.sendReadReceipts.reply-cache.jsonlto0600.Risks and Mitigations
imsginstalls older than therpc_methodsreporting fail closed for newly named methods. Mitigation: documented; capability gating uses per-method feature detection; one-time warning per restart when bridge is up but typing/read are gated off so the missing receipt is attributable.imsg launchis running. Mitigation: explicit "bridge unavailable" errors; lazy probe on first action so first action afterimsg launchsucceeds without manualchannels statusrefresh.reply-cache.jsonlpermission tightening breaks setups where the file is read by a different process under the same user. Mitigation: the cache is written and read only by the gateway process; same-UID readers were the threat model, not a legitimate sharing surface.AI/Vibe-coded
[AI-assisted]on changelog entries.codex review --base origin/main— will run before requesting review and address findings before un-drafting.🤖 Generated with Claude Code
Docs: https://docs.openclaw.ai/channels/imessage
Real behavior proof
Captured 2026-05-06 from the actively-running gateway PID 47600 on macOS Sequoia 15.x with
steipete/imsginstalled. All identifiers redacted.Behavior or issue addressed: the bundled
imessageplugin was a passive AppleScript shim — tapbacks, threaded replies, edit/unsend, expressive effects, attachments, and group management were unreachable, inbound chats showed "Delivered" with no typing indicator, and outbound messages addressed bychat_guidcould re-feed the agent its own reply through thechat.dbecho path. This PR wires the plugin toimsgover JSON-RPC, capability-gates each action viaimsg status --jsonrpc_methods, surfaces typing + read receipts inbound, broadens echo dedupe across all four chat-scope shapes (chat_id/chat_guid/chat_identifier/imessage:<handle>), and clampsreply-cache.jsonlandsent-echoes.jsonlto0600/0700.Real environment tested: macOS Sequoia 15.x with Messages.app signed in,
steipete/imsg 0.6.0installed at/Users/<user>/GitHub/imsg/.build/release/imsg, gateway running asai.openclaw.gatewayLaunchAgent, dist built from this branch tip withpnpm build && pnpm ui:build, single iMessage account (channels.imessage.default) routing inbound to a real production agent over realchat.db.Exact steps or command run after this patch:
imsg --version— confirm the bridge build.imsg status --json— confirmrpc_methodsandselectorscapability matrix; verifytyping,read,chats.markUnread,group.{rename,setIcon,addParticipant,removeParticipant,leave},retractMessagePartare present.openclaw channels statusandopenclaw channels status --probe— confirmiMessage default: enabled, configured, running, works.stat -f "%Sm %Sp (%p) %z bytes %N" ~/.openclaw/imessage/*.jsonl— confirm cache-file modes.~/.openclaw/logs/gateway.logand~/.openclaw/logs/openclaw.logfor the warn-once typing/read-gated message firing in production when the olderimsgbuild was active, and confirm it stops firing after upgrade.Evidence after fix (terminal output, redacted runtime logs, copied live output):
Redacted runtime log excerpt — the warn-once message we coded in
monitor-provider.ts:107-129firing in production when an olderimsgbuild was active:Observed result after fix: Capability matrix exposes
typing,read,retractMessagePart, group ops as expected on the upgradedimsg 0.6.0.editMessage/editMessageItemare correctlyfalseon this macOS /imsgcombo, andactions.ts:340-347correctly hideseditfrom the action surface as a result.reply-cache.jsonlis observably0600on disk with parent dir0700— the prior security commit verified end-to-end on a live system.sent-echoes.jsonlis currently0644on the running dist (still pre-redeploy of the newfix(security): sent-echoes.jsonl owner-onlycommit) — that's exactly the gap the new commit closes; will flip to0600on next gateway rebuild. The warn-once telemetry was observed firing once per gateway start when applicable and not re-firing thereafter, confirming both the per-method feature detection and thefired = truede-duplication.What was not tested: No live edit/unsend run on this dist (older
imsgselectors do not exposeeditMessageso that path is gated off; the newrequireFromMeenforcement was tested via unit tests against the sameresolveIMessageMessageIdcodepath the live action handler uses).setGroupIconagainst pre-private-API macOS recipients was not exercised. Cross-Mac-user attachment paths inside Tailscale relays beyond my own host were not exercised. Mocks/unit tests, CI, lint, and typechecks all pass (pnpm test:extension imessage→ 257/257) but are supplemental only — the live evidence above is the primary proof.