


























@@ -163,8 +163,13 @@ function buildDefaultResolveRoute(): ResolvedAgentRoute {
163163matchedBy: "default",
164164};
165165}
166+let currentRuntimeConfig = {} as ClawdbotConfig;
167+166168function createFeishuBotRuntime(overrides: DeepPartial<PluginRuntime> = {}): PluginRuntime {
167169return {
170+config: {
171+current: vi.fn(() => currentRuntimeConfig),
172+},
168173channel: {
169174routing: {
170175resolveAgentRoute: resolveAgentRouteMock,
@@ -413,7 +418,11 @@ afterAll(() => {
413418vi.resetModules();
414419});
415420416-async function dispatchMessage(params: { cfg: ClawdbotConfig; event: FeishuMessageEvent }) {
421+async function dispatchMessage(params: {
422+cfg: ClawdbotConfig;
423+currentCfg?: ClawdbotConfig;
424+event: FeishuMessageEvent;
425+}) {
417426const runtime = createRuntimeEnv();
418427const feishuConfig = params.cfg.channels?.feishu;
419428const cfg =
@@ -429,6 +438,7 @@ async function dispatchMessage(params: { cfg: ClawdbotConfig; event: FeishuMessa
429438},
430439} as ClawdbotConfig)
431440 : params.cfg;
441+currentRuntimeConfig = params.currentCfg ?? cfg;
432442await handleFeishuMessage({
433443 cfg,
434444event: params.event,
@@ -455,7 +465,10 @@ describe("handleFeishuMessage ACP routing", () => {
455465mockTouchBinding.mockReset();
456466mockResolveFeishuReasoningPreviewEnabled.mockReset().mockReturnValue(false);
457467mockTranscribeFirstAudio.mockReset().mockResolvedValue(undefined);
458-mockMaybeCreateDynamicAgent.mockReset().mockResolvedValue({ created: false });
468+mockMaybeCreateDynamicAgent.mockReset().mockImplementation(async ({ cfg }) => ({
469+created: false,
470+updatedCfg: cfg,
471+}));
459472mockResolveAgentRoute.mockReset().mockReturnValue({
460473 ...buildDefaultResolveRoute(),
461474sessionKey: "agent:main:feishu:direct:ou_sender_1",
@@ -976,7 +989,9 @@ describe("handleFeishuMessage command authorization", () => {
976989},
977990);
978991const mockResolveCommandAuthorizedFromAuthorizers = vi.fn(() => false);
979-const mockShouldComputeCommandAuthorized = vi.fn(() => true);
992+const mockShouldComputeCommandAuthorized = vi.fn<
993+PluginRuntime["channel"]["commands"]["shouldComputeCommandAuthorized"]
994+>(() => true);
980995const mockReadAllowFromStore = vi.fn().mockResolvedValue([]);
981996const mockUpsertPairingRequest = vi.fn().mockResolvedValue({ code: "ABCDEFGH", created: false });
982997const mockBuildPairingReply = vi.fn(() => "Pairing response");
@@ -1009,7 +1024,10 @@ describe("handleFeishuMessage command authorization", () => {
10091024mockResolveBoundConversation.mockReset().mockReturnValue(null);
10101025mockTouchBinding.mockReset();
10111026mockTranscribeFirstAudio.mockReset().mockResolvedValue(undefined);
1012-mockMaybeCreateDynamicAgent.mockReset().mockResolvedValue({ created: false });
1027+mockMaybeCreateDynamicAgent.mockReset().mockImplementation(async ({ cfg }) => ({
1028+created: false,
1029+updatedCfg: cfg,
1030+}));
10131031mockResolveAgentRoute.mockReturnValue(buildDefaultResolveRoute());
10141032mockCreateFeishuClient.mockReturnValue({
10151033contact: {
@@ -1216,7 +1234,7 @@ describe("handleFeishuMessage command authorization", () => {
12161234expect(ensureNoVisibleReplyFallback).toHaveBeenCalledWith("dispatch-complete-no-visible-reply");
12171235});
121812361219-it("passes disabled config-write policy to dynamic agent creation", async () => {
1237+it("uses refreshed config for dynamic agent dispatch", async () => {
12201238mockShouldComputeCommandAuthorized.mockReturnValue(false);
1221123912221240const cfg: ClawdbotConfig = {
@@ -1231,6 +1249,22 @@ describe("handleFeishuMessage command authorization", () => {
12311249},
12321250},
12331251} as ClawdbotConfig;
1252+const refreshedCfg = {
1253+ ...cfg,
1254+agents: {
1255+list: [
1256+{
1257+id: "feishu-ou-attacker",
1258+workspace: "/tmp/feishu-ou-attacker",
1259+agentDir: "/tmp/feishu-ou-attacker/agent",
1260+},
1261+],
1262+},
1263+} as ClawdbotConfig;
1264+mockMaybeCreateDynamicAgent.mockResolvedValueOnce({
1265+created: false,
1266+updatedCfg: refreshedCfg,
1267+});
1234126812351269const event: FeishuMessageEvent = {
12361270sender: {
@@ -1250,12 +1284,177 @@ describe("handleFeishuMessage command authorization", () => {
12501284await dispatchMessage({ cfg, event });
1251128512521286const dynamicAgentRequest = mockCallArg<{
1253-configWritesAllowed?: boolean;
1287+accountId?: string;
12541288senderOpenId?: string;
12551289}>(mockMaybeCreateDynamicAgent, 0, 0);
12561290expect(dynamicAgentRequest.senderOpenId).toBe("ou-attacker");
1257-expect(dynamicAgentRequest.configWritesAllowed).toBe(false);
1258-expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(1);
1291+expect(dynamicAgentRequest.accountId).toBe("default");
1292+expect(mockCreateFeishuReplyDispatcher).toHaveBeenCalledWith(
1293+expect.objectContaining({ cfg: refreshedCfg }),
1294+);
1295+expect(mockDispatchReplyFromConfig).toHaveBeenCalledWith(
1296+expect.objectContaining({ cfg: refreshedCfg }),
1297+);
1298+});
1299+1300+it("drops a DM denied by refreshed dynamic-agent policy", async () => {
1301+mockShouldComputeCommandAuthorized.mockReturnValue(false);
1302+1303+const cfg = {
1304+channels: {
1305+feishu: {
1306+dmPolicy: "open",
1307+allowFrom: ["*"],
1308+dynamicAgentCreation: { enabled: true },
1309+},
1310+},
1311+} as ClawdbotConfig;
1312+const refreshedCfg = {
1313+channels: {
1314+feishu: {
1315+dmPolicy: "allowlist",
1316+allowFrom: ["ou-admin"],
1317+dynamicAgentCreation: { enabled: true },
1318+},
1319+},
1320+} as ClawdbotConfig;
1321+await dispatchMessage({
1322+ cfg,
1323+currentCfg: refreshedCfg,
1324+event: {
1325+sender: { sender_id: { open_id: "ou-attacker" } },
1326+message: {
1327+message_id: "msg-refreshed-policy-deny",
1328+chat_id: "oc-dm",
1329+chat_type: "p2p",
1330+message_type: "text",
1331+content: JSON.stringify({ text: "hello" }),
1332+},
1333+},
1334+});
1335+1336+expect(mockMaybeCreateDynamicAgent).not.toHaveBeenCalled();
1337+expect(mockFinalizeInboundContext).not.toHaveBeenCalled();
1338+expect(mockCreateFeishuReplyDispatcher).not.toHaveBeenCalled();
1339+expect(mockDispatchReplyFromConfig).not.toHaveBeenCalled();
1340+});
1341+1342+it("reauthorizes current policy before dispatching an existing bound route", async () => {
1343+mockShouldComputeCommandAuthorized.mockReturnValue(false);
1344+mockResolveAgentRoute.mockReturnValue({
1345+ ...buildDefaultResolveRoute(),
1346+matchedBy: "binding.peer",
1347+});
1348+const cfg = {
1349+channels: { feishu: { dmPolicy: "open", allowFrom: ["*"] } },
1350+} as ClawdbotConfig;
1351+const currentCfg = {
1352+channels: { feishu: { dmPolicy: "allowlist", allowFrom: ["ou-admin"] } },
1353+} as ClawdbotConfig;
1354+1355+await dispatchMessage({
1356+ cfg,
1357+ currentCfg,
1358+event: {
1359+sender: { sender_id: { open_id: "ou-attacker" } },
1360+message: {
1361+message_id: "msg-bound-refreshed-policy-deny",
1362+chat_id: "oc-dm",
1363+chat_type: "p2p",
1364+message_type: "text",
1365+content: JSON.stringify({ text: "hello" }),
1366+},
1367+},
1368+});
1369+1370+expect(mockFinalizeInboundContext).not.toHaveBeenCalled();
1371+expect(mockDispatchReplyFromConfig).not.toHaveBeenCalled();
1372+});
1373+1374+it("issues a pairing challenge before dynamic creation when current policy requires it", async () => {
1375+mockShouldComputeCommandAuthorized.mockReturnValue(false);
1376+mockReadAllowFromStore.mockResolvedValue([]);
1377+mockUpsertPairingRequest.mockResolvedValue({ code: "ABCDEFGH", created: true });
1378+1379+const cfg = {
1380+channels: {
1381+feishu: {
1382+dmPolicy: "open",
1383+allowFrom: ["*"],
1384+dynamicAgentCreation: { enabled: true },
1385+},
1386+},
1387+} as ClawdbotConfig;
1388+const currentCfg = {
1389+channels: {
1390+feishu: {
1391+dmPolicy: "pairing",
1392+allowFrom: [],
1393+dynamicAgentCreation: { enabled: true },
1394+},
1395+},
1396+} as ClawdbotConfig;
1397+1398+await dispatchMessage({
1399+ cfg,
1400+ currentCfg,
1401+event: {
1402+sender: { sender_id: { open_id: "ou-attacker" } },
1403+message: {
1404+message_id: "msg-refreshed-policy-pairing",
1405+chat_id: "oc-dm",
1406+chat_type: "p2p",
1407+message_type: "text",
1408+content: JSON.stringify({ text: "hello" }),
1409+},
1410+},
1411+});
1412+1413+expect(mockMaybeCreateDynamicAgent).not.toHaveBeenCalled();
1414+expect(mockUpsertPairingRequest).toHaveBeenCalledTimes(1);
1415+expect(mockSendMessageFeishu).toHaveBeenCalledTimes(1);
1416+expect(mockDispatchReplyFromConfig).not.toHaveBeenCalled();
1417+});
1418+1419+it("recomputes command authorization against refreshed dynamic-agent config", async () => {
1420+const cfg = {
1421+channels: {
1422+feishu: {
1423+dmPolicy: "open",
1424+allowFrom: ["*"],
1425+dynamicAgentCreation: { enabled: true },
1426+},
1427+},
1428+} as ClawdbotConfig;
1429+const refreshedCfg = {
1430+ ...cfg,
1431+commands: { useAccessGroups: true },
1432+} as ClawdbotConfig;
1433+mockShouldComputeCommandAuthorized.mockImplementation((_body, candidateCfg) => {
1434+return candidateCfg === refreshedCfg;
1435+});
1436+mockMaybeCreateDynamicAgent.mockResolvedValueOnce({
1437+created: false,
1438+updatedCfg: refreshedCfg,
1439+});
1440+1441+await dispatchMessage({
1442+ cfg,
1443+event: {
1444+sender: { sender_id: { open_id: "ou-attacker" } },
1445+message: {
1446+message_id: "msg-refreshed-command-auth",
1447+chat_id: "oc-dm",
1448+chat_type: "p2p",
1449+message_type: "text",
1450+content: JSON.stringify({ text: "/status" }),
1451+},
1452+},
1453+});
1454+1455+expect(mockShouldComputeCommandAuthorized).toHaveBeenCalledWith("/status", refreshedCfg);
1456+const context = mockCallArg<{ CommandAuthorized?: boolean }>(mockFinalizeInboundContext, 0, 0);
1457+expect(context.CommandAuthorized).toBe(true);
12591458});
1260145912611460it("blocks open DMs when a restrictive allowlist does not match", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。