Compare commits
10 Commits
main
...
vincentkoc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56b5340a72 | ||
|
|
3bdf9c7677 | ||
|
|
84c619d957 | ||
|
|
50976575e1 | ||
|
|
078e622161 | ||
|
|
df2db2175e | ||
|
|
28b17181f6 | ||
|
|
a66b179b12 | ||
|
|
60f1842472 | ||
|
|
1f7892852e |
@ -68,6 +68,8 @@ Docs: https://docs.openclaw.ai
|
||||
- Routing/binding lookup scalability: pre-index route bindings by channel/account and avoid full binding-list rescans on channel-account cache rollover, preventing multi-second `resolveAgentRoute` stalls in large binding configurations. (#36915) Thanks @songchenghao.
|
||||
- Browser/session cleanup: track browser tabs opened by session-scoped browser tool runs and close tracked tabs during `sessions.reset`/`sessions.delete` runtime cleanup, preventing orphaned tabs and unbounded browser memory growth after session teardown. (#36666) Thanks @Harnoor6693.
|
||||
- Slack/local file upload allowlist parity: propagate `mediaLocalRoots` through the Slack send action pipeline so workspace-rooted attachments pass `assertLocalMediaAllowed` checks while non-allowlisted paths remain blocked. (synthesis: #36656; overlap considered from #36516, #36496, #36493, #36484, #32648, #30888) Thanks @2233admin.
|
||||
- Message tool agent-scoped media roots: resolve the effective agent for direct tool runs even without an `agentSessionKey`, so cron and hook sessions keep the agent workspace in `mediaLocalRoots` and can send workspace files again. (#36185) Thanks @ravz and @vincentkoc.
|
||||
- iMessage/image attachment root parity: merge iMessage `attachmentRoots` into the image tool allowlist and honor wildcard local roots during local media validation, restoring image-tool access to iMessage attachments after the 2026.2.26 hardening changes. (#30170) Thanks @macminikenny1 and @vincentkoc.
|
||||
- Agents/compaction safeguard pre-check: skip embedded compaction before entering the Pi SDK when a session has no real conversation messages, avoiding unnecessary LLM API calls on idle sessions. (#36451) thanks @Sid-Qin.
|
||||
- Config/schema cache key stability: build merged schema cache keys with incremental hashing to avoid large single-string serialization and prevent `RangeError: Invalid string length` on high-cardinality plugin/channel metadata. (#36603) Thanks @powermaster888.
|
||||
- iMessage/cron completion announces: strip leaked inline reply tags (for example `[[reply_to:6100]]`) from user-visible completion text so announcement deliveries do not expose threading metadata. (#24600) Thanks @vincentkoc.
|
||||
|
||||
@ -80,6 +80,12 @@ export function createOpenClawTools(options?: {
|
||||
? createImageTool({
|
||||
config: options?.config,
|
||||
agentDir: options.agentDir,
|
||||
agentId:
|
||||
options?.requesterAgentIdOverride ??
|
||||
resolveSessionAgentId({
|
||||
sessionKey: options?.agentSessionKey,
|
||||
config: options?.config,
|
||||
}),
|
||||
workspaceDir,
|
||||
sandbox:
|
||||
options?.sandboxRoot && options?.sandboxFsBridge
|
||||
@ -113,6 +119,7 @@ export function createOpenClawTools(options?: {
|
||||
? null
|
||||
: createMessageTool({
|
||||
agentAccountId: options?.agentAccountId,
|
||||
agentId: options?.requesterAgentIdOverride,
|
||||
agentSessionKey: options?.agentSessionKey,
|
||||
config: options?.config,
|
||||
currentChannelId: options?.currentChannelId,
|
||||
|
||||
@ -48,6 +48,21 @@ async function withTempWorkspacePng(
|
||||
}
|
||||
}
|
||||
|
||||
async function withTempAttachmentPng(
|
||||
cb: (args: { attachmentRoot: string; imagePath: string }) => Promise<void>,
|
||||
) {
|
||||
const attachmentParent = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-imessage-"));
|
||||
try {
|
||||
const attachmentRoot = path.join(attachmentParent, "Library", "Messages", "Attachments");
|
||||
await fs.mkdir(attachmentRoot, { recursive: true });
|
||||
const imagePath = path.join(attachmentRoot, "photo.png");
|
||||
await fs.writeFile(imagePath, Buffer.from(ONE_PIXEL_PNG_B64, "base64"));
|
||||
await cb({ attachmentRoot, imagePath });
|
||||
} finally {
|
||||
await fs.rm(attachmentParent, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function stubMinimaxOkFetch() {
|
||||
const fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
@ -495,6 +510,27 @@ describe("image tool implicit imageModel config", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("allows iMessage attachment paths from configured attachment roots", async () => {
|
||||
await withTempAttachmentPng(async ({ attachmentRoot, imagePath }) => {
|
||||
const fetch = stubMinimaxOkFetch();
|
||||
await withTempAgentDir(async (agentDir) => {
|
||||
const cfg: OpenClawConfig = {
|
||||
...createMinimaxImageConfig(),
|
||||
channels: {
|
||||
imessage: {
|
||||
attachmentRoots: [attachmentRoot],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const tool = createRequiredImageTool({ config: cfg, agentDir });
|
||||
|
||||
await expectImageToolExecOk(tool, imagePath);
|
||||
expect(fetch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("respects fsPolicy.workspaceOnly for non-sandbox image paths", async () => {
|
||||
await withTempWorkspacePng(async ({ workspaceDir, imagePath }) => {
|
||||
const fetch = stubMinimaxOkFetch();
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { type Context, complete } from "@mariozechner/pi-ai";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { getAgentScopedMediaLocalRoots } from "../../media/local-roots.js";
|
||||
import { resolveUserPath } from "../../utils.js";
|
||||
import { loadWebMedia } from "../../web/media.js";
|
||||
import { minimaxUnderstandImage } from "../minimax-vlm.js";
|
||||
@ -270,6 +271,7 @@ async function runImagePrompt(params: {
|
||||
export function createImageTool(options?: {
|
||||
config?: OpenClawConfig;
|
||||
agentDir?: string;
|
||||
agentId?: string;
|
||||
workspaceDir?: string;
|
||||
sandbox?: ImageSandboxConfig;
|
||||
fsPolicy?: ToolFsPolicy;
|
||||
@ -298,9 +300,15 @@ export function createImageTool(options?: {
|
||||
? "Analyze one or more images with a vision model. Use image for a single path/URL, or images for multiple (up to 20). Only use this tool when images were NOT already provided in the user's message. Images mentioned in the prompt are automatically visible to you."
|
||||
: "Analyze one or more images with the configured image model (agents.defaults.imageModel). Use image for a single path/URL, or images for multiple (up to 20). Provide a prompt describing what to analyze.";
|
||||
|
||||
const localRoots = resolveMediaToolLocalRoots(options?.workspaceDir, {
|
||||
workspaceOnly: options?.fsPolicy?.workspaceOnly === true,
|
||||
});
|
||||
const localRoots =
|
||||
options?.fsPolicy?.workspaceOnly === true
|
||||
? resolveMediaToolLocalRoots(options?.workspaceDir, { workspaceOnly: true })
|
||||
: Array.from(
|
||||
new Set([
|
||||
...getAgentScopedMediaLocalRoots(options?.config ?? {}, options?.agentId),
|
||||
...resolveMediaToolLocalRoots(options?.workspaceDir),
|
||||
]),
|
||||
);
|
||||
|
||||
return {
|
||||
label: "Image",
|
||||
|
||||
@ -116,6 +116,25 @@ describe("message tool agent routing", () => {
|
||||
expect(call?.agentId).toBe("alpha");
|
||||
expect(call?.sessionKey).toBe("agent:alpha:main");
|
||||
});
|
||||
|
||||
it("uses the explicit agentId override when no session key is present", async () => {
|
||||
mockSendResult();
|
||||
|
||||
const tool = createMessageTool({
|
||||
agentId: "cron-agent",
|
||||
config: {} as never,
|
||||
});
|
||||
|
||||
await tool.execute("1", {
|
||||
action: "send",
|
||||
target: "telegram:123",
|
||||
message: "hi",
|
||||
});
|
||||
|
||||
const call = mocks.runMessageAction.mock.calls[0]?.[0];
|
||||
expect(call?.agentId).toBe("cron-agent");
|
||||
expect(call?.sessionKey).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("message tool path passthrough", () => {
|
||||
|
||||
@ -488,6 +488,7 @@ const MessageToolSchema = buildMessageToolSchemaFromActions(AllMessageActions, {
|
||||
type MessageToolOptions = {
|
||||
agentAccountId?: string;
|
||||
agentSessionKey?: string;
|
||||
agentId?: string;
|
||||
config?: OpenClawConfig;
|
||||
currentChannelId?: string;
|
||||
currentChannelProvider?: string;
|
||||
@ -775,9 +776,9 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
|
||||
gateway,
|
||||
toolContext,
|
||||
sessionKey: options?.agentSessionKey,
|
||||
agentId: options?.agentSessionKey
|
||||
? resolveSessionAgentId({ sessionKey: options.agentSessionKey, config: cfg })
|
||||
: undefined,
|
||||
agentId:
|
||||
options?.agentId?.trim() ||
|
||||
resolveSessionAgentId({ sessionKey: options?.agentSessionKey, config: cfg }),
|
||||
sandboxRoot: options?.sandboxRoot,
|
||||
abortSignal: signal,
|
||||
});
|
||||
|
||||
44
src/media/local-roots.test.ts
Normal file
44
src/media/local-roots.test.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { getAgentScopedMediaLocalRoots } from "./local-roots.js";
|
||||
|
||||
describe("getAgentScopedMediaLocalRoots", () => {
|
||||
it("merges iMessage attachment roots into the agent-scoped allowlist", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
imessage: {
|
||||
attachmentRoots: ["/Users/*/Library/Messages/Attachments"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(getAgentScopedMediaLocalRoots(cfg)).toContain("/Users/*/Library/Messages/Attachments");
|
||||
});
|
||||
|
||||
it("adds the resolved agent workspace without dropping attachment roots", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
channels: {
|
||||
imessage: {
|
||||
attachmentRoots: ["/tmp/imessage-attachments"],
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
list: [
|
||||
{
|
||||
id: "clawdy",
|
||||
workspace: "~/agent-workspace",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const roots = getAgentScopedMediaLocalRoots(cfg, "clawdy");
|
||||
|
||||
expect(roots).toContain("/tmp/imessage-attachments");
|
||||
expect(roots).toContain(path.resolve(resolveStateDir(), "workspace"));
|
||||
expect(roots).toContain(path.resolve(resolveUserPath("~/agent-workspace")));
|
||||
});
|
||||
});
|
||||
@ -3,6 +3,7 @@ import { resolveAgentWorkspaceDir } from "../agents/agent-scope.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveStateDir } from "../config/paths.js";
|
||||
import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
|
||||
import { resolveIMessageAttachmentRoots } from "./inbound-path-policy.js";
|
||||
|
||||
type BuildMediaLocalRootsOptions = {
|
||||
preferredTmpDir?: string;
|
||||
@ -41,6 +42,11 @@ export function getAgentScopedMediaLocalRoots(
|
||||
agentId?: string,
|
||||
): readonly string[] {
|
||||
const roots = buildMediaLocalRoots(resolveStateDir());
|
||||
for (const attachmentRoot of resolveIMessageAttachmentRoots({ cfg })) {
|
||||
if (!roots.includes(attachmentRoot)) {
|
||||
roots.push(attachmentRoot);
|
||||
}
|
||||
}
|
||||
if (!agentId?.trim()) {
|
||||
return roots;
|
||||
}
|
||||
|
||||
@ -502,4 +502,30 @@ describe("local media root guard", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("allows wildcard-style local roots for iMessage attachment paths", async () => {
|
||||
const attachmentRoot = path.join(os.tmpdir(), "openclaw-imessage-root");
|
||||
const attachmentFile = path.join(
|
||||
attachmentRoot,
|
||||
"alice",
|
||||
"Library",
|
||||
"Messages",
|
||||
"Attachments",
|
||||
"x.bin",
|
||||
);
|
||||
const readFile = vi.fn(async () => Buffer.from("generated-media"));
|
||||
const wildcardRoot = path.join(attachmentRoot, "*", "Library", "Messages", "Attachments");
|
||||
|
||||
await expect(
|
||||
loadWebMedia(attachmentFile, {
|
||||
maxBytes: 1024 * 1024,
|
||||
localRoots: [wildcardRoot],
|
||||
readFile,
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
kind: "unknown",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
optimizeImageToPng,
|
||||
resizeToJpeg,
|
||||
} from "../media/image-ops.js";
|
||||
import { isInboundPathAllowed } from "../media/inbound-path-policy.js";
|
||||
import { getDefaultMediaLocalRoots } from "../media/local-roots.js";
|
||||
import { detectMime, extensionForMime, kindFromMime } from "../media/mime.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
@ -115,6 +116,12 @@ async function assertLocalMediaAllowed(
|
||||
}
|
||||
}
|
||||
for (const root of roots) {
|
||||
if (root.includes("*")) {
|
||||
if (isInboundPathAllowed({ filePath: resolved, roots: [root] })) {
|
||||
return;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let resolvedRoot: string;
|
||||
try {
|
||||
resolvedRoot = await fs.realpath(root);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user