@newbase-clawchat/openclaw-clawchat 2026.5.12-2 → 2026.5.12-21
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/README.md +39 -17
- package/dist/index.js +3 -1
- package/dist/src/api-client.js +71 -12
- package/dist/src/api-types.test-d.js +10 -0
- package/dist/src/channel.js +5 -5
- package/dist/src/channel.setup.js +4 -17
- package/dist/src/clawchat-memory.js +290 -0
- package/dist/src/clawchat-metadata.js +235 -0
- package/dist/src/client.js +31 -93
- package/dist/src/commands.js +3 -3
- package/dist/src/config.js +58 -3
- package/dist/src/group-message-coalescer.js +107 -0
- package/dist/src/inbound.js +24 -28
- package/dist/src/login.runtime.js +82 -19
- package/dist/src/media-runtime.js +2 -3
- package/dist/src/message-mapper.js +1 -1
- package/dist/src/mock-transport.js +31 -0
- package/dist/src/outbound.js +281 -56
- package/dist/src/plugin-prompts.js +76 -0
- package/dist/src/profile-prompt.js +150 -0
- package/dist/src/profile-sync.js +169 -0
- package/dist/src/prompt-injection.js +25 -0
- package/dist/src/protocol-types.js +63 -0
- package/dist/src/protocol-types.typecheck.js +1 -0
- package/dist/src/protocol.js +2 -2
- package/dist/src/reply-dispatcher.js +143 -40
- package/dist/src/runtime.js +813 -109
- package/dist/src/storage.js +636 -0
- package/dist/src/tools-schema.js +70 -10
- package/dist/src/tools.js +600 -112
- package/dist/src/ws-alignment.js +8 -0
- package/dist/src/ws-client.js +588 -0
- package/index.ts +6 -1
- package/openclaw.plugin.json +44 -4
- package/package.json +4 -3
- package/prompts/platform.md +7 -0
- package/skills/clawchat/SKILL.md +90 -0
- package/src/api-client.test.ts +360 -15
- package/src/api-client.ts +127 -25
- package/src/api-types.test-d.ts +12 -0
- package/src/api-types.ts +71 -4
- package/src/buffered-stream.test.ts +1 -1
- package/src/buffered-stream.ts +1 -1
- package/src/channel.outbound.test.ts +270 -60
- package/src/channel.setup.ts +9 -18
- package/src/channel.test.ts +33 -25
- package/src/channel.ts +5 -7
- package/src/clawchat-memory.test.ts +372 -0
- package/src/clawchat-memory.ts +363 -0
- package/src/clawchat-metadata.test.ts +350 -0
- package/src/clawchat-metadata.ts +352 -0
- package/src/client.test.ts +57 -48
- package/src/client.ts +37 -129
- package/src/commands.test.ts +2 -2
- package/src/commands.ts +3 -3
- package/src/config.test.ts +169 -4
- package/src/config.ts +86 -6
- package/src/group-message-coalescer.test.ts +223 -0
- package/src/group-message-coalescer.ts +154 -0
- package/src/inbound.test.ts +106 -19
- package/src/inbound.ts +31 -35
- package/src/login.runtime.test.ts +294 -11
- package/src/login.runtime.ts +90 -21
- package/src/manifest.test.ts +86 -14
- package/src/media-runtime.test.ts +31 -2
- package/src/media-runtime.ts +7 -10
- package/src/message-mapper.test.ts +2 -2
- package/src/message-mapper.ts +2 -2
- package/src/mock-transport.test.ts +35 -0
- package/src/mock-transport.ts +38 -0
- package/src/outbound.test.ts +811 -95
- package/src/outbound.ts +332 -65
- package/src/plugin-entry.test.ts +3 -1
- package/src/plugin-prompts.test.ts +78 -0
- package/src/plugin-prompts.ts +92 -0
- package/src/profile-prompt.test.ts +435 -0
- package/src/profile-prompt.ts +208 -0
- package/src/profile-sync.test.ts +611 -0
- package/src/profile-sync.ts +268 -0
- package/src/prompt-injection.test.ts +39 -0
- package/src/prompt-injection.ts +45 -0
- package/src/protocol-types.test.ts +69 -0
- package/src/protocol-types.ts +296 -0
- package/src/protocol-types.typecheck.ts +89 -0
- package/src/protocol.ts +2 -2
- package/src/reply-dispatcher.test.ts +720 -135
- package/src/reply-dispatcher.ts +174 -42
- package/src/runtime.test.ts +3884 -337
- package/src/runtime.ts +956 -128
- package/src/storage.test.ts +692 -0
- package/src/storage.ts +989 -0
- package/src/streaming.test.ts +1 -1
- package/src/streaming.ts +1 -1
- package/src/tools-schema.ts +115 -13
- package/src/tools.test.ts +501 -10
- package/src/tools.ts +739 -133
- package/src/ws-alignment.ts +9 -0
- package/src/ws-client.test.ts +1218 -0
- package/src/ws-client.ts +662 -0
|
@@ -30,7 +30,7 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
30
30
|
it("works with no prior setup (baseUrl / websocketUrl default built-in)", async () => {
|
|
31
31
|
const cfg = buildCfg({}); // empty section — resolver provides defaults
|
|
32
32
|
const agentsConnect = vi.fn().mockResolvedValue({
|
|
33
|
-
agent: { user_id: "u" },
|
|
33
|
+
agent: { user_id: "u", owner_id: "owner-u" },
|
|
34
34
|
access_token: "t",
|
|
35
35
|
refresh_token: "r",
|
|
36
36
|
});
|
|
@@ -106,6 +106,9 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
106
106
|
expect(section.token).toBe("access-tok");
|
|
107
107
|
expect(section.refreshToken).toBe("refresh-tok");
|
|
108
108
|
expect(section.userId).toBe("agent-123");
|
|
109
|
+
expect(section.ownerUserId).toBe("owner-1");
|
|
110
|
+
expect(section.groupMode).toBe("all");
|
|
111
|
+
expect(section.groupCommandMode).toBe("owner");
|
|
109
112
|
// Existing baseUrl and websocketUrl are preserved — agents/connect
|
|
110
113
|
// does not return them.
|
|
111
114
|
expect(section.baseUrl).toBe("https://api.example.com");
|
|
@@ -113,13 +116,54 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
113
116
|
expect(log).toHaveBeenCalledWith(expect.stringContaining("login succeeded"));
|
|
114
117
|
});
|
|
115
118
|
|
|
119
|
+
it("persists latest activation bootstrap metadata to the sqlite store after config write", async () => {
|
|
120
|
+
const cfg = buildCfg({
|
|
121
|
+
baseUrl: "https://api.example.com",
|
|
122
|
+
websocketUrl: "wss://ws.example.com/v2/client",
|
|
123
|
+
});
|
|
124
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
125
|
+
agent: { user_id: "agent-123", owner_id: "owner-123", nickname: "Bot" },
|
|
126
|
+
access_token: "access-plain",
|
|
127
|
+
refresh_token: "refresh-plain",
|
|
128
|
+
conversation: { id: "conv-activation" },
|
|
129
|
+
});
|
|
130
|
+
const persistConfig = vi.fn();
|
|
131
|
+
const upsertActivation = vi.fn();
|
|
132
|
+
|
|
133
|
+
await runOpenclawClawlingLogin({
|
|
134
|
+
cfg,
|
|
135
|
+
runtime: { log: vi.fn() },
|
|
136
|
+
readInviteCode: async () => "INV-ABC",
|
|
137
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
138
|
+
persistConfig,
|
|
139
|
+
store: { upsertActivation },
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
expect(upsertActivation).toHaveBeenCalledTimes(1);
|
|
143
|
+
expect(persistConfig.mock.invocationCallOrder[0]).toBeLessThan(
|
|
144
|
+
upsertActivation.mock.invocationCallOrder[0],
|
|
145
|
+
);
|
|
146
|
+
expect(upsertActivation).toHaveBeenCalledWith({
|
|
147
|
+
platform: "openclaw",
|
|
148
|
+
accountId: "default",
|
|
149
|
+
userId: "agent-123",
|
|
150
|
+
ownerUserId: "owner-123",
|
|
151
|
+
conversationId: "conv-activation",
|
|
152
|
+
loginMethod: "login",
|
|
153
|
+
});
|
|
154
|
+
expect(upsertActivation.mock.calls[0]![0]).not.toHaveProperty("accessToken");
|
|
155
|
+
expect(upsertActivation.mock.calls[0]![0]).not.toHaveProperty("refreshToken");
|
|
156
|
+
expect(upsertActivation.mock.calls[0]![0]).not.toHaveProperty("baseUrl");
|
|
157
|
+
expect(upsertActivation.mock.calls[0]![0]).not.toHaveProperty("websocketUrl");
|
|
158
|
+
});
|
|
159
|
+
|
|
116
160
|
it("uses the runtime config mutator with restart intent after credential writes", async () => {
|
|
117
161
|
const cfg = buildCfg({
|
|
118
162
|
baseUrl: "https://api.example.com",
|
|
119
163
|
websocketUrl: "wss://ws.example.com/v2/client",
|
|
120
164
|
});
|
|
121
165
|
const agentsConnect = vi.fn().mockResolvedValue({
|
|
122
|
-
agent: { user_id: "agent-123", nickname: "Bot" },
|
|
166
|
+
agent: { user_id: "agent-123", owner_id: "owner-123", nickname: "Bot" },
|
|
123
167
|
access_token: "access-tok",
|
|
124
168
|
refresh_token: "refresh-tok",
|
|
125
169
|
});
|
|
@@ -151,6 +195,9 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
151
195
|
expect(section.token).toBe("access-tok");
|
|
152
196
|
expect(section.refreshToken).toBe("refresh-tok");
|
|
153
197
|
expect(section.userId).toBe("agent-123");
|
|
198
|
+
expect(section.ownerUserId).toBe("owner-123");
|
|
199
|
+
expect(section.groupMode).toBe("all");
|
|
200
|
+
expect(section.groupCommandMode).toBe("owner");
|
|
154
201
|
const plugins = (mutatedCfg! as OpenClawConfig & {
|
|
155
202
|
plugins: { allow?: string[]; entries: Record<string, Record<string, unknown>> };
|
|
156
203
|
}).plugins;
|
|
@@ -178,7 +225,7 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
178
225
|
},
|
|
179
226
|
} as unknown as OpenClawConfig;
|
|
180
227
|
const agentsConnect = vi.fn().mockResolvedValue({
|
|
181
|
-
agent: { user_id: "agent-123" },
|
|
228
|
+
agent: { user_id: "agent-123", owner_id: "owner-123" },
|
|
182
229
|
access_token: "access-tok",
|
|
183
230
|
refresh_token: "refresh-tok",
|
|
184
231
|
});
|
|
@@ -202,6 +249,7 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
202
249
|
websocketUrl: "wss://ws.example.com/v2/client",
|
|
203
250
|
token: "access-tok",
|
|
204
251
|
userId: "agent-123",
|
|
252
|
+
ownerUserId: "owner-123",
|
|
205
253
|
refreshToken: "refresh-tok",
|
|
206
254
|
});
|
|
207
255
|
});
|
|
@@ -212,7 +260,7 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
212
260
|
baseUrl: "https://api.example.com",
|
|
213
261
|
});
|
|
214
262
|
const agentsConnect = vi.fn().mockResolvedValue({
|
|
215
|
-
agent: { user_id: "agent-123" },
|
|
263
|
+
agent: { user_id: "agent-123", owner_id: "owner-123" },
|
|
216
264
|
access_token: "access-tok",
|
|
217
265
|
refresh_token: "refresh-tok",
|
|
218
266
|
});
|
|
@@ -231,6 +279,65 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
231
279
|
expect(section.enabled).toBe(true);
|
|
232
280
|
expect(section.token).toBe("access-tok");
|
|
233
281
|
expect(section.userId).toBe("agent-123");
|
|
282
|
+
expect(section.ownerUserId).toBe("owner-123");
|
|
283
|
+
expect(section.groupMode).toBe("all");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("does not overwrite an explicit mention groupMode during login", async () => {
|
|
287
|
+
const cfg = buildCfg({
|
|
288
|
+
baseUrl: "https://api.example.com",
|
|
289
|
+
groupMode: "mention",
|
|
290
|
+
groupCommandMode: "off",
|
|
291
|
+
});
|
|
292
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
293
|
+
agent: { user_id: "agent-123", owner_id: "owner-123" },
|
|
294
|
+
access_token: "access-tok",
|
|
295
|
+
refresh_token: "refresh-tok",
|
|
296
|
+
});
|
|
297
|
+
const persistConfig = vi.fn();
|
|
298
|
+
|
|
299
|
+
await runOpenclawClawlingLogin({
|
|
300
|
+
cfg,
|
|
301
|
+
runtime: { log: vi.fn() },
|
|
302
|
+
readInviteCode: async () => "INV-ABC",
|
|
303
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
304
|
+
persistConfig,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig;
|
|
308
|
+
const section = (savedCfg.channels as Record<string, Record<string, unknown>>)[CHANNEL_ID]!;
|
|
309
|
+
expect(section.groupMode).toBe("mention");
|
|
310
|
+
expect(section.groupCommandMode).toBe("off");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it("does not overwrite per-group groupMode settings during login", async () => {
|
|
314
|
+
const cfg = buildCfg({
|
|
315
|
+
baseUrl: "https://api.example.com",
|
|
316
|
+
groupMode: "all",
|
|
317
|
+
groups: {
|
|
318
|
+
"group-quiet": { groupMode: "mention" },
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
322
|
+
agent: { user_id: "agent-123", owner_id: "owner-123" },
|
|
323
|
+
access_token: "access-tok",
|
|
324
|
+
refresh_token: "refresh-tok",
|
|
325
|
+
});
|
|
326
|
+
const persistConfig = vi.fn();
|
|
327
|
+
|
|
328
|
+
await runOpenclawClawlingLogin({
|
|
329
|
+
cfg,
|
|
330
|
+
runtime: { log: vi.fn() },
|
|
331
|
+
readInviteCode: async () => "INV-ABC",
|
|
332
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
333
|
+
persistConfig,
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig;
|
|
337
|
+
const section = (savedCfg.channels as Record<string, Record<string, unknown>>)[CHANNEL_ID]!;
|
|
338
|
+
expect(section.groups).toEqual({
|
|
339
|
+
"group-quiet": { groupMode: "mention" },
|
|
340
|
+
});
|
|
234
341
|
});
|
|
235
342
|
|
|
236
343
|
it("allows openclaw-clawchat plugin tools after successful login without replacing policy", async () => {
|
|
@@ -244,7 +351,7 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
244
351
|
},
|
|
245
352
|
} as unknown as OpenClawConfig;
|
|
246
353
|
const agentsConnect = vi.fn().mockResolvedValue({
|
|
247
|
-
agent: { user_id: "agent-123" },
|
|
354
|
+
agent: { user_id: "agent-123", owner_id: "owner-123" },
|
|
248
355
|
access_token: "access-tok",
|
|
249
356
|
refresh_token: "refresh-tok",
|
|
250
357
|
});
|
|
@@ -282,7 +389,7 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
282
389
|
},
|
|
283
390
|
} as unknown as OpenClawConfig;
|
|
284
391
|
const agentsConnect = vi.fn().mockResolvedValue({
|
|
285
|
-
agent: { user_id: "agent-123" },
|
|
392
|
+
agent: { user_id: "agent-123", owner_id: "owner-123" },
|
|
286
393
|
access_token: "access-tok",
|
|
287
394
|
refresh_token: "refresh-tok",
|
|
288
395
|
});
|
|
@@ -326,7 +433,7 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
326
433
|
it("rejects responses that omit required fields", async () => {
|
|
327
434
|
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
328
435
|
const agentsConnect = vi.fn().mockResolvedValue({
|
|
329
|
-
agent: { user_id: "" },
|
|
436
|
+
agent: { user_id: "", owner_id: "owner-123" },
|
|
330
437
|
access_token: "t",
|
|
331
438
|
refresh_token: "r",
|
|
332
439
|
});
|
|
@@ -341,13 +448,185 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
341
448
|
).rejects.toThrow(/missing required fields/);
|
|
342
449
|
});
|
|
343
450
|
|
|
451
|
+
it("rejects whitespace-only required agents/connect response fields", async () => {
|
|
452
|
+
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
453
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
454
|
+
agent: { user_id: " ", owner_id: "owner-123" },
|
|
455
|
+
access_token: "t",
|
|
456
|
+
refresh_token: "r",
|
|
457
|
+
});
|
|
458
|
+
await expect(
|
|
459
|
+
runOpenclawClawlingLogin({
|
|
460
|
+
cfg,
|
|
461
|
+
runtime: { log: vi.fn() },
|
|
462
|
+
readInviteCode: async () => "ok",
|
|
463
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
464
|
+
persistConfig: vi.fn(),
|
|
465
|
+
}),
|
|
466
|
+
).rejects.toThrow(/missing required fields/);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("rejects whitespace-only agents/connect access tokens", async () => {
|
|
470
|
+
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
471
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
472
|
+
agent: { user_id: "agent-123", owner_id: "owner-123" },
|
|
473
|
+
access_token: " ",
|
|
474
|
+
refresh_token: "r",
|
|
475
|
+
});
|
|
476
|
+
await expect(
|
|
477
|
+
runOpenclawClawlingLogin({
|
|
478
|
+
cfg,
|
|
479
|
+
runtime: { log: vi.fn() },
|
|
480
|
+
readInviteCode: async () => "ok",
|
|
481
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
482
|
+
persistConfig: vi.fn(),
|
|
483
|
+
}),
|
|
484
|
+
).rejects.toThrow(/access_token/);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it("rejects agents/connect responses that omit agent owner ids", async () => {
|
|
488
|
+
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
489
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
490
|
+
agent: { user_id: "agent-123" },
|
|
491
|
+
access_token: "t",
|
|
492
|
+
refresh_token: "r",
|
|
493
|
+
});
|
|
494
|
+
await expect(
|
|
495
|
+
runOpenclawClawlingLogin({
|
|
496
|
+
cfg,
|
|
497
|
+
runtime: { log: vi.fn() },
|
|
498
|
+
readInviteCode: async () => "ok",
|
|
499
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
500
|
+
persistConfig: vi.fn(),
|
|
501
|
+
}),
|
|
502
|
+
).rejects.toThrow(/agent\.owner_id/);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it("rejects empty returned agent owner ids", async () => {
|
|
506
|
+
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
507
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
508
|
+
agent: { user_id: "agent-123", owner_id: "" },
|
|
509
|
+
access_token: "t",
|
|
510
|
+
refresh_token: "r",
|
|
511
|
+
});
|
|
512
|
+
await expect(
|
|
513
|
+
runOpenclawClawlingLogin({
|
|
514
|
+
cfg,
|
|
515
|
+
runtime: { log: vi.fn() },
|
|
516
|
+
readInviteCode: async () => "ok",
|
|
517
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
518
|
+
persistConfig: vi.fn(),
|
|
519
|
+
}),
|
|
520
|
+
).rejects.toThrow(/agent\.owner_id/);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it("rejects whitespace-only returned agent owner ids", async () => {
|
|
524
|
+
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
525
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
526
|
+
agent: { user_id: "agent-123", owner_id: " " },
|
|
527
|
+
access_token: "t",
|
|
528
|
+
refresh_token: "r",
|
|
529
|
+
});
|
|
530
|
+
await expect(
|
|
531
|
+
runOpenclawClawlingLogin({
|
|
532
|
+
cfg,
|
|
533
|
+
runtime: { log: vi.fn() },
|
|
534
|
+
readInviteCode: async () => "ok",
|
|
535
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
536
|
+
persistConfig: vi.fn(),
|
|
537
|
+
}),
|
|
538
|
+
).rejects.toThrow(/missing required fields/);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it("rejects whitespace-only activation conversation ids when present", async () => {
|
|
542
|
+
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
543
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
544
|
+
agent: { user_id: "agent-123", owner_id: "owner-123" },
|
|
545
|
+
access_token: "t",
|
|
546
|
+
refresh_token: "r",
|
|
547
|
+
conversation: { id: " " },
|
|
548
|
+
});
|
|
549
|
+
await expect(
|
|
550
|
+
runOpenclawClawlingLogin({
|
|
551
|
+
cfg,
|
|
552
|
+
runtime: { log: vi.fn() },
|
|
553
|
+
readInviteCode: async () => "ok",
|
|
554
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
555
|
+
persistConfig: vi.fn(),
|
|
556
|
+
}),
|
|
557
|
+
).rejects.toThrow(/missing required fields/);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it("trims persisted agents/connect credentials and activation conversation id", async () => {
|
|
561
|
+
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
562
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
563
|
+
agent: { user_id: " agent-123 ", owner_id: " owner-123 ", nickname: "Bot" },
|
|
564
|
+
access_token: " access-tok ",
|
|
565
|
+
refresh_token: " refresh-tok ",
|
|
566
|
+
conversation: { id: " conv-activation " },
|
|
567
|
+
});
|
|
568
|
+
const persistConfig = vi.fn();
|
|
569
|
+
const upsertActivation = vi.fn();
|
|
570
|
+
|
|
571
|
+
await runOpenclawClawlingLogin({
|
|
572
|
+
cfg,
|
|
573
|
+
runtime: { log: vi.fn() },
|
|
574
|
+
readInviteCode: async () => "ok",
|
|
575
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
576
|
+
persistConfig,
|
|
577
|
+
store: { upsertActivation },
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig;
|
|
581
|
+
const section = (savedCfg.channels as Record<string, Record<string, unknown>>)[CHANNEL_ID]!;
|
|
582
|
+
expect(section.token).toBe("access-tok");
|
|
583
|
+
expect(section.refreshToken).toBe("refresh-tok");
|
|
584
|
+
expect(section.userId).toBe("agent-123");
|
|
585
|
+
expect(section.ownerUserId).toBe("owner-123");
|
|
586
|
+
expect(upsertActivation).toHaveBeenCalledWith({
|
|
587
|
+
platform: "openclaw",
|
|
588
|
+
accountId: "default",
|
|
589
|
+
userId: "agent-123",
|
|
590
|
+
ownerUserId: "owner-123",
|
|
591
|
+
conversationId: "conv-activation",
|
|
592
|
+
loginMethod: "login",
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it("does not preserve a stale refresh token when agents/connect returns a blank refresh token", async () => {
|
|
597
|
+
const cfg = buildCfg({
|
|
598
|
+
baseUrl: "https://api.example.com",
|
|
599
|
+
refreshToken: "stale-refresh",
|
|
600
|
+
});
|
|
601
|
+
const agentsConnect = vi.fn().mockResolvedValue({
|
|
602
|
+
agent: { user_id: "agent-123", owner_id: "owner-123" },
|
|
603
|
+
access_token: "access-tok",
|
|
604
|
+
refresh_token: " ",
|
|
605
|
+
});
|
|
606
|
+
const persistConfig = vi.fn();
|
|
607
|
+
|
|
608
|
+
await runOpenclawClawlingLogin({
|
|
609
|
+
cfg,
|
|
610
|
+
runtime: { log: vi.fn() },
|
|
611
|
+
readInviteCode: async () => "ok",
|
|
612
|
+
apiClientFactory: () => makeApiClient({ agentsConnect }),
|
|
613
|
+
persistConfig,
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
const savedCfg = persistConfig.mock.calls[0]![0] as OpenClawConfig;
|
|
617
|
+
const section = (savedCfg.channels as Record<string, Record<string, unknown>>)[CHANNEL_ID]!;
|
|
618
|
+
expect(section.token).toBe("access-tok");
|
|
619
|
+
expect(section.userId).toBe("agent-123");
|
|
620
|
+
expect(section.refreshToken).toBeUndefined();
|
|
621
|
+
});
|
|
622
|
+
|
|
344
623
|
it("persists the config automatically after receiving the token (no further prompts)", async () => {
|
|
345
624
|
const cfg = buildCfg({
|
|
346
625
|
baseUrl: "https://api.example.com",
|
|
347
626
|
websocketUrl: "wss://ws.example.com",
|
|
348
627
|
});
|
|
349
628
|
const agentsConnect = vi.fn().mockResolvedValue({
|
|
350
|
-
agent: { user_id: "agent-9", nickname: "Nine" },
|
|
629
|
+
agent: { id: "agt-9", user_id: "agent-9", owner_id: "owner-9", nickname: "Nine" },
|
|
351
630
|
access_token: "ACC-0123456789",
|
|
352
631
|
refresh_token: "REF-xyz",
|
|
353
632
|
});
|
|
@@ -366,7 +645,9 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
366
645
|
expect(persistCalls).toHaveLength(1);
|
|
367
646
|
const section = (persistCalls[0]!.channels as Record<string, Record<string, unknown>>)[CHANNEL_ID]!;
|
|
368
647
|
expect(section.token).toBe("ACC-0123456789");
|
|
648
|
+
expect(section.agentId).toBe("agt-9");
|
|
369
649
|
expect(section.userId).toBe("agent-9");
|
|
650
|
+
expect(section.ownerUserId).toBe("owner-9");
|
|
370
651
|
expect(section.refreshToken).toBe("REF-xyz");
|
|
371
652
|
// Existing URL fields are preserved.
|
|
372
653
|
expect(section.baseUrl).toBe("https://api.example.com");
|
|
@@ -376,12 +657,13 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
376
657
|
expect(logMessages.some((m) => /Config file updated/.test(m))).toBe(true);
|
|
377
658
|
// Token in logs is redacted.
|
|
378
659
|
expect(logMessages.every((m) => !m.includes("ACC-0123456789"))).toBe(true);
|
|
660
|
+
expect(logMessages.every((m) => !m.includes("ACC-") && !m.includes("6789"))).toBe(true);
|
|
379
661
|
});
|
|
380
662
|
|
|
381
663
|
it("login logs don't leak the /agents/connect endpoint path or base URL", async () => {
|
|
382
664
|
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
383
665
|
const agentsConnect = vi.fn().mockResolvedValue({
|
|
384
|
-
agent: { user_id: "u" },
|
|
666
|
+
agent: { user_id: "u", owner_id: "owner-u" },
|
|
385
667
|
access_token: "t",
|
|
386
668
|
refresh_token: "r",
|
|
387
669
|
});
|
|
@@ -402,7 +684,7 @@ describe("runOpenclawClawlingLogin", () => {
|
|
|
402
684
|
it("trims the invite code before sending", async () => {
|
|
403
685
|
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
404
686
|
const agentsConnect = vi.fn().mockResolvedValue({
|
|
405
|
-
agent: { user_id: "u" },
|
|
687
|
+
agent: { user_id: "u", owner_id: "owner-u" },
|
|
406
688
|
access_token: "t",
|
|
407
689
|
refresh_token: "r",
|
|
408
690
|
});
|
|
@@ -425,7 +707,7 @@ describe("runOpenclawClawlingLogin (non-interactive via readInviteCode)", () =>
|
|
|
425
707
|
it("performs a full login when called with a fixed readInviteCode (programmatic path)", async () => {
|
|
426
708
|
const cfg = buildCfg({ baseUrl: "https://api.example.com" });
|
|
427
709
|
const agentsConnect = vi.fn().mockResolvedValue({
|
|
428
|
-
agent: { user_id: "agent-7", nickname: "Seven" },
|
|
710
|
+
agent: { user_id: "agent-7", owner_id: "owner-7", nickname: "Seven" },
|
|
429
711
|
access_token: "acc-non-interactive",
|
|
430
712
|
refresh_token: "ref-ni",
|
|
431
713
|
});
|
|
@@ -447,6 +729,7 @@ describe("runOpenclawClawlingLogin (non-interactive via readInviteCode)", () =>
|
|
|
447
729
|
["openclaw-clawchat"] as Record<string, unknown>;
|
|
448
730
|
expect(section.token).toBe("acc-non-interactive");
|
|
449
731
|
expect(section.userId).toBe("agent-7");
|
|
732
|
+
expect(section.ownerUserId).toBe("owner-7");
|
|
450
733
|
expect(section.refreshToken).toBe("ref-ni");
|
|
451
734
|
});
|
|
452
735
|
});
|
package/src/login.runtime.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
mergeOpenclawClawchatToolAllow,
|
|
9
9
|
resolveOpenclawClawlingAccount,
|
|
10
10
|
} from "./config.ts";
|
|
11
|
+
import { getClawChatStore, type ClawChatStore } from "./storage.ts";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Platform tag sent to `/v1/agents/connect`. Identifies the host of this
|
|
@@ -43,6 +44,10 @@ export interface LoginParams {
|
|
|
43
44
|
mutateConfigFile?: OpenclawClawchatMutateConfigFile;
|
|
44
45
|
/** Test-only config persistence override. */
|
|
45
46
|
persistConfig?: (cfg: OpenClawConfig) => Promise<void> | void;
|
|
47
|
+
/** Test/runtime override for best-effort activation persistence. */
|
|
48
|
+
store?: Pick<ClawChatStore, "upsertActivation">;
|
|
49
|
+
/** Optional database path resolved by the host runtime. */
|
|
50
|
+
dbPath?: string;
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
/**
|
|
@@ -73,14 +78,26 @@ async function promptInviteCodeFromStdin(runtime: {
|
|
|
73
78
|
function buildLoginConfig(cfg: OpenClawConfig, result: AgentConnectResult): OpenClawConfig {
|
|
74
79
|
const channels = (cfg.channels ?? {}) as Record<string, unknown>;
|
|
75
80
|
const existing = (channels[CHANNEL_ID] ?? {}) as Record<string, unknown>;
|
|
81
|
+
const groupMode = existing.groupMode === "mention" || existing.groupMode === "all"
|
|
82
|
+
? existing.groupMode
|
|
83
|
+
: "all";
|
|
84
|
+
const groupCommandMode = existing.groupCommandMode === "all" || existing.groupCommandMode === "off"
|
|
85
|
+
? existing.groupCommandMode
|
|
86
|
+
: "owner";
|
|
76
87
|
const nextSection: Record<string, unknown> = {
|
|
77
88
|
...existing,
|
|
78
89
|
enabled: true,
|
|
90
|
+
groupMode,
|
|
91
|
+
groupCommandMode,
|
|
79
92
|
token: result.access_token,
|
|
93
|
+
...(result.agent.id ? { agentId: result.agent.id } : {}),
|
|
80
94
|
userId: result.agent.user_id,
|
|
95
|
+
ownerUserId: result.agent.owner_id,
|
|
81
96
|
};
|
|
82
97
|
if (result.refresh_token) {
|
|
83
98
|
nextSection.refreshToken = result.refresh_token;
|
|
99
|
+
} else {
|
|
100
|
+
delete nextSection.refreshToken;
|
|
84
101
|
}
|
|
85
102
|
return mergeOpenclawClawchatRuntimePluginActivation(
|
|
86
103
|
mergeOpenclawClawchatToolAllow({
|
|
@@ -96,7 +113,7 @@ async function persistLoginConfig(
|
|
|
96
113
|
): Promise<void> {
|
|
97
114
|
if (params.mutateConfigFile) {
|
|
98
115
|
params.runtime.log(
|
|
99
|
-
`Persisting ClawChat credentials and plugin activation for userId=${result.agent.user_id} with Gateway restart intent.`,
|
|
116
|
+
`Persisting ClawChat credentials and plugin activation for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id} with Gateway restart intent.`,
|
|
100
117
|
);
|
|
101
118
|
await params.mutateConfigFile({
|
|
102
119
|
afterWrite: {
|
|
@@ -108,18 +125,18 @@ async function persistLoginConfig(
|
|
|
108
125
|
},
|
|
109
126
|
});
|
|
110
127
|
params.runtime.log(
|
|
111
|
-
`ClawChat credentials and plugin activation persisted for userId=${result.agent.user_id}.`,
|
|
128
|
+
`ClawChat credentials and plugin activation persisted for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id}.`,
|
|
112
129
|
);
|
|
113
130
|
return;
|
|
114
131
|
}
|
|
115
132
|
|
|
116
133
|
if (params.persistConfig) {
|
|
117
134
|
params.runtime.log(
|
|
118
|
-
`Persisting ClawChat credentials and plugin activation for userId=${result.agent.user_id}.`,
|
|
135
|
+
`Persisting ClawChat credentials and plugin activation for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id}.`,
|
|
119
136
|
);
|
|
120
137
|
await params.persistConfig(buildLoginConfig(params.cfg, result));
|
|
121
138
|
params.runtime.log(
|
|
122
|
-
`ClawChat credentials and plugin activation persisted for userId=${result.agent.user_id}.`,
|
|
139
|
+
`ClawChat credentials and plugin activation persisted for userId=${result.agent.user_id} ownerUserId=${result.agent.owner_id}.`,
|
|
123
140
|
);
|
|
124
141
|
return;
|
|
125
142
|
}
|
|
@@ -127,8 +144,26 @@ async function persistLoginConfig(
|
|
|
127
144
|
throw new Error("openclaw-clawchat: mutateConfigFile is required to persist login credentials");
|
|
128
145
|
}
|
|
129
146
|
|
|
147
|
+
function requireConnectString(value: unknown, fieldName: string): string {
|
|
148
|
+
if (typeof value !== "string") {
|
|
149
|
+
throw new Error(`agents/connect response missing required fields (${fieldName})`);
|
|
150
|
+
}
|
|
151
|
+
const trimmed = value.trim();
|
|
152
|
+
if (!trimmed) {
|
|
153
|
+
throw new Error(`agents/connect response missing required fields (${fieldName})`);
|
|
154
|
+
}
|
|
155
|
+
return trimmed;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function readOptionalConnectString(value: unknown, fieldName: string): string | undefined {
|
|
159
|
+
if (value == null) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
return requireConnectString(value, fieldName);
|
|
163
|
+
}
|
|
164
|
+
|
|
130
165
|
/**
|
|
131
|
-
* Run the invite-code credential exchange used by `/clawchat-
|
|
166
|
+
* Run the invite-code credential exchange used by `/clawchat-activate`,
|
|
132
167
|
* `openclaw channels add --channel openclaw-clawchat --token <invite-code>`,
|
|
133
168
|
* and `openclaw channels login --channel openclaw-clawchat`:
|
|
134
169
|
* 1. Read the existing channel section; require `baseUrl` to be set so we
|
|
@@ -178,29 +213,63 @@ export async function runOpenclawClawlingLogin(params: LoginParams): Promise<voi
|
|
|
178
213
|
throw err;
|
|
179
214
|
}
|
|
180
215
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
216
|
+
const accessToken = requireConnectString(result?.access_token, "access_token");
|
|
217
|
+
const agentUserId = requireConnectString(result?.agent?.user_id, "agent.user_id");
|
|
218
|
+
const ownerUserId = requireConnectString(result?.agent?.owner_id, "agent.owner_id");
|
|
219
|
+
const agentId = readOptionalConnectString(result?.agent?.id, "agent.id");
|
|
220
|
+
|
|
221
|
+
let conversationId: string | null = null;
|
|
222
|
+
if (result?.conversation != null) {
|
|
223
|
+
conversationId = requireConnectString(result.conversation.id, "conversation.id");
|
|
185
224
|
}
|
|
186
225
|
|
|
187
|
-
const
|
|
226
|
+
const normalizedResult: AgentConnectResult = {
|
|
227
|
+
...result,
|
|
228
|
+
access_token: accessToken,
|
|
229
|
+
refresh_token: typeof result?.refresh_token === "string" ? result.refresh_token.trim() : "",
|
|
230
|
+
agent: {
|
|
231
|
+
...result.agent,
|
|
232
|
+
...(agentId ? { id: agentId } : {}),
|
|
233
|
+
owner_id: ownerUserId,
|
|
234
|
+
user_id: agentUserId,
|
|
235
|
+
},
|
|
236
|
+
...(conversationId
|
|
237
|
+
? {
|
|
238
|
+
conversation: {
|
|
239
|
+
...result.conversation,
|
|
240
|
+
id: conversationId,
|
|
241
|
+
},
|
|
242
|
+
}
|
|
243
|
+
: {}),
|
|
244
|
+
};
|
|
245
|
+
|
|
188
246
|
runtime.log(
|
|
189
|
-
`Updating config: channels.${CHANNEL_ID}.token=${
|
|
190
|
-
|
|
247
|
+
`Updating config: channels.${CHANNEL_ID}.token=[REDACTED] agentId=${normalizedResult.agent.id || "-"} userId=${normalizedResult.agent.user_id} ownerUserId=${normalizedResult.agent.owner_id}${
|
|
248
|
+
normalizedResult.refresh_token ? " refreshToken=[REDACTED]" : ""
|
|
191
249
|
} plugins.entries.${CHANNEL_ID}.enabled=true plugins.allow+=${CHANNEL_ID} …`,
|
|
192
250
|
);
|
|
193
|
-
await persistLoginConfig(params,
|
|
251
|
+
await persistLoginConfig(params, normalizedResult);
|
|
252
|
+
try {
|
|
253
|
+
const store =
|
|
254
|
+
params.store ??
|
|
255
|
+
getClawChatStore({
|
|
256
|
+
...(params.dbPath ? { dbPath: params.dbPath } : {}),
|
|
257
|
+
log: { error: runtime.log },
|
|
258
|
+
});
|
|
259
|
+
store.upsertActivation({
|
|
260
|
+
platform: "openclaw",
|
|
261
|
+
accountId: account.accountId,
|
|
262
|
+
userId: normalizedResult.agent.user_id,
|
|
263
|
+
ownerUserId: normalizedResult.agent.owner_id,
|
|
264
|
+
conversationId: normalizedResult.conversation?.id ?? null,
|
|
265
|
+
loginMethod: "login",
|
|
266
|
+
});
|
|
267
|
+
} catch {
|
|
268
|
+
runtime.log("openclaw-clawchat sqlite activation persistence failed; login continues.");
|
|
269
|
+
}
|
|
194
270
|
runtime.log(`Config file updated.`);
|
|
195
271
|
|
|
196
272
|
runtime.log(
|
|
197
|
-
`openclaw-clawchat login succeeded (user_id=${
|
|
273
|
+
`openclaw-clawchat login succeeded (user_id=${normalizedResult.agent.user_id}, owner_user_id=${normalizedResult.agent.owner_id}, nickname=${normalizedResult.agent.nickname || "-"}).`,
|
|
198
274
|
);
|
|
199
275
|
}
|
|
200
|
-
|
|
201
|
-
/** Shortens a token for display logs without revealing the full secret. */
|
|
202
|
-
function redactToken(token: string): string {
|
|
203
|
-
if (!token) return "(empty)";
|
|
204
|
-
if (token.length <= 8) return "***";
|
|
205
|
-
return `${token.slice(0, 4)}…${token.slice(-4)}`;
|
|
206
|
-
}
|