activeclaw 2026.3.10 → 2026.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.json +3 -3
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/dist/plugin-sdk/feishu.js +2 -2
- package/dist/plugin-sdk/googlechat.js +2 -2
- package/dist/plugin-sdk/msteams.js +2 -2
- package/dist/plugin-sdk/nextcloud-talk.js +2 -2
- package/dist/plugin-sdk/signal.js +2 -2
- package/dist/plugin-sdk/slack.js +2 -2
- package/extensions/hub/src/channel.test.ts +166 -0
- package/extensions/hub/src/channel.ts +16 -17
- package/extensions/hub/src/inbound.test.ts +172 -0
- package/extensions/hub/src/send.test.ts +97 -0
- package/extensions/hub/src/send.ts +2 -1
- package/extensions/hub/src/targets.test.ts +29 -0
- package/extensions/hub/src/targets.ts +21 -0
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
0e9f701206ce2812d0e33952c84b8517021bd775c5fb5b1e0e423e5f9de2d26d
|
|
@@ -4,7 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import chalk, { Chalk } from "chalk";
|
|
6
6
|
import { Logger } from "tslog";
|
|
7
|
-
import
|
|
7
|
+
import json5 from "json5";
|
|
8
8
|
import { promisify } from "node:util";
|
|
9
9
|
import fs$1, { mkdtemp, rm } from "node:fs/promises";
|
|
10
10
|
import { execFile, spawn } from "node:child_process";
|
|
@@ -372,7 +372,7 @@ function readLoggingConfig() {
|
|
|
372
372
|
try {
|
|
373
373
|
if (!fs.existsSync(configPath)) return;
|
|
374
374
|
const raw = fs.readFileSync(configPath, "utf-8");
|
|
375
|
-
const logging =
|
|
375
|
+
const logging = json5.parse(raw)?.logging;
|
|
376
376
|
if (!logging || typeof logging !== "object" || Array.isArray(logging)) return;
|
|
377
377
|
return logging;
|
|
378
378
|
} catch {
|
|
@@ -5,7 +5,7 @@ import chalk, { Chalk } from "chalk";
|
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import { Logger } from "tslog";
|
|
7
7
|
import os from "node:os";
|
|
8
|
-
import
|
|
8
|
+
import json5 from "json5";
|
|
9
9
|
import { promisify } from "node:util";
|
|
10
10
|
import { execFile } from "node:child_process";
|
|
11
11
|
import { getOAuthProviders } from "@mariozechner/pi-ai";
|
|
@@ -415,7 +415,7 @@ function readLoggingConfig() {
|
|
|
415
415
|
try {
|
|
416
416
|
if (!fs.existsSync(configPath)) return;
|
|
417
417
|
const raw = fs.readFileSync(configPath, "utf-8");
|
|
418
|
-
const logging =
|
|
418
|
+
const logging = json5.parse(raw)?.logging;
|
|
419
419
|
if (!logging || typeof logging !== "object" || Array.isArray(logging)) return;
|
|
420
420
|
return logging;
|
|
421
421
|
} catch {
|
|
@@ -4,7 +4,7 @@ import fs, { constants } from "node:fs";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import chalk, { Chalk } from "chalk";
|
|
6
6
|
import { Logger } from "tslog";
|
|
7
|
-
import
|
|
7
|
+
import json5 from "json5";
|
|
8
8
|
import { promisify } from "node:util";
|
|
9
9
|
import fs$1 from "node:fs/promises";
|
|
10
10
|
import { execFile } from "node:child_process";
|
|
@@ -373,7 +373,7 @@ function readLoggingConfig() {
|
|
|
373
373
|
try {
|
|
374
374
|
if (!fs.existsSync(configPath)) return;
|
|
375
375
|
const raw = fs.readFileSync(configPath, "utf-8");
|
|
376
|
-
const logging =
|
|
376
|
+
const logging = json5.parse(raw)?.logging;
|
|
377
377
|
if (!logging || typeof logging !== "object" || Array.isArray(logging)) return;
|
|
378
378
|
return logging;
|
|
379
379
|
} catch {
|
|
@@ -6,7 +6,7 @@ import fs from "node:fs";
|
|
|
6
6
|
import os from "node:os";
|
|
7
7
|
import chalk, { Chalk } from "chalk";
|
|
8
8
|
import { Logger } from "tslog";
|
|
9
|
-
import
|
|
9
|
+
import json5 from "json5";
|
|
10
10
|
import { format, promisify } from "node:util";
|
|
11
11
|
import fs$1 from "node:fs/promises";
|
|
12
12
|
import process$1 from "node:process";
|
|
@@ -722,7 +722,7 @@ function readLoggingConfig() {
|
|
|
722
722
|
try {
|
|
723
723
|
if (!fs.existsSync(configPath)) return;
|
|
724
724
|
const raw = fs.readFileSync(configPath, "utf-8");
|
|
725
|
-
const logging =
|
|
725
|
+
const logging = json5.parse(raw)?.logging;
|
|
726
726
|
if (!logging || typeof logging !== "object" || Array.isArray(logging)) return;
|
|
727
727
|
return logging;
|
|
728
728
|
} catch {
|
|
@@ -4,7 +4,7 @@ import chalk, { Chalk } from "chalk";
|
|
|
4
4
|
import fs, { constants, createWriteStream } from "node:fs";
|
|
5
5
|
import { Logger } from "tslog";
|
|
6
6
|
import os from "node:os";
|
|
7
|
-
import
|
|
7
|
+
import JSON5 from "json5";
|
|
8
8
|
import { promisify } from "node:util";
|
|
9
9
|
import fs$1 from "node:fs/promises";
|
|
10
10
|
import "@clack/prompts";
|
|
@@ -780,7 +780,7 @@ function readLoggingConfig() {
|
|
|
780
780
|
try {
|
|
781
781
|
if (!fs.existsSync(configPath)) return;
|
|
782
782
|
const raw = fs.readFileSync(configPath, "utf-8");
|
|
783
|
-
const logging =
|
|
783
|
+
const logging = JSON5.parse(raw)?.logging;
|
|
784
784
|
if (!logging || typeof logging !== "object" || Array.isArray(logging)) return;
|
|
785
785
|
return logging;
|
|
786
786
|
} catch {
|
package/dist/plugin-sdk/slack.js
CHANGED
|
@@ -4,7 +4,7 @@ import chalk, { Chalk } from "chalk";
|
|
|
4
4
|
import fs from "node:fs";
|
|
5
5
|
import { Logger } from "tslog";
|
|
6
6
|
import os from "node:os";
|
|
7
|
-
import
|
|
7
|
+
import json5 from "json5";
|
|
8
8
|
import { promisify } from "node:util";
|
|
9
9
|
import fs$1 from "node:fs/promises";
|
|
10
10
|
import { execFile, spawn } from "node:child_process";
|
|
@@ -772,7 +772,7 @@ function readLoggingConfig() {
|
|
|
772
772
|
try {
|
|
773
773
|
if (!fs.existsSync(configPath)) return;
|
|
774
774
|
const raw = fs.readFileSync(configPath, "utf-8");
|
|
775
|
-
const logging =
|
|
775
|
+
const logging = json5.parse(raw)?.logging;
|
|
776
776
|
if (!logging || typeof logging !== "object" || Array.isArray(logging)) return;
|
|
777
777
|
return logging;
|
|
778
778
|
} catch {
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
deleteAccountFromConfigSectionMock,
|
|
5
|
+
listHubAccountIdsMock,
|
|
6
|
+
resolveDefaultHubAccountIdMock,
|
|
7
|
+
resolveHubAccountMock,
|
|
8
|
+
sendMessageHubMock,
|
|
9
|
+
setAccountEnabledInConfigSectionMock,
|
|
10
|
+
} = vi.hoisted(() => ({
|
|
11
|
+
deleteAccountFromConfigSectionMock: vi.fn(() => ({})),
|
|
12
|
+
listHubAccountIdsMock: vi.fn(() => ["default"]),
|
|
13
|
+
resolveDefaultHubAccountIdMock: vi.fn(() => "default"),
|
|
14
|
+
resolveHubAccountMock: vi.fn(),
|
|
15
|
+
sendMessageHubMock: vi.fn(),
|
|
16
|
+
setAccountEnabledInConfigSectionMock: vi.fn(() => ({})),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock("openclaw/plugin-sdk", () => ({
|
|
20
|
+
buildBaseAccountStatusSnapshot: vi.fn(() => ({})),
|
|
21
|
+
buildBaseChannelStatusSummary: vi.fn(() => ({})),
|
|
22
|
+
buildChannelConfigSchema: vi.fn((schema: unknown) => schema),
|
|
23
|
+
DEFAULT_ACCOUNT_ID: "default",
|
|
24
|
+
deleteAccountFromConfigSection: deleteAccountFromConfigSectionMock,
|
|
25
|
+
formatPairingApproveHint: vi.fn(() => "approve via hub"),
|
|
26
|
+
PAIRING_APPROVED_MESSAGE: "approved",
|
|
27
|
+
setAccountEnabledInConfigSection: setAccountEnabledInConfigSectionMock,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
vi.mock("./accounts.js", () => ({
|
|
31
|
+
listHubAccountIds: listHubAccountIdsMock,
|
|
32
|
+
resolveDefaultHubAccountId: resolveDefaultHubAccountIdMock,
|
|
33
|
+
resolveHubAccount: resolveHubAccountMock,
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
vi.mock("./config-schema.js", () => ({
|
|
37
|
+
HubConfigSchema: { type: "object" },
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
vi.mock("./monitor.js", () => ({
|
|
41
|
+
monitorHubProvider: vi.fn(),
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
vi.mock("./onboarding.js", () => ({
|
|
45
|
+
hubOnboardingAdapter: {},
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
vi.mock("./probe.js", () => ({
|
|
49
|
+
probeHub: vi.fn(),
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
vi.mock("./runtime.js", () => ({
|
|
53
|
+
getHubRuntime: vi.fn(() => ({
|
|
54
|
+
channel: {
|
|
55
|
+
activity: {
|
|
56
|
+
record: vi.fn(),
|
|
57
|
+
},
|
|
58
|
+
text: {
|
|
59
|
+
chunkMarkdownText: vi.fn(),
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
})),
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
vi.mock("./send.js", () => ({
|
|
66
|
+
sendMessageHub: sendMessageHubMock,
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
const { hubPlugin } = await import("./channel.js");
|
|
70
|
+
|
|
71
|
+
describe("hubPlugin normalization", () => {
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
sendMessageHubMock.mockReset();
|
|
74
|
+
sendMessageHubMock.mockResolvedValue({
|
|
75
|
+
messageId: "hub-1",
|
|
76
|
+
target: "brain",
|
|
77
|
+
});
|
|
78
|
+
resolveHubAccountMock.mockReset();
|
|
79
|
+
resolveHubAccountMock.mockReturnValue({
|
|
80
|
+
accountId: "default",
|
|
81
|
+
name: "Hub",
|
|
82
|
+
enabled: true,
|
|
83
|
+
configured: true,
|
|
84
|
+
url: "https://hub.example.test",
|
|
85
|
+
agentId: "sender",
|
|
86
|
+
secretSource: "inline",
|
|
87
|
+
config: {
|
|
88
|
+
allowFrom: [" hub:Brain ", "*", "CombinatorAgent ", "hub:"],
|
|
89
|
+
defaultTo: " hub:TargetAgent ",
|
|
90
|
+
dmPolicy: "open",
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("normalizes outbound hub targets", () => {
|
|
96
|
+
expect(hubPlugin.messaging?.normalizeTarget?.(" hub:Brain ")).toBe("Brain");
|
|
97
|
+
expect(hubPlugin.messaging?.normalizeTarget?.("CombinatorAgent")).toBe("CombinatorAgent");
|
|
98
|
+
expect(hubPlugin.messaging?.normalizeTarget?.("hub:")).toBeUndefined();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("normalizes pairing allow entries and approval targets", async () => {
|
|
102
|
+
expect(hubPlugin.pairing?.normalizeAllowEntry?.(" hub:Brain ")).toBe("brain");
|
|
103
|
+
expect(hubPlugin.pairing?.normalizeAllowEntry?.("hub:*")).toBe("*");
|
|
104
|
+
|
|
105
|
+
await hubPlugin.pairing?.notifyApproval?.({ id: " hub:Brain " } as any);
|
|
106
|
+
|
|
107
|
+
expect(sendMessageHubMock).toHaveBeenCalledWith("Brain", "approved");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("normalizes config-derived allowFrom and defaultTo values", () => {
|
|
111
|
+
const cfg = { channels: { hub: {} } };
|
|
112
|
+
|
|
113
|
+
expect(hubPlugin.config.resolveAllowFrom({ cfg, accountId: "default" } as any)).toEqual([
|
|
114
|
+
"brain",
|
|
115
|
+
"*",
|
|
116
|
+
"combinatoragent",
|
|
117
|
+
]);
|
|
118
|
+
expect(
|
|
119
|
+
hubPlugin.config.formatAllowFrom({
|
|
120
|
+
allowFrom: [" hub:Brain ", "COMBINATORAGENT", "*", "hub:"],
|
|
121
|
+
} as any),
|
|
122
|
+
).toEqual(["brain", "combinatoragent", "*"]);
|
|
123
|
+
expect(hubPlugin.config.resolveDefaultTo({ cfg, accountId: "default" } as any)).toBe(
|
|
124
|
+
"TargetAgent",
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("normalizes dm-policy entries, resolver ids, and directory peers", async () => {
|
|
129
|
+
const cfg = { channels: { hub: {} } };
|
|
130
|
+
const account = resolveHubAccountMock.mock.results[0]?.value ?? resolveHubAccountMock();
|
|
131
|
+
const dmPolicy = hubPlugin.security.resolveDmPolicy({
|
|
132
|
+
cfg,
|
|
133
|
+
accountId: "default",
|
|
134
|
+
account,
|
|
135
|
+
} as any);
|
|
136
|
+
|
|
137
|
+
expect(dmPolicy.normalizeEntry(" hub:Brain ")).toBe("brain");
|
|
138
|
+
expect(dmPolicy.normalizeEntry("hub:*")).toBe("*");
|
|
139
|
+
|
|
140
|
+
await expect(
|
|
141
|
+
hubPlugin.resolver.resolveTargets({
|
|
142
|
+
inputs: [" hub:Brain ", " hub: ", "CombinatorAgent"],
|
|
143
|
+
} as any),
|
|
144
|
+
).resolves.toEqual([
|
|
145
|
+
{ input: " hub:Brain ", resolved: true, id: "Brain", name: "Brain" },
|
|
146
|
+
{ input: " hub: ", resolved: false, note: "empty target" },
|
|
147
|
+
{
|
|
148
|
+
input: "CombinatorAgent",
|
|
149
|
+
resolved: true,
|
|
150
|
+
id: "CombinatorAgent",
|
|
151
|
+
name: "CombinatorAgent",
|
|
152
|
+
},
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
await expect(
|
|
156
|
+
hubPlugin.directory.listPeers({
|
|
157
|
+
cfg,
|
|
158
|
+
accountId: "default",
|
|
159
|
+
limit: 10,
|
|
160
|
+
} as any),
|
|
161
|
+
).resolves.toEqual([
|
|
162
|
+
{ kind: "user", id: "brain" },
|
|
163
|
+
{ kind: "user", id: "combinatoragent" },
|
|
164
|
+
]);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -21,6 +21,7 @@ import { hubOnboardingAdapter } from "./onboarding.js";
|
|
|
21
21
|
import { probeHub } from "./probe.js";
|
|
22
22
|
import { getHubRuntime } from "./runtime.js";
|
|
23
23
|
import { sendMessageHub } from "./send.js";
|
|
24
|
+
import { normalizeHubAllowEntry, normalizeHubTarget } from "./targets.js";
|
|
24
25
|
import type { CoreConfig, HubProbe } from "./types.js";
|
|
25
26
|
|
|
26
27
|
export const hubPlugin: ChannelPlugin<ResolvedHubAccount, HubProbe> = {
|
|
@@ -38,9 +39,9 @@ export const hubPlugin: ChannelPlugin<ResolvedHubAccount, HubProbe> = {
|
|
|
38
39
|
onboarding: hubOnboardingAdapter,
|
|
39
40
|
pairing: {
|
|
40
41
|
idLabel: "hubAgent",
|
|
41
|
-
normalizeAllowEntry: (entry) =>
|
|
42
|
+
normalizeAllowEntry: (entry) => normalizeHubAllowEntry(entry) ?? "",
|
|
42
43
|
notifyApproval: async ({ id }) => {
|
|
43
|
-
const target = String(id)
|
|
44
|
+
const target = normalizeHubTarget(String(id));
|
|
44
45
|
if (!target) {
|
|
45
46
|
throw new Error(`invalid Hub pairing id: ${id}`);
|
|
46
47
|
}
|
|
@@ -84,14 +85,15 @@ export const hubPlugin: ChannelPlugin<ResolvedHubAccount, HubProbe> = {
|
|
|
84
85
|
secretSource: account.secretSource,
|
|
85
86
|
}),
|
|
86
87
|
resolveAllowFrom: ({ cfg, accountId }) =>
|
|
87
|
-
(resolveHubAccount({ cfg: cfg as CoreConfig, accountId }).config.allowFrom ?? [])
|
|
88
|
-
(entry) => String(entry)
|
|
89
|
-
|
|
88
|
+
(resolveHubAccount({ cfg: cfg as CoreConfig, accountId }).config.allowFrom ?? [])
|
|
89
|
+
.map((entry) => normalizeHubAllowEntry(String(entry)))
|
|
90
|
+
.filter((entry): entry is string => Boolean(entry)),
|
|
90
91
|
formatAllowFrom: ({ allowFrom }) =>
|
|
91
|
-
allowFrom
|
|
92
|
+
allowFrom
|
|
93
|
+
.map((entry) => normalizeHubAllowEntry(String(entry)))
|
|
94
|
+
.filter((entry): entry is string => Boolean(entry)),
|
|
92
95
|
resolveDefaultTo: ({ cfg, accountId }) =>
|
|
93
|
-
resolveHubAccount({ cfg: cfg as CoreConfig, accountId }).config.defaultTo
|
|
94
|
-
undefined,
|
|
96
|
+
normalizeHubTarget(resolveHubAccount({ cfg: cfg as CoreConfig, accountId }).config.defaultTo),
|
|
95
97
|
},
|
|
96
98
|
security: {
|
|
97
99
|
resolveDmPolicy: ({ cfg, accountId, account }) => {
|
|
@@ -106,15 +108,12 @@ export const hubPlugin: ChannelPlugin<ResolvedHubAccount, HubProbe> = {
|
|
|
106
108
|
policyPath: `${basePath}dmPolicy`,
|
|
107
109
|
allowFromPath: `${basePath}allowFrom`,
|
|
108
110
|
approveHint: formatPairingApproveHint("hub"),
|
|
109
|
-
normalizeEntry: (raw) =>
|
|
111
|
+
normalizeEntry: (raw) => normalizeHubAllowEntry(raw) ?? "",
|
|
110
112
|
};
|
|
111
113
|
},
|
|
112
114
|
},
|
|
113
115
|
messaging: {
|
|
114
|
-
normalizeTarget: (input) =>
|
|
115
|
-
const trimmed = String(input ?? "").trim();
|
|
116
|
-
return trimmed || undefined;
|
|
117
|
-
},
|
|
116
|
+
normalizeTarget: (input) => normalizeHubTarget(input),
|
|
118
117
|
targetResolver: {
|
|
119
118
|
looksLikeId: (input) => Boolean(String(input ?? "").trim()),
|
|
120
119
|
hint: "<agent-id>",
|
|
@@ -123,11 +122,11 @@ export const hubPlugin: ChannelPlugin<ResolvedHubAccount, HubProbe> = {
|
|
|
123
122
|
resolver: {
|
|
124
123
|
resolveTargets: async ({ inputs }) => {
|
|
125
124
|
return inputs.map((input) => {
|
|
126
|
-
const
|
|
127
|
-
if (!
|
|
125
|
+
const normalized = normalizeHubTarget(String(input));
|
|
126
|
+
if (!normalized) {
|
|
128
127
|
return { input, resolved: false, note: "empty target" };
|
|
129
128
|
}
|
|
130
|
-
return { input, resolved: true, id:
|
|
129
|
+
return { input, resolved: true, id: normalized, name: normalized };
|
|
131
130
|
});
|
|
132
131
|
},
|
|
133
132
|
},
|
|
@@ -138,7 +137,7 @@ export const hubPlugin: ChannelPlugin<ResolvedHubAccount, HubProbe> = {
|
|
|
138
137
|
const q = query?.trim().toLowerCase() ?? "";
|
|
139
138
|
const ids = new Set<string>();
|
|
140
139
|
for (const entry of account.config.allowFrom ?? []) {
|
|
141
|
-
const normalized = String(entry)
|
|
140
|
+
const normalized = normalizeHubAllowEntry(String(entry));
|
|
142
141
|
if (normalized && normalized !== "*") {
|
|
143
142
|
ids.add(normalized);
|
|
144
143
|
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
createNormalizedOutboundDelivererMock,
|
|
5
|
+
createReplyPrefixOptionsMock,
|
|
6
|
+
createScopedPairingAccessMock,
|
|
7
|
+
dispatchReplyWithBufferedBlockDispatcherMock,
|
|
8
|
+
emitInboundHistoryMock,
|
|
9
|
+
emitOutboundHistoryMock,
|
|
10
|
+
formatTextWithAttachmentLinksMock,
|
|
11
|
+
getHubRuntimeMock,
|
|
12
|
+
logInboundDropMock,
|
|
13
|
+
readStoreAllowFromForDmPolicyMock,
|
|
14
|
+
recordInboundSessionMock,
|
|
15
|
+
resolveControlCommandGateMock,
|
|
16
|
+
resolveEffectiveAllowFromListsMock,
|
|
17
|
+
resolveOutboundMediaUrlsMock,
|
|
18
|
+
sendMessageHubMock,
|
|
19
|
+
} = vi.hoisted(() => ({
|
|
20
|
+
createNormalizedOutboundDelivererMock: vi.fn(),
|
|
21
|
+
createReplyPrefixOptionsMock: vi.fn(),
|
|
22
|
+
createScopedPairingAccessMock: vi.fn(),
|
|
23
|
+
dispatchReplyWithBufferedBlockDispatcherMock: vi.fn(),
|
|
24
|
+
emitInboundHistoryMock: vi.fn(),
|
|
25
|
+
emitOutboundHistoryMock: vi.fn(),
|
|
26
|
+
formatTextWithAttachmentLinksMock: vi.fn(),
|
|
27
|
+
getHubRuntimeMock: vi.fn(),
|
|
28
|
+
logInboundDropMock: vi.fn(),
|
|
29
|
+
readStoreAllowFromForDmPolicyMock: vi.fn(),
|
|
30
|
+
recordInboundSessionMock: vi.fn(),
|
|
31
|
+
resolveControlCommandGateMock: vi.fn(),
|
|
32
|
+
resolveEffectiveAllowFromListsMock: vi.fn(),
|
|
33
|
+
resolveOutboundMediaUrlsMock: vi.fn(),
|
|
34
|
+
sendMessageHubMock: vi.fn(),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
vi.mock("openclaw/plugin-sdk", () => ({
|
|
38
|
+
createScopedPairingAccess: createScopedPairingAccessMock,
|
|
39
|
+
createNormalizedOutboundDeliverer: createNormalizedOutboundDelivererMock,
|
|
40
|
+
createReplyPrefixOptions: createReplyPrefixOptionsMock,
|
|
41
|
+
emitInboundHistory: emitInboundHistoryMock,
|
|
42
|
+
emitOutboundHistory: emitOutboundHistoryMock,
|
|
43
|
+
formatTextWithAttachmentLinks: formatTextWithAttachmentLinksMock,
|
|
44
|
+
logInboundDrop: logInboundDropMock,
|
|
45
|
+
readStoreAllowFromForDmPolicy: readStoreAllowFromForDmPolicyMock,
|
|
46
|
+
resolveControlCommandGate: resolveControlCommandGateMock,
|
|
47
|
+
resolveOutboundMediaUrls: resolveOutboundMediaUrlsMock,
|
|
48
|
+
resolveEffectiveAllowFromLists: resolveEffectiveAllowFromListsMock,
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
vi.mock("./runtime.js", () => ({
|
|
52
|
+
getHubRuntime: getHubRuntimeMock,
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
vi.mock("./send.js", () => ({
|
|
56
|
+
sendMessageHub: sendMessageHubMock,
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
import { handleHubInbound } from "./inbound.js";
|
|
60
|
+
|
|
61
|
+
describe("handleHubInbound", () => {
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
createScopedPairingAccessMock.mockReset();
|
|
64
|
+
createNormalizedOutboundDelivererMock.mockReset();
|
|
65
|
+
createReplyPrefixOptionsMock.mockReset();
|
|
66
|
+
dispatchReplyWithBufferedBlockDispatcherMock.mockReset();
|
|
67
|
+
emitInboundHistoryMock.mockReset();
|
|
68
|
+
emitOutboundHistoryMock.mockReset();
|
|
69
|
+
formatTextWithAttachmentLinksMock.mockReset();
|
|
70
|
+
getHubRuntimeMock.mockReset();
|
|
71
|
+
logInboundDropMock.mockReset();
|
|
72
|
+
readStoreAllowFromForDmPolicyMock.mockReset();
|
|
73
|
+
recordInboundSessionMock.mockReset();
|
|
74
|
+
resolveControlCommandGateMock.mockReset();
|
|
75
|
+
resolveEffectiveAllowFromListsMock.mockReset();
|
|
76
|
+
resolveOutboundMediaUrlsMock.mockReset();
|
|
77
|
+
sendMessageHubMock.mockReset();
|
|
78
|
+
|
|
79
|
+
createScopedPairingAccessMock.mockReturnValue({
|
|
80
|
+
readStoreForDmPolicy: vi.fn(),
|
|
81
|
+
upsertPairingRequest: vi.fn(),
|
|
82
|
+
});
|
|
83
|
+
createNormalizedOutboundDelivererMock.mockImplementation((deliver: unknown) => deliver);
|
|
84
|
+
createReplyPrefixOptionsMock.mockReturnValue({
|
|
85
|
+
onModelSelected: vi.fn(),
|
|
86
|
+
includePrefix: false,
|
|
87
|
+
});
|
|
88
|
+
readStoreAllowFromForDmPolicyMock.mockResolvedValue([]);
|
|
89
|
+
resolveEffectiveAllowFromListsMock.mockReturnValue({
|
|
90
|
+
effectiveAllowFrom: ["*"],
|
|
91
|
+
});
|
|
92
|
+
resolveControlCommandGateMock.mockReturnValue({
|
|
93
|
+
commandAuthorized: true,
|
|
94
|
+
shouldBlock: false,
|
|
95
|
+
});
|
|
96
|
+
resolveOutboundMediaUrlsMock.mockReturnValue([]);
|
|
97
|
+
formatTextWithAttachmentLinksMock.mockImplementation((text: string) => text);
|
|
98
|
+
recordInboundSessionMock.mockResolvedValue(undefined);
|
|
99
|
+
dispatchReplyWithBufferedBlockDispatcherMock.mockImplementation(
|
|
100
|
+
async (params: Record<string, any>) => {
|
|
101
|
+
await params.dispatcherOptions.deliver({ text: "reply body" });
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
getHubRuntimeMock.mockReturnValue({
|
|
105
|
+
channel: {
|
|
106
|
+
commands: {
|
|
107
|
+
shouldHandleTextCommands: vi.fn(() => true),
|
|
108
|
+
},
|
|
109
|
+
reply: {
|
|
110
|
+
dispatchReplyWithBufferedBlockDispatcher: dispatchReplyWithBufferedBlockDispatcherMock,
|
|
111
|
+
finalizeInboundContext: vi.fn((ctx: Record<string, unknown>) => ctx),
|
|
112
|
+
formatAgentEnvelope: vi.fn(() => "formatted inbound"),
|
|
113
|
+
resolveEnvelopeFormatOptions: vi.fn(() => ({})),
|
|
114
|
+
},
|
|
115
|
+
routing: {
|
|
116
|
+
resolveAgentRoute: vi.fn(() => ({
|
|
117
|
+
agentId: "main",
|
|
118
|
+
sessionKey: "agent:main:hub:direct:CombinatorAgent",
|
|
119
|
+
accountId: "default",
|
|
120
|
+
})),
|
|
121
|
+
},
|
|
122
|
+
session: {
|
|
123
|
+
readSessionUpdatedAt: vi.fn(() => undefined),
|
|
124
|
+
recordInboundSession: recordInboundSessionMock,
|
|
125
|
+
resolveStorePath: vi.fn(() => "/tmp/openclaw-store"),
|
|
126
|
+
},
|
|
127
|
+
text: {
|
|
128
|
+
hasControlCommand: vi.fn(() => false),
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("replies to the plain Hub id while keeping prefixed conversation history keys", async () => {
|
|
135
|
+
const sendReplyMock = vi.fn().mockResolvedValue(undefined);
|
|
136
|
+
|
|
137
|
+
await handleHubInbound({
|
|
138
|
+
message: {
|
|
139
|
+
messageId: "msg-1",
|
|
140
|
+
from: "CombinatorAgent",
|
|
141
|
+
text: "hello",
|
|
142
|
+
timestamp: 123,
|
|
143
|
+
},
|
|
144
|
+
account: {
|
|
145
|
+
accountId: "default",
|
|
146
|
+
config: {
|
|
147
|
+
dmPolicy: "open",
|
|
148
|
+
allowFrom: ["*"],
|
|
149
|
+
},
|
|
150
|
+
} as any,
|
|
151
|
+
config: {
|
|
152
|
+
commands: {},
|
|
153
|
+
session: {
|
|
154
|
+
store: {},
|
|
155
|
+
},
|
|
156
|
+
} as any,
|
|
157
|
+
runtime: {
|
|
158
|
+
error: vi.fn(),
|
|
159
|
+
log: vi.fn(),
|
|
160
|
+
} as any,
|
|
161
|
+
sendReply: sendReplyMock,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(sendReplyMock).toHaveBeenCalledWith("CombinatorAgent", "reply body");
|
|
165
|
+
expect(sendMessageHubMock).not.toHaveBeenCalled();
|
|
166
|
+
expect(emitOutboundHistoryMock).toHaveBeenCalledWith(
|
|
167
|
+
expect.objectContaining({
|
|
168
|
+
conversationKey: "hub:CombinatorAgent",
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
convertMarkdownTablesMock,
|
|
5
|
+
fetchWithSsrFGuardMock,
|
|
6
|
+
getHubRuntimeMock,
|
|
7
|
+
loadConfigMock,
|
|
8
|
+
recordActivityMock,
|
|
9
|
+
resolveHubAccountMock,
|
|
10
|
+
resolveMarkdownTableModeMock,
|
|
11
|
+
releaseMock,
|
|
12
|
+
} = vi.hoisted(() => ({
|
|
13
|
+
convertMarkdownTablesMock: vi.fn(),
|
|
14
|
+
fetchWithSsrFGuardMock: vi.fn(),
|
|
15
|
+
getHubRuntimeMock: vi.fn(),
|
|
16
|
+
loadConfigMock: vi.fn(),
|
|
17
|
+
recordActivityMock: vi.fn(),
|
|
18
|
+
resolveHubAccountMock: vi.fn(),
|
|
19
|
+
resolveMarkdownTableModeMock: vi.fn(),
|
|
20
|
+
releaseMock: vi.fn(),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
vi.mock("openclaw/plugin-sdk", () => ({
|
|
24
|
+
fetchWithSsrFGuard: fetchWithSsrFGuardMock,
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
vi.mock("./accounts.js", () => ({
|
|
28
|
+
resolveHubAccount: resolveHubAccountMock,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
vi.mock("./runtime.js", () => ({
|
|
32
|
+
getHubRuntime: getHubRuntimeMock,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
import { sendMessageHub } from "./send.js";
|
|
36
|
+
|
|
37
|
+
describe("sendMessageHub", () => {
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
convertMarkdownTablesMock.mockReset();
|
|
40
|
+
fetchWithSsrFGuardMock.mockReset();
|
|
41
|
+
getHubRuntimeMock.mockReset();
|
|
42
|
+
loadConfigMock.mockReset();
|
|
43
|
+
recordActivityMock.mockReset();
|
|
44
|
+
releaseMock.mockReset();
|
|
45
|
+
resolveHubAccountMock.mockReset();
|
|
46
|
+
resolveMarkdownTableModeMock.mockReset();
|
|
47
|
+
|
|
48
|
+
releaseMock.mockResolvedValue(undefined);
|
|
49
|
+
loadConfigMock.mockReturnValue({});
|
|
50
|
+
resolveMarkdownTableModeMock.mockReturnValue("off");
|
|
51
|
+
convertMarkdownTablesMock.mockImplementation((text: string) => text);
|
|
52
|
+
recordActivityMock.mockReturnValue(undefined);
|
|
53
|
+
resolveHubAccountMock.mockReturnValue({
|
|
54
|
+
configured: true,
|
|
55
|
+
accountId: "default",
|
|
56
|
+
url: "https://hub.example.test",
|
|
57
|
+
agentId: "sender",
|
|
58
|
+
secret: "shared-secret",
|
|
59
|
+
});
|
|
60
|
+
getHubRuntimeMock.mockReturnValue({
|
|
61
|
+
config: { loadConfig: loadConfigMock },
|
|
62
|
+
channel: {
|
|
63
|
+
text: {
|
|
64
|
+
resolveMarkdownTableMode: resolveMarkdownTableModeMock,
|
|
65
|
+
convertMarkdownTables: convertMarkdownTablesMock,
|
|
66
|
+
},
|
|
67
|
+
activity: {
|
|
68
|
+
record: recordActivityMock,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
fetchWithSsrFGuardMock.mockResolvedValue({
|
|
73
|
+
response: {
|
|
74
|
+
ok: true,
|
|
75
|
+
},
|
|
76
|
+
release: releaseMock,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("posts to the plain agent id when given a hub:-prefixed recipient", async () => {
|
|
81
|
+
const result = await sendMessageHub(" hub:Brain ", "hello");
|
|
82
|
+
|
|
83
|
+
expect(fetchWithSsrFGuardMock).toHaveBeenCalledWith(
|
|
84
|
+
expect.objectContaining({
|
|
85
|
+
url: "https://hub.example.test/agents/Brain/message",
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
expect(result.target).toBe("Brain");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("rejects empty recipients after normalization", async () => {
|
|
92
|
+
await expect(sendMessageHub("hub:", "hello")).rejects.toThrow(
|
|
93
|
+
"Hub send target must be non-empty",
|
|
94
|
+
);
|
|
95
|
+
expect(fetchWithSsrFGuardMock).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk";
|
|
2
2
|
import { resolveHubAccount } from "./accounts.js";
|
|
3
3
|
import { getHubRuntime } from "./runtime.js";
|
|
4
|
+
import { normalizeHubTarget } from "./targets.js";
|
|
4
5
|
import type { CoreConfig } from "./types.js";
|
|
5
6
|
|
|
6
7
|
type SendHubOptions = {
|
|
@@ -27,7 +28,7 @@ export async function sendMessageHub(
|
|
|
27
28
|
);
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
const target = to
|
|
31
|
+
const target = normalizeHubTarget(to);
|
|
31
32
|
if (!target) {
|
|
32
33
|
throw new Error("Hub send target must be non-empty");
|
|
33
34
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { normalizeHubAllowEntry, normalizeHubTarget } from "./targets.js";
|
|
3
|
+
|
|
4
|
+
describe("Hub target normalization", () => {
|
|
5
|
+
it("strips a leading hub: prefix and trims whitespace", () => {
|
|
6
|
+
expect(normalizeHubTarget(" hub:Brain ")).toBe("Brain");
|
|
7
|
+
expect(normalizeHubTarget("HUB:CombinatorAgent")).toBe("CombinatorAgent");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("keeps plain agent ids intact", () => {
|
|
11
|
+
expect(normalizeHubTarget("brain")).toBe("brain");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("normalizes allowlist entries to lowercase agent ids", () => {
|
|
15
|
+
expect(normalizeHubAllowEntry(" hub:Brain ")).toBe("brain");
|
|
16
|
+
expect(normalizeHubAllowEntry("CombinatorAgent")).toBe("combinatoragent");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("preserves wildcard allowlist entries", () => {
|
|
20
|
+
expect(normalizeHubAllowEntry("*")).toBe("*");
|
|
21
|
+
expect(normalizeHubAllowEntry("hub:*")).toBe("*");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("returns undefined for empty values", () => {
|
|
25
|
+
expect(normalizeHubTarget("")).toBeUndefined();
|
|
26
|
+
expect(normalizeHubTarget("hub:")).toBeUndefined();
|
|
27
|
+
expect(normalizeHubAllowEntry(" ")).toBeUndefined();
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const HUB_PREFIX_RE = /^hub:/i;
|
|
2
|
+
|
|
3
|
+
export function normalizeHubTarget(raw: string | null | undefined): string | undefined {
|
|
4
|
+
const trimmed = String(raw ?? "").trim();
|
|
5
|
+
if (!trimmed) {
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
const normalized = trimmed.replace(HUB_PREFIX_RE, "").trim();
|
|
9
|
+
return normalized || undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function normalizeHubAllowEntry(raw: string | null | undefined): string | undefined {
|
|
13
|
+
const normalized = normalizeHubTarget(raw);
|
|
14
|
+
if (!normalized) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
if (normalized === "*") {
|
|
18
|
+
return normalized;
|
|
19
|
+
}
|
|
20
|
+
return normalized.toLowerCase();
|
|
21
|
+
}
|
package/package.json
CHANGED