惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

L
LangChain Blog
阮一峰的网络日志
阮一峰的网络日志
WordPress大学
WordPress大学
博客园 - 司徒正美
罗磊的独立博客
D
Docker
Last Week in AI
Last Week in AI
爱范儿
爱范儿
M
MIT News - Artificial intelligence
V
V2EX
Google DeepMind News
Google DeepMind News
小众软件
小众软件
Apple Machine Learning Research
Apple Machine Learning Research
Microsoft Security Blog
Microsoft Security Blog
T
Tailwind CSS Blog
MyScale Blog
MyScale Blog
V
Visual Studio Blog
博客园 - 叶小钗
B
Blog RSS Feed
A
About on SuperTechFans
F
Fortinet All Blogs
T
The Blog of Author Tim Ferriss
Martin Fowler
Martin Fowler
P
Proofpoint News Feed

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
fix(skills): honor tool policy for inline dispatch (#7852...
eleqtrizit · 2026-05-17 · via Recent Commits to openclaw:main

@@ -1,3 +1,6 @@

1+

import fs from "node:fs/promises";

2+

import os from "node:os";

3+

import path from "node:path";

14

import { beforeEach, describe, expect, it, vi } from "vitest";

25

import type { SkillCommandSpec } from "../../agents/skills.js";

36

import type { SessionEntry } from "../../config/sessions.js";

@@ -47,6 +50,16 @@ const createTypingController = (): TypingController => ({

4750

cleanup: vi.fn(),

4851

});

495253+

async function writeSessionStore(

54+

storeTemplate: string,

55+

agentId: string,

56+

entries: Record<string, unknown>,

57+

) {

58+

const storePath = storeTemplate.replaceAll("{agentId}", agentId);

59+

await fs.mkdir(path.dirname(storePath), { recursive: true });

60+

await fs.writeFile(storePath, JSON.stringify(entries, null, 2), "utf-8");

61+

}

62+5063

const createHandleInlineActionsInput = (params: {

5164

ctx: ReturnType<typeof buildTestCtx>;

5265

typing: TypingController;

@@ -789,4 +802,321 @@ describe("handleInlineActions", () => {

789802

expect(blockedToolCall?.[2]).toBe(abortController.signal);

790803

expect(typing.cleanup).toHaveBeenCalledTimes(1);

791804

});

805+806+

it("does not execute inline tool dispatch targets denied by tool policy", async () => {

807+

const typing = createTypingController();

808+

const toolExecute = vi.fn(async () => ({ content: "sent" }));

809+

createOpenClawToolsMock.mockReturnValue([

810+

{

811+

name: "message",

812+

execute: toolExecute,

813+

},

814+

]);

815+816+

const ctx = buildTestCtx({

817+

Body: "/send_status hello",

818+

CommandBody: "/send_status hello",

819+

});

820+

const skillCommands: SkillCommandSpec[] = [

821+

{

822+

name: "send_status",

823+

skillName: "send-status",

824+

description: "Send a status update",

825+

dispatch: {

826+

kind: "tool",

827+

toolName: "message",

828+

argMode: "raw",

829+

},

830+

sourceFilePath: "/tmp/plugin/commands/send-status.md",

831+

},

832+

];

833+834+

const result = await handleInlineActions(

835+

createHandleInlineActionsInput({

836+

ctx,

837+

typing,

838+

cleanedBody: "/send_status hello",

839+

command: {

840+

isAuthorizedSender: true,

841+

senderId: "sender-1",

842+

senderIsOwner: true,

843+

abortKey: "sender-1",

844+

rawBodyNormalized: "/send_status hello",

845+

commandBodyNormalized: "/send_status hello",

846+

},

847+

overrides: {

848+

cfg: { commands: { text: true }, tools: { deny: ["message"] } },

849+

allowTextCommands: true,

850+

skillCommands,

851+

},

852+

}),

853+

);

854+855+

expect(result).toEqual({

856+

kind: "reply",

857+

reply: { text: "❌ Tool not available: message" },

858+

});

859+

expect(toolExecute).not.toHaveBeenCalled();

860+

});

861+862+

it("does not execute inline tool dispatch targets outside tool allowlists", async () => {

863+

const typing = createTypingController();

864+

const messageExecute = vi.fn(async () => ({ content: "sent" }));

865+

const sessionsExecute = vi.fn(async () => ({ content: "listed" }));

866+

createOpenClawToolsMock.mockReturnValue([

867+

{

868+

name: "message",

869+

execute: messageExecute,

870+

},

871+

{

872+

name: "sessions_list",

873+

execute: sessionsExecute,

874+

},

875+

]);

876+877+

const ctx = buildTestCtx({

878+

Body: "/send_status hello",

879+

CommandBody: "/send_status hello",

880+

});

881+

const skillCommands: SkillCommandSpec[] = [

882+

{

883+

name: "send_status",

884+

skillName: "send-status",

885+

description: "Send a status update",

886+

dispatch: {

887+

kind: "tool",

888+

toolName: "message",

889+

argMode: "raw",

890+

},

891+

sourceFilePath: "/tmp/plugin/commands/send-status.md",

892+

},

893+

];

894+895+

const result = await handleInlineActions(

896+

createHandleInlineActionsInput({

897+

ctx,

898+

typing,

899+

cleanedBody: "/send_status hello",

900+

command: {

901+

isAuthorizedSender: true,

902+

senderId: "sender-1",

903+

senderIsOwner: true,

904+

abortKey: "sender-1",

905+

rawBodyNormalized: "/send_status hello",

906+

commandBodyNormalized: "/send_status hello",

907+

},

908+

overrides: {

909+

cfg: { commands: { text: true }, tools: { allow: ["sessions_list"] } },

910+

allowTextCommands: true,

911+

skillCommands,

912+

},

913+

}),

914+

);

915+916+

expect(result).toEqual({

917+

kind: "reply",

918+

reply: { text: "❌ Tool not available: message" },

919+

});

920+

expect(messageExecute).not.toHaveBeenCalled();

921+

expect(sessionsExecute).not.toHaveBeenCalled();

922+

});

923+924+

it("applies sender-specific tool policy to inline tool dispatch", async () => {

925+

const typing = createTypingController();

926+

const toolExecute = vi.fn(async () => ({ content: "sent" }));

927+

createOpenClawToolsMock.mockReturnValue([

928+

{

929+

name: "message",

930+

execute: toolExecute,

931+

},

932+

]);

933+934+

const ctx = buildTestCtx({

935+

Body: "/send_status hello",

936+

CommandBody: "/send_status hello",

937+

});

938+

const skillCommands: SkillCommandSpec[] = [

939+

{

940+

name: "send_status",

941+

skillName: "send-status",

942+

description: "Send a status update",

943+

dispatch: {

944+

kind: "tool",

945+

toolName: "message",

946+

argMode: "raw",

947+

},

948+

sourceFilePath: "/tmp/plugin/commands/send-status.md",

949+

},

950+

];

951+952+

const result = await handleInlineActions(

953+

createHandleInlineActionsInput({

954+

ctx,

955+

typing,

956+

cleanedBody: "/send_status hello",

957+

command: {

958+

isAuthorizedSender: true,

959+

senderId: "sender-1",

960+

senderIsOwner: true,

961+

abortKey: "sender-1",

962+

rawBodyNormalized: "/send_status hello",

963+

commandBodyNormalized: "/send_status hello",

964+

},

965+

overrides: {

966+

cfg: {

967+

commands: { text: true },

968+

tools: { toolsBySender: { "id:sender-1": { deny: ["message"] } } },

969+

},

970+

allowTextCommands: true,

971+

skillCommands,

972+

},

973+

}),

974+

);

975+976+

expect(result).toEqual({

977+

kind: "reply",

978+

reply: { text: "❌ Tool not available: message" },

979+

});

980+

expect(toolExecute).not.toHaveBeenCalled();

981+

});

982+983+

it("applies subagent policy to ACP envelope inline dispatch sessions", async () => {

984+

const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-inline-acp-policy-"));

985+

try {

986+

const storeTemplate = path.join(tmpDir, "sessions-{agentId}.json");

987+

await writeSessionStore(storeTemplate, "main", {

988+

"agent:main:acp:leaf": {

989+

sessionId: "session-acp-leaf",

990+

updatedAt: Date.now(),

991+

spawnedBy: "agent:main:subagent:parent",

992+

spawnDepth: 2,

993+

subagentRole: "leaf",

994+

subagentControlScope: "none",

995+

},

996+

});

997+998+

const typing = createTypingController();

999+

const toolExecute = vi.fn(async () => ({ content: "spawned" }));

1000+

createOpenClawToolsMock.mockReturnValue([

1001+

{

1002+

name: "sessions_spawn",

1003+

execute: toolExecute,

1004+

},

1005+

]);

1006+1007+

const ctx = buildTestCtx({

1008+

Body: "/spawn_subagent investigate",

1009+

CommandBody: "/spawn_subagent investigate",

1010+

});

1011+

const skillCommands: SkillCommandSpec[] = [

1012+

{

1013+

name: "spawn_subagent",

1014+

skillName: "spawn-subagent",

1015+

description: "Spawn a subagent",

1016+

dispatch: {

1017+

kind: "tool",

1018+

toolName: "sessions_spawn",

1019+

argMode: "raw",

1020+

},

1021+

sourceFilePath: "/tmp/plugin/commands/spawn-subagent.md",

1022+

},

1023+

];

1024+1025+

const result = await handleInlineActions(

1026+

createHandleInlineActionsInput({

1027+

ctx,

1028+

typing,

1029+

cleanedBody: "/spawn_subagent investigate",

1030+

command: {

1031+

isAuthorizedSender: true,

1032+

senderId: "sender-1",

1033+

senderIsOwner: true,

1034+

abortKey: "sender-1",

1035+

rawBodyNormalized: "/spawn_subagent investigate",

1036+

commandBodyNormalized: "/spawn_subagent investigate",

1037+

},

1038+

overrides: {

1039+

cfg: {

1040+

commands: { text: true },

1041+

session: { store: storeTemplate },

1042+

agents: { defaults: { subagents: { maxSpawnDepth: 2 } } },

1043+

},

1044+

sessionKey: "agent:main:acp:leaf",

1045+

allowTextCommands: true,

1046+

skillCommands,

1047+

},

1048+

}),

1049+

);

1050+1051+

expect(result).toEqual({

1052+

kind: "reply",

1053+

reply: { text: "❌ Tool not available: sessions_spawn" },

1054+

});

1055+

expect(toolExecute).not.toHaveBeenCalled();

1056+

} finally {

1057+

await fs.rm(tmpDir, { recursive: true, force: true });

1058+

}

1059+

});

1060+1061+

it("passes sandboxed runtime state into inline tool construction", async () => {

1062+

const typing = createTypingController();

1063+

const toolExecute = vi.fn(async () => ({ content: "listed" }));

1064+

createOpenClawToolsMock.mockReturnValue([

1065+

{

1066+

name: "sessions_list",

1067+

execute: toolExecute,

1068+

},

1069+

]);

1070+1071+

const ctx = buildTestCtx({

1072+

Body: "/list_sessions now",

1073+

CommandBody: "/list_sessions now",

1074+

});

1075+

const skillCommands: SkillCommandSpec[] = [

1076+

{

1077+

name: "list_sessions",

1078+

skillName: "list-sessions",

1079+

description: "List sessions",

1080+

dispatch: {

1081+

kind: "tool",

1082+

toolName: "sessions_list",

1083+

argMode: "raw",

1084+

},

1085+

sourceFilePath: "/tmp/plugin/commands/list-sessions.md",

1086+

},

1087+

];

1088+1089+

const result = await handleInlineActions(

1090+

createHandleInlineActionsInput({

1091+

ctx,

1092+

typing,

1093+

cleanedBody: "/list_sessions now",

1094+

command: {

1095+

isAuthorizedSender: true,

1096+

senderId: "sender-1",

1097+

senderIsOwner: true,

1098+

abortKey: "sender-1",

1099+

rawBodyNormalized: "/list_sessions now",

1100+

commandBodyNormalized: "/list_sessions now",

1101+

},

1102+

overrides: {

1103+

cfg: {

1104+

commands: { text: true },

1105+

agents: { defaults: { sandbox: { mode: "all" } } },

1106+

},

1107+

sessionKey: "agent:main:thread",

1108+

allowTextCommands: true,

1109+

skillCommands,

1110+

},

1111+

}),

1112+

);

1113+1114+

expect(result).toEqual({ kind: "reply", reply: { text: "listed" } });

1115+

expect(createOpenClawToolsMock).toHaveBeenCalledWith(

1116+

expect.objectContaining({

1117+

sandboxed: true,

1118+

}),

1119+

);

1120+

expect(toolExecute).toHaveBeenCalled();

1121+

});

7921122

});