@botcord/daemon 0.2.24 → 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.
- package/dist/openclaw-discovery.d.ts +1 -0
- package/dist/openclaw-discovery.js +17 -2
- package/dist/provision.d.ts +1 -0
- package/dist/provision.js +33 -1
- package/package.json +1 -1
- package/src/__tests__/openclaw-discovery.test.ts +44 -0
- package/src/__tests__/runtime-discovery.test.ts +40 -0
- package/src/openclaw-discovery.ts +19 -3
- package/src/provision.ts +38 -1
|
@@ -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
|
}
|
package/dist/provision.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
@@ -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
|
-
|
|
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,
|