@botcord/daemon 0.2.5 → 0.2.8
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/agent-discovery.d.ts +4 -0
- package/dist/agent-discovery.js +8 -0
- package/dist/agent-workspace.d.ts +62 -0
- package/dist/agent-workspace.js +140 -8
- package/dist/config.d.ts +64 -1
- package/dist/config.js +73 -1
- package/dist/daemon-config-map.d.ts +27 -9
- package/dist/daemon-config-map.js +105 -8
- package/dist/daemon.d.ts +2 -0
- package/dist/daemon.js +76 -6
- package/dist/doctor.d.ts +27 -1
- package/dist/doctor.js +22 -1
- package/dist/gateway/cli-resolver.d.ts +34 -0
- package/dist/gateway/cli-resolver.js +74 -0
- package/dist/gateway/dispatcher.d.ts +31 -1
- package/dist/gateway/dispatcher.js +337 -29
- package/dist/gateway/gateway.d.ts +29 -1
- package/dist/gateway/gateway.js +10 -0
- package/dist/gateway/index.d.ts +2 -0
- package/dist/gateway/index.js +2 -0
- package/dist/gateway/policy-resolver.d.ts +57 -0
- package/dist/gateway/policy-resolver.js +123 -0
- package/dist/gateway/runtimes/acp-stream.d.ts +99 -0
- package/dist/gateway/runtimes/acp-stream.js +394 -0
- package/dist/gateway/runtimes/codex.js +7 -0
- package/dist/gateway/runtimes/hermes-agent.d.ts +83 -0
- package/dist/gateway/runtimes/hermes-agent.js +180 -0
- package/dist/gateway/runtimes/ndjson-stream.d.ts +7 -2
- package/dist/gateway/runtimes/ndjson-stream.js +16 -3
- package/dist/gateway/runtimes/openclaw-acp.d.ts +44 -0
- package/dist/gateway/runtimes/openclaw-acp.js +500 -0
- package/dist/gateway/runtimes/registry.d.ts +4 -0
- package/dist/gateway/runtimes/registry.js +22 -0
- package/dist/gateway/transcript-paths.d.ts +30 -0
- package/dist/gateway/transcript-paths.js +114 -0
- package/dist/gateway/transcript.d.ts +123 -0
- package/dist/gateway/transcript.js +147 -0
- package/dist/gateway/types.d.ts +31 -0
- package/dist/index.js +309 -27
- package/dist/mention-scan.d.ts +22 -0
- package/dist/mention-scan.js +35 -0
- package/dist/openclaw-discovery.d.ts +28 -0
- package/dist/openclaw-discovery.js +228 -0
- package/dist/provision.d.ts +113 -1
- package/dist/provision.js +564 -12
- package/dist/system-context.d.ts +5 -4
- package/dist/system-context.js +35 -5
- package/dist/url-utils.d.ts +9 -0
- package/dist/url-utils.js +18 -0
- package/package.json +3 -2
- package/src/__tests__/agent-workspace.test.ts +93 -0
- package/src/__tests__/daemon-config-map.test.ts +79 -0
- package/src/__tests__/openclaw-acp.test.ts +234 -0
- package/src/__tests__/openclaw-discovery.test.ts +150 -0
- package/src/__tests__/policy-resolver.test.ts +124 -0
- package/src/__tests__/policy-updated-handler.test.ts +144 -0
- package/src/__tests__/provision.test.ts +265 -0
- package/src/__tests__/system-context.test.ts +52 -0
- package/src/__tests__/url-utils.test.ts +37 -0
- package/src/agent-discovery.ts +8 -0
- package/src/agent-workspace.ts +173 -7
- package/src/config.ts +168 -4
- package/src/daemon-config-map.ts +154 -9
- package/src/daemon.ts +96 -6
- package/src/doctor.ts +49 -2
- package/src/gateway/__tests__/dispatcher.test.ts +65 -0
- package/src/gateway/__tests__/hermes-agent-adapter.test.ts +302 -0
- package/src/gateway/__tests__/transcript.test.ts +496 -0
- package/src/gateway/cli-resolver.ts +92 -0
- package/src/gateway/dispatcher.ts +394 -26
- package/src/gateway/gateway.ts +46 -0
- package/src/gateway/index.ts +25 -0
- package/src/gateway/policy-resolver.ts +171 -0
- package/src/gateway/runtimes/acp-stream.ts +535 -0
- package/src/gateway/runtimes/codex.ts +7 -0
- package/src/gateway/runtimes/hermes-agent.ts +206 -0
- package/src/gateway/runtimes/ndjson-stream.ts +16 -3
- package/src/gateway/runtimes/openclaw-acp.ts +606 -0
- package/src/gateway/runtimes/registry.ts +24 -0
- package/src/gateway/transcript-paths.ts +145 -0
- package/src/gateway/transcript.ts +300 -0
- package/src/gateway/types.ts +32 -0
- package/src/index.ts +321 -30
- package/src/mention-scan.ts +38 -0
- package/src/openclaw-discovery.ts +262 -0
- package/src/provision.ts +682 -14
- package/src/system-context.ts +41 -9
- package/src/url-utils.ts +17 -0
package/src/provision.ts
CHANGED
|
@@ -14,16 +14,21 @@ import {
|
|
|
14
14
|
derivePublicKey,
|
|
15
15
|
loadStoredCredentials,
|
|
16
16
|
writeCredentialsFile,
|
|
17
|
+
type AgentIdentitySnapshot,
|
|
17
18
|
type ControlAck,
|
|
18
19
|
type ControlFrame,
|
|
20
|
+
type HelloParams,
|
|
19
21
|
type ListRuntimesResult,
|
|
20
22
|
type ProvisionAgentParams,
|
|
21
23
|
type RevokeAgentParams,
|
|
22
24
|
type RevokeAgentResult,
|
|
23
25
|
type RuntimeProbeResult,
|
|
24
26
|
type StoredBotCordCredentials,
|
|
27
|
+
type UpdateAgentParams,
|
|
25
28
|
} from "@botcord/protocol-core";
|
|
26
29
|
import type { Gateway } from "./gateway/index.js";
|
|
30
|
+
import type { PolicyResolverLike } from "./gateway/policy-resolver.js";
|
|
31
|
+
import type { PolicyUpdatedParams } from "@botcord/protocol-core";
|
|
27
32
|
import type {
|
|
28
33
|
GatewayChannelConfig,
|
|
29
34
|
GatewayRuntimeSnapshot,
|
|
@@ -36,15 +41,21 @@ import {
|
|
|
36
41
|
type RouteRule,
|
|
37
42
|
type RouteRuleMatch,
|
|
38
43
|
} from "./config.js";
|
|
39
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
BOTCORD_CHANNEL_TYPE,
|
|
46
|
+
buildManagedRoutes,
|
|
47
|
+
prepareGatewayProfile,
|
|
48
|
+
} from "./daemon-config-map.js";
|
|
40
49
|
import {
|
|
41
50
|
agentHomeDir,
|
|
42
51
|
agentStateDir,
|
|
43
52
|
agentWorkspaceDir,
|
|
53
|
+
applyAgentIdentity,
|
|
44
54
|
ensureAgentWorkspace,
|
|
45
55
|
} from "./agent-workspace.js";
|
|
46
56
|
import { detectRuntimes, getAdapterModule } from "./adapters/runtimes.js";
|
|
47
57
|
import { log as daemonLog } from "./log.js";
|
|
58
|
+
import { discoverAgentCredentials } from "./agent-discovery.js";
|
|
48
59
|
|
|
49
60
|
/** Options accepted by {@link createProvisioner}. */
|
|
50
61
|
export interface ProvisionerOptions {
|
|
@@ -55,6 +66,14 @@ export interface ProvisionerOptions {
|
|
|
55
66
|
* run without a real Hub.
|
|
56
67
|
*/
|
|
57
68
|
register?: typeof BotCordClient.register;
|
|
69
|
+
/**
|
|
70
|
+
* Optional policy-resolver handle (PR3). When present, the
|
|
71
|
+
* `policy_updated` control frame routes through it: cache is invalidated
|
|
72
|
+
* for the (agent, room?) pair, and any embedded `policy` payload is
|
|
73
|
+
* applied directly so the next inbound sees the fresh policy without an
|
|
74
|
+
* extra round-trip.
|
|
75
|
+
*/
|
|
76
|
+
policyResolver?: PolicyResolverLike;
|
|
58
77
|
}
|
|
59
78
|
|
|
60
79
|
/** The value a frame handler returns (minus the `id` which the channel fills in). */
|
|
@@ -70,6 +89,7 @@ export function createProvisioner(opts: ProvisionerOptions): (
|
|
|
70
89
|
) => Promise<AckBody> {
|
|
71
90
|
const gateway = opts.gateway;
|
|
72
91
|
const register = opts.register ?? BotCordClient.register;
|
|
92
|
+
const policyResolver = opts.policyResolver;
|
|
73
93
|
|
|
74
94
|
return async (frame: ControlFrame): Promise<AckBody> => {
|
|
75
95
|
daemonLog.debug("provision.dispatch", { type: frame.type, id: frame.id });
|
|
@@ -77,6 +97,38 @@ export function createProvisioner(opts: ProvisionerOptions): (
|
|
|
77
97
|
case CONTROL_FRAME_TYPES.PING:
|
|
78
98
|
return { ok: true, result: { pong: true, ts: Date.now() } };
|
|
79
99
|
|
|
100
|
+
case CONTROL_FRAME_TYPES.HELLO: {
|
|
101
|
+
const params = (frame.params ?? {}) as unknown as HelloParams;
|
|
102
|
+
const result = applyHelloIdentitySnapshot(params.agents);
|
|
103
|
+
daemonLog.debug("hello: identity snapshot applied", {
|
|
104
|
+
frameId: frame.id,
|
|
105
|
+
received: params.agents?.length ?? 0,
|
|
106
|
+
updated: result.updated,
|
|
107
|
+
skipped: result.skipped,
|
|
108
|
+
});
|
|
109
|
+
return { ok: true, result };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
case CONTROL_FRAME_TYPES.UPDATE_AGENT: {
|
|
113
|
+
const params = (frame.params ?? {}) as unknown as UpdateAgentParams;
|
|
114
|
+
if (!params.agentId) {
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
error: { code: "bad_params", message: "update_agent requires params.agentId" },
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const result = applyAgentIdentity(params.agentId, {
|
|
121
|
+
displayName: params.displayName,
|
|
122
|
+
bio: params.bio,
|
|
123
|
+
});
|
|
124
|
+
daemonLog.info("update_agent applied", {
|
|
125
|
+
agentId: params.agentId,
|
|
126
|
+
changed: result.changed,
|
|
127
|
+
skipped: result.skipped ?? null,
|
|
128
|
+
});
|
|
129
|
+
return { ok: true, result };
|
|
130
|
+
}
|
|
131
|
+
|
|
80
132
|
case CONTROL_FRAME_TYPES.PROVISION_AGENT: {
|
|
81
133
|
const params = (frame.params ?? {}) as unknown as ProvisionAgentParams;
|
|
82
134
|
daemonLog.info("provision_agent: start", {
|
|
@@ -86,6 +138,18 @@ export function createProvisioner(opts: ProvisionerOptions): (
|
|
|
86
138
|
name: params.name ?? null,
|
|
87
139
|
});
|
|
88
140
|
const agent = await provisionAgent(params, { gateway, register });
|
|
141
|
+
// Seed the policy resolver from the optional `defaultAttention` /
|
|
142
|
+
// `attentionKeywords` fields (PR3, control-frame.ts). Hub builds that
|
|
143
|
+
// don't yet emit these stay backwards-compatible — the resolver just
|
|
144
|
+
// falls back to `mode=always` until a `policy_updated` frame arrives.
|
|
145
|
+
if (policyResolver && params.defaultAttention) {
|
|
146
|
+
policyResolver.put(agent.agentId, null, {
|
|
147
|
+
mode: params.defaultAttention,
|
|
148
|
+
keywords: Array.isArray(params.attentionKeywords)
|
|
149
|
+
? params.attentionKeywords.slice()
|
|
150
|
+
: [],
|
|
151
|
+
});
|
|
152
|
+
}
|
|
89
153
|
return {
|
|
90
154
|
ok: true,
|
|
91
155
|
result: {
|
|
@@ -125,8 +189,57 @@ export function createProvisioner(opts: ProvisionerOptions): (
|
|
|
125
189
|
return { ok: true, result: res };
|
|
126
190
|
}
|
|
127
191
|
|
|
192
|
+
case CONTROL_FRAME_TYPES.POLICY_UPDATED: {
|
|
193
|
+
const params = (frame.params ?? {}) as unknown as PolicyUpdatedParams;
|
|
194
|
+
const agentId = params.agent_id;
|
|
195
|
+
if (typeof agentId !== "string" || !agentId) {
|
|
196
|
+
return {
|
|
197
|
+
ok: false,
|
|
198
|
+
error: { code: "bad_params", message: "policy_updated requires agent_id" },
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
if (!policyResolver) {
|
|
202
|
+
// No resolver wired — quietly succeed; the daemon may be running
|
|
203
|
+
// without the gateway-level attention gate (e.g. legacy boot path).
|
|
204
|
+
daemonLog.debug("policy_updated: no resolver — noop", { agentId });
|
|
205
|
+
return { ok: true, result: { agent_id: agentId, applied: false } };
|
|
206
|
+
}
|
|
207
|
+
const roomId = typeof params.room_id === "string" ? params.room_id : undefined;
|
|
208
|
+
if (params.policy) {
|
|
209
|
+
// Embedded policy payload — install directly to avoid a refetch.
|
|
210
|
+
policyResolver.put(agentId, roomId ?? null, {
|
|
211
|
+
mode: params.policy.mode,
|
|
212
|
+
keywords: Array.isArray(params.policy.keywords)
|
|
213
|
+
? params.policy.keywords.slice()
|
|
214
|
+
: [],
|
|
215
|
+
...(typeof params.policy.muted_until === "number"
|
|
216
|
+
? { muted_until: params.policy.muted_until }
|
|
217
|
+
: {}),
|
|
218
|
+
});
|
|
219
|
+
} else {
|
|
220
|
+
policyResolver.invalidate(agentId, roomId);
|
|
221
|
+
}
|
|
222
|
+
daemonLog.info("policy_updated: applied", {
|
|
223
|
+
agentId,
|
|
224
|
+
roomId: roomId ?? null,
|
|
225
|
+
embedded: !!params.policy,
|
|
226
|
+
});
|
|
227
|
+
return {
|
|
228
|
+
ok: true,
|
|
229
|
+
result: { agent_id: agentId, applied: true, embedded: !!params.policy },
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
128
233
|
case CONTROL_FRAME_TYPES.LIST_RUNTIMES: {
|
|
129
|
-
|
|
234
|
+
// Async path so the openclaw-acp endpoints get probed inline; gateway
|
|
235
|
+
// / WS errors are swallowed inside `collectRuntimeSnapshotAsync`.
|
|
236
|
+
let cfgForProbe: { openclawGateways?: any[] } | undefined;
|
|
237
|
+
try {
|
|
238
|
+
cfgForProbe = loadConfig();
|
|
239
|
+
} catch {
|
|
240
|
+
cfgForProbe = undefined;
|
|
241
|
+
}
|
|
242
|
+
const snapshot = await collectRuntimeSnapshotAsync({ cfg: cfgForProbe });
|
|
130
243
|
daemonLog.debug("list_runtimes", { count: snapshot.runtimes.length });
|
|
131
244
|
return { ok: true, result: snapshot };
|
|
132
245
|
}
|
|
@@ -155,6 +268,8 @@ interface ProvisionCtx {
|
|
|
155
268
|
register: typeof BotCordClient.register;
|
|
156
269
|
}
|
|
157
270
|
|
|
271
|
+
const openclawProvisionLocks = new Map<string, Promise<unknown>>();
|
|
272
|
+
|
|
158
273
|
async function provisionAgent(
|
|
159
274
|
params: ProvisionAgentParams,
|
|
160
275
|
ctx: ProvisionCtx,
|
|
@@ -166,13 +281,53 @@ async function provisionAgent(
|
|
|
166
281
|
const explicitCwd = params.credentials?.cwd ?? params.cwd;
|
|
167
282
|
assertSafeCwd(explicitCwd);
|
|
168
283
|
|
|
284
|
+
const openclawSel = pickOpenclawSelection(params);
|
|
285
|
+
if (openclawSel.gateway && openclawSel.agent) {
|
|
286
|
+
return withOpenclawProvisionLock(openclawSel.gateway, openclawSel.agent, async () => {
|
|
287
|
+
const existing = findCredentialsByOpenclaw(openclawSel.gateway!, openclawSel.agent!);
|
|
288
|
+
if (existing) {
|
|
289
|
+
daemonLog.info("provision_agent: openclaw binding already exists", {
|
|
290
|
+
gateway: openclawSel.gateway,
|
|
291
|
+
openclawAgent: openclawSel.agent,
|
|
292
|
+
agentId: existing.agentId,
|
|
293
|
+
});
|
|
294
|
+
return installExistingOpenclawBinding(existing.agentId, ctx);
|
|
295
|
+
}
|
|
296
|
+
const cfg = loadConfig();
|
|
297
|
+
const credentials = await materializeCredentials(params, cfg, ctx, explicitCwd);
|
|
298
|
+
return installLocalAgent(credentials, {
|
|
299
|
+
...ctx,
|
|
300
|
+
cfg,
|
|
301
|
+
bio: params.bio,
|
|
302
|
+
source: params.credentials ? "hub-supplied" : "registered",
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
169
307
|
const cfg = loadConfig();
|
|
170
308
|
const credentials = await materializeCredentials(params, cfg, ctx, explicitCwd);
|
|
309
|
+
return installLocalAgent(credentials, {
|
|
310
|
+
...ctx,
|
|
311
|
+
cfg,
|
|
312
|
+
bio: params.bio,
|
|
313
|
+
source: params.credentials ? "hub-supplied" : "registered",
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function installLocalAgent(
|
|
318
|
+
credentials: StoredBotCordCredentials,
|
|
319
|
+
ctx: ProvisionCtx & {
|
|
320
|
+
cfg: DaemonConfig;
|
|
321
|
+
bio?: string;
|
|
322
|
+
source: "hub-supplied" | "registered" | "adopted-openclaw";
|
|
323
|
+
},
|
|
324
|
+
): Promise<ProvisionedAgent> {
|
|
325
|
+
const cfg = ctx.cfg;
|
|
171
326
|
daemonLog.debug("provision: credentials materialized", {
|
|
172
327
|
agentId: credentials.agentId,
|
|
173
328
|
hubUrl: credentials.hubUrl,
|
|
174
329
|
runtime: credentials.runtime ?? null,
|
|
175
|
-
source:
|
|
330
|
+
source: ctx.source,
|
|
176
331
|
});
|
|
177
332
|
|
|
178
333
|
const credentialsFile = writeCredentialsFile(
|
|
@@ -186,7 +341,7 @@ async function provisionAgent(
|
|
|
186
341
|
try {
|
|
187
342
|
ensureAgentWorkspace(credentials.agentId, {
|
|
188
343
|
displayName: credentials.displayName,
|
|
189
|
-
bio:
|
|
344
|
+
bio: ctx.bio,
|
|
190
345
|
runtime: credentials.runtime,
|
|
191
346
|
keyId: credentials.keyId,
|
|
192
347
|
savedAt: credentials.savedAt,
|
|
@@ -246,11 +401,7 @@ async function provisionAgent(
|
|
|
246
401
|
// Hot-add the synthesized per-agent managed route so the next turn picks
|
|
247
402
|
// the agent's runtime + workspace cwd without waiting for reload_config.
|
|
248
403
|
try {
|
|
249
|
-
|
|
250
|
-
match: { accountId: credentials.agentId },
|
|
251
|
-
runtime: credentials.runtime ?? cfg.defaultRoute.adapter,
|
|
252
|
-
cwd: credentials.cwd ?? agentWorkspaceDir(credentials.agentId),
|
|
253
|
-
});
|
|
404
|
+
upsertManagedRouteForCredentials(credentials, cfg, ctx.gateway);
|
|
254
405
|
} catch (err) {
|
|
255
406
|
// Rollback the channel + config + credentials on managed-route failure
|
|
256
407
|
// (shouldn't happen — pure map op — but keeps the invariant tight).
|
|
@@ -289,6 +440,63 @@ async function provisionAgent(
|
|
|
289
440
|
};
|
|
290
441
|
}
|
|
291
442
|
|
|
443
|
+
function upsertManagedRouteForCredentials(
|
|
444
|
+
credentials: StoredBotCordCredentials,
|
|
445
|
+
cfg: DaemonConfig,
|
|
446
|
+
gateway: Gateway,
|
|
447
|
+
): void {
|
|
448
|
+
const synthRoute: import("./gateway/index.js").GatewayRoute = {
|
|
449
|
+
match: { accountId: credentials.agentId },
|
|
450
|
+
runtime: credentials.runtime ?? cfg.defaultRoute.adapter,
|
|
451
|
+
cwd: credentials.cwd ?? agentWorkspaceDir(credentials.agentId),
|
|
452
|
+
};
|
|
453
|
+
if (synthRoute.runtime === "openclaw-acp") {
|
|
454
|
+
const profile = (cfg.openclawGateways ?? []).find(
|
|
455
|
+
(g) => g.name === credentials.openclawGateway,
|
|
456
|
+
);
|
|
457
|
+
if (profile) {
|
|
458
|
+
const prepared = prepareGatewayProfile(profile);
|
|
459
|
+
synthRoute.gateway = {
|
|
460
|
+
name: prepared.name,
|
|
461
|
+
url: prepared.url,
|
|
462
|
+
...(prepared.resolvedToken ? { token: prepared.resolvedToken } : {}),
|
|
463
|
+
...(credentials.openclawAgent
|
|
464
|
+
? { openclawAgent: credentials.openclawAgent }
|
|
465
|
+
: prepared.defaultAgent
|
|
466
|
+
? { openclawAgent: prepared.defaultAgent }
|
|
467
|
+
: {}),
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
gateway.upsertManagedRoute(credentials.agentId, synthRoute);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async function installExistingOpenclawBinding(
|
|
475
|
+
agentId: string,
|
|
476
|
+
ctx: ProvisionCtx,
|
|
477
|
+
): Promise<ProvisionedAgent> {
|
|
478
|
+
const credentialsFile = defaultCredentialsFile(agentId);
|
|
479
|
+
const credentials = loadStoredCredentials(credentialsFile);
|
|
480
|
+
const cfg = loadConfig();
|
|
481
|
+
const updated = addAgentToConfig(cfg, credentials.agentId);
|
|
482
|
+
if (updated) saveConfig(updated);
|
|
483
|
+
const snap = ctx.gateway.snapshot();
|
|
484
|
+
if (!snap.channels[credentials.agentId]) {
|
|
485
|
+
await ctx.gateway.addChannel({
|
|
486
|
+
id: credentials.agentId,
|
|
487
|
+
type: BOTCORD_CHANNEL_TYPE,
|
|
488
|
+
accountId: credentials.agentId,
|
|
489
|
+
agentId: credentials.agentId,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
upsertManagedRouteForCredentials(credentials, cfg, ctx.gateway);
|
|
493
|
+
return {
|
|
494
|
+
agentId: credentials.agentId,
|
|
495
|
+
hubUrl: credentials.hubUrl,
|
|
496
|
+
credentialsFile,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
292
500
|
async function materializeCredentials(
|
|
293
501
|
params: ProvisionAgentParams,
|
|
294
502
|
cfg: DaemonConfig,
|
|
@@ -332,6 +540,9 @@ async function materializeCredentials(
|
|
|
332
540
|
if (typeof c.tokenExpiresAt === "number") record.tokenExpiresAt = c.tokenExpiresAt;
|
|
333
541
|
if (runtime) record.runtime = runtime;
|
|
334
542
|
record.cwd = cwd;
|
|
543
|
+
const openclawSel = pickOpenclawSelection(params);
|
|
544
|
+
if (openclawSel.gateway) record.openclawGateway = openclawSel.gateway;
|
|
545
|
+
if (openclawSel.agent) record.openclawAgent = openclawSel.agent;
|
|
335
546
|
return record;
|
|
336
547
|
}
|
|
337
548
|
|
|
@@ -361,9 +572,175 @@ async function materializeCredentials(
|
|
|
361
572
|
};
|
|
362
573
|
if (runtime) record.runtime = runtime;
|
|
363
574
|
record.cwd = cwd;
|
|
575
|
+
const openclawSel = pickOpenclawSelection(params);
|
|
576
|
+
if (openclawSel.gateway) record.openclawGateway = openclawSel.gateway;
|
|
577
|
+
if (openclawSel.agent) record.openclawAgent = openclawSel.agent;
|
|
364
578
|
return record;
|
|
365
579
|
}
|
|
366
580
|
|
|
581
|
+
/**
|
|
582
|
+
* Resolve OpenClaw routing selection from a `provision_agent` frame. Top-level
|
|
583
|
+
* `params.openclaw` (nested) wins over the flat `credentials.openclaw*` mirror.
|
|
584
|
+
* Returning `{}` is fine — only meaningful when the agent's runtime is
|
|
585
|
+
* `openclaw-acp`, and `buildManagedRoutes` falls back to defaultRoute.gateway
|
|
586
|
+
* when both are missing.
|
|
587
|
+
*/
|
|
588
|
+
function pickOpenclawSelection(
|
|
589
|
+
params: ProvisionAgentParams,
|
|
590
|
+
): { gateway?: string; agent?: string } {
|
|
591
|
+
const out: { gateway?: string; agent?: string } = {};
|
|
592
|
+
const top = params.openclaw;
|
|
593
|
+
if (top && typeof top.gateway === "string" && top.gateway.length > 0) {
|
|
594
|
+
out.gateway = top.gateway;
|
|
595
|
+
if (typeof top.agent === "string" && top.agent.length > 0) out.agent = top.agent;
|
|
596
|
+
return out;
|
|
597
|
+
}
|
|
598
|
+
const flat = params.credentials;
|
|
599
|
+
if (flat) {
|
|
600
|
+
if (typeof flat.openclawGateway === "string" && flat.openclawGateway.length > 0) {
|
|
601
|
+
out.gateway = flat.openclawGateway;
|
|
602
|
+
}
|
|
603
|
+
if (typeof flat.openclawAgent === "string" && flat.openclawAgent.length > 0) {
|
|
604
|
+
out.agent = flat.openclawAgent;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return out;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
async function withOpenclawProvisionLock<T>(
|
|
611
|
+
gateway: string,
|
|
612
|
+
agent: string,
|
|
613
|
+
fn: () => Promise<T>,
|
|
614
|
+
): Promise<T> {
|
|
615
|
+
const key = `${gateway}\0${agent}`;
|
|
616
|
+
const prev = openclawProvisionLocks.get(key) ?? Promise.resolve();
|
|
617
|
+
let release!: () => void;
|
|
618
|
+
const current = new Promise<void>((resolve) => {
|
|
619
|
+
release = resolve;
|
|
620
|
+
});
|
|
621
|
+
const chain = prev.then(() => current);
|
|
622
|
+
openclawProvisionLocks.set(key, chain);
|
|
623
|
+
await prev.catch(() => undefined);
|
|
624
|
+
try {
|
|
625
|
+
return await fn();
|
|
626
|
+
} finally {
|
|
627
|
+
release();
|
|
628
|
+
if (openclawProvisionLocks.get(key) === chain) {
|
|
629
|
+
openclawProvisionLocks.delete(key);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function findCredentialsByOpenclaw(
|
|
635
|
+
gateway: string,
|
|
636
|
+
openclawAgent: string,
|
|
637
|
+
): { agentId: string; credentialsFile: string } | null {
|
|
638
|
+
const discovered = discoverAgentCredentials({
|
|
639
|
+
credentialsDir: path.join(homedir(), ".botcord", "credentials"),
|
|
640
|
+
});
|
|
641
|
+
for (const a of discovered.agents) {
|
|
642
|
+
if (a.openclawGateway === gateway && a.openclawAgent === openclawAgent) {
|
|
643
|
+
return { agentId: a.agentId, credentialsFile: a.credentialsFile };
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
export interface AdoptDiscoveredOpenclawAgentsResult {
|
|
650
|
+
adopted: string[];
|
|
651
|
+
skipped: Array<{ gateway: string; openclawAgent?: string; reason: string }>;
|
|
652
|
+
failed: Array<{ gateway: string; openclawAgent?: string; error: string }>;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export async function adoptDiscoveredOpenclawAgents(ctx: {
|
|
656
|
+
gateway: Gateway;
|
|
657
|
+
register?: typeof BotCordClient.register;
|
|
658
|
+
cfg?: DaemonConfig;
|
|
659
|
+
timeoutMs?: number;
|
|
660
|
+
probe?: WsEndpointProbeFn;
|
|
661
|
+
}): Promise<AdoptDiscoveredOpenclawAgentsResult> {
|
|
662
|
+
const register = ctx.register ?? BotCordClient.register;
|
|
663
|
+
const cfg = ctx.cfg ?? loadConfig();
|
|
664
|
+
const result: AdoptDiscoveredOpenclawAgentsResult = {
|
|
665
|
+
adopted: [],
|
|
666
|
+
skipped: [],
|
|
667
|
+
failed: [],
|
|
668
|
+
};
|
|
669
|
+
for (const gw of cfg.openclawGateways ?? []) {
|
|
670
|
+
let probeResult: Awaited<ReturnType<typeof probeOpenclawAgents>>;
|
|
671
|
+
try {
|
|
672
|
+
probeResult = await probeOpenclawAgents(gw, {
|
|
673
|
+
timeoutMs: ctx.timeoutMs,
|
|
674
|
+
probe: ctx.probe,
|
|
675
|
+
});
|
|
676
|
+
} catch (err) {
|
|
677
|
+
result.failed.push({
|
|
678
|
+
gateway: gw.name,
|
|
679
|
+
error: err instanceof Error ? err.message : String(err),
|
|
680
|
+
});
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
if (!probeResult.ok) {
|
|
684
|
+
result.skipped.push({
|
|
685
|
+
gateway: gw.name,
|
|
686
|
+
reason: probeResult.error ?? "gateway_unreachable",
|
|
687
|
+
});
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
for (const oc of probeResult.agents ?? []) {
|
|
691
|
+
await withOpenclawProvisionLock(gw.name, oc.id, async () => {
|
|
692
|
+
const existing = findCredentialsByOpenclaw(gw.name, oc.id);
|
|
693
|
+
if (existing) {
|
|
694
|
+
result.skipped.push({
|
|
695
|
+
gateway: gw.name,
|
|
696
|
+
openclawAgent: oc.id,
|
|
697
|
+
reason: "already_bound",
|
|
698
|
+
});
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
const freshCfg = loadConfig();
|
|
702
|
+
if (!inferHubUrl(freshCfg)) {
|
|
703
|
+
result.skipped.push({
|
|
704
|
+
gateway: gw.name,
|
|
705
|
+
openclawAgent: oc.id,
|
|
706
|
+
reason: "missing_hub_url",
|
|
707
|
+
});
|
|
708
|
+
daemonLog.warn("openclaw adopt skipped: no known hubUrl", {
|
|
709
|
+
gateway: gw.name,
|
|
710
|
+
openclawAgent: oc.id,
|
|
711
|
+
});
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
try {
|
|
715
|
+
const params: ProvisionAgentParams = {
|
|
716
|
+
runtime: "openclaw-acp",
|
|
717
|
+
name: oc.name ?? `openclaw-${oc.id}`,
|
|
718
|
+
openclaw: { gateway: gw.name, agent: oc.id },
|
|
719
|
+
};
|
|
720
|
+
const credentials = await materializeCredentials(params, freshCfg, {
|
|
721
|
+
gateway: ctx.gateway,
|
|
722
|
+
register,
|
|
723
|
+
}, undefined);
|
|
724
|
+
const installed = await installLocalAgent(credentials, {
|
|
725
|
+
gateway: ctx.gateway,
|
|
726
|
+
register,
|
|
727
|
+
cfg: freshCfg,
|
|
728
|
+
source: "adopted-openclaw",
|
|
729
|
+
});
|
|
730
|
+
result.adopted.push(installed.agentId);
|
|
731
|
+
} catch (err) {
|
|
732
|
+
result.failed.push({
|
|
733
|
+
gateway: gw.name,
|
|
734
|
+
openclawAgent: oc.id,
|
|
735
|
+
error: err instanceof Error ? err.message : String(err),
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return result;
|
|
742
|
+
}
|
|
743
|
+
|
|
367
744
|
async function revokeAgent(
|
|
368
745
|
params: RevokeAgentParams,
|
|
369
746
|
ctx: { gateway: Gateway },
|
|
@@ -555,6 +932,277 @@ export function collectRuntimeSnapshot(): ListRuntimesResult {
|
|
|
555
932
|
return { runtimes, probedAt: Date.now() };
|
|
556
933
|
}
|
|
557
934
|
|
|
935
|
+
/** Maximum number of `endpoints[]` entries persisted per runtime (RFC §3.8.2). */
|
|
936
|
+
export const RUNTIME_ENDPOINTS_CAP = 32;
|
|
937
|
+
|
|
938
|
+
/** Injection seam for L2 + L3 endpoint probes — kept testable + side-effect-free. */
|
|
939
|
+
export type WsEndpointProbeFn = (args: {
|
|
940
|
+
url: string;
|
|
941
|
+
token?: string;
|
|
942
|
+
timeoutMs: number;
|
|
943
|
+
}) => Promise<{
|
|
944
|
+
ok: boolean;
|
|
945
|
+
version?: string;
|
|
946
|
+
/**
|
|
947
|
+
* L3 — populated when `agents.list` succeeds. `id` is the stable key
|
|
948
|
+
* consumed by route lookups / `openclawAgent`; `name` is display-only.
|
|
949
|
+
*/
|
|
950
|
+
agents?: Array<{
|
|
951
|
+
id: string;
|
|
952
|
+
name?: string;
|
|
953
|
+
workspace?: string;
|
|
954
|
+
model?: { name?: string; provider?: string };
|
|
955
|
+
}>;
|
|
956
|
+
error?: string;
|
|
957
|
+
}>;
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* Default L2 + L3 probe — opens a WS handshake against the OpenClaw gateway
|
|
961
|
+
* and, when the connection is up, issues a JSON-RPC `agents.list` request to
|
|
962
|
+
* enumerate configured agent profiles. Best-effort: a successful WS open with
|
|
963
|
+
* a failed `agents.list` still reports `ok: true` (just without `agents`),
|
|
964
|
+
* matching the RFC's "agents populated only when listing succeeded" rule.
|
|
965
|
+
*
|
|
966
|
+
* Method name and result shape follow OpenClaw:
|
|
967
|
+
* `~/claws/openclaw/src/gateway/server-methods/agents.ts:416` and
|
|
968
|
+
* `~/claws/openclaw/src/gateway/session-utils.ts:783` —
|
|
969
|
+
* `{ defaultId, mainKey, scope, agents: [{ id, name?, identity?, workspace, model? }] }`.
|
|
970
|
+
*/
|
|
971
|
+
async function defaultWsProbe(args: {
|
|
972
|
+
url: string;
|
|
973
|
+
token?: string;
|
|
974
|
+
timeoutMs: number;
|
|
975
|
+
}): Promise<{
|
|
976
|
+
ok: boolean;
|
|
977
|
+
version?: string;
|
|
978
|
+
agents?: Array<{
|
|
979
|
+
id: string;
|
|
980
|
+
name?: string;
|
|
981
|
+
workspace?: string;
|
|
982
|
+
model?: { name?: string; provider?: string };
|
|
983
|
+
}>;
|
|
984
|
+
error?: string;
|
|
985
|
+
}> {
|
|
986
|
+
type AgentRow = {
|
|
987
|
+
id: string;
|
|
988
|
+
name?: string;
|
|
989
|
+
workspace?: string;
|
|
990
|
+
model?: { name?: string; provider?: string };
|
|
991
|
+
};
|
|
992
|
+
type ProbeResult = {
|
|
993
|
+
ok: boolean;
|
|
994
|
+
version?: string;
|
|
995
|
+
agents?: AgentRow[];
|
|
996
|
+
error?: string;
|
|
997
|
+
};
|
|
998
|
+
const { default: WebSocket } = await import("ws");
|
|
999
|
+
return new Promise<ProbeResult>((resolve) => {
|
|
1000
|
+
let settled = false;
|
|
1001
|
+
let ws: any;
|
|
1002
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1003
|
+
const settle = (v: ProbeResult): void => {
|
|
1004
|
+
if (settled) return;
|
|
1005
|
+
settled = true;
|
|
1006
|
+
if (timer) clearTimeout(timer);
|
|
1007
|
+
try {
|
|
1008
|
+
ws?.terminate();
|
|
1009
|
+
} catch {
|
|
1010
|
+
// ignore
|
|
1011
|
+
}
|
|
1012
|
+
resolve(v);
|
|
1013
|
+
};
|
|
1014
|
+
try {
|
|
1015
|
+
const headers: Record<string, string> = {};
|
|
1016
|
+
if (args.token) headers["Authorization"] = `Bearer ${args.token}`;
|
|
1017
|
+
ws = new WebSocket(args.url, { headers });
|
|
1018
|
+
} catch (err) {
|
|
1019
|
+
resolve({ ok: false, error: (err as Error).message });
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
timer = setTimeout(() => settle({ ok: false, error: "timeout" }), args.timeoutMs);
|
|
1023
|
+
const requestId = "probe-agents-list";
|
|
1024
|
+
ws.on("open", () => {
|
|
1025
|
+
// L3: enumerate agent profiles. We don't fail the L2 result if this
|
|
1026
|
+
// call fails — the gateway is reachable either way.
|
|
1027
|
+
try {
|
|
1028
|
+
ws.send(
|
|
1029
|
+
JSON.stringify({
|
|
1030
|
+
jsonrpc: "2.0",
|
|
1031
|
+
id: requestId,
|
|
1032
|
+
method: "agents.list",
|
|
1033
|
+
params: {},
|
|
1034
|
+
}),
|
|
1035
|
+
);
|
|
1036
|
+
} catch (err) {
|
|
1037
|
+
settle({ ok: true, error: `agents.list send failed: ${(err as Error).message}` });
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
ws.on("message", (raw: Buffer | string) => {
|
|
1041
|
+
try {
|
|
1042
|
+
const msg = JSON.parse(typeof raw === "string" ? raw : raw.toString("utf8"));
|
|
1043
|
+
if (msg?.id !== requestId) return; // ignore unrelated frames
|
|
1044
|
+
if (msg.error) {
|
|
1045
|
+
settle({ ok: true, error: String(msg.error?.message ?? "agents.list error") });
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
const list = Array.isArray(msg.result?.agents) ? msg.result.agents : [];
|
|
1049
|
+
const agents: AgentRow[] = [];
|
|
1050
|
+
for (const a of list) {
|
|
1051
|
+
if (!a || typeof a.id !== "string" || a.id.length === 0) continue;
|
|
1052
|
+
const row: AgentRow = { id: a.id };
|
|
1053
|
+
if (typeof a.name === "string") row.name = a.name;
|
|
1054
|
+
if (typeof a.workspace === "string") row.workspace = a.workspace;
|
|
1055
|
+
if (a.model && typeof a.model === "object") {
|
|
1056
|
+
const model: { name?: string; provider?: string } = {};
|
|
1057
|
+
if (typeof a.model.name === "string") model.name = a.model.name;
|
|
1058
|
+
if (typeof a.model.provider === "string") model.provider = a.model.provider;
|
|
1059
|
+
if (model.name || model.provider) row.model = model;
|
|
1060
|
+
}
|
|
1061
|
+
agents.push(row);
|
|
1062
|
+
}
|
|
1063
|
+
settle({ ok: true, agents });
|
|
1064
|
+
} catch (err) {
|
|
1065
|
+
settle({ ok: true, error: `agents.list parse failed: ${(err as Error).message}` });
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
ws.on("error", (err: Error) => {
|
|
1069
|
+
settle({ ok: false, error: err.message });
|
|
1070
|
+
});
|
|
1071
|
+
ws.on("close", () => {
|
|
1072
|
+
// If the socket closes before `agents.list` resolved we still treat
|
|
1073
|
+
// L2 as ok (open fired) and emit no agents.
|
|
1074
|
+
settle({ ok: true });
|
|
1075
|
+
});
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
export async function probeOpenclawAgents(
|
|
1080
|
+
profile: { url: string; token?: string; tokenFile?: string },
|
|
1081
|
+
opts: { timeoutMs?: number; probe?: WsEndpointProbeFn } = {},
|
|
1082
|
+
): Promise<{
|
|
1083
|
+
ok: boolean;
|
|
1084
|
+
version?: string;
|
|
1085
|
+
agents?: Array<{
|
|
1086
|
+
id: string;
|
|
1087
|
+
name?: string;
|
|
1088
|
+
workspace?: string;
|
|
1089
|
+
model?: { name?: string; provider?: string };
|
|
1090
|
+
}>;
|
|
1091
|
+
error?: string;
|
|
1092
|
+
}> {
|
|
1093
|
+
const probe = opts.probe ?? defaultWsProbe;
|
|
1094
|
+
const prepared = prepareGatewayProfile({
|
|
1095
|
+
name: "probe",
|
|
1096
|
+
url: profile.url,
|
|
1097
|
+
...(profile.token ? { token: profile.token } : {}),
|
|
1098
|
+
...(profile.tokenFile ? { tokenFile: profile.tokenFile } : {}),
|
|
1099
|
+
});
|
|
1100
|
+
return probe({
|
|
1101
|
+
url: profile.url,
|
|
1102
|
+
token: prepared.resolvedToken,
|
|
1103
|
+
timeoutMs: opts.timeoutMs ?? 3000,
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* Async variant that includes L2 (gateway reachability) and L3 (agent listing)
|
|
1109
|
+
* probes for runtimes that talk to external services. Used by the production
|
|
1110
|
+
* `list_runtimes` and first-connect snapshot paths.
|
|
1111
|
+
*
|
|
1112
|
+
* `cfg` is optional so existing callers without a loaded config (e.g. tests)
|
|
1113
|
+
* can keep using the sync `collectRuntimeSnapshot()` — when absent, the result
|
|
1114
|
+
* is identical to that function.
|
|
1115
|
+
*/
|
|
1116
|
+
export async function collectRuntimeSnapshotAsync(opts: {
|
|
1117
|
+
cfg?: { openclawGateways?: Array<{ name: string; url: string; token?: string; tokenFile?: string }> };
|
|
1118
|
+
wsProbe?: WsEndpointProbeFn;
|
|
1119
|
+
timeoutMs?: number;
|
|
1120
|
+
} = {}): Promise<ListRuntimesResult> {
|
|
1121
|
+
const base = collectRuntimeSnapshot();
|
|
1122
|
+
const gateways = opts.cfg?.openclawGateways ?? [];
|
|
1123
|
+
if (gateways.length === 0) return base;
|
|
1124
|
+
// Default daemon-side budget is 3s — it must stay below the Hub's
|
|
1125
|
+
// `list_runtimes` ack wait (5s, see backend/hub/routers/daemon_control.py)
|
|
1126
|
+
// so a single slow gateway can't blow the whole snapshot to a 504.
|
|
1127
|
+
const timeoutMs = opts.timeoutMs ?? 3000;
|
|
1128
|
+
const capped = gateways.slice(0, RUNTIME_ENDPOINTS_CAP);
|
|
1129
|
+
const endpoints = await Promise.all(
|
|
1130
|
+
capped.map(async (g) => {
|
|
1131
|
+
try {
|
|
1132
|
+
const res = await probeOpenclawAgents(g, {
|
|
1133
|
+
probe: opts.wsProbe,
|
|
1134
|
+
timeoutMs,
|
|
1135
|
+
});
|
|
1136
|
+
const entry: any = { name: g.name, url: g.url, reachable: res.ok };
|
|
1137
|
+
if (res.version) entry.version = res.version;
|
|
1138
|
+
if (res.error) entry.error = res.error;
|
|
1139
|
+
if (res.agents) entry.agents = res.agents;
|
|
1140
|
+
return entry;
|
|
1141
|
+
} catch (err) {
|
|
1142
|
+
return {
|
|
1143
|
+
name: g.name,
|
|
1144
|
+
url: g.url,
|
|
1145
|
+
reachable: false,
|
|
1146
|
+
error: (err as Error).message,
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
}),
|
|
1150
|
+
);
|
|
1151
|
+
const out: ListRuntimesResult = { ...base };
|
|
1152
|
+
out.runtimes = base.runtimes.map((r) =>
|
|
1153
|
+
r.id === "openclaw-acp" ? { ...r, endpoints } : r,
|
|
1154
|
+
);
|
|
1155
|
+
return out;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// ---------------------------------------------------------------------------
|
|
1159
|
+
// hello agents snapshot (lightweight identity sync)
|
|
1160
|
+
// ---------------------------------------------------------------------------
|
|
1161
|
+
|
|
1162
|
+
interface HelloIdentityResult {
|
|
1163
|
+
updated: number;
|
|
1164
|
+
skipped: number;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
/**
|
|
1168
|
+
* Reconcile every agent identity carried by the `hello.agents` snapshot
|
|
1169
|
+
* against the on-disk `identity.md`. Best-effort: a malformed entry or a
|
|
1170
|
+
* file-system error for one agent never aborts the rest.
|
|
1171
|
+
*
|
|
1172
|
+
* Identity-snapshot semantics intentionally only touch the metadata
|
|
1173
|
+
* line + Bio body — Role/Boundaries paragraphs the user authored locally
|
|
1174
|
+
* are preserved (see `applyAgentIdentity`). Missing identity.md files
|
|
1175
|
+
* (agent provisioned on a different daemon, or workspace cleared) are
|
|
1176
|
+
* silently skipped.
|
|
1177
|
+
*/
|
|
1178
|
+
export function applyHelloIdentitySnapshot(
|
|
1179
|
+
snapshot: AgentIdentitySnapshot[] | undefined,
|
|
1180
|
+
): HelloIdentityResult {
|
|
1181
|
+
const out: HelloIdentityResult = { updated: 0, skipped: 0 };
|
|
1182
|
+
if (!Array.isArray(snapshot)) return out;
|
|
1183
|
+
for (const entry of snapshot) {
|
|
1184
|
+
if (!entry || typeof entry.agentId !== "string") {
|
|
1185
|
+
out.skipped += 1;
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
try {
|
|
1189
|
+
const result = applyAgentIdentity(entry.agentId, {
|
|
1190
|
+
displayName: entry.displayName,
|
|
1191
|
+
bio: entry.bio,
|
|
1192
|
+
});
|
|
1193
|
+
if (result.changed) out.updated += 1;
|
|
1194
|
+
else out.skipped += 1;
|
|
1195
|
+
} catch (err) {
|
|
1196
|
+
out.skipped += 1;
|
|
1197
|
+
daemonLog.warn("hello.identity apply failed", {
|
|
1198
|
+
agentId: entry.agentId,
|
|
1199
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
return out;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
558
1206
|
// ---------------------------------------------------------------------------
|
|
559
1207
|
// reload_config / list_agents / set_route handlers (P3)
|
|
560
1208
|
// ---------------------------------------------------------------------------
|
|
@@ -641,17 +1289,19 @@ export async function reloadConfig(ctx: { gateway: Gateway }): Promise<ReloadRes
|
|
|
641
1289
|
*/
|
|
642
1290
|
function readAgentRuntimesFromCredentials(
|
|
643
1291
|
agentIds: string[],
|
|
644
|
-
): Record<string, { runtime?: string; cwd?: string }> {
|
|
645
|
-
const out: Record<string, { runtime?: string; cwd?: string }> = {};
|
|
1292
|
+
): Record<string, { runtime?: string; cwd?: string; openclawGateway?: string; openclawAgent?: string }> {
|
|
1293
|
+
const out: Record<string, { runtime?: string; cwd?: string; openclawGateway?: string; openclawAgent?: string }> = {};
|
|
646
1294
|
for (const id of agentIds) {
|
|
647
1295
|
const file = defaultCredentialsFile(id);
|
|
648
1296
|
try {
|
|
649
1297
|
if (!existsSync(file)) continue;
|
|
650
1298
|
const creds = loadStoredCredentials(file);
|
|
651
|
-
const entry: { runtime?: string; cwd?: string } = {};
|
|
1299
|
+
const entry: { runtime?: string; cwd?: string; openclawGateway?: string; openclawAgent?: string } = {};
|
|
652
1300
|
if (creds.runtime) entry.runtime = creds.runtime;
|
|
653
1301
|
if (creds.cwd) entry.cwd = creds.cwd;
|
|
654
|
-
if (
|
|
1302
|
+
if (creds.openclawGateway) entry.openclawGateway = creds.openclawGateway;
|
|
1303
|
+
if (creds.openclawAgent) entry.openclawAgent = creds.openclawAgent;
|
|
1304
|
+
if (entry.runtime || entry.cwd || entry.openclawGateway || entry.openclawAgent) out[id] = entry;
|
|
655
1305
|
} catch {
|
|
656
1306
|
// best-effort — skip agents with unreadable credentials
|
|
657
1307
|
}
|
|
@@ -782,11 +1432,21 @@ export function setRoute(params: unknown): SetRouteResult {
|
|
|
782
1432
|
match.conversationPrefix = p.pattern;
|
|
783
1433
|
}
|
|
784
1434
|
|
|
1435
|
+
// Fall back to defaultRoute.extraArgs (mirrors adapter/cwd inheritance
|
|
1436
|
+
// above) so dashboard-driven `set_route` calls that only carry agentId +
|
|
1437
|
+
// pattern still pick up operator-wide flags like `--permission-mode
|
|
1438
|
+
// bypassPermissions`. Without this, every newly provisioned agent lost
|
|
1439
|
+
// those flags and Bash/MCP tool calls would deadlock on permission prompts.
|
|
1440
|
+
const extraArgs = Array.isArray(route?.extraArgs)
|
|
1441
|
+
? route!.extraArgs!.slice()
|
|
1442
|
+
: Array.isArray(cfg.defaultRoute.extraArgs)
|
|
1443
|
+
? cfg.defaultRoute.extraArgs.slice()
|
|
1444
|
+
: undefined;
|
|
785
1445
|
const newRule: RouteRule = {
|
|
786
1446
|
match,
|
|
787
1447
|
adapter,
|
|
788
1448
|
cwd,
|
|
789
|
-
...(
|
|
1449
|
+
...(extraArgs ? { extraArgs } : {}),
|
|
790
1450
|
};
|
|
791
1451
|
|
|
792
1452
|
const routes = Array.isArray(cfg.routes) ? cfg.routes.slice() : [];
|
|
@@ -872,5 +1532,13 @@ function inferHubUrl(cfg: DaemonConfig): string | null {
|
|
|
872
1532
|
// skip
|
|
873
1533
|
}
|
|
874
1534
|
}
|
|
1535
|
+
if (ids.length === 0) {
|
|
1536
|
+
const discovered = discoverAgentCredentials({
|
|
1537
|
+
credentialsDir: path.join(homedir(), ".botcord", "credentials"),
|
|
1538
|
+
});
|
|
1539
|
+
for (const a of discovered.agents) {
|
|
1540
|
+
if (a.hubUrl) return a.hubUrl;
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
875
1543
|
return null;
|
|
876
1544
|
}
|