@botcord/daemon 0.2.23 → 0.2.25
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/gateway/dispatcher.js +35 -1
- package/dist/gateway/runtimes/openclaw-acp.js +2 -4
- package/dist/provision.js +60 -6
- package/package.json +1 -1
- package/src/__tests__/openclaw-acp.test.ts +17 -3
- package/src/__tests__/provision.test.ts +39 -0
- package/src/gateway/__tests__/dispatcher.test.ts +12 -0
- package/src/gateway/dispatcher.ts +35 -1
- package/src/gateway/runtimes/openclaw-acp.ts +2 -7
- package/src/provision.ts +68 -6
|
@@ -1015,6 +1015,40 @@ export class Dispatcher {
|
|
|
1015
1015
|
const replyText = (result.text || "").trim();
|
|
1016
1016
|
const finalTextField = truncateTextField(result.text || "");
|
|
1017
1017
|
if (!replyText) {
|
|
1018
|
+
if (result.error) {
|
|
1019
|
+
this.log.warn("dispatcher: runtime returned error without reply text", {
|
|
1020
|
+
agentId: msg.accountId,
|
|
1021
|
+
roomId: msg.conversation.id,
|
|
1022
|
+
topicId: msg.conversation.threadId ?? null,
|
|
1023
|
+
turnId,
|
|
1024
|
+
runtime: route.runtime,
|
|
1025
|
+
error: result.error,
|
|
1026
|
+
});
|
|
1027
|
+
if (isOwnerChat) {
|
|
1028
|
+
const sendResult = await this.sendReply(channel, {
|
|
1029
|
+
channel: msg.channel,
|
|
1030
|
+
accountId: msg.accountId,
|
|
1031
|
+
conversationId: msg.conversation.id,
|
|
1032
|
+
threadId: msg.conversation.threadId ?? null,
|
|
1033
|
+
text: `⚠️ Runtime error: ${truncate(result.error, 500)}`,
|
|
1034
|
+
replyTo: msg.id,
|
|
1035
|
+
traceId: msg.trace?.id ?? null,
|
|
1036
|
+
}, turnId);
|
|
1037
|
+
this.emitOutbound({
|
|
1038
|
+
turnId,
|
|
1039
|
+
msg,
|
|
1040
|
+
runtime: route.runtime,
|
|
1041
|
+
runtimeSessionId: result.newSessionId || null,
|
|
1042
|
+
startedAt: slot.dispatchedAt,
|
|
1043
|
+
costUsd: result.costUsd,
|
|
1044
|
+
finalText: finalTextField,
|
|
1045
|
+
deliveryStatus: sendResult.ok ? "delivered" : "send_failed",
|
|
1046
|
+
deliveryReason: sendResult.ok ? null : sendResult.error,
|
|
1047
|
+
blocks: slot.blocks,
|
|
1048
|
+
});
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1018
1052
|
this.emitOutbound({
|
|
1019
1053
|
turnId,
|
|
1020
1054
|
msg,
|
|
@@ -1024,7 +1058,7 @@ export class Dispatcher {
|
|
|
1024
1058
|
costUsd: result.costUsd,
|
|
1025
1059
|
finalText: finalTextField,
|
|
1026
1060
|
deliveryStatus: "empty_text",
|
|
1027
|
-
deliveryReason: null,
|
|
1061
|
+
deliveryReason: result.error ?? null,
|
|
1028
1062
|
blocks: slot.blocks,
|
|
1029
1063
|
});
|
|
1030
1064
|
return;
|
|
@@ -97,11 +97,9 @@ export class OpenclawAcpAdapter {
|
|
|
97
97
|
if (!gateway) {
|
|
98
98
|
return failResult(opts.sessionId ?? "", "openclaw-acp: missing gateway endpoint (route.gateway not resolved)");
|
|
99
99
|
}
|
|
100
|
-
|
|
101
|
-
return failResult(opts.sessionId ?? "", `openclaw-acp: gateway "${gateway.name}" did not resolve an openclawAgent (set defaultAgent on the profile or openclawAgent on the route)`);
|
|
102
|
-
}
|
|
100
|
+
const openclawAgent = gateway.openclawAgent ?? "default";
|
|
103
101
|
const sessionKey = buildAcpSessionKey({
|
|
104
|
-
openclawAgent
|
|
102
|
+
openclawAgent,
|
|
105
103
|
accountId: opts.accountId,
|
|
106
104
|
// The dispatcher passes `context.conversationKey` in for routing;
|
|
107
105
|
// fall back to a stable per-accountId key when it's not present (e.g.
|
package/dist/provision.js
CHANGED
|
@@ -189,7 +189,9 @@ async function provisionAgent(params, ctx) {
|
|
|
189
189
|
// that hole by moving the check to the union of both.
|
|
190
190
|
const explicitCwd = params.credentials?.cwd ?? params.cwd;
|
|
191
191
|
assertSafeCwd(explicitCwd);
|
|
192
|
-
const
|
|
192
|
+
const initialCfg = loadConfig();
|
|
193
|
+
const openclawSel = await resolveProvisionOpenclawSelection(params, initialCfg);
|
|
194
|
+
const resolvedParams = withResolvedOpenclawSelection(params, openclawSel);
|
|
193
195
|
if (openclawSel.gateway && openclawSel.agent) {
|
|
194
196
|
return withOpenclawProvisionLock(openclawSel.gateway, openclawSel.agent, async () => {
|
|
195
197
|
const existing = findCredentialsByOpenclaw(openclawSel.gateway, openclawSel.agent);
|
|
@@ -202,7 +204,7 @@ async function provisionAgent(params, ctx) {
|
|
|
202
204
|
return installExistingOpenclawBinding(existing.agentId, ctx);
|
|
203
205
|
}
|
|
204
206
|
const cfg = loadConfig();
|
|
205
|
-
const credentials = await materializeCredentials(
|
|
207
|
+
const credentials = await materializeCredentials(resolvedParams, cfg, ctx, explicitCwd);
|
|
206
208
|
return installLocalAgent(credentials, {
|
|
207
209
|
...ctx,
|
|
208
210
|
cfg,
|
|
@@ -211,11 +213,10 @@ async function provisionAgent(params, ctx) {
|
|
|
211
213
|
});
|
|
212
214
|
});
|
|
213
215
|
}
|
|
214
|
-
const
|
|
215
|
-
const credentials = await materializeCredentials(params, cfg, ctx, explicitCwd);
|
|
216
|
+
const credentials = await materializeCredentials(resolvedParams, initialCfg, ctx, explicitCwd);
|
|
216
217
|
return installLocalAgent(credentials, {
|
|
217
218
|
...ctx,
|
|
218
|
-
cfg,
|
|
219
|
+
cfg: initialCfg,
|
|
219
220
|
bio: params.bio,
|
|
220
221
|
source: params.credentials ? "hub-supplied" : "registered",
|
|
221
222
|
});
|
|
@@ -542,6 +543,59 @@ function pickOpenclawSelection(params) {
|
|
|
542
543
|
}
|
|
543
544
|
return out;
|
|
544
545
|
}
|
|
546
|
+
async function resolveProvisionOpenclawSelection(params, cfg) {
|
|
547
|
+
const out = pickOpenclawSelection(params);
|
|
548
|
+
if (!out.gateway || out.agent)
|
|
549
|
+
return out;
|
|
550
|
+
const profile = (cfg.openclawGateways ?? []).find((g) => g.name === out.gateway);
|
|
551
|
+
if (!profile)
|
|
552
|
+
return out;
|
|
553
|
+
const prepared = prepareGatewayProfile(profile);
|
|
554
|
+
if (prepared.defaultAgent) {
|
|
555
|
+
out.agent = prepared.defaultAgent;
|
|
556
|
+
return out;
|
|
557
|
+
}
|
|
558
|
+
if (isLoopbackUrl(prepared.url)) {
|
|
559
|
+
const localAgents = readLocalOpenclawAgents();
|
|
560
|
+
const defaultAgent = localAgents?.find((a) => a.id === "default") ?? (localAgents?.length === 1 ? localAgents[0] : undefined);
|
|
561
|
+
if (defaultAgent) {
|
|
562
|
+
out.agent = defaultAgent.id;
|
|
563
|
+
return out;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
try {
|
|
567
|
+
const probeResult = await probeOpenclawAgents(prepared);
|
|
568
|
+
if (probeResult.ok) {
|
|
569
|
+
const agents = probeResult.agents ?? [];
|
|
570
|
+
const defaultAgent = agents.find((a) => a.id === "default") ?? (agents.length === 1 ? agents[0] : undefined);
|
|
571
|
+
if (defaultAgent) {
|
|
572
|
+
out.agent = defaultAgent.id;
|
|
573
|
+
return out;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
catch (err) {
|
|
578
|
+
daemonLog.debug("provision_agent: openclaw default probe failed", {
|
|
579
|
+
gateway: out.gateway,
|
|
580
|
+
error: err instanceof Error ? err.message : String(err),
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
if (isLoopbackUrl(prepared.url))
|
|
584
|
+
out.agent = "default";
|
|
585
|
+
return out;
|
|
586
|
+
}
|
|
587
|
+
function withResolvedOpenclawSelection(params, selection) {
|
|
588
|
+
if (!selection.gateway)
|
|
589
|
+
return params;
|
|
590
|
+
return {
|
|
591
|
+
...params,
|
|
592
|
+
openclaw: {
|
|
593
|
+
...(params.openclaw ?? {}),
|
|
594
|
+
gateway: selection.gateway,
|
|
595
|
+
...(selection.agent ? { agent: selection.agent } : {}),
|
|
596
|
+
},
|
|
597
|
+
};
|
|
598
|
+
}
|
|
545
599
|
async function withOpenclawProvisionLock(gateway, agent, fn) {
|
|
546
600
|
const key = `${gateway}\0${agent}`;
|
|
547
601
|
const prev = openclawProvisionLocks.get(key) ?? Promise.resolve();
|
|
@@ -1068,7 +1122,7 @@ function readLocalOpenclawAgents() {
|
|
|
1068
1122
|
try {
|
|
1069
1123
|
const file = path.join(homedir(), ".openclaw", "openclaw.json");
|
|
1070
1124
|
if (!existsSync(file))
|
|
1071
|
-
return
|
|
1125
|
+
return [{ id: "default" }];
|
|
1072
1126
|
const cfg = JSON.parse(readFileSync(file, "utf8"));
|
|
1073
1127
|
const list = Array.isArray(cfg?.agents?.list) ? cfg.agents.list : [];
|
|
1074
1128
|
const defaultId = typeof cfg?.agents?.defaults?.id === "string" ? cfg.agents.defaults.id : "default";
|
package/package.json
CHANGED
|
@@ -69,12 +69,26 @@ describe("OpenclawAcpAdapter.run", () => {
|
|
|
69
69
|
expect(res.error).toMatch(/missing gateway/);
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
it("
|
|
73
|
-
const
|
|
72
|
+
it("defaults to OpenClaw's default agent when gateway has no openclawAgent resolved", async () => {
|
|
73
|
+
const child = new FakeChild();
|
|
74
|
+
const adapter = new OpenclawAcpAdapter({ spawnFn: makeSpawn(child) });
|
|
74
75
|
const gateway: ResolvedOpenclawGateway = {
|
|
75
76
|
name: "local",
|
|
76
77
|
url: "ws://127.0.0.1:1",
|
|
77
78
|
};
|
|
79
|
+
child.stdin.on("data", (chunk: Buffer) => {
|
|
80
|
+
for (const line of chunk.toString("utf8").split("\n").filter(Boolean)) {
|
|
81
|
+
const frame = JSON.parse(line);
|
|
82
|
+
if (frame.method === "initialize") {
|
|
83
|
+
child.stdout.write(JSON.stringify({ jsonrpc: "2.0", id: frame.id, result: { protocolVersion: 1 } }) + "\n");
|
|
84
|
+
} else if (frame.method === "session/new") {
|
|
85
|
+
expect(frame.params._meta.sessionKey).toContain("agent:default:");
|
|
86
|
+
child.stdout.write(JSON.stringify({ jsonrpc: "2.0", id: frame.id, result: { sessionId: "sid-default" } }) + "\n");
|
|
87
|
+
} else if (frame.method === "session/prompt") {
|
|
88
|
+
child.stdout.write(JSON.stringify({ jsonrpc: "2.0", id: frame.id, result: { text: "ok" } }) + "\n");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
78
92
|
const res = await adapter.run({
|
|
79
93
|
text: "hi",
|
|
80
94
|
sessionId: null,
|
|
@@ -84,7 +98,7 @@ describe("OpenclawAcpAdapter.run", () => {
|
|
|
84
98
|
trustLevel: "owner",
|
|
85
99
|
gateway,
|
|
86
100
|
});
|
|
87
|
-
expect(res.
|
|
101
|
+
expect(res.text).toBe("ok");
|
|
88
102
|
});
|
|
89
103
|
|
|
90
104
|
it("performs initialize → newSession → prompt and returns final text", async () => {
|
|
@@ -585,6 +585,45 @@ describe("provision_agent seeds workspace + hot-adds managed route", () => {
|
|
|
585
585
|
});
|
|
586
586
|
});
|
|
587
587
|
|
|
588
|
+
it("binds OpenClaw default agent when provisioning only specifies a loopback gateway", async () => {
|
|
589
|
+
await withSandboxHome(async ({ tmp, fs, path: nodePath }) => {
|
|
590
|
+
mockState.cfg = {
|
|
591
|
+
defaultRoute: { adapter: "claude-code", cwd: "/tmp" },
|
|
592
|
+
routes: [],
|
|
593
|
+
streamBlocks: true,
|
|
594
|
+
openclawGateways: [{ name: "local", url: "ws://127.0.0.1:18789" }],
|
|
595
|
+
};
|
|
596
|
+
const gw = makeFakeGateway();
|
|
597
|
+
const provisioner = createProvisioner({
|
|
598
|
+
gateway: gw as unknown as Parameters<typeof createProvisioner>[0]["gateway"],
|
|
599
|
+
});
|
|
600
|
+
const privateKey = Buffer.alloc(32, 14).toString("base64");
|
|
601
|
+
const ack = await provisioner({
|
|
602
|
+
id: "req_openclaw_default",
|
|
603
|
+
type: CONTROL_FRAME_TYPES.PROVISION_AGENT,
|
|
604
|
+
params: {
|
|
605
|
+
runtime: "openclaw-acp",
|
|
606
|
+
openclaw: { gateway: "local" },
|
|
607
|
+
credentials: {
|
|
608
|
+
agentId: "ag_openclaw_default",
|
|
609
|
+
keyId: "k_ocd",
|
|
610
|
+
privateKey,
|
|
611
|
+
hubUrl: "https://hub.example",
|
|
612
|
+
},
|
|
613
|
+
},
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
expect(ack.ok).toBe(true);
|
|
617
|
+
const credFile = nodePath.join(tmp, ".botcord", "credentials", "ag_openclaw_default.json");
|
|
618
|
+
const saved = JSON.parse(fs.readFileSync(credFile, "utf8")) as Record<string, unknown>;
|
|
619
|
+
expect(saved.openclawGateway).toBe("local");
|
|
620
|
+
expect(saved.openclawAgent).toBe("default");
|
|
621
|
+
const route = gw.listManagedRoutes().find((r) => r.match?.accountId === "ag_openclaw_default");
|
|
622
|
+
expect(route?.gateway?.name).toBe("local");
|
|
623
|
+
expect(route?.gateway?.openclawAgent).toBe("default");
|
|
624
|
+
});
|
|
625
|
+
});
|
|
626
|
+
|
|
588
627
|
it("defaults cwd to agentWorkspaceDir on the slow path (daemon register)", async () => {
|
|
589
628
|
await withSandboxHome(async ({ tmp, fs, path: nodePath }) => {
|
|
590
629
|
// Seed an existing credential so `inferHubUrl` finds a hubUrl.
|
|
@@ -430,6 +430,18 @@ describe("Dispatcher", () => {
|
|
|
430
430
|
expect(store.all().length).toBe(0);
|
|
431
431
|
});
|
|
432
432
|
|
|
433
|
+
it("runtime empty text with error: sends owner-chat error reply", async () => {
|
|
434
|
+
const runtime = new FakeRuntime({ reply: "", newSessionId: "", errorText: "missing openclawAgent" });
|
|
435
|
+
const channel = new FakeChannel();
|
|
436
|
+
const { dispatcher } = await scaffold({ channel, runtimeFactory: () => runtime });
|
|
437
|
+
|
|
438
|
+
await dispatcher.handle(makeEnvelope({ id: "msg_error" }));
|
|
439
|
+
|
|
440
|
+
expect(channel.sends.length).toBe(1);
|
|
441
|
+
expect(channel.sends[0].message.text).toContain("Runtime error");
|
|
442
|
+
expect(channel.sends[0].message.text).toContain("missing openclawAgent");
|
|
443
|
+
});
|
|
444
|
+
|
|
433
445
|
it("cancel-previous: prior turn is aborted and does not write session, new turn writes", async () => {
|
|
434
446
|
const prior = new FakeRuntime({ hang: true, newSessionId: "prior-sid" });
|
|
435
447
|
const newer = new FakeRuntime({ reply: "newer", newSessionId: "newer-sid" });
|
|
@@ -1228,6 +1228,40 @@ export class Dispatcher {
|
|
|
1228
1228
|
const finalTextField = truncateTextField(result.text || "");
|
|
1229
1229
|
|
|
1230
1230
|
if (!replyText) {
|
|
1231
|
+
if (result.error) {
|
|
1232
|
+
this.log.warn("dispatcher: runtime returned error without reply text", {
|
|
1233
|
+
agentId: msg.accountId,
|
|
1234
|
+
roomId: msg.conversation.id,
|
|
1235
|
+
topicId: msg.conversation.threadId ?? null,
|
|
1236
|
+
turnId,
|
|
1237
|
+
runtime: route.runtime,
|
|
1238
|
+
error: result.error,
|
|
1239
|
+
});
|
|
1240
|
+
if (isOwnerChat) {
|
|
1241
|
+
const sendResult = await this.sendReply(channel, {
|
|
1242
|
+
channel: msg.channel,
|
|
1243
|
+
accountId: msg.accountId,
|
|
1244
|
+
conversationId: msg.conversation.id,
|
|
1245
|
+
threadId: msg.conversation.threadId ?? null,
|
|
1246
|
+
text: `⚠️ Runtime error: ${truncate(result.error, 500)}`,
|
|
1247
|
+
replyTo: msg.id,
|
|
1248
|
+
traceId: msg.trace?.id ?? null,
|
|
1249
|
+
}, turnId);
|
|
1250
|
+
this.emitOutbound({
|
|
1251
|
+
turnId,
|
|
1252
|
+
msg,
|
|
1253
|
+
runtime: route.runtime,
|
|
1254
|
+
runtimeSessionId: result.newSessionId || null,
|
|
1255
|
+
startedAt: slot.dispatchedAt,
|
|
1256
|
+
costUsd: result.costUsd,
|
|
1257
|
+
finalText: finalTextField,
|
|
1258
|
+
deliveryStatus: sendResult.ok ? "delivered" : "send_failed",
|
|
1259
|
+
deliveryReason: sendResult.ok ? null : sendResult.error,
|
|
1260
|
+
blocks: slot.blocks,
|
|
1261
|
+
});
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1231
1265
|
this.emitOutbound({
|
|
1232
1266
|
turnId,
|
|
1233
1267
|
msg,
|
|
@@ -1237,7 +1271,7 @@ export class Dispatcher {
|
|
|
1237
1271
|
costUsd: result.costUsd,
|
|
1238
1272
|
finalText: finalTextField,
|
|
1239
1273
|
deliveryStatus: "empty_text",
|
|
1240
|
-
deliveryReason: null,
|
|
1274
|
+
deliveryReason: result.error ?? null,
|
|
1241
1275
|
blocks: slot.blocks,
|
|
1242
1276
|
});
|
|
1243
1277
|
return;
|
|
@@ -169,14 +169,9 @@ export class OpenclawAcpAdapter implements RuntimeAdapter {
|
|
|
169
169
|
"openclaw-acp: missing gateway endpoint (route.gateway not resolved)",
|
|
170
170
|
);
|
|
171
171
|
}
|
|
172
|
-
|
|
173
|
-
return failResult(
|
|
174
|
-
opts.sessionId ?? "",
|
|
175
|
-
`openclaw-acp: gateway "${gateway.name}" did not resolve an openclawAgent (set defaultAgent on the profile or openclawAgent on the route)`,
|
|
176
|
-
);
|
|
177
|
-
}
|
|
172
|
+
const openclawAgent = gateway.openclawAgent ?? "default";
|
|
178
173
|
const sessionKey = buildAcpSessionKey({
|
|
179
|
-
openclawAgent
|
|
174
|
+
openclawAgent,
|
|
180
175
|
accountId: opts.accountId,
|
|
181
176
|
// The dispatcher passes `context.conversationKey` in for routing;
|
|
182
177
|
// fall back to a stable per-accountId key when it's not present (e.g.
|
package/src/provision.ts
CHANGED
|
@@ -316,7 +316,9 @@ async function provisionAgent(
|
|
|
316
316
|
const explicitCwd = params.credentials?.cwd ?? params.cwd;
|
|
317
317
|
assertSafeCwd(explicitCwd);
|
|
318
318
|
|
|
319
|
-
const
|
|
319
|
+
const initialCfg = loadConfig();
|
|
320
|
+
const openclawSel = await resolveProvisionOpenclawSelection(params, initialCfg);
|
|
321
|
+
const resolvedParams = withResolvedOpenclawSelection(params, openclawSel);
|
|
320
322
|
if (openclawSel.gateway && openclawSel.agent) {
|
|
321
323
|
return withOpenclawProvisionLock(openclawSel.gateway, openclawSel.agent, async () => {
|
|
322
324
|
const existing = findCredentialsByOpenclaw(openclawSel.gateway!, openclawSel.agent!);
|
|
@@ -329,7 +331,7 @@ async function provisionAgent(
|
|
|
329
331
|
return installExistingOpenclawBinding(existing.agentId, ctx);
|
|
330
332
|
}
|
|
331
333
|
const cfg = loadConfig();
|
|
332
|
-
const credentials = await materializeCredentials(
|
|
334
|
+
const credentials = await materializeCredentials(resolvedParams, cfg, ctx, explicitCwd);
|
|
333
335
|
return installLocalAgent(credentials, {
|
|
334
336
|
...ctx,
|
|
335
337
|
cfg,
|
|
@@ -339,11 +341,10 @@ async function provisionAgent(
|
|
|
339
341
|
});
|
|
340
342
|
}
|
|
341
343
|
|
|
342
|
-
const
|
|
343
|
-
const credentials = await materializeCredentials(params, cfg, ctx, explicitCwd);
|
|
344
|
+
const credentials = await materializeCredentials(resolvedParams, initialCfg, ctx, explicitCwd);
|
|
344
345
|
return installLocalAgent(credentials, {
|
|
345
346
|
...ctx,
|
|
346
|
-
cfg,
|
|
347
|
+
cfg: initialCfg,
|
|
347
348
|
bio: params.bio,
|
|
348
349
|
source: params.credentials ? "hub-supplied" : "registered",
|
|
349
350
|
});
|
|
@@ -688,6 +689,67 @@ function pickOpenclawSelection(
|
|
|
688
689
|
return out;
|
|
689
690
|
}
|
|
690
691
|
|
|
692
|
+
async function resolveProvisionOpenclawSelection(
|
|
693
|
+
params: ProvisionAgentParams,
|
|
694
|
+
cfg: DaemonConfig,
|
|
695
|
+
): Promise<{ gateway?: string; agent?: string }> {
|
|
696
|
+
const out = pickOpenclawSelection(params);
|
|
697
|
+
if (!out.gateway || out.agent) return out;
|
|
698
|
+
|
|
699
|
+
const profile = (cfg.openclawGateways ?? []).find((g) => g.name === out.gateway);
|
|
700
|
+
if (!profile) return out;
|
|
701
|
+
|
|
702
|
+
const prepared = prepareGatewayProfile(profile);
|
|
703
|
+
if (prepared.defaultAgent) {
|
|
704
|
+
out.agent = prepared.defaultAgent;
|
|
705
|
+
return out;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (isLoopbackUrl(prepared.url)) {
|
|
709
|
+
const localAgents = readLocalOpenclawAgents();
|
|
710
|
+
const defaultAgent = localAgents?.find((a) => a.id === "default") ?? (localAgents?.length === 1 ? localAgents[0] : undefined);
|
|
711
|
+
if (defaultAgent) {
|
|
712
|
+
out.agent = defaultAgent.id;
|
|
713
|
+
return out;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
try {
|
|
718
|
+
const probeResult = await probeOpenclawAgents(prepared);
|
|
719
|
+
if (probeResult.ok) {
|
|
720
|
+
const agents = probeResult.agents ?? [];
|
|
721
|
+
const defaultAgent = agents.find((a) => a.id === "default") ?? (agents.length === 1 ? agents[0] : undefined);
|
|
722
|
+
if (defaultAgent) {
|
|
723
|
+
out.agent = defaultAgent.id;
|
|
724
|
+
return out;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
} catch (err) {
|
|
728
|
+
daemonLog.debug("provision_agent: openclaw default probe failed", {
|
|
729
|
+
gateway: out.gateway,
|
|
730
|
+
error: err instanceof Error ? err.message : String(err),
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (isLoopbackUrl(prepared.url)) out.agent = "default";
|
|
735
|
+
return out;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function withResolvedOpenclawSelection(
|
|
739
|
+
params: ProvisionAgentParams,
|
|
740
|
+
selection: { gateway?: string; agent?: string },
|
|
741
|
+
): ProvisionAgentParams {
|
|
742
|
+
if (!selection.gateway) return params;
|
|
743
|
+
return {
|
|
744
|
+
...params,
|
|
745
|
+
openclaw: {
|
|
746
|
+
...(params.openclaw ?? {}),
|
|
747
|
+
gateway: selection.gateway,
|
|
748
|
+
...(selection.agent ? { agent: selection.agent } : {}),
|
|
749
|
+
},
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
|
|
691
753
|
async function withOpenclawProvisionLock<T>(
|
|
692
754
|
gateway: string,
|
|
693
755
|
agent: string,
|
|
@@ -1294,7 +1356,7 @@ function readLocalOpenclawAgents(): Array<{
|
|
|
1294
1356
|
}> | null {
|
|
1295
1357
|
try {
|
|
1296
1358
|
const file = path.join(homedir(), ".openclaw", "openclaw.json");
|
|
1297
|
-
if (!existsSync(file)) return
|
|
1359
|
+
if (!existsSync(file)) return [{ id: "default" }];
|
|
1298
1360
|
const cfg = JSON.parse(readFileSync(file, "utf8")) as any;
|
|
1299
1361
|
const list = Array.isArray(cfg?.agents?.list) ? cfg.agents.list : [];
|
|
1300
1362
|
const defaultId = typeof cfg?.agents?.defaults?.id === "string" ? cfg.agents.defaults.id : "default";
|