









@@ -1,3 +1,6 @@
1+import fs from "node:fs/promises";
2+import os from "node:os";
3+import path from "node:path";
14import { beforeEach, describe, expect, it, vi } from "vitest";
25import type { SkillCommandSpec } from "../../agents/skills.js";
36import type { SessionEntry } from "../../config/sessions.js";
@@ -47,6 +50,16 @@ const createTypingController = (): TypingController => ({
4750cleanup: 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+5063const createHandleInlineActionsInput = (params: {
5164ctx: ReturnType<typeof buildTestCtx>;
5265typing: TypingController;
@@ -789,4 +802,321 @@ describe("handleInlineActions", () => {
789802expect(blockedToolCall?.[2]).toBe(abortController.signal);
790803expect(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});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。