@botcord/daemon 0.2.25 → 0.2.26

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.
@@ -24,5 +24,6 @@ export declare function discoverLocalOpenclawGateways(opts?: OpenclawGatewayDisc
24
24
  export declare function mergeOpenclawGateways(cfg: DaemonConfig, found: DiscoveredOpenclawGateway[]): MergeOpenclawGatewayResult;
25
25
  export declare function defaultOpenclawDiscoverySearchPaths(): string[];
26
26
  export declare function defaultOpenclawDiscoveryPorts(): number[];
27
+ export declare function defaultOpenclawDiscoveryTokenFilePaths(): string[];
27
28
  export declare function openclawDiscoveryConfigEnabled(cfg: DaemonConfig): boolean;
28
29
  export declare function openclawAutoProvisionEnabled(cfg: DaemonConfig): boolean;
@@ -1,10 +1,15 @@
1
- import { readdirSync, readFileSync, statSync } from "node:fs";
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import path from "node:path";
4
4
  import { log as daemonLog } from "./log.js";
5
5
  import { probeOpenclawAgents } from "./provision.js";
6
6
  const DEFAULT_SEARCH_PATHS = ["~/.openclaw/", "/etc/openclaw/"];
7
7
  const DEFAULT_PORTS = [18789, 16200];
8
+ const DEFAULT_TOKEN_FILE_PATHS = [
9
+ "/run/openclaw/gateway-token",
10
+ "/var/run/openclaw/gateway-token",
11
+ "~/.openclaw/gateway-token",
12
+ ];
8
13
  export async function discoverLocalOpenclawGateways(opts = {}) {
9
14
  const found = [];
10
15
  for (const root of opts.searchPaths ?? DEFAULT_SEARCH_PATHS) {
@@ -12,7 +17,7 @@ export async function discoverLocalOpenclawGateways(opts = {}) {
12
17
  }
13
18
  const env = opts.env ?? process.env;
14
19
  found.push(...discoverFromEnv(env));
15
- const envAuth = pickOpenclawEnvAuth(env);
20
+ const envAuth = pickOpenclawEnvAuth(env) ?? pickDefaultTokenFile();
16
21
  const ports = opts.defaultPorts ?? DEFAULT_PORTS;
17
22
  if (ports.length > 0) {
18
23
  await Promise.all(ports.map(async (port) => {
@@ -55,6 +60,13 @@ function pickOpenclawEnvAuth(env) {
55
60
  const tokenFile = pickEnv(env, "OPENCLAW_ACP_TOKEN_FILE") ?? pickEnv(env, "OPENCLAW_GATEWAY_TOKEN_FILE");
56
61
  if (tokenFile)
57
62
  return { tokenFile };
63
+ return undefined;
64
+ }
65
+ function pickDefaultTokenFile() {
66
+ for (const tokenFile of DEFAULT_TOKEN_FILE_PATHS) {
67
+ if (existsSync(expandHome(tokenFile)))
68
+ return { tokenFile };
69
+ }
58
70
  return {};
59
71
  }
60
72
  function urlFromGatewayPort(env) {
@@ -292,6 +304,9 @@ export function defaultOpenclawDiscoverySearchPaths() {
292
304
  export function defaultOpenclawDiscoveryPorts() {
293
305
  return DEFAULT_PORTS.slice();
294
306
  }
307
+ export function defaultOpenclawDiscoveryTokenFilePaths() {
308
+ return DEFAULT_TOKEN_FILE_PATHS.slice();
309
+ }
295
310
  export function openclawDiscoveryConfigEnabled(cfg) {
296
311
  return cfg.openclawDiscovery?.enabled !== false;
297
312
  }
@@ -126,6 +126,7 @@ export type WsEndpointProbeFn = (args: {
126
126
  }>;
127
127
  error?: string;
128
128
  }>;
129
+ export declare function classifyOpenclawAuthError(message: string | undefined): "missing_token" | "auth_required" | null;
129
130
  export declare function probeOpenclawAgents(profile: {
130
131
  url: string;
131
132
  token?: string;
package/dist/provision.js CHANGED
@@ -953,6 +953,23 @@ export function collectRuntimeSnapshot(opts = {}) {
953
953
  }
954
954
  /** Maximum number of `endpoints[]` entries persisted per runtime (RFC §3.8.2). */
955
955
  export const RUNTIME_ENDPOINTS_CAP = 32;
956
+ export function classifyOpenclawAuthError(message) {
957
+ const text = (message ?? "").toLowerCase();
958
+ if (!text)
959
+ return null;
960
+ if (text.includes("token missing") ||
961
+ text.includes("missing token") ||
962
+ text.includes("gateway token missing")) {
963
+ return "missing_token";
964
+ }
965
+ if (text.includes("unauthorized") ||
966
+ text.includes("authentication required") ||
967
+ text.includes("auth required") ||
968
+ text.includes("missing auth")) {
969
+ return "auth_required";
970
+ }
971
+ return null;
972
+ }
956
973
  /**
957
974
  * Default L2 + L3 probe — speaks OpenClaw's WS frame protocol against the
958
975
  * gateway and enumerates agent profiles via `agents.list`.
@@ -1061,7 +1078,8 @@ async function defaultWsProbe(args) {
1061
1078
  if (msg.id === CONNECT_ID) {
1062
1079
  if (!msg.ok) {
1063
1080
  const errMsg = msg.error?.message ? String(msg.error.message) : "connect rejected";
1064
- settle({ ok: true, error: errMsg });
1081
+ const authStatus = classifyOpenclawAuthError(errMsg);
1082
+ settle({ ok: authStatus ? false : true, error: errMsg });
1065
1083
  return;
1066
1084
  }
1067
1085
  const v = msg.payload?.server?.version;
@@ -1262,6 +1280,20 @@ export async function collectRuntimeSnapshotAsync(opts = {}) {
1262
1280
  probe: opts.wsProbe,
1263
1281
  timeoutMs,
1264
1282
  });
1283
+ const authStatus = classifyOpenclawAuthError(res.error);
1284
+ if (!res.ok && authStatus) {
1285
+ const message = authStatus === "missing_token"
1286
+ ? "OpenClaw gateway requires token; configure OPENCLAW_GATEWAY_TOKEN or tokenFile"
1287
+ : "OpenClaw gateway requires authentication; configure gateway credentials";
1288
+ return {
1289
+ name: g.name,
1290
+ url: g.url,
1291
+ reachable: false,
1292
+ status: authStatus,
1293
+ error: res.error ?? message,
1294
+ diagnostics: [{ code: authStatus, message }],
1295
+ };
1296
+ }
1265
1297
  const entry = {
1266
1298
  name: g.name,
1267
1299
  url: g.url,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botcord/daemon",
3
- "version": "0.2.25",
3
+ "version": "0.2.26",
4
4
  "description": "BotCord local daemon — bridges Hub inbox push to local Claude Code / Codex / Gemini CLIs",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import { afterEach, describe, expect, it, vi } from "vitest";
5
5
  import {
6
6
  defaultOpenclawDiscoveryPorts,
7
+ defaultOpenclawDiscoveryTokenFilePaths,
7
8
  discoverLocalOpenclawGateways,
8
9
  mergeOpenclawGateways,
9
10
  } from "../openclaw-discovery.js";
@@ -220,6 +221,49 @@ describe("discoverLocalOpenclawGateways", () => {
220
221
  ]);
221
222
  });
222
223
 
224
+ it("attaches conventional tokenFile fallback to default-port discovery", async () => {
225
+ const home = tempDir();
226
+ const prevHome = process.env.HOME;
227
+ process.env.HOME = home;
228
+ mkdirSync(path.join(home, ".openclaw"), { recursive: true });
229
+ const tokenFile = path.join(home, ".openclaw", "gateway-token");
230
+ writeFileSync(tokenFile, "gateway-token\n");
231
+ const probe = vi.fn<WsEndpointProbeFn>(async () => ({
232
+ ok: true,
233
+ agents: [],
234
+ }));
235
+
236
+ try {
237
+ const found = await discoverLocalOpenclawGateways({
238
+ searchPaths: [],
239
+ defaultPorts: [16200],
240
+ probe,
241
+ timeoutMs: 10,
242
+ env: {},
243
+ });
244
+
245
+ expect(defaultOpenclawDiscoveryTokenFilePaths()).toEqual(
246
+ expect.arrayContaining(["~/.openclaw/gateway-token"]),
247
+ );
248
+ expect(probe).toHaveBeenCalledWith(
249
+ expect.objectContaining({
250
+ url: "ws://127.0.0.1:16200",
251
+ token: "gateway-token",
252
+ }),
253
+ );
254
+ expect(found).toEqual([
255
+ expect.objectContaining({
256
+ url: "ws://127.0.0.1:16200",
257
+ tokenFile: "~/.openclaw/gateway-token",
258
+ source: "default-port",
259
+ }),
260
+ ]);
261
+ } finally {
262
+ if (prevHome === undefined) delete process.env.HOME;
263
+ else process.env.HOME = prevHome;
264
+ }
265
+ });
266
+
223
267
  it("prefers config-file auth details over lower-priority duplicate sources", async () => {
224
268
  const dir = tempDir();
225
269
  writeFileSync(
@@ -194,6 +194,46 @@ describe("collectRuntimeSnapshotAsync", () => {
194
194
  rmSync(tmp, { recursive: true, force: true });
195
195
  }
196
196
  });
197
+
198
+ it("reports missing_token when an OpenClaw gateway requires auth without a token", async () => {
199
+ setRuntimes([
200
+ {
201
+ id: "openclaw-acp",
202
+ displayName: "OpenClaw",
203
+ binary: "openclaw",
204
+ supportsRun: true,
205
+ result: { available: true },
206
+ },
207
+ ]);
208
+
209
+ const snap = await collectRuntimeSnapshotAsync({
210
+ cfg: {
211
+ openclawGateways: [{ name: "local", url: "ws://127.0.0.1:16200" }],
212
+ },
213
+ wsProbe: async () => ({
214
+ ok: false,
215
+ error: "unauthorized: gateway token missing (set gateway.remote.token to match gateway.auth.token)",
216
+ }),
217
+ });
218
+
219
+ const runtime = snap.runtimes.find((r) => r.id === "openclaw-acp");
220
+ expect(runtime?.endpoints).toEqual([
221
+ expect.objectContaining({
222
+ name: "local",
223
+ reachable: false,
224
+ status: "missing_token",
225
+ error:
226
+ "unauthorized: gateway token missing (set gateway.remote.token to match gateway.auth.token)",
227
+ diagnostics: [
228
+ {
229
+ code: "missing_token",
230
+ message:
231
+ "OpenClaw gateway requires token; configure OPENCLAW_GATEWAY_TOKEN or tokenFile",
232
+ },
233
+ ],
234
+ }),
235
+ ]);
236
+ });
197
237
  });
198
238
 
199
239
  interface FakeGateway {
@@ -1,4 +1,4 @@
1
- import { readdirSync, readFileSync, statSync } from "node:fs";
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import path from "node:path";
4
4
  import type { DaemonConfig, OpenclawGatewayProfile } from "./config.js";
@@ -31,6 +31,11 @@ export interface MergeOpenclawGatewayResult {
31
31
 
32
32
  const DEFAULT_SEARCH_PATHS = ["~/.openclaw/", "/etc/openclaw/"];
33
33
  const DEFAULT_PORTS = [18789, 16200];
34
+ const DEFAULT_TOKEN_FILE_PATHS = [
35
+ "/run/openclaw/gateway-token",
36
+ "/var/run/openclaw/gateway-token",
37
+ "~/.openclaw/gateway-token",
38
+ ];
34
39
 
35
40
  export async function discoverLocalOpenclawGateways(
36
41
  opts: OpenclawGatewayDiscoveryOptions = {},
@@ -42,7 +47,7 @@ export async function discoverLocalOpenclawGateways(
42
47
 
43
48
  const env = opts.env ?? process.env;
44
49
  found.push(...discoverFromEnv(env));
45
- const envAuth = pickOpenclawEnvAuth(env);
50
+ const envAuth = pickOpenclawEnvAuth(env) ?? pickDefaultTokenFile();
46
51
 
47
52
  const ports = opts.defaultPorts ?? DEFAULT_PORTS;
48
53
  if (ports.length > 0) {
@@ -87,12 +92,19 @@ function discoverFromEnv(env: NodeJS.ProcessEnv): DiscoveredOpenclawGateway[] {
87
92
  ];
88
93
  }
89
94
 
90
- function pickOpenclawEnvAuth(env: NodeJS.ProcessEnv): { token?: string; tokenFile?: string } {
95
+ function pickOpenclawEnvAuth(env: NodeJS.ProcessEnv): { token?: string; tokenFile?: string } | undefined {
91
96
  const token = pickEnv(env, "OPENCLAW_ACP_TOKEN") ?? pickEnv(env, "OPENCLAW_GATEWAY_TOKEN");
92
97
  if (token) return { token };
93
98
  const tokenFile =
94
99
  pickEnv(env, "OPENCLAW_ACP_TOKEN_FILE") ?? pickEnv(env, "OPENCLAW_GATEWAY_TOKEN_FILE");
95
100
  if (tokenFile) return { tokenFile };
101
+ return undefined;
102
+ }
103
+
104
+ function pickDefaultTokenFile(): { tokenFile?: string } {
105
+ for (const tokenFile of DEFAULT_TOKEN_FILE_PATHS) {
106
+ if (existsSync(expandHome(tokenFile))) return { tokenFile };
107
+ }
96
108
  return {};
97
109
  }
98
110
 
@@ -327,6 +339,10 @@ export function defaultOpenclawDiscoveryPorts(): number[] {
327
339
  return DEFAULT_PORTS.slice();
328
340
  }
329
341
 
342
+ export function defaultOpenclawDiscoveryTokenFilePaths(): string[] {
343
+ return DEFAULT_TOKEN_FILE_PATHS.slice();
344
+ }
345
+
330
346
  export function openclawDiscoveryConfigEnabled(cfg: DaemonConfig): boolean {
331
347
  return cfg.openclawDiscovery?.enabled !== false;
332
348
  }
package/src/provision.ts CHANGED
@@ -1154,6 +1154,27 @@ export type WsEndpointProbeFn = (args: {
1154
1154
  error?: string;
1155
1155
  }>;
1156
1156
 
1157
+ export function classifyOpenclawAuthError(message: string | undefined): "missing_token" | "auth_required" | null {
1158
+ const text = (message ?? "").toLowerCase();
1159
+ if (!text) return null;
1160
+ if (
1161
+ text.includes("token missing") ||
1162
+ text.includes("missing token") ||
1163
+ text.includes("gateway token missing")
1164
+ ) {
1165
+ return "missing_token";
1166
+ }
1167
+ if (
1168
+ text.includes("unauthorized") ||
1169
+ text.includes("authentication required") ||
1170
+ text.includes("auth required") ||
1171
+ text.includes("missing auth")
1172
+ ) {
1173
+ return "auth_required";
1174
+ }
1175
+ return null;
1176
+ }
1177
+
1157
1178
  /**
1158
1179
  * Default L2 + L3 probe — speaks OpenClaw's WS frame protocol against the
1159
1180
  * gateway and enumerates agent profiles via `agents.list`.
@@ -1278,7 +1299,8 @@ async function defaultWsProbe(args: {
1278
1299
  if (msg.id === CONNECT_ID) {
1279
1300
  if (!msg.ok) {
1280
1301
  const errMsg = msg.error?.message ? String(msg.error.message) : "connect rejected";
1281
- settle({ ok: true, error: errMsg });
1302
+ const authStatus = classifyOpenclawAuthError(errMsg);
1303
+ settle({ ok: authStatus ? false : true, error: errMsg });
1282
1304
  return;
1283
1305
  }
1284
1306
  const v = msg.payload?.server?.version;
@@ -1491,6 +1513,21 @@ export async function collectRuntimeSnapshotAsync(opts: {
1491
1513
  probe: opts.wsProbe,
1492
1514
  timeoutMs,
1493
1515
  });
1516
+ const authStatus = classifyOpenclawAuthError(res.error);
1517
+ if (!res.ok && authStatus) {
1518
+ const message =
1519
+ authStatus === "missing_token"
1520
+ ? "OpenClaw gateway requires token; configure OPENCLAW_GATEWAY_TOKEN or tokenFile"
1521
+ : "OpenClaw gateway requires authentication; configure gateway credentials";
1522
+ return {
1523
+ name: g.name,
1524
+ url: g.url,
1525
+ reachable: false,
1526
+ status: authStatus,
1527
+ error: res.error ?? message,
1528
+ diagnostics: [{ code: authStatus, message }],
1529
+ };
1530
+ }
1494
1531
  const entry: any = {
1495
1532
  name: g.name,
1496
1533
  url: g.url,