fix(feishu): fail fast on local media allowlist violations #51347
This commit is contained in:
parent
5e417b44e1
commit
6d81281811
@ -121,6 +121,35 @@ describe("feishuOutbound.sendText local-image auto-convert", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("throws clear guidance when local-image path is outside allowlist", async () => {
|
||||
const { dir, file } = await createTmpImage();
|
||||
sendMediaFeishuMock.mockRejectedValueOnce({
|
||||
name: "LocalMediaAccessError",
|
||||
code: "path-not-allowed",
|
||||
message: `Local media path is not under an allowed directory: ${file}`,
|
||||
});
|
||||
try {
|
||||
let message = "";
|
||||
try {
|
||||
await sendText({
|
||||
cfg: {} as any,
|
||||
to: "chat_1",
|
||||
text: file,
|
||||
accountId: "main",
|
||||
mediaLocalRoots: ["/state/media", "/state/workspace"],
|
||||
});
|
||||
} catch (err) {
|
||||
message = err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
expect(message).toContain("channels.feishu.mediaLocalRoots is unsupported");
|
||||
expect(message).toContain("Allowed local media roots: /state/media, /state/workspace");
|
||||
expect(message).toContain("macOS note: OpenClaw uses os.tmpdir()");
|
||||
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("uses markdown cards when renderMode=card", async () => {
|
||||
const result = await sendText({
|
||||
cfg: {
|
||||
@ -357,3 +386,57 @@ describe("feishuOutbound.sendMedia renderMode", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("feishuOutbound.sendMedia failure handling", () => {
|
||||
beforeEach(() => {
|
||||
resetOutboundMocks();
|
||||
});
|
||||
|
||||
it("throws non-zero guidance for path-not-allowed media paths", async () => {
|
||||
sendMediaFeishuMock.mockRejectedValueOnce({
|
||||
name: "LocalMediaAccessError",
|
||||
code: "path-not-allowed",
|
||||
message: "Local media path is not under an allowed directory: /tmp/test.pdf",
|
||||
});
|
||||
|
||||
let message = "";
|
||||
try {
|
||||
await feishuOutbound.sendMedia?.({
|
||||
cfg: {} as any,
|
||||
to: "chat_1",
|
||||
text: "",
|
||||
mediaUrl: "/tmp/test.pdf",
|
||||
mediaLocalRoots: ["/state/media", "/state/workspace"],
|
||||
accountId: "main",
|
||||
});
|
||||
} catch (err) {
|
||||
message = err instanceof Error ? err.message : String(err);
|
||||
}
|
||||
expect(message).toContain("command exits with non-zero status");
|
||||
expect(message).toContain("channels.feishu.mediaLocalRoots is unsupported");
|
||||
expect(message).toContain("Allowed local media roots: /state/media, /state/workspace");
|
||||
|
||||
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
|
||||
expect(sendMarkdownCardFeishuMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps URL-link fallback for non-allowlist media failures", async () => {
|
||||
sendMediaFeishuMock.mockRejectedValueOnce(new Error("upstream upload failed"));
|
||||
|
||||
await feishuOutbound.sendMedia?.({
|
||||
cfg: {} as any,
|
||||
to: "chat_1",
|
||||
text: "",
|
||||
mediaUrl: "https://example.com/file.pdf",
|
||||
accountId: "main",
|
||||
});
|
||||
|
||||
expect(sendMessageFeishuMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
to: "chat_1",
|
||||
text: "📎 https://example.com/file.pdf",
|
||||
accountId: "main",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { createAttachedChannelResultAdapter } from "openclaw/plugin-sdk/channel-send-result";
|
||||
import { getDefaultMediaLocalRoots } from "openclaw/plugin-sdk/media-runtime";
|
||||
import type { ChannelOutboundAdapter } from "../runtime-api.js";
|
||||
import { resolveFeishuAccount } from "./accounts.js";
|
||||
import { sendMediaFeishu } from "./media.js";
|
||||
@ -59,6 +60,53 @@ function resolveReplyToMessageId(params: {
|
||||
return trimmed || undefined;
|
||||
}
|
||||
|
||||
function findLocalMediaPathNotAllowedError(err: unknown): unknown {
|
||||
let current: unknown = err;
|
||||
let depth = 0;
|
||||
while (current && typeof current === "object" && depth < 5) {
|
||||
const maybe = current as { name?: unknown; code?: unknown; cause?: unknown };
|
||||
if (maybe.name === "LocalMediaAccessError" && maybe.code === "path-not-allowed") {
|
||||
return current;
|
||||
}
|
||||
current = maybe.cause;
|
||||
depth += 1;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildFeishuLocalMediaGuidanceError(params: {
|
||||
err: unknown;
|
||||
mediaUrl: string;
|
||||
mediaLocalRoots?: readonly string[];
|
||||
}): Error {
|
||||
const roots =
|
||||
params.mediaLocalRoots && params.mediaLocalRoots.length > 0
|
||||
? [...params.mediaLocalRoots]
|
||||
: [...getDefaultMediaLocalRoots()];
|
||||
const stagingDir = roots.find((root) => path.basename(root) === "media") ?? "(stateDir)/media";
|
||||
const details =
|
||||
params.err instanceof Error
|
||||
? params.err.message
|
||||
: typeof params.err === "object" &&
|
||||
params.err &&
|
||||
"message" in params.err &&
|
||||
typeof (params.err as { message?: unknown }).message === "string"
|
||||
? ((params.err as { message: string }).message ?? "")
|
||||
: String(params.err);
|
||||
return new Error(
|
||||
[
|
||||
"Feishu media upload failed; local media path is outside the allowed roots (command exits with non-zero status).",
|
||||
"channels.feishu.mediaLocalRoots is unsupported for Feishu and ignored.",
|
||||
`Rejected path: ${params.mediaUrl}`,
|
||||
`Allowed local media roots: ${roots.join(", ")}`,
|
||||
`Recommended staging directory: ${stagingDir}`,
|
||||
"macOS note: OpenClaw uses os.tmpdir() (typically /var/folders/.../T), not /tmp.",
|
||||
`Details: ${details}`,
|
||||
].join(" "),
|
||||
{ cause: params.err instanceof Error ? params.err : undefined },
|
||||
);
|
||||
}
|
||||
|
||||
async function sendOutboundText(params: {
|
||||
cfg: Parameters<typeof sendMessageFeishu>[0]["cfg"];
|
||||
to: string;
|
||||
@ -110,6 +158,13 @@ export const feishuOutbound: ChannelOutboundAdapter = {
|
||||
mediaLocalRoots,
|
||||
});
|
||||
} catch (err) {
|
||||
if (findLocalMediaPathNotAllowedError(err)) {
|
||||
throw buildFeishuLocalMediaGuidanceError({
|
||||
err,
|
||||
mediaUrl: localImagePath,
|
||||
mediaLocalRoots,
|
||||
});
|
||||
}
|
||||
console.error(`[feishu] local image path auto-send failed:`, err);
|
||||
// fall through to plain text as last resort
|
||||
}
|
||||
@ -179,6 +234,9 @@ export const feishuOutbound: ChannelOutboundAdapter = {
|
||||
replyToMessageId,
|
||||
});
|
||||
} catch (err) {
|
||||
if (findLocalMediaPathNotAllowedError(err)) {
|
||||
throw buildFeishuLocalMediaGuidanceError({ err, mediaUrl, mediaLocalRoots });
|
||||
}
|
||||
// Log the error for debugging
|
||||
console.error(`[feishu] sendMediaFeishu failed:`, err);
|
||||
// Fallback to URL link if upload fails
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user