@botcord/daemon 0.2.6 → 0.2.9
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/config.d.ts +15 -0
- package/dist/config.js +16 -0
- package/dist/daemon.js +24 -1
- package/dist/index.js +24 -1
- package/dist/openclaw-discovery.d.ts +28 -0
- package/dist/openclaw-discovery.js +272 -0
- package/dist/provision.d.ts +41 -0
- package/dist/provision.js +369 -90
- package/package.json +2 -2
- package/src/__tests__/openclaw-discovery.test.ts +198 -0
- package/src/__tests__/provision.test.ts +105 -0
- package/src/config.ts +36 -0
- package/src/daemon.ts +30 -1
- package/src/index.ts +27 -1
- package/src/openclaw-discovery.ts +305 -0
- package/src/provision.ts +411 -86
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
discoverLocalOpenclawGateways,
|
|
7
|
+
mergeOpenclawGateways,
|
|
8
|
+
} from "../openclaw-discovery.js";
|
|
9
|
+
import type { DaemonConfig } from "../config.js";
|
|
10
|
+
import type { WsEndpointProbeFn } from "../provision.js";
|
|
11
|
+
|
|
12
|
+
let tmp: string | null = null;
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
if (tmp) rmSync(tmp, { recursive: true, force: true });
|
|
16
|
+
tmp = null;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function tempDir(): string {
|
|
20
|
+
tmp = mkdtempSync(path.join(tmpdir(), "openclaw-discovery-"));
|
|
21
|
+
return tmp;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function baseConfig(): DaemonConfig {
|
|
25
|
+
return {
|
|
26
|
+
defaultRoute: { adapter: "claude-code", cwd: "/tmp" },
|
|
27
|
+
routes: [],
|
|
28
|
+
streamBlocks: true,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("discoverLocalOpenclawGateways", () => {
|
|
33
|
+
it("discovers JSON and TOML acp config files", async () => {
|
|
34
|
+
const dir = tempDir();
|
|
35
|
+
writeFileSync(
|
|
36
|
+
path.join(dir, "one.json"),
|
|
37
|
+
JSON.stringify({ acp: { url: "ws://127.0.0.1:18789/acp", tokenFile: "/tmp/token" } }),
|
|
38
|
+
);
|
|
39
|
+
writeFileSync(
|
|
40
|
+
path.join(dir, "two.toml"),
|
|
41
|
+
['[acp]', 'url = "ws://127.0.0.1:18790/acp"', 'token = "secret"'].join("\n"),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const found = await discoverLocalOpenclawGateways({
|
|
45
|
+
searchPaths: [dir],
|
|
46
|
+
defaultPorts: [],
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
expect(found).toEqual(
|
|
50
|
+
expect.arrayContaining([
|
|
51
|
+
expect.objectContaining({
|
|
52
|
+
url: "ws://127.0.0.1:18789/acp",
|
|
53
|
+
tokenFile: "/tmp/token",
|
|
54
|
+
source: "config-file",
|
|
55
|
+
}),
|
|
56
|
+
expect.objectContaining({
|
|
57
|
+
url: "ws://127.0.0.1:18790/acp",
|
|
58
|
+
token: "secret",
|
|
59
|
+
source: "config-file",
|
|
60
|
+
}),
|
|
61
|
+
]),
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("parses OpenClaw's native gateway.port + auth.token shape", async () => {
|
|
66
|
+
const dir = tempDir();
|
|
67
|
+
writeFileSync(
|
|
68
|
+
path.join(dir, "openclaw.json"),
|
|
69
|
+
JSON.stringify({
|
|
70
|
+
gateway: {
|
|
71
|
+
port: 18789,
|
|
72
|
+
bind: "loopback",
|
|
73
|
+
auth: { mode: "token", token: "native-token" },
|
|
74
|
+
},
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const found = await discoverLocalOpenclawGateways({
|
|
79
|
+
searchPaths: [dir],
|
|
80
|
+
defaultPorts: [],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(found).toEqual([
|
|
84
|
+
expect.objectContaining({
|
|
85
|
+
url: "ws://127.0.0.1:18789",
|
|
86
|
+
token: "native-token",
|
|
87
|
+
source: "config-file",
|
|
88
|
+
}),
|
|
89
|
+
]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("uses OPENCLAW_ACP_URL and token env vars", async () => {
|
|
93
|
+
const found = await discoverLocalOpenclawGateways({
|
|
94
|
+
searchPaths: [],
|
|
95
|
+
defaultPorts: [],
|
|
96
|
+
env: {
|
|
97
|
+
OPENCLAW_ACP_URL: "ws://127.0.0.1:18888/acp",
|
|
98
|
+
OPENCLAW_ACP_TOKEN: "env-token",
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(found).toEqual([
|
|
103
|
+
expect.objectContaining({
|
|
104
|
+
url: "ws://127.0.0.1:18888/acp",
|
|
105
|
+
token: "env-token",
|
|
106
|
+
source: "env",
|
|
107
|
+
}),
|
|
108
|
+
]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("adds default-port candidates only when the probe succeeds", async () => {
|
|
112
|
+
const probe = vi.fn<WsEndpointProbeFn>(async ({ url }) => ({
|
|
113
|
+
ok: url.includes("18789"),
|
|
114
|
+
agents: [],
|
|
115
|
+
}));
|
|
116
|
+
|
|
117
|
+
const found = await discoverLocalOpenclawGateways({
|
|
118
|
+
searchPaths: [],
|
|
119
|
+
defaultPorts: [18789, 18790],
|
|
120
|
+
probe,
|
|
121
|
+
timeoutMs: 10,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(probe).toHaveBeenCalledTimes(2);
|
|
125
|
+
expect(found.map((g) => g.url)).toEqual(["ws://127.0.0.1:18789"]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("prefers config-file auth details over lower-priority duplicate sources", async () => {
|
|
129
|
+
const dir = tempDir();
|
|
130
|
+
writeFileSync(
|
|
131
|
+
path.join(dir, "one.json"),
|
|
132
|
+
JSON.stringify({ acp: { url: "ws://127.0.0.1:18789", token: "file-token" } }),
|
|
133
|
+
);
|
|
134
|
+
const probe = vi.fn<WsEndpointProbeFn>(async () => ({ ok: true }));
|
|
135
|
+
|
|
136
|
+
const found = await discoverLocalOpenclawGateways({
|
|
137
|
+
searchPaths: [dir],
|
|
138
|
+
defaultPorts: [18789],
|
|
139
|
+
probe,
|
|
140
|
+
env: {
|
|
141
|
+
OPENCLAW_ACP_URL: "ws://127.0.0.1:18789",
|
|
142
|
+
OPENCLAW_ACP_TOKEN: "env-token",
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(found).toHaveLength(1);
|
|
147
|
+
expect(found[0]).toEqual(
|
|
148
|
+
expect.objectContaining({ source: "config-file", token: "file-token" }),
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("mergeOpenclawGateways", () => {
|
|
154
|
+
it("backfills token onto an existing profile that lacks one", () => {
|
|
155
|
+
const cfg = baseConfig();
|
|
156
|
+
cfg.openclawGateways = [
|
|
157
|
+
{ name: "openclaw-127-0-0-1-18789", url: "ws://127.0.0.1:18789" },
|
|
158
|
+
];
|
|
159
|
+
const merged = mergeOpenclawGateways(cfg, [
|
|
160
|
+
{
|
|
161
|
+
name: "openclaw-127-0-0-1-18789",
|
|
162
|
+
url: "ws://127.0.0.1:18789",
|
|
163
|
+
token: "discovered",
|
|
164
|
+
source: "config-file",
|
|
165
|
+
},
|
|
166
|
+
]);
|
|
167
|
+
|
|
168
|
+
expect(merged.changed).toBe(true);
|
|
169
|
+
expect(merged.added).toEqual([]);
|
|
170
|
+
expect(merged.cfg.openclawGateways).toEqual([
|
|
171
|
+
{ name: "openclaw-127-0-0-1-18789", url: "ws://127.0.0.1:18789", token: "discovered" },
|
|
172
|
+
]);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("appends new URLs and keeps existing profiles untouched", () => {
|
|
176
|
+
const cfg = baseConfig();
|
|
177
|
+
cfg.openclawGateways = [{ name: "local", url: "ws://127.0.0.1:18789/acp", token: "user-token" }];
|
|
178
|
+
const merged = mergeOpenclawGateways(cfg, [
|
|
179
|
+
{
|
|
180
|
+
name: "openclaw-127-0-0-1-18789",
|
|
181
|
+
url: "ws://127.0.0.1:18789/acp",
|
|
182
|
+
token: "discovered-token",
|
|
183
|
+
source: "env",
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: "openclaw-127-0-0-1-18790",
|
|
187
|
+
url: "ws://127.0.0.1:18790/acp",
|
|
188
|
+
source: "default-port",
|
|
189
|
+
},
|
|
190
|
+
]);
|
|
191
|
+
|
|
192
|
+
expect(merged.changed).toBe(true);
|
|
193
|
+
expect(merged.cfg.openclawGateways).toEqual([
|
|
194
|
+
{ name: "local", url: "ws://127.0.0.1:18789/acp", token: "user-token" },
|
|
195
|
+
{ name: "openclaw-127-0-0-1-18790", url: "ws://127.0.0.1:18790/acp" },
|
|
196
|
+
]);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -26,6 +26,7 @@ vi.mock("../config.js", async () => {
|
|
|
26
26
|
|
|
27
27
|
const {
|
|
28
28
|
addAgentToConfig,
|
|
29
|
+
adoptDiscoveredOpenclawAgents,
|
|
29
30
|
removeAgentFromConfig,
|
|
30
31
|
reloadConfig,
|
|
31
32
|
setRoute,
|
|
@@ -779,6 +780,110 @@ describe("provision_agent seeds workspace + hot-adds managed route", () => {
|
|
|
779
780
|
});
|
|
780
781
|
});
|
|
781
782
|
|
|
783
|
+
describe("adoptDiscoveredOpenclawAgents", () => {
|
|
784
|
+
it("registers unbound OpenClaw agents and writes the routing binding", async () => {
|
|
785
|
+
await withSandboxHome(async ({ tmp, fs, path: nodePath }) => {
|
|
786
|
+
const credDir = nodePath.join(tmp, ".botcord", "credentials");
|
|
787
|
+
fs.mkdirSync(credDir, { recursive: true });
|
|
788
|
+
fs.writeFileSync(
|
|
789
|
+
nodePath.join(credDir, "ag_seed.json"),
|
|
790
|
+
JSON.stringify({
|
|
791
|
+
version: 1,
|
|
792
|
+
hubUrl: "https://hub.example",
|
|
793
|
+
agentId: "ag_seed",
|
|
794
|
+
keyId: "k_seed",
|
|
795
|
+
privateKey: Buffer.alloc(32, 5).toString("base64"),
|
|
796
|
+
savedAt: new Date().toISOString(),
|
|
797
|
+
}),
|
|
798
|
+
);
|
|
799
|
+
mockState.cfg = {
|
|
800
|
+
defaultRoute: { adapter: "claude-code", cwd: "/tmp" },
|
|
801
|
+
routes: [],
|
|
802
|
+
streamBlocks: true,
|
|
803
|
+
agents: ["ag_seed"],
|
|
804
|
+
openclawGateways: [{ name: "local", url: "ws://127.0.0.1:18789" }],
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
const gw = makeFakeGateway(["ag_seed"]);
|
|
808
|
+
const register = vi.fn(async () => ({
|
|
809
|
+
agentId: "ag_adopted",
|
|
810
|
+
keyId: "k_adopted",
|
|
811
|
+
privateKey: Buffer.alloc(32, 31).toString("base64"),
|
|
812
|
+
publicKey: Buffer.alloc(32, 32).toString("base64"),
|
|
813
|
+
hubUrl: "https://hub.example",
|
|
814
|
+
token: "tok",
|
|
815
|
+
expiresAt: Date.now() + 60_000,
|
|
816
|
+
}));
|
|
817
|
+
|
|
818
|
+
const res = await adoptDiscoveredOpenclawAgents({
|
|
819
|
+
gateway: gw as unknown as Parameters<typeof adoptDiscoveredOpenclawAgents>[0]["gateway"],
|
|
820
|
+
register: register as unknown as Parameters<typeof adoptDiscoveredOpenclawAgents>[0]["register"],
|
|
821
|
+
cfg: mockState.cfg as unknown as DaemonConfig,
|
|
822
|
+
probe: async () => ({ ok: true, agents: [{ id: "main", name: "Main Agent" }] }),
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
expect(res.adopted).toEqual(["ag_adopted"]);
|
|
826
|
+
expect(register).toHaveBeenCalledWith("https://hub.example", "Main Agent", undefined);
|
|
827
|
+
const saved = JSON.parse(
|
|
828
|
+
fs.readFileSync(nodePath.join(credDir, "ag_adopted.json"), "utf8"),
|
|
829
|
+
) as Record<string, unknown>;
|
|
830
|
+
expect(saved.runtime).toBe("openclaw-acp");
|
|
831
|
+
expect(saved.openclawGateway).toBe("local");
|
|
832
|
+
expect(saved.openclawAgent).toBe("main");
|
|
833
|
+
expect((mockState.cfg.agents as string[])).toContain("ag_adopted");
|
|
834
|
+
expect(gw.addChannel).toHaveBeenCalledWith(
|
|
835
|
+
expect.objectContaining({ id: "ag_adopted", type: "botcord" }),
|
|
836
|
+
);
|
|
837
|
+
const route = gw.listManagedRoutes().find((r) => r.match?.accountId === "ag_adopted");
|
|
838
|
+
expect(route?.runtime).toBe("openclaw-acp");
|
|
839
|
+
expect(route?.gateway?.name).toBe("local");
|
|
840
|
+
expect(route?.gateway?.openclawAgent).toBe("main");
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
it("skips an OpenClaw agent that is already bound in credentials", async () => {
|
|
845
|
+
await withSandboxHome(async ({ tmp, fs, path: nodePath }) => {
|
|
846
|
+
const credDir = nodePath.join(tmp, ".botcord", "credentials");
|
|
847
|
+
fs.mkdirSync(credDir, { recursive: true });
|
|
848
|
+
fs.writeFileSync(
|
|
849
|
+
nodePath.join(credDir, "ag_existing.json"),
|
|
850
|
+
JSON.stringify({
|
|
851
|
+
version: 1,
|
|
852
|
+
hubUrl: "https://hub.example",
|
|
853
|
+
agentId: "ag_existing",
|
|
854
|
+
keyId: "k_existing",
|
|
855
|
+
privateKey: Buffer.alloc(32, 6).toString("base64"),
|
|
856
|
+
savedAt: new Date().toISOString(),
|
|
857
|
+
runtime: "openclaw-acp",
|
|
858
|
+
openclawGateway: "local",
|
|
859
|
+
openclawAgent: "main",
|
|
860
|
+
}),
|
|
861
|
+
);
|
|
862
|
+
mockState.cfg = {
|
|
863
|
+
defaultRoute: { adapter: "claude-code", cwd: "/tmp" },
|
|
864
|
+
routes: [],
|
|
865
|
+
streamBlocks: true,
|
|
866
|
+
agents: ["ag_existing"],
|
|
867
|
+
openclawGateways: [{ name: "local", url: "ws://127.0.0.1:18789" }],
|
|
868
|
+
};
|
|
869
|
+
const register = vi.fn();
|
|
870
|
+
|
|
871
|
+
const res = await adoptDiscoveredOpenclawAgents({
|
|
872
|
+
gateway: makeFakeGateway(["ag_existing"]) as unknown as Parameters<typeof adoptDiscoveredOpenclawAgents>[0]["gateway"],
|
|
873
|
+
register: register as unknown as Parameters<typeof adoptDiscoveredOpenclawAgents>[0]["register"],
|
|
874
|
+
cfg: mockState.cfg as unknown as DaemonConfig,
|
|
875
|
+
probe: async () => ({ ok: true, agents: [{ id: "main" }] }),
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
expect(res.adopted).toEqual([]);
|
|
879
|
+
expect(res.skipped).toEqual([
|
|
880
|
+
{ gateway: "local", openclawAgent: "main", reason: "already_bound" },
|
|
881
|
+
]);
|
|
882
|
+
expect(register).not.toHaveBeenCalled();
|
|
883
|
+
});
|
|
884
|
+
});
|
|
885
|
+
});
|
|
886
|
+
|
|
782
887
|
// ---------------------------------------------------------------------------
|
|
783
888
|
// revoke_agent — new flag semantics (plan §11.3)
|
|
784
889
|
// ---------------------------------------------------------------------------
|
package/src/config.ts
CHANGED
|
@@ -88,6 +88,17 @@ export interface AgentDiscoveryConfig {
|
|
|
88
88
|
credentialsDir?: string;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
export interface OpenclawDiscoveryConfig {
|
|
92
|
+
/** Defaults to true. */
|
|
93
|
+
enabled?: boolean;
|
|
94
|
+
/** Overrides the local config-file search roots. */
|
|
95
|
+
searchPaths?: string[];
|
|
96
|
+
/** Overrides the local loopback ports to probe. */
|
|
97
|
+
defaultPorts?: number[];
|
|
98
|
+
/** Defaults to true. When false, discovery only persists gateways. */
|
|
99
|
+
autoProvision?: boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
91
102
|
export interface DaemonConfig {
|
|
92
103
|
/**
|
|
93
104
|
* @deprecated Kept for backward compatibility with pre-multi-agent configs.
|
|
@@ -131,6 +142,12 @@ export interface DaemonConfig {
|
|
|
131
142
|
* so the dispatcher never re-queries this list.
|
|
132
143
|
*/
|
|
133
144
|
openclawGateways?: OpenclawGatewayProfile[];
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Daemon-side local OpenClaw discovery. Omitted means enabled with default
|
|
148
|
+
* search paths/ports and automatic adoption of discovered agents.
|
|
149
|
+
*/
|
|
150
|
+
openclawDiscovery?: OpenclawDiscoveryConfig;
|
|
134
151
|
}
|
|
135
152
|
|
|
136
153
|
/**
|
|
@@ -357,6 +374,25 @@ export function loadConfig(): DaemonConfig {
|
|
|
357
374
|
}
|
|
358
375
|
out.agentDiscovery = copy;
|
|
359
376
|
}
|
|
377
|
+
const openclawDiscovery = parsed.openclawDiscovery;
|
|
378
|
+
if (openclawDiscovery && typeof openclawDiscovery === "object") {
|
|
379
|
+
const copy: OpenclawDiscoveryConfig = {};
|
|
380
|
+
if (typeof openclawDiscovery.enabled === "boolean") copy.enabled = openclawDiscovery.enabled;
|
|
381
|
+
if (Array.isArray(openclawDiscovery.searchPaths)) {
|
|
382
|
+
copy.searchPaths = openclawDiscovery.searchPaths.filter(
|
|
383
|
+
(p): p is string => typeof p === "string" && p.length > 0,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
if (Array.isArray(openclawDiscovery.defaultPorts)) {
|
|
387
|
+
copy.defaultPorts = openclawDiscovery.defaultPorts.filter(
|
|
388
|
+
(p): p is number => Number.isInteger(p) && p > 0 && p < 65536,
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
if (typeof openclawDiscovery.autoProvision === "boolean") {
|
|
392
|
+
copy.autoProvision = openclawDiscovery.autoProvision;
|
|
393
|
+
}
|
|
394
|
+
out.openclawDiscovery = copy;
|
|
395
|
+
}
|
|
360
396
|
return out;
|
|
361
397
|
}
|
|
362
398
|
|
package/src/daemon.ts
CHANGED
|
@@ -23,7 +23,12 @@ import { ensureAgentWorkspace } from "./agent-workspace.js";
|
|
|
23
23
|
import { ControlChannel } from "./control-channel.js";
|
|
24
24
|
import { toGatewayConfig } from "./daemon-config-map.js";
|
|
25
25
|
import { log as daemonLog } from "./log.js";
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
adoptDiscoveredOpenclawAgents,
|
|
28
|
+
collectRuntimeSnapshot,
|
|
29
|
+
createProvisioner,
|
|
30
|
+
} from "./provision.js";
|
|
31
|
+
import { openclawAutoProvisionEnabled } from "./openclaw-discovery.js";
|
|
27
32
|
import { SnapshotWriter } from "./snapshot-writer.js";
|
|
28
33
|
import { createDaemonSystemContextBuilder } from "./system-context.js";
|
|
29
34
|
import { createRoomStaticContextBuilder } from "./room-context.js";
|
|
@@ -422,6 +427,30 @@ export async function startDaemon(opts: DaemonRuntimeOptions): Promise<DaemonHan
|
|
|
422
427
|
await gateway.start();
|
|
423
428
|
logger.info("daemon started", { agents: agentIds });
|
|
424
429
|
|
|
430
|
+
if (openclawAutoProvisionEnabled(opts.config)) {
|
|
431
|
+
try {
|
|
432
|
+
const adopted = await adoptDiscoveredOpenclawAgents({
|
|
433
|
+
gateway,
|
|
434
|
+
cfg: opts.config,
|
|
435
|
+
});
|
|
436
|
+
if (
|
|
437
|
+
adopted.adopted.length > 0 ||
|
|
438
|
+
adopted.failed.length > 0 ||
|
|
439
|
+
adopted.skipped.length > 0
|
|
440
|
+
) {
|
|
441
|
+
logger.info("openclaw auto-provision completed", {
|
|
442
|
+
adopted: adopted.adopted,
|
|
443
|
+
skipped: adopted.skipped,
|
|
444
|
+
failed: adopted.failed,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
} catch (err) {
|
|
448
|
+
logger.warn("openclaw auto-provision failed; continuing", {
|
|
449
|
+
error: err instanceof Error ? err.message : String(err),
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
425
454
|
// Control channel is optional — daemon still runs (data-plane only)
|
|
426
455
|
// when user-auth hasn't been set up yet. Operators can `login` later
|
|
427
456
|
// without restarting, but for P0 we require a restart to pick it up.
|
package/src/index.ts
CHANGED
|
@@ -57,6 +57,11 @@ import {
|
|
|
57
57
|
updateWorkingMemory,
|
|
58
58
|
DEFAULT_SECTION,
|
|
59
59
|
} from "./working-memory.js";
|
|
60
|
+
import {
|
|
61
|
+
discoverLocalOpenclawGateways,
|
|
62
|
+
mergeOpenclawGateways,
|
|
63
|
+
openclawDiscoveryConfigEnabled,
|
|
64
|
+
} from "./openclaw-discovery.js";
|
|
60
65
|
|
|
61
66
|
const ADAPTER_LIST = listAdapterIds().join("|");
|
|
62
67
|
|
|
@@ -402,7 +407,28 @@ async function ensureUserAuthForStart(args: ParsedArgs): Promise<UserAuthRecord
|
|
|
402
407
|
}
|
|
403
408
|
|
|
404
409
|
async function cmdStart(args: ParsedArgs): Promise<void> {
|
|
405
|
-
|
|
410
|
+
let cfg = loadOrInitConfig(args);
|
|
411
|
+
if (openclawDiscoveryConfigEnabled(cfg)) {
|
|
412
|
+
try {
|
|
413
|
+
const found = await discoverLocalOpenclawGateways({
|
|
414
|
+
searchPaths: cfg.openclawDiscovery?.searchPaths,
|
|
415
|
+
defaultPorts: cfg.openclawDiscovery?.defaultPorts,
|
|
416
|
+
timeoutMs: 500,
|
|
417
|
+
});
|
|
418
|
+
const merged = mergeOpenclawGateways(cfg, found);
|
|
419
|
+
if (merged.changed) {
|
|
420
|
+
cfg = merged.cfg;
|
|
421
|
+
saveConfig(cfg);
|
|
422
|
+
log.info("openclaw discovery: gateways merged", {
|
|
423
|
+
added: merged.added.map((g) => ({ name: g.name, url: g.url })),
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
} catch (err) {
|
|
427
|
+
log.warn("openclaw discovery failed; continuing", {
|
|
428
|
+
error: err instanceof Error ? err.message : String(err),
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
}
|
|
406
432
|
// Foreground is now the default. --background (alias -d) detaches.
|
|
407
433
|
// --foreground is still accepted (no-op) for backwards compatibility and
|
|
408
434
|
// is also what the detached child re-execs itself with.
|