@botcord/daemon 0.2.35 → 0.2.37

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.
Files changed (68) hide show
  1. package/dist/config.d.ts +30 -1
  2. package/dist/config.js +27 -0
  3. package/dist/daemon-config-map.d.ts +3 -0
  4. package/dist/daemon-config-map.js +30 -0
  5. package/dist/daemon.d.ts +15 -1
  6. package/dist/daemon.js +56 -11
  7. package/dist/gateway/channels/botcord.js +44 -0
  8. package/dist/gateway/channels/http-types.d.ts +19 -0
  9. package/dist/gateway/channels/http-types.js +1 -0
  10. package/dist/gateway/channels/index.d.ts +5 -0
  11. package/dist/gateway/channels/index.js +5 -0
  12. package/dist/gateway/channels/login-session.d.ts +83 -0
  13. package/dist/gateway/channels/login-session.js +99 -0
  14. package/dist/gateway/channels/secret-store.d.ts +21 -0
  15. package/dist/gateway/channels/secret-store.js +75 -0
  16. package/dist/gateway/channels/state-store.d.ts +60 -0
  17. package/dist/gateway/channels/state-store.js +173 -0
  18. package/dist/gateway/channels/telegram.d.ts +31 -0
  19. package/dist/gateway/channels/telegram.js +371 -0
  20. package/dist/gateway/channels/text-split.d.ts +13 -0
  21. package/dist/gateway/channels/text-split.js +33 -0
  22. package/dist/gateway/channels/url-guard.d.ts +18 -0
  23. package/dist/gateway/channels/url-guard.js +53 -0
  24. package/dist/gateway/channels/wechat-http.d.ts +18 -0
  25. package/dist/gateway/channels/wechat-http.js +28 -0
  26. package/dist/gateway/channels/wechat-login.d.ts +36 -0
  27. package/dist/gateway/channels/wechat-login.js +62 -0
  28. package/dist/gateway/channels/wechat.d.ts +40 -0
  29. package/dist/gateway/channels/wechat.js +472 -0
  30. package/dist/gateway/runtimes/openclaw-acp.js +211 -6
  31. package/dist/gateway/types.d.ts +10 -0
  32. package/dist/gateway-control.d.ts +53 -0
  33. package/dist/gateway-control.js +638 -0
  34. package/dist/openclaw-discovery.js +1 -1
  35. package/dist/provision.d.ts +7 -0
  36. package/dist/provision.js +255 -5
  37. package/package.json +1 -1
  38. package/src/__tests__/gateway-control.test.ts +499 -0
  39. package/src/__tests__/openclaw-acp.test.ts +63 -0
  40. package/src/__tests__/openclaw-discovery.test.ts +36 -0
  41. package/src/__tests__/provision.test.ts +179 -0
  42. package/src/__tests__/secret-store.test.ts +70 -0
  43. package/src/__tests__/state-store.test.ts +119 -0
  44. package/src/__tests__/third-party-gateway.test.ts +126 -0
  45. package/src/__tests__/url-guard.test.ts +85 -0
  46. package/src/__tests__/wechat-channel.test.ts +1134 -0
  47. package/src/config.ts +72 -1
  48. package/src/daemon-config-map.ts +24 -0
  49. package/src/daemon.ts +70 -11
  50. package/src/gateway/__tests__/botcord-channel.test.ts +1 -1
  51. package/src/gateway/__tests__/telegram-channel.test.ts +555 -0
  52. package/src/gateway/channels/botcord.ts +39 -0
  53. package/src/gateway/channels/http-types.ts +22 -0
  54. package/src/gateway/channels/index.ts +22 -0
  55. package/src/gateway/channels/login-session.ts +135 -0
  56. package/src/gateway/channels/secret-store.ts +100 -0
  57. package/src/gateway/channels/state-store.ts +213 -0
  58. package/src/gateway/channels/telegram.ts +469 -0
  59. package/src/gateway/channels/text-split.ts +29 -0
  60. package/src/gateway/channels/url-guard.ts +55 -0
  61. package/src/gateway/channels/wechat-http.ts +35 -0
  62. package/src/gateway/channels/wechat-login.ts +90 -0
  63. package/src/gateway/channels/wechat.ts +572 -0
  64. package/src/gateway/runtimes/openclaw-acp.ts +211 -7
  65. package/src/gateway/types.ts +10 -0
  66. package/src/gateway-control.ts +709 -0
  67. package/src/openclaw-discovery.ts +1 -1
  68. package/src/provision.ts +336 -5
@@ -525,5 +525,5 @@ export function openclawDiscoveryConfigEnabled(cfg: DaemonConfig): boolean {
525
525
  }
526
526
 
527
527
  export function openclawAutoProvisionEnabled(cfg: DaemonConfig): boolean {
528
- return cfg.openclawDiscovery?.autoProvision !== false;
528
+ return cfg.openclawDiscovery?.autoProvision === true;
529
529
  }
package/src/provision.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * side effects (register agent, write credentials, load route, add/remove
5
5
  * gateway channel) and return an ack payload.
6
6
  */
7
- import { existsSync, readdirSync, readFileSync, rmSync, unlinkSync } from "node:fs";
7
+ import { existsSync, lstatSync, readdirSync, readFileSync, rmSync, statSync, unlinkSync } from "node:fs";
8
8
  import { homedir } from "node:os";
9
9
  import path from "node:path";
10
10
  import {
@@ -56,6 +56,8 @@ import {
56
56
  ensureAgentWorkspace,
57
57
  } from "./agent-workspace.js";
58
58
  import { detectRuntimes, getAdapterModule } from "./adapters/runtimes.js";
59
+ import { createGatewayControl } from "./gateway-control.js";
60
+ import type { LoginSessionStore } from "./gateway/channels/login-session.js";
59
61
  import {
60
62
  hermesProfileHomeDir,
61
63
  isValidHermesProfileName,
@@ -114,6 +116,12 @@ export interface ProvisionerOptions {
114
116
  * the next restart.
115
117
  */
116
118
  onAgentInstalled?: OnAgentInstalledHook;
119
+ /**
120
+ * Optional shared login-session store for the third-party gateway login
121
+ * frames. Tests inject a stub clock; production lets `createGatewayControl`
122
+ * spin up a default in-memory store.
123
+ */
124
+ loginSessions?: LoginSessionStore;
117
125
  }
118
126
 
119
127
  /** The value a frame handler returns (minus the `id` which the channel fills in). */
@@ -131,6 +139,10 @@ export function createProvisioner(opts: ProvisionerOptions): (
131
139
  const register = opts.register ?? BotCordClient.register;
132
140
  const policyResolver = opts.policyResolver;
133
141
  const onAgentInstalled = opts.onAgentInstalled;
142
+ const gatewayControl = createGatewayControl({
143
+ gateway,
144
+ ...(opts.loginSessions ? { loginSessions: opts.loginSessions } : {}),
145
+ });
134
146
 
135
147
  return async (frame: ControlFrame): Promise<AckBody> => {
136
148
  daemonLog.debug("provision.dispatch", { type: frame.type, id: frame.id });
@@ -310,6 +322,72 @@ export function createProvisioner(opts: ProvisionerOptions): (
310
322
  return { ok: true, result: snapshot };
311
323
  }
312
324
 
325
+ case CONTROL_FRAME_TYPES.LIST_GATEWAYS:
326
+ return gatewayControl.handleList();
327
+
328
+ case CONTROL_FRAME_TYPES.UPSERT_GATEWAY: {
329
+ const v = validateGatewayParams(frame.params, {
330
+ required: ["id", "type", "accountId"],
331
+ });
332
+ if (!v.ok) return v.ack;
333
+ return gatewayControl.handleUpsert(
334
+ v.params as unknown as Parameters<typeof gatewayControl.handleUpsert>[0],
335
+ );
336
+ }
337
+
338
+ case CONTROL_FRAME_TYPES.REMOVE_GATEWAY: {
339
+ const v = validateGatewayParams(frame.params, { required: ["id"] });
340
+ if (!v.ok) return v.ack;
341
+ return gatewayControl.handleRemove(
342
+ v.params as unknown as Parameters<typeof gatewayControl.handleRemove>[0],
343
+ );
344
+ }
345
+
346
+ case CONTROL_FRAME_TYPES.TEST_GATEWAY: {
347
+ const v = validateGatewayParams(frame.params, { required: ["id"] });
348
+ if (!v.ok) return v.ack;
349
+ return gatewayControl.handleTest(
350
+ v.params as unknown as Parameters<typeof gatewayControl.handleTest>[0],
351
+ );
352
+ }
353
+
354
+ case CONTROL_FRAME_TYPES.GATEWAY_LOGIN_START: {
355
+ const v = validateGatewayParams(frame.params, {
356
+ required: ["provider", "accountId"],
357
+ });
358
+ if (!v.ok) return v.ack;
359
+ return gatewayControl.handleLoginStart(
360
+ v.params as unknown as Parameters<typeof gatewayControl.handleLoginStart>[0],
361
+ );
362
+ }
363
+
364
+ case CONTROL_FRAME_TYPES.GATEWAY_LOGIN_STATUS: {
365
+ const v = validateGatewayParams(frame.params, {
366
+ required: ["provider", "loginId"],
367
+ });
368
+ if (!v.ok) return v.ack;
369
+ return gatewayControl.handleLoginStatus(
370
+ v.params as unknown as Parameters<typeof gatewayControl.handleLoginStatus>[0],
371
+ );
372
+ }
373
+
374
+ case "list_agent_files": {
375
+ const params = (frame.params ?? {}) as unknown as ListAgentFilesParams;
376
+ if (!params.agentId) {
377
+ return {
378
+ ok: false,
379
+ error: { code: "bad_params", message: "list_agent_files requires params.agentId" },
380
+ };
381
+ }
382
+ const result = listAgentRuntimeFiles(params);
383
+ daemonLog.debug("list_agent_files", {
384
+ agentId: params.agentId,
385
+ fileId: params.fileId ?? null,
386
+ count: result.files.length,
387
+ });
388
+ return { ok: true, result };
389
+ }
390
+
313
391
  default:
314
392
  daemonLog.warn("provision.dispatch: unknown frame type", {
315
393
  type: frame.type,
@@ -323,6 +401,42 @@ export function createProvisioner(opts: ProvisionerOptions): (
323
401
  };
324
402
  }
325
403
 
404
+ // W8: hand-written runtime validator for the third-party gateway frame
405
+ // params. Rejects malformed payloads with a structured `bad_params` ack
406
+ // before they hit the per-handler logic, so an attacker can't smuggle a
407
+ // non-object `params` (e.g. `null`, an array, a string) through the type
408
+ // cast and trigger a downstream `TypeError` we don't surface.
409
+ type GatewayParamAck = { ok: false; error: { code: string; message: string } };
410
+ type ValidateResult =
411
+ | { ok: true; params: Record<string, unknown> }
412
+ | { ok: false; ack: GatewayParamAck };
413
+
414
+ function validateGatewayParams(
415
+ raw: unknown,
416
+ spec: { required: ReadonlyArray<string> },
417
+ ): ValidateResult {
418
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
419
+ return {
420
+ ok: false,
421
+ ack: { ok: false, error: { code: "bad_params", message: "params must be an object" } },
422
+ };
423
+ }
424
+ const params = raw as Record<string, unknown>;
425
+ for (const key of spec.required) {
426
+ const v = params[key];
427
+ if (typeof v !== "string" || v.length === 0) {
428
+ return {
429
+ ok: false,
430
+ ack: {
431
+ ok: false,
432
+ error: { code: "bad_params", message: `params.${key} must be a non-empty string` },
433
+ },
434
+ };
435
+ }
436
+ }
437
+ return { ok: true, params };
438
+ }
439
+
326
440
  interface ProvisionedAgent {
327
441
  agentId: string;
328
442
  hubUrl: string;
@@ -337,6 +451,192 @@ interface ProvisionCtx {
337
451
 
338
452
  const openclawProvisionLocks = new Map<string, Promise<unknown>>();
339
453
 
454
+ const RUNTIME_FILE_READ_CAP_BYTES = 128 * 1024;
455
+
456
+ interface ListAgentFilesParams {
457
+ agentId: string;
458
+ fileId?: string;
459
+ }
460
+
461
+ interface AgentRuntimeFile {
462
+ id: string;
463
+ name: string;
464
+ scope: "workspace" | "hermes" | "openclaw";
465
+ runtime?: string;
466
+ profile?: string;
467
+ size?: number;
468
+ mtimeMs?: number;
469
+ content?: string;
470
+ truncated?: boolean;
471
+ error?: string;
472
+ }
473
+
474
+ interface ListAgentFilesResult {
475
+ agentId: string;
476
+ runtime?: string;
477
+ files: AgentRuntimeFile[];
478
+ }
479
+
480
+ interface RuntimeFileCandidate {
481
+ id: string;
482
+ name: string;
483
+ scope: AgentRuntimeFile["scope"];
484
+ root: string;
485
+ relativePath: string;
486
+ runtime?: string;
487
+ profile?: string;
488
+ }
489
+
490
+ function listAgentRuntimeFiles(params: ListAgentFilesParams): ListAgentFilesResult {
491
+ const agentId = params.agentId;
492
+ const credentials = loadStoredCredentials(defaultCredentialsFile(agentId));
493
+ const candidates = runtimeFileCandidates(credentials);
494
+ const selected = params.fileId
495
+ ? candidates.filter((candidate) => candidate.id === params.fileId)
496
+ : candidates;
497
+ return {
498
+ agentId,
499
+ ...(credentials.runtime ? { runtime: credentials.runtime } : {}),
500
+ files: selected.map(readRuntimeFileCandidate).filter(Boolean) as AgentRuntimeFile[],
501
+ };
502
+ }
503
+
504
+ function runtimeFileCandidates(credentials: StoredBotCordCredentials): RuntimeFileCandidate[] {
505
+ const agentId = credentials.agentId;
506
+ const runtime = credentials.runtime;
507
+ const out: RuntimeFileCandidate[] = [];
508
+
509
+ addWorkspaceFiles(out, agentId, runtime);
510
+
511
+ if (runtime === "hermes-agent") {
512
+ addHermesFiles(out, credentials);
513
+ }
514
+ if (runtime === "openclaw-acp") {
515
+ addOpenclawFiles(out, credentials);
516
+ }
517
+
518
+ return out;
519
+ }
520
+
521
+ function addWorkspaceFiles(
522
+ out: RuntimeFileCandidate[],
523
+ agentId: string,
524
+ runtime?: string,
525
+ ): void {
526
+ const root = agentWorkspaceDir(agentId);
527
+ for (const file of ["AGENTS.md", "CLAUDE.md", "identity.md", "memory.md", "task.md"]) {
528
+ out.push({
529
+ id: `workspace:${file}`,
530
+ name: `workspace/${file}`,
531
+ scope: "workspace",
532
+ root,
533
+ relativePath: file,
534
+ ...(runtime ? { runtime } : {}),
535
+ });
536
+ }
537
+ }
538
+
539
+ function addHermesFiles(
540
+ out: RuntimeFileCandidate[],
541
+ credentials: StoredBotCordCredentials,
542
+ ): void {
543
+ if (credentials.hermesProfile) {
544
+ const profile = credentials.hermesProfile;
545
+ const root = hermesProfileHomeDir(profile);
546
+ for (const file of ["SOUL.md", "AGENTS.md", "memories/MEMORY.md"]) {
547
+ out.push({
548
+ id: `hermes-profile:${profile}:${file}`,
549
+ name: `hermes/${profile}/${file}`,
550
+ scope: "hermes",
551
+ root,
552
+ relativePath: file,
553
+ runtime: "hermes-agent",
554
+ profile,
555
+ });
556
+ }
557
+ } else {
558
+ const root = path.join(agentHomeDir(credentials.agentId), "hermes-home");
559
+ for (const file of ["SOUL.md", "AGENTS.md", "memories/MEMORY.md"]) {
560
+ out.push({
561
+ id: `hermes-home:${file}`,
562
+ name: `hermes-home/${file}`,
563
+ scope: "hermes",
564
+ root,
565
+ relativePath: file,
566
+ runtime: "hermes-agent",
567
+ });
568
+ }
569
+ }
570
+
571
+ out.push({
572
+ id: "hermes-workspace:AGENTS.md",
573
+ name: "hermes-workspace/AGENTS.md",
574
+ scope: "hermes",
575
+ root: path.join(agentHomeDir(credentials.agentId), "hermes-workspace"),
576
+ relativePath: "AGENTS.md",
577
+ runtime: "hermes-agent",
578
+ ...(credentials.hermesProfile ? { profile: credentials.hermesProfile } : {}),
579
+ });
580
+ }
581
+
582
+ function addOpenclawFiles(
583
+ out: RuntimeFileCandidate[],
584
+ credentials: StoredBotCordCredentials,
585
+ ): void {
586
+ if (!credentials.openclawAgent) return;
587
+ const local = readLocalOpenclawAgents();
588
+ if (!local) return;
589
+ const profile = local.find((entry) => entry.id === credentials.openclawAgent);
590
+ if (!profile?.workspace) return;
591
+ for (const file of ["SOUL.md", "MEMORY.md", "AGENTS.md"]) {
592
+ out.push({
593
+ id: `openclaw:${credentials.openclawAgent}:${file}`,
594
+ name: `openclaw/${credentials.openclawAgent}/${file}`,
595
+ scope: "openclaw",
596
+ root: profile.workspace,
597
+ relativePath: file,
598
+ runtime: "openclaw-acp",
599
+ profile: credentials.openclawAgent,
600
+ });
601
+ }
602
+ }
603
+
604
+ function readRuntimeFileCandidate(candidate: RuntimeFileCandidate): AgentRuntimeFile | null {
605
+ const resolved = path.resolve(candidate.root, candidate.relativePath);
606
+ const root = path.resolve(candidate.root);
607
+ if (resolved !== root && !resolved.startsWith(root + path.sep)) {
608
+ return null;
609
+ }
610
+
611
+ const base: AgentRuntimeFile = {
612
+ id: candidate.id,
613
+ name: candidate.name,
614
+ scope: candidate.scope,
615
+ ...(candidate.runtime ? { runtime: candidate.runtime } : {}),
616
+ ...(candidate.profile ? { profile: candidate.profile } : {}),
617
+ };
618
+
619
+ try {
620
+ const lst = lstatSync(resolved);
621
+ if (lst.isSymbolicLink() || !lst.isFile()) return null;
622
+ const st = statSync(resolved);
623
+ base.size = st.size;
624
+ base.mtimeMs = st.mtimeMs;
625
+ if (st.size > RUNTIME_FILE_READ_CAP_BYTES) {
626
+ base.truncated = true;
627
+ return base;
628
+ }
629
+ base.content = readFileSync(resolved, "utf8");
630
+ return base;
631
+ } catch (err) {
632
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") return null;
633
+ return {
634
+ ...base,
635
+ error: err instanceof Error ? err.message : String(err),
636
+ };
637
+ }
638
+ }
639
+
340
640
  async function provisionAgent(
341
641
  params: ProvisionAgentParams,
342
642
  ctx: ProvisionCtx,
@@ -1556,10 +1856,15 @@ function readLocalOpenclawAgents(): Array<{
1556
1856
  }> | null {
1557
1857
  try {
1558
1858
  const file = path.join(homedir(), ".openclaw", "openclaw.json");
1559
- if (!existsSync(file)) return [{ id: "default" }];
1859
+ if (!existsSync(file)) return readLocalOpenclawAgentDirs() ?? [{ id: "default" }];
1560
1860
  const cfg = JSON.parse(readFileSync(file, "utf8")) as any;
1561
1861
  const list = Array.isArray(cfg?.agents?.list) ? cfg.agents.list : [];
1562
- const defaultId = typeof cfg?.agents?.defaults?.id === "string" ? cfg.agents.defaults.id : "default";
1862
+ const explicitDefaultId =
1863
+ typeof cfg?.agents?.defaults?.id === "string" && cfg.agents.defaults.id
1864
+ ? cfg.agents.defaults.id
1865
+ : null;
1866
+ const dirAgents = readLocalOpenclawAgentDirs();
1867
+ const defaultId = explicitDefaultId ?? (list.length === 0 && !dirAgents ? "default" : null);
1563
1868
  const seen = new Set<string>();
1564
1869
  const out: Array<{ id: string; name?: string; workspace?: string; model?: { name?: string; provider?: string } }> = [];
1565
1870
  const push = (raw: any, fallbackId?: string): void => {
@@ -1581,15 +1886,41 @@ function readLocalOpenclawAgents(): Array<{
1581
1886
  }
1582
1887
  out.push(row);
1583
1888
  };
1584
- // Default agent first so it surfaces at the top of the dropdown.
1585
- push({ id: defaultId, workspace: cfg?.agents?.defaults?.workspace, model: cfg?.agents?.defaults?.model }, defaultId);
1889
+ // Explicit default agent first so it surfaces at the top of the dropdown.
1890
+ if (defaultId) push({ id: defaultId, workspace: cfg?.agents?.defaults?.workspace, model: cfg?.agents?.defaults?.model }, defaultId);
1586
1891
  for (const entry of list) push(entry);
1892
+ for (const entry of dirAgents ?? []) push(entry);
1587
1893
  return out;
1588
1894
  } catch {
1589
1895
  return null;
1590
1896
  }
1591
1897
  }
1592
1898
 
1899
+ function readLocalOpenclawAgentDirs(): Array<{
1900
+ id: string;
1901
+ workspace?: string;
1902
+ }> | null {
1903
+ try {
1904
+ const dir = path.join(homedir(), ".openclaw", "agents");
1905
+ if (!existsSync(dir)) return null;
1906
+ const agents = readdirSync(dir, { withFileTypes: true })
1907
+ .filter((entry) => entry.isDirectory() && entry.name.length > 0)
1908
+ .map((entry) => ({
1909
+ id: entry.name,
1910
+ workspace: path.join(dir, entry.name),
1911
+ }));
1912
+ if (agents.length === 0) return null;
1913
+ agents.sort((a, b) => {
1914
+ if (a.id === "main") return -1;
1915
+ if (b.id === "main") return 1;
1916
+ return a.id.localeCompare(b.id);
1917
+ });
1918
+ return agents;
1919
+ } catch {
1920
+ return null;
1921
+ }
1922
+ }
1923
+
1593
1924
  function resolveOpenclawIdentityName(
1594
1925
  agentId: string,
1595
1926
  workspace?: string,