Skip to content

Commit cb83f12

Browse files
committed
fix(imessage): cover all attachment param aliases in reply guard
Codex review (P2) flagged that the original guard only checked four param names. The shared message-tool send schema also accepts `path` (plus agents commonly reach for `fileUrl`, `mediaUrls`), so a reply that supplied any of those still fell through to `sendRichMessage` and silently dropped the attachment exactly like the bug being fixed. Hoist the alias list into a constant, replace the four-key boolean OR with a `find` over it, and add `hasNonEmptyAttachmentValue` so array-shaped params (`mediaUrls: [...]`) also trip the guard. Test now exercises every alias, including the array variant.
1 parent 79a1c9e commit cb83f12

2 files changed

Lines changed: 56 additions & 9 deletions

File tree

extensions/imessage/src/actions.test.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,17 @@ describe("imessage message actions", () => {
290290
});
291291
runtimeMock.resolveChatGuidForTarget.mockResolvedValue("iMessage;+;resolved-ident");
292292

293-
for (const attachmentField of ["media", "mediaUrl", "filePath", "buffer"]) {
293+
const stringValue = "/tmp/anything.png";
294+
const cases: Array<{ field: string; value: unknown }> = [
295+
{ field: "media", value: stringValue },
296+
{ field: "mediaUrl", value: stringValue },
297+
{ field: "mediaUrls", value: [stringValue, "/tmp/extra.png"] },
298+
{ field: "filePath", value: stringValue },
299+
{ field: "fileUrl", value: stringValue },
300+
{ field: "path", value: stringValue },
301+
{ field: "buffer", value: stringValue },
302+
];
303+
for (const { field, value } of cases) {
294304
runtimeMock.sendRichMessage.mockClear();
295305
await expect(
296306
imessageMessageActions.handleAction?.({
@@ -300,7 +310,7 @@ describe("imessage message actions", () => {
300310
chatIdentifier: "team-thread",
301311
messageId: "message-guid",
302312
text: "🦞 here it is",
303-
[attachmentField]: "/tmp/anything.png",
313+
[field]: value,
304314
},
305315
} as never),
306316
).rejects.toThrow(/iMessage reply does not support attachments/);

extensions/imessage/src/actions.ts

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,43 @@ function decodeBase64Buffer(params: Record<string, unknown>, action: string): Ui
240240
return Uint8Array.from(Buffer.from(base64Buffer, "base64"));
241241
}
242242

243+
// Param names that any caller would reasonably treat as "attach this file
244+
// to the message". Covers everything the shared message-tool send schema
245+
// declares (`media`, `buffer`, `path`, `filePath`) plus the plural/url
246+
// variants that agents and channel adapters tend to reach for, so the reply
247+
// guard refuses identically regardless of which alias the caller picked.
248+
const REPLY_ATTACHMENT_PARAM_NAMES: readonly string[] = [
249+
"media",
250+
"mediaUrl",
251+
"mediaUrls",
252+
"filePath",
253+
"fileUrl",
254+
"path",
255+
"buffer",
256+
] as const;
257+
258+
// Truthy attachment-param check that tolerates string, array, and object
259+
// shapes — agents commonly pass `media: "/tmp/x.png"` as a string but also
260+
// `mediaUrls: ["/tmp/x.png"]` as an array, and we want both to trip the
261+
// reply guard rather than silently falling through.
262+
function hasNonEmptyAttachmentValue(params: Record<string, unknown>, name: string): boolean {
263+
const value = params[name];
264+
if (value == null) {
265+
return false;
266+
}
267+
if (typeof value === "string") {
268+
return value.trim().length > 0;
269+
}
270+
if (Array.isArray(value)) {
271+
return value.some(
272+
(entry) =>
273+
(typeof entry === "string" && entry.trim().length > 0) ||
274+
(entry != null && typeof entry === "object"),
275+
);
276+
}
277+
return typeof value === "object";
278+
}
279+
243280
// Whitelist of expressive-send effect IDs the bridge accepts. Restricting
244281
// to a fixed set lets us return a clear error for typos ("invisible_ink"
245282
// vs "invisibleink") instead of silently forwarding gibberish to the
@@ -472,13 +509,13 @@ export const imessageMessageActions: ChannelMessageActionAdapter = {
472509
// does not accept attachments — the only path that handles a file is
473510
// `imsg send --file`, used by sendAttachment/upload-file. Silently
474511
// dropping the caller's media looked like success and let agents ship
475-
// a "here's your image" reply with no attachment attached.
476-
const hasMediaParam =
477-
readStringParam(params, "media") != null ||
478-
readStringParam(params, "mediaUrl") != null ||
479-
readStringParam(params, "filePath") != null ||
480-
readStringParam(params, "buffer") != null;
481-
if (hasMediaParam) {
512+
// a "here's your image" reply with no attachment attached. Cover every
513+
// attachment-bearing alias the message-tool send schema exposes plus
514+
// common plural/url variants agents reach for.
515+
const attachmentParam = REPLY_ATTACHMENT_PARAM_NAMES.find((name) =>
516+
hasNonEmptyAttachmentValue(params, name),
517+
);
518+
if (attachmentParam) {
482519
throw new Error(
483520
"iMessage reply does not support attachments. Use action 'upload-file' " +
484521
"(with filePath/filename) or action 'send' (with media) to deliver the file, " +

0 commit comments

Comments
 (0)