From 26eea79f96526c301d73446b42d180fcfbe0ec56 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Thu, 12 Mar 2026 04:04:53 +0000 Subject: [PATCH] Matrix: honor scoped media roots --- .../src/actions.account-propagation.test.ts | 29 +++++++++++++++++++ extensions/matrix/src/actions.ts | 3 +- .../matrix/src/matrix/actions/messages.ts | 1 + .../matrix/src/matrix/actions/profile.ts | 5 +++- extensions/matrix/src/matrix/actions/types.ts | 1 + extensions/matrix/src/matrix/send.test.ts | 20 ++++++++++++- extensions/matrix/src/matrix/send.ts | 5 +++- extensions/matrix/src/matrix/send/types.ts | 1 + extensions/matrix/src/outbound.test.ts | 2 ++ extensions/matrix/src/outbound.ts | 13 ++++++++- extensions/matrix/src/profile-update.ts | 2 ++ extensions/matrix/src/tool-actions.test.ts | 24 +++++++++++++++ extensions/matrix/src/tool-actions.ts | 3 ++ src/infra/outbound/deliver.ts | 1 + 14 files changed, 105 insertions(+), 5 deletions(-) diff --git a/extensions/matrix/src/actions.account-propagation.test.ts b/extensions/matrix/src/actions.account-propagation.test.ts index 5a3299330a8..c74c9ca7e35 100644 --- a/extensions/matrix/src/actions.account-propagation.test.ts +++ b/extensions/matrix/src/actions.account-propagation.test.ts @@ -60,6 +60,7 @@ describe("matrixMessageActions account propagation", () => { accountId: "ops", }), expect.any(Object), + { mediaLocalRoots: undefined }, ); }); @@ -80,6 +81,7 @@ describe("matrixMessageActions account propagation", () => { accountId: "ops", }), expect.any(Object), + { mediaLocalRoots: undefined }, ); }); @@ -103,6 +105,7 @@ describe("matrixMessageActions account propagation", () => { avatarUrl: "mxc://example/avatar", }), expect.any(Object), + { mediaLocalRoots: undefined }, ); }); @@ -124,6 +127,32 @@ describe("matrixMessageActions account propagation", () => { avatarPath: "/tmp/avatar.jpg", }), expect.any(Object), + { mediaLocalRoots: undefined }, + ); + }); + + it("forwards mediaLocalRoots for media sends", async () => { + await matrixMessageActions.handleAction?.( + createContext({ + action: "send", + accountId: "ops", + mediaLocalRoots: ["/tmp/openclaw-matrix-test"], + params: { + to: "room:!room:example", + message: "hello", + media: "file:///tmp/photo.png", + }, + }), + ); + + expect(mocks.handleMatrixAction).toHaveBeenCalledWith( + expect.objectContaining({ + action: "sendMessage", + accountId: "ops", + mediaUrl: "file:///tmp/photo.png", + }), + expect.any(Object), + { mediaLocalRoots: ["/tmp/openclaw-matrix-test"] }, ); }); }); diff --git a/extensions/matrix/src/actions.ts b/extensions/matrix/src/actions.ts index 2954a1bae69..50fb6249ade 100644 --- a/extensions/matrix/src/actions.ts +++ b/extensions/matrix/src/actions.ts @@ -93,7 +93,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = { return { to }; }, handleAction: async (ctx: ChannelMessageActionContext) => { - const { action, params, cfg, accountId } = ctx; + const { action, params, cfg, accountId, mediaLocalRoots } = ctx; const dispatch = async (actionParams: Record) => await handleMatrixAction( { @@ -101,6 +101,7 @@ export const matrixMessageActions: ChannelMessageActionAdapter = { ...(accountId ? { accountId } : {}), }, cfg as CoreConfig, + { mediaLocalRoots }, ); const resolveRoomId = () => readStringParam(params, "roomId") ?? diff --git a/extensions/matrix/src/matrix/actions/messages.ts b/extensions/matrix/src/matrix/actions/messages.ts index ac62fc5e9a9..03b9ba847c8 100644 --- a/extensions/matrix/src/matrix/actions/messages.ts +++ b/extensions/matrix/src/matrix/actions/messages.ts @@ -26,6 +26,7 @@ export async function sendMatrixMessage( return await sendMessageMatrix(to, content, { cfg: opts.cfg, mediaUrl: opts.mediaUrl, + mediaLocalRoots: opts.mediaLocalRoots, replyToId: opts.replyToId, threadId: opts.threadId, accountId: opts.accountId ?? undefined, diff --git a/extensions/matrix/src/matrix/actions/profile.ts b/extensions/matrix/src/matrix/actions/profile.ts index c31f69478a5..d4ff78cc45d 100644 --- a/extensions/matrix/src/matrix/actions/profile.ts +++ b/extensions/matrix/src/matrix/actions/profile.ts @@ -26,7 +26,10 @@ export async function updateMatrixOwnProfile( avatarPath: avatarPath || undefined, loadAvatarFromUrl: async (url, maxBytes) => await runtime.media.loadWebMedia(url, maxBytes), loadAvatarFromPath: async (path, maxBytes) => - await runtime.media.loadWebMedia(path, maxBytes), + await runtime.media.loadWebMedia(path, { + maxBytes, + localRoots: opts.mediaLocalRoots, + }), }); }, "persist", diff --git a/extensions/matrix/src/matrix/actions/types.ts b/extensions/matrix/src/matrix/actions/types.ts index 672b6f6a52b..e950c6f800b 100644 --- a/extensions/matrix/src/matrix/actions/types.ts +++ b/extensions/matrix/src/matrix/actions/types.ts @@ -48,6 +48,7 @@ export type RoomTopicEventContent = { export type MatrixActionClientOpts = { client?: MatrixClient; cfg?: CoreConfig; + mediaLocalRoots?: readonly string[]; timeoutMs?: number; accountId?: string | null; readiness?: "none" | "prepared" | "started"; diff --git a/extensions/matrix/src/matrix/send.test.ts b/extensions/matrix/src/matrix/send.test.ts index ac5a63a567d..66cab1d9bdd 100644 --- a/extensions/matrix/src/matrix/send.test.ts +++ b/extensions/matrix/src/matrix/send.test.ts @@ -232,9 +232,27 @@ describe("sendMessageMatrix media", () => { }); expect(loadConfigMock).not.toHaveBeenCalled(); - expect(loadWebMediaMock).toHaveBeenCalledWith("file:///tmp/photo.png", 1024 * 1024); + expect(loadWebMediaMock).toHaveBeenCalledWith("file:///tmp/photo.png", { + maxBytes: 1024 * 1024, + localRoots: undefined, + }); expect(resolveTextChunkLimitMock).toHaveBeenCalledWith(explicitCfg, "matrix", "ops"); }); + + it("passes caller mediaLocalRoots to media loading", async () => { + const { client } = makeClient(); + + await sendMessageMatrix("room:!room:example", "caption", { + client, + mediaUrl: "file:///tmp/photo.png", + mediaLocalRoots: ["/tmp/openclaw-matrix-test"], + }); + + expect(loadWebMediaMock).toHaveBeenCalledWith("file:///tmp/photo.png", { + maxBytes: undefined, + localRoots: ["/tmp/openclaw-matrix-test"], + }); + }); }); describe("sendMessageMatrix threads", () => { diff --git a/extensions/matrix/src/matrix/send.ts b/extensions/matrix/src/matrix/send.ts index 27df666a37d..411fdbc10d9 100644 --- a/extensions/matrix/src/matrix/send.ts +++ b/extensions/matrix/src/matrix/send.ts @@ -109,7 +109,10 @@ export async function sendMessageMatrix( let lastMessageId = ""; if (opts.mediaUrl) { const maxBytes = resolveMediaMaxBytes(opts.accountId, cfg); - const media = await getCore().media.loadWebMedia(opts.mediaUrl, maxBytes); + const media = await getCore().media.loadWebMedia(opts.mediaUrl, { + maxBytes, + localRoots: opts.mediaLocalRoots, + }); const uploaded = await uploadMediaMaybeEncrypted(client, roomId, media.buffer, { contentType: media.contentType, filename: media.fileName, diff --git a/extensions/matrix/src/matrix/send/types.ts b/extensions/matrix/src/matrix/send/types.ts index bb035bfd4db..2d2d8bf3715 100644 --- a/extensions/matrix/src/matrix/send/types.ts +++ b/extensions/matrix/src/matrix/send/types.ts @@ -88,6 +88,7 @@ export type MatrixSendOpts = { client?: import("../sdk.js").MatrixClient; cfg?: CoreConfig; mediaUrl?: string; + mediaLocalRoots?: readonly string[]; accountId?: string; replyToId?: string; threadId?: string | number | null; diff --git a/extensions/matrix/src/outbound.test.ts b/extensions/matrix/src/outbound.test.ts index e0b62c1c00b..bd7b6595f7e 100644 --- a/extensions/matrix/src/outbound.test.ts +++ b/extensions/matrix/src/outbound.test.ts @@ -75,6 +75,7 @@ describe("matrixOutbound cfg threading", () => { to: "room:!room:example", text: "caption", mediaUrl: "file:///tmp/cat.png", + mediaLocalRoots: ["/tmp/openclaw"], accountId: "default", }); @@ -84,6 +85,7 @@ describe("matrixOutbound cfg threading", () => { expect.objectContaining({ cfg, mediaUrl: "file:///tmp/cat.png", + mediaLocalRoots: ["/tmp/openclaw"], }), ); }); diff --git a/extensions/matrix/src/outbound.ts b/extensions/matrix/src/outbound.ts index be4f8d3426d..ca870d404be 100644 --- a/extensions/matrix/src/outbound.ts +++ b/extensions/matrix/src/outbound.ts @@ -23,13 +23,24 @@ export const matrixOutbound: ChannelOutboundAdapter = { roomId: result.roomId, }; }, - sendMedia: async ({ cfg, to, text, mediaUrl, deps, replyToId, threadId, accountId }) => { + sendMedia: async ({ + cfg, + to, + text, + mediaUrl, + mediaLocalRoots, + deps, + replyToId, + threadId, + accountId, + }) => { const send = deps?.sendMatrix ?? sendMessageMatrix; const resolvedThreadId = threadId !== undefined && threadId !== null ? String(threadId) : undefined; const result = await send(to, text, { cfg, mediaUrl, + mediaLocalRoots, replyToId: replyToId ?? undefined, threadId: resolvedThreadId, accountId: accountId ?? undefined, diff --git a/extensions/matrix/src/profile-update.ts b/extensions/matrix/src/profile-update.ts index 69c7f03c5f7..8de5726f8d9 100644 --- a/extensions/matrix/src/profile-update.ts +++ b/extensions/matrix/src/profile-update.ts @@ -24,6 +24,7 @@ export async function applyMatrixProfileUpdate(params: { displayName?: string; avatarUrl?: string; avatarPath?: string; + mediaLocalRoots?: readonly string[]; }): Promise { const runtime = getMatrixRuntime(); const persistedCfg = runtime.config.loadConfig() as CoreConfig; @@ -41,6 +42,7 @@ export async function applyMatrixProfileUpdate(params: { displayName: displayName ?? undefined, avatarUrl: avatarUrl ?? undefined, avatarPath: avatarPath ?? undefined, + mediaLocalRoots: params.mediaLocalRoots, }); const persistedAvatarUrl = synced.uploadedAvatarSource && synced.resolvedAvatarUrl ? synced.resolvedAvatarUrl : avatarUrl; diff --git a/extensions/matrix/src/tool-actions.test.ts b/extensions/matrix/src/tool-actions.test.ts index 12287436406..bcb5380e845 100644 --- a/extensions/matrix/src/tool-actions.test.ts +++ b/extensions/matrix/src/tool-actions.test.ts @@ -194,17 +194,41 @@ describe("handleMatrixAction pollVote", () => { threadId: "$thread", }, cfg, + { mediaLocalRoots: ["/tmp/openclaw-matrix-test"] }, ); expect(mocks.sendMatrixMessage).toHaveBeenCalledWith("room:!room:example", "hello", { cfg, accountId: "ops", mediaUrl: undefined, + mediaLocalRoots: ["/tmp/openclaw-matrix-test"], replyToId: undefined, threadId: "$thread", }); }); + it("passes mediaLocalRoots to profile updates", async () => { + const cfg = { channels: { matrix: { actions: { profile: true } } } } as CoreConfig; + await handleMatrixAction( + { + action: "setProfile", + accountId: "ops", + avatarPath: "/tmp/avatar.jpg", + }, + cfg, + { mediaLocalRoots: ["/tmp/openclaw-matrix-test"] }, + ); + + expect(mocks.applyMatrixProfileUpdate).toHaveBeenCalledWith( + expect.objectContaining({ + cfg, + account: "ops", + avatarPath: "/tmp/avatar.jpg", + mediaLocalRoots: ["/tmp/openclaw-matrix-test"], + }), + ); + }); + it("passes account-scoped opts to pin listing", async () => { const cfg = { channels: { matrix: { actions: { pins: true } } } } as CoreConfig; await handleMatrixAction( diff --git a/extensions/matrix/src/tool-actions.ts b/extensions/matrix/src/tool-actions.ts index 2ae4d341bd4..de191513cbf 100644 --- a/extensions/matrix/src/tool-actions.ts +++ b/extensions/matrix/src/tool-actions.ts @@ -129,6 +129,7 @@ function readNumericArrayParam( export async function handleMatrixAction( params: Record, cfg: CoreConfig, + opts: { mediaLocalRoots?: readonly string[] } = {}, ): Promise> { const action = readStringParam(params, "action", { required: true }); const accountId = readStringParam(params, "accountId") ?? undefined; @@ -204,6 +205,7 @@ export async function handleMatrixAction( const threadId = readStringParam(params, "threadId"); const result = await sendMatrixMessage(to, content, { mediaUrl: mediaUrl ?? undefined, + mediaLocalRoots: opts.mediaLocalRoots, replyToId: replyToId ?? undefined, threadId: threadId ?? undefined, ...clientOpts, @@ -278,6 +280,7 @@ export async function handleMatrixAction( displayName: readStringParam(params, "displayName") ?? readStringParam(params, "name"), avatarUrl: readStringParam(params, "avatarUrl"), avatarPath, + mediaLocalRoots: opts.mediaLocalRoots, }); return jsonResult({ ok: true, ...result }); } diff --git a/src/infra/outbound/deliver.ts b/src/infra/outbound/deliver.ts index 7bf131cd9f0..0c80f54bb90 100644 --- a/src/infra/outbound/deliver.ts +++ b/src/infra/outbound/deliver.ts @@ -57,6 +57,7 @@ type SendMatrixMessage = ( cfg?: OpenClawConfig; accountId?: string; mediaUrl?: string; + mediaLocalRoots?: readonly string[]; replyToId?: string; threadId?: string; timeoutMs?: number;