@botcord/daemon 0.2.4 → 0.2.6
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 +7 -3
- package/dist/agent-discovery.js +9 -1
- package/dist/agent-workspace.d.ts +62 -0
- package/dist/agent-workspace.js +140 -10
- package/dist/config.d.ts +49 -1
- package/dist/config.js +57 -1
- package/dist/control-channel.d.ts +1 -4
- package/dist/control-channel.js +1 -4
- package/dist/daemon-config-map.d.ts +29 -12
- package/dist/daemon-config-map.js +105 -8
- package/dist/daemon.d.ts +2 -0
- package/dist/daemon.js +52 -5
- 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 +66 -1
- package/dist/gateway/dispatcher.js +583 -56
- 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 +286 -27
- package/dist/mention-scan.d.ts +22 -0
- package/dist/mention-scan.js +35 -0
- package/dist/provision.d.ts +73 -3
- package/dist/provision.js +373 -12
- package/dist/system-context.d.ts +5 -4
- package/dist/system-context.js +35 -5
- package/dist/turn-text.js +20 -1
- package/dist/url-utils.d.ts +9 -0
- package/dist/url-utils.js +18 -0
- package/dist/user-auth.js +0 -2
- package/dist/working-memory.js +1 -1
- package/package.json +2 -1
- 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__/policy-resolver.test.ts +124 -0
- package/src/__tests__/policy-updated-handler.test.ts +144 -0
- package/src/__tests__/provision.test.ts +160 -0
- package/src/__tests__/system-context.test.ts +52 -0
- package/src/__tests__/url-utils.test.ts +37 -0
- package/src/agent-discovery.ts +12 -4
- package/src/agent-workspace.ts +173 -9
- package/src/config.ts +132 -4
- package/src/control-channel.ts +1 -4
- package/src/daemon-config-map.ts +156 -12
- package/src/daemon.ts +66 -5
- package/src/doctor.ts +49 -2
- package/src/gateway/__tests__/dispatcher.test.ts +440 -2
- 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 +681 -58
- 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 +295 -30
- package/src/mention-scan.ts +38 -0
- package/src/provision.ts +446 -20
- package/src/system-context.ts +41 -9
- package/src/turn-text.ts +22 -1
- package/src/url-utils.ts +17 -0
- package/src/user-auth.ts +0 -2
- package/src/working-memory.ts +1 -1
package/dist/provision.js
CHANGED
|
@@ -3,16 +3,14 @@
|
|
|
3
3
|
* to this module with the parsed {@link ControlFrame}; we execute the
|
|
4
4
|
* side effects (register agent, write credentials, load route, add/remove
|
|
5
5
|
* gateway channel) and return an ack payload.
|
|
6
|
-
*
|
|
7
|
-
* See `docs/daemon-control-plane-plan.md` §4.3, §5.3, §8.
|
|
8
6
|
*/
|
|
9
7
|
import { existsSync, rmSync, unlinkSync } from "node:fs";
|
|
10
8
|
import { homedir } from "node:os";
|
|
11
9
|
import path from "node:path";
|
|
12
10
|
import { BotCordClient, CONTROL_FRAME_TYPES, defaultCredentialsFile, derivePublicKey, loadStoredCredentials, writeCredentialsFile, } from "@botcord/protocol-core";
|
|
13
11
|
import { loadConfig, resolveConfiguredAgentIds, saveConfig, } from "./config.js";
|
|
14
|
-
import { BOTCORD_CHANNEL_TYPE, buildManagedRoutes } from "./daemon-config-map.js";
|
|
15
|
-
import { agentHomeDir, agentStateDir, agentWorkspaceDir, ensureAgentWorkspace, } from "./agent-workspace.js";
|
|
12
|
+
import { BOTCORD_CHANNEL_TYPE, buildManagedRoutes, prepareGatewayProfile, } from "./daemon-config-map.js";
|
|
13
|
+
import { agentHomeDir, agentStateDir, agentWorkspaceDir, applyAgentIdentity, ensureAgentWorkspace, } from "./agent-workspace.js";
|
|
16
14
|
import { detectRuntimes, getAdapterModule } from "./adapters/runtimes.js";
|
|
17
15
|
import { log as daemonLog } from "./log.js";
|
|
18
16
|
/**
|
|
@@ -23,11 +21,42 @@ import { log as daemonLog } from "./log.js";
|
|
|
23
21
|
export function createProvisioner(opts) {
|
|
24
22
|
const gateway = opts.gateway;
|
|
25
23
|
const register = opts.register ?? BotCordClient.register;
|
|
24
|
+
const policyResolver = opts.policyResolver;
|
|
26
25
|
return async (frame) => {
|
|
27
26
|
daemonLog.debug("provision.dispatch", { type: frame.type, id: frame.id });
|
|
28
27
|
switch (frame.type) {
|
|
29
28
|
case CONTROL_FRAME_TYPES.PING:
|
|
30
29
|
return { ok: true, result: { pong: true, ts: Date.now() } };
|
|
30
|
+
case CONTROL_FRAME_TYPES.HELLO: {
|
|
31
|
+
const params = (frame.params ?? {});
|
|
32
|
+
const result = applyHelloIdentitySnapshot(params.agents);
|
|
33
|
+
daemonLog.debug("hello: identity snapshot applied", {
|
|
34
|
+
frameId: frame.id,
|
|
35
|
+
received: params.agents?.length ?? 0,
|
|
36
|
+
updated: result.updated,
|
|
37
|
+
skipped: result.skipped,
|
|
38
|
+
});
|
|
39
|
+
return { ok: true, result };
|
|
40
|
+
}
|
|
41
|
+
case CONTROL_FRAME_TYPES.UPDATE_AGENT: {
|
|
42
|
+
const params = (frame.params ?? {});
|
|
43
|
+
if (!params.agentId) {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
error: { code: "bad_params", message: "update_agent requires params.agentId" },
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const result = applyAgentIdentity(params.agentId, {
|
|
50
|
+
displayName: params.displayName,
|
|
51
|
+
bio: params.bio,
|
|
52
|
+
});
|
|
53
|
+
daemonLog.info("update_agent applied", {
|
|
54
|
+
agentId: params.agentId,
|
|
55
|
+
changed: result.changed,
|
|
56
|
+
skipped: result.skipped ?? null,
|
|
57
|
+
});
|
|
58
|
+
return { ok: true, result };
|
|
59
|
+
}
|
|
31
60
|
case CONTROL_FRAME_TYPES.PROVISION_AGENT: {
|
|
32
61
|
const params = (frame.params ?? {});
|
|
33
62
|
daemonLog.info("provision_agent: start", {
|
|
@@ -37,6 +66,18 @@ export function createProvisioner(opts) {
|
|
|
37
66
|
name: params.name ?? null,
|
|
38
67
|
});
|
|
39
68
|
const agent = await provisionAgent(params, { gateway, register });
|
|
69
|
+
// Seed the policy resolver from the optional `defaultAttention` /
|
|
70
|
+
// `attentionKeywords` fields (PR3, control-frame.ts). Hub builds that
|
|
71
|
+
// don't yet emit these stay backwards-compatible — the resolver just
|
|
72
|
+
// falls back to `mode=always` until a `policy_updated` frame arrives.
|
|
73
|
+
if (policyResolver && params.defaultAttention) {
|
|
74
|
+
policyResolver.put(agent.agentId, null, {
|
|
75
|
+
mode: params.defaultAttention,
|
|
76
|
+
keywords: Array.isArray(params.attentionKeywords)
|
|
77
|
+
? params.attentionKeywords.slice()
|
|
78
|
+
: [],
|
|
79
|
+
});
|
|
80
|
+
}
|
|
40
81
|
return {
|
|
41
82
|
ok: true,
|
|
42
83
|
result: {
|
|
@@ -71,8 +112,58 @@ export function createProvisioner(opts) {
|
|
|
71
112
|
const res = setRoute(frame.params ?? {});
|
|
72
113
|
return { ok: true, result: res };
|
|
73
114
|
}
|
|
115
|
+
case CONTROL_FRAME_TYPES.POLICY_UPDATED: {
|
|
116
|
+
const params = (frame.params ?? {});
|
|
117
|
+
const agentId = params.agent_id;
|
|
118
|
+
if (typeof agentId !== "string" || !agentId) {
|
|
119
|
+
return {
|
|
120
|
+
ok: false,
|
|
121
|
+
error: { code: "bad_params", message: "policy_updated requires agent_id" },
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
if (!policyResolver) {
|
|
125
|
+
// No resolver wired — quietly succeed; the daemon may be running
|
|
126
|
+
// without the gateway-level attention gate (e.g. legacy boot path).
|
|
127
|
+
daemonLog.debug("policy_updated: no resolver — noop", { agentId });
|
|
128
|
+
return { ok: true, result: { agent_id: agentId, applied: false } };
|
|
129
|
+
}
|
|
130
|
+
const roomId = typeof params.room_id === "string" ? params.room_id : undefined;
|
|
131
|
+
if (params.policy) {
|
|
132
|
+
// Embedded policy payload — install directly to avoid a refetch.
|
|
133
|
+
policyResolver.put(agentId, roomId ?? null, {
|
|
134
|
+
mode: params.policy.mode,
|
|
135
|
+
keywords: Array.isArray(params.policy.keywords)
|
|
136
|
+
? params.policy.keywords.slice()
|
|
137
|
+
: [],
|
|
138
|
+
...(typeof params.policy.muted_until === "number"
|
|
139
|
+
? { muted_until: params.policy.muted_until }
|
|
140
|
+
: {}),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
policyResolver.invalidate(agentId, roomId);
|
|
145
|
+
}
|
|
146
|
+
daemonLog.info("policy_updated: applied", {
|
|
147
|
+
agentId,
|
|
148
|
+
roomId: roomId ?? null,
|
|
149
|
+
embedded: !!params.policy,
|
|
150
|
+
});
|
|
151
|
+
return {
|
|
152
|
+
ok: true,
|
|
153
|
+
result: { agent_id: agentId, applied: true, embedded: !!params.policy },
|
|
154
|
+
};
|
|
155
|
+
}
|
|
74
156
|
case CONTROL_FRAME_TYPES.LIST_RUNTIMES: {
|
|
75
|
-
|
|
157
|
+
// Async path so the openclaw-acp endpoints get probed inline; gateway
|
|
158
|
+
// / WS errors are swallowed inside `collectRuntimeSnapshotAsync`.
|
|
159
|
+
let cfgForProbe;
|
|
160
|
+
try {
|
|
161
|
+
cfgForProbe = loadConfig();
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
cfgForProbe = undefined;
|
|
165
|
+
}
|
|
166
|
+
const snapshot = await collectRuntimeSnapshotAsync({ cfg: cfgForProbe });
|
|
76
167
|
daemonLog.debug("list_runtimes", { count: snapshot.runtimes.length });
|
|
77
168
|
return { ok: true, result: snapshot };
|
|
78
169
|
}
|
|
@@ -176,11 +267,35 @@ async function provisionAgent(params, ctx) {
|
|
|
176
267
|
// Hot-add the synthesized per-agent managed route so the next turn picks
|
|
177
268
|
// the agent's runtime + workspace cwd without waiting for reload_config.
|
|
178
269
|
try {
|
|
179
|
-
|
|
270
|
+
const synthRoute = {
|
|
180
271
|
match: { accountId: credentials.agentId },
|
|
181
272
|
runtime: credentials.runtime ?? cfg.defaultRoute.adapter,
|
|
182
273
|
cwd: credentials.cwd ?? agentWorkspaceDir(credentials.agentId),
|
|
183
|
-
}
|
|
274
|
+
};
|
|
275
|
+
if (synthRoute.runtime === "openclaw-acp") {
|
|
276
|
+
// Resolve gateway from the freshly written credentials + the live
|
|
277
|
+
// openclawGateways registry. A missing/unknown gateway here yields a
|
|
278
|
+
// disabled route (set_route style); next turn for this agent falls
|
|
279
|
+
// back to defaultRoute. Caller already validated via reload semantics.
|
|
280
|
+
const profile = (cfg.openclawGateways ?? []).find((g) => g.name === credentials.openclawGateway);
|
|
281
|
+
if (profile) {
|
|
282
|
+
// Run the same tokenFile-aware resolver `toGatewayConfig` uses so the
|
|
283
|
+
// first turn after provisioning doesn't auth-fail when the gateway
|
|
284
|
+
// ships its bearer via `tokenFile` instead of an inline `token`.
|
|
285
|
+
const prepared = prepareGatewayProfile(profile);
|
|
286
|
+
synthRoute.gateway = {
|
|
287
|
+
name: prepared.name,
|
|
288
|
+
url: prepared.url,
|
|
289
|
+
...(prepared.resolvedToken ? { token: prepared.resolvedToken } : {}),
|
|
290
|
+
...(credentials.openclawAgent
|
|
291
|
+
? { openclawAgent: credentials.openclawAgent }
|
|
292
|
+
: prepared.defaultAgent
|
|
293
|
+
? { openclawAgent: prepared.defaultAgent }
|
|
294
|
+
: {}),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
ctx.gateway.upsertManagedRoute(credentials.agentId, synthRoute);
|
|
184
299
|
}
|
|
185
300
|
catch (err) {
|
|
186
301
|
// Rollback the channel + config + credentials on managed-route failure
|
|
@@ -222,9 +337,9 @@ async function provisionAgent(params, ctx) {
|
|
|
222
337
|
};
|
|
223
338
|
}
|
|
224
339
|
async function materializeCredentials(params, cfg, ctx, explicitCwd) {
|
|
225
|
-
// Runtime is an agent property
|
|
226
|
-
//
|
|
227
|
-
//
|
|
340
|
+
// Runtime is an agent property. Hub is authoritative; top-level `runtime`
|
|
341
|
+
// wins, `adapter` is a one-release alias, and `credentials.runtime` is the
|
|
342
|
+
// per-agent cached copy.
|
|
228
343
|
const runtime = pickRuntime(params);
|
|
229
344
|
if (runtime)
|
|
230
345
|
assertKnownRuntime(runtime);
|
|
@@ -261,6 +376,11 @@ async function materializeCredentials(params, cfg, ctx, explicitCwd) {
|
|
|
261
376
|
if (runtime)
|
|
262
377
|
record.runtime = runtime;
|
|
263
378
|
record.cwd = cwd;
|
|
379
|
+
const openclawSel = pickOpenclawSelection(params);
|
|
380
|
+
if (openclawSel.gateway)
|
|
381
|
+
record.openclawGateway = openclawSel.gateway;
|
|
382
|
+
if (openclawSel.agent)
|
|
383
|
+
record.openclawAgent = openclawSel.agent;
|
|
264
384
|
return record;
|
|
265
385
|
}
|
|
266
386
|
// Slow path: daemon registers a fresh identity against Hub. We need a
|
|
@@ -288,8 +408,40 @@ async function materializeCredentials(params, cfg, ctx, explicitCwd) {
|
|
|
288
408
|
if (runtime)
|
|
289
409
|
record.runtime = runtime;
|
|
290
410
|
record.cwd = cwd;
|
|
411
|
+
const openclawSel = pickOpenclawSelection(params);
|
|
412
|
+
if (openclawSel.gateway)
|
|
413
|
+
record.openclawGateway = openclawSel.gateway;
|
|
414
|
+
if (openclawSel.agent)
|
|
415
|
+
record.openclawAgent = openclawSel.agent;
|
|
291
416
|
return record;
|
|
292
417
|
}
|
|
418
|
+
/**
|
|
419
|
+
* Resolve OpenClaw routing selection from a `provision_agent` frame. Top-level
|
|
420
|
+
* `params.openclaw` (nested) wins over the flat `credentials.openclaw*` mirror.
|
|
421
|
+
* Returning `{}` is fine — only meaningful when the agent's runtime is
|
|
422
|
+
* `openclaw-acp`, and `buildManagedRoutes` falls back to defaultRoute.gateway
|
|
423
|
+
* when both are missing.
|
|
424
|
+
*/
|
|
425
|
+
function pickOpenclawSelection(params) {
|
|
426
|
+
const out = {};
|
|
427
|
+
const top = params.openclaw;
|
|
428
|
+
if (top && typeof top.gateway === "string" && top.gateway.length > 0) {
|
|
429
|
+
out.gateway = top.gateway;
|
|
430
|
+
if (typeof top.agent === "string" && top.agent.length > 0)
|
|
431
|
+
out.agent = top.agent;
|
|
432
|
+
return out;
|
|
433
|
+
}
|
|
434
|
+
const flat = params.credentials;
|
|
435
|
+
if (flat) {
|
|
436
|
+
if (typeof flat.openclawGateway === "string" && flat.openclawGateway.length > 0) {
|
|
437
|
+
out.gateway = flat.openclawGateway;
|
|
438
|
+
}
|
|
439
|
+
if (typeof flat.openclawAgent === "string" && flat.openclawAgent.length > 0) {
|
|
440
|
+
out.agent = flat.openclawAgent;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return out;
|
|
444
|
+
}
|
|
293
445
|
async function revokeAgent(params, ctx) {
|
|
294
446
|
if (!params.agentId) {
|
|
295
447
|
throw new Error("revoke_agent requires params.agentId");
|
|
@@ -479,6 +631,201 @@ export function collectRuntimeSnapshot() {
|
|
|
479
631
|
});
|
|
480
632
|
return { runtimes, probedAt: Date.now() };
|
|
481
633
|
}
|
|
634
|
+
/** Maximum number of `endpoints[]` entries persisted per runtime (RFC §3.8.2). */
|
|
635
|
+
export const RUNTIME_ENDPOINTS_CAP = 32;
|
|
636
|
+
/**
|
|
637
|
+
* Default L2 + L3 probe — opens a WS handshake against the OpenClaw gateway
|
|
638
|
+
* and, when the connection is up, issues a JSON-RPC `agents.list` request to
|
|
639
|
+
* enumerate configured agent profiles. Best-effort: a successful WS open with
|
|
640
|
+
* a failed `agents.list` still reports `ok: true` (just without `agents`),
|
|
641
|
+
* matching the RFC's "agents populated only when listing succeeded" rule.
|
|
642
|
+
*
|
|
643
|
+
* Method name and result shape follow OpenClaw:
|
|
644
|
+
* `~/claws/openclaw/src/gateway/server-methods/agents.ts:416` and
|
|
645
|
+
* `~/claws/openclaw/src/gateway/session-utils.ts:783` —
|
|
646
|
+
* `{ defaultId, mainKey, scope, agents: [{ id, name?, identity?, workspace, model? }] }`.
|
|
647
|
+
*/
|
|
648
|
+
async function defaultWsProbe(args) {
|
|
649
|
+
const { default: WebSocket } = await import("ws");
|
|
650
|
+
return new Promise((resolve) => {
|
|
651
|
+
let settled = false;
|
|
652
|
+
let ws;
|
|
653
|
+
let timer;
|
|
654
|
+
const settle = (v) => {
|
|
655
|
+
if (settled)
|
|
656
|
+
return;
|
|
657
|
+
settled = true;
|
|
658
|
+
if (timer)
|
|
659
|
+
clearTimeout(timer);
|
|
660
|
+
try {
|
|
661
|
+
ws?.terminate();
|
|
662
|
+
}
|
|
663
|
+
catch {
|
|
664
|
+
// ignore
|
|
665
|
+
}
|
|
666
|
+
resolve(v);
|
|
667
|
+
};
|
|
668
|
+
try {
|
|
669
|
+
const headers = {};
|
|
670
|
+
if (args.token)
|
|
671
|
+
headers["Authorization"] = `Bearer ${args.token}`;
|
|
672
|
+
ws = new WebSocket(args.url, { headers });
|
|
673
|
+
}
|
|
674
|
+
catch (err) {
|
|
675
|
+
resolve({ ok: false, error: err.message });
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
timer = setTimeout(() => settle({ ok: false, error: "timeout" }), args.timeoutMs);
|
|
679
|
+
const requestId = "probe-agents-list";
|
|
680
|
+
ws.on("open", () => {
|
|
681
|
+
// L3: enumerate agent profiles. We don't fail the L2 result if this
|
|
682
|
+
// call fails — the gateway is reachable either way.
|
|
683
|
+
try {
|
|
684
|
+
ws.send(JSON.stringify({
|
|
685
|
+
jsonrpc: "2.0",
|
|
686
|
+
id: requestId,
|
|
687
|
+
method: "agents.list",
|
|
688
|
+
params: {},
|
|
689
|
+
}));
|
|
690
|
+
}
|
|
691
|
+
catch (err) {
|
|
692
|
+
settle({ ok: true, error: `agents.list send failed: ${err.message}` });
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
ws.on("message", (raw) => {
|
|
696
|
+
try {
|
|
697
|
+
const msg = JSON.parse(typeof raw === "string" ? raw : raw.toString("utf8"));
|
|
698
|
+
if (msg?.id !== requestId)
|
|
699
|
+
return; // ignore unrelated frames
|
|
700
|
+
if (msg.error) {
|
|
701
|
+
settle({ ok: true, error: String(msg.error?.message ?? "agents.list error") });
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
const list = Array.isArray(msg.result?.agents) ? msg.result.agents : [];
|
|
705
|
+
const agents = [];
|
|
706
|
+
for (const a of list) {
|
|
707
|
+
if (!a || typeof a.id !== "string" || a.id.length === 0)
|
|
708
|
+
continue;
|
|
709
|
+
const row = { id: a.id };
|
|
710
|
+
if (typeof a.name === "string")
|
|
711
|
+
row.name = a.name;
|
|
712
|
+
if (typeof a.workspace === "string")
|
|
713
|
+
row.workspace = a.workspace;
|
|
714
|
+
if (a.model && typeof a.model === "object") {
|
|
715
|
+
const model = {};
|
|
716
|
+
if (typeof a.model.name === "string")
|
|
717
|
+
model.name = a.model.name;
|
|
718
|
+
if (typeof a.model.provider === "string")
|
|
719
|
+
model.provider = a.model.provider;
|
|
720
|
+
if (model.name || model.provider)
|
|
721
|
+
row.model = model;
|
|
722
|
+
}
|
|
723
|
+
agents.push(row);
|
|
724
|
+
}
|
|
725
|
+
settle({ ok: true, agents });
|
|
726
|
+
}
|
|
727
|
+
catch (err) {
|
|
728
|
+
settle({ ok: true, error: `agents.list parse failed: ${err.message}` });
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
ws.on("error", (err) => {
|
|
732
|
+
settle({ ok: false, error: err.message });
|
|
733
|
+
});
|
|
734
|
+
ws.on("close", () => {
|
|
735
|
+
// If the socket closes before `agents.list` resolved we still treat
|
|
736
|
+
// L2 as ok (open fired) and emit no agents.
|
|
737
|
+
settle({ ok: true });
|
|
738
|
+
});
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Async variant that includes L2 (gateway reachability) and L3 (agent listing)
|
|
743
|
+
* probes for runtimes that talk to external services. Used by the production
|
|
744
|
+
* `list_runtimes` and first-connect snapshot paths.
|
|
745
|
+
*
|
|
746
|
+
* `cfg` is optional so existing callers without a loaded config (e.g. tests)
|
|
747
|
+
* can keep using the sync `collectRuntimeSnapshot()` — when absent, the result
|
|
748
|
+
* is identical to that function.
|
|
749
|
+
*/
|
|
750
|
+
export async function collectRuntimeSnapshotAsync(opts = {}) {
|
|
751
|
+
const base = collectRuntimeSnapshot();
|
|
752
|
+
const gateways = opts.cfg?.openclawGateways ?? [];
|
|
753
|
+
if (gateways.length === 0)
|
|
754
|
+
return base;
|
|
755
|
+
const probe = opts.wsProbe ?? defaultWsProbe;
|
|
756
|
+
// Default daemon-side budget is 3s — it must stay below the Hub's
|
|
757
|
+
// `list_runtimes` ack wait (5s, see backend/hub/routers/daemon_control.py)
|
|
758
|
+
// so a single slow gateway can't blow the whole snapshot to a 504.
|
|
759
|
+
const timeoutMs = opts.timeoutMs ?? 3000;
|
|
760
|
+
const capped = gateways.slice(0, RUNTIME_ENDPOINTS_CAP);
|
|
761
|
+
const endpoints = await Promise.all(capped.map(async (g) => {
|
|
762
|
+
// Resolve `tokenFile` here so token-file-only profiles probe with auth
|
|
763
|
+
// and aren't falsely marked unreachable in the dashboard.
|
|
764
|
+
const prepared = prepareGatewayProfile(g);
|
|
765
|
+
try {
|
|
766
|
+
const res = await probe({ url: g.url, token: prepared.resolvedToken, timeoutMs });
|
|
767
|
+
const entry = { name: g.name, url: g.url, reachable: res.ok };
|
|
768
|
+
if (res.version)
|
|
769
|
+
entry.version = res.version;
|
|
770
|
+
if (res.error)
|
|
771
|
+
entry.error = res.error;
|
|
772
|
+
if (res.agents)
|
|
773
|
+
entry.agents = res.agents;
|
|
774
|
+
return entry;
|
|
775
|
+
}
|
|
776
|
+
catch (err) {
|
|
777
|
+
return {
|
|
778
|
+
name: g.name,
|
|
779
|
+
url: g.url,
|
|
780
|
+
reachable: false,
|
|
781
|
+
error: err.message,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
}));
|
|
785
|
+
const out = { ...base };
|
|
786
|
+
out.runtimes = base.runtimes.map((r) => r.id === "openclaw-acp" ? { ...r, endpoints } : r);
|
|
787
|
+
return out;
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* Reconcile every agent identity carried by the `hello.agents` snapshot
|
|
791
|
+
* against the on-disk `identity.md`. Best-effort: a malformed entry or a
|
|
792
|
+
* file-system error for one agent never aborts the rest.
|
|
793
|
+
*
|
|
794
|
+
* Identity-snapshot semantics intentionally only touch the metadata
|
|
795
|
+
* line + Bio body — Role/Boundaries paragraphs the user authored locally
|
|
796
|
+
* are preserved (see `applyAgentIdentity`). Missing identity.md files
|
|
797
|
+
* (agent provisioned on a different daemon, or workspace cleared) are
|
|
798
|
+
* silently skipped.
|
|
799
|
+
*/
|
|
800
|
+
export function applyHelloIdentitySnapshot(snapshot) {
|
|
801
|
+
const out = { updated: 0, skipped: 0 };
|
|
802
|
+
if (!Array.isArray(snapshot))
|
|
803
|
+
return out;
|
|
804
|
+
for (const entry of snapshot) {
|
|
805
|
+
if (!entry || typeof entry.agentId !== "string") {
|
|
806
|
+
out.skipped += 1;
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
try {
|
|
810
|
+
const result = applyAgentIdentity(entry.agentId, {
|
|
811
|
+
displayName: entry.displayName,
|
|
812
|
+
bio: entry.bio,
|
|
813
|
+
});
|
|
814
|
+
if (result.changed)
|
|
815
|
+
out.updated += 1;
|
|
816
|
+
else
|
|
817
|
+
out.skipped += 1;
|
|
818
|
+
}
|
|
819
|
+
catch (err) {
|
|
820
|
+
out.skipped += 1;
|
|
821
|
+
daemonLog.warn("hello.identity apply failed", {
|
|
822
|
+
agentId: entry.agentId,
|
|
823
|
+
error: err instanceof Error ? err.message : String(err),
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return out;
|
|
828
|
+
}
|
|
482
829
|
/**
|
|
483
830
|
* Re-read `config.json` and reconcile the running gateway against it. New
|
|
484
831
|
* agents in config but not in gateway snapshot → `addChannel`; agents in
|
|
@@ -566,7 +913,11 @@ function readAgentRuntimesFromCredentials(agentIds) {
|
|
|
566
913
|
entry.runtime = creds.runtime;
|
|
567
914
|
if (creds.cwd)
|
|
568
915
|
entry.cwd = creds.cwd;
|
|
569
|
-
if (
|
|
916
|
+
if (creds.openclawGateway)
|
|
917
|
+
entry.openclawGateway = creds.openclawGateway;
|
|
918
|
+
if (creds.openclawAgent)
|
|
919
|
+
entry.openclawAgent = creds.openclawAgent;
|
|
920
|
+
if (entry.runtime || entry.cwd || entry.openclawGateway || entry.openclawAgent)
|
|
570
921
|
out[id] = entry;
|
|
571
922
|
}
|
|
572
923
|
catch {
|
|
@@ -654,11 +1005,21 @@ export function setRoute(params) {
|
|
|
654
1005
|
if (p.pattern && typeof p.pattern === "string" && !match.conversationPrefix) {
|
|
655
1006
|
match.conversationPrefix = p.pattern;
|
|
656
1007
|
}
|
|
1008
|
+
// Fall back to defaultRoute.extraArgs (mirrors adapter/cwd inheritance
|
|
1009
|
+
// above) so dashboard-driven `set_route` calls that only carry agentId +
|
|
1010
|
+
// pattern still pick up operator-wide flags like `--permission-mode
|
|
1011
|
+
// bypassPermissions`. Without this, every newly provisioned agent lost
|
|
1012
|
+
// those flags and Bash/MCP tool calls would deadlock on permission prompts.
|
|
1013
|
+
const extraArgs = Array.isArray(route?.extraArgs)
|
|
1014
|
+
? route.extraArgs.slice()
|
|
1015
|
+
: Array.isArray(cfg.defaultRoute.extraArgs)
|
|
1016
|
+
? cfg.defaultRoute.extraArgs.slice()
|
|
1017
|
+
: undefined;
|
|
657
1018
|
const newRule = {
|
|
658
1019
|
match,
|
|
659
1020
|
adapter,
|
|
660
1021
|
cwd,
|
|
661
|
-
...(
|
|
1022
|
+
...(extraArgs ? { extraArgs } : {}),
|
|
662
1023
|
};
|
|
663
1024
|
const routes = Array.isArray(cfg.routes) ? cfg.routes.slice() : [];
|
|
664
1025
|
// Replace an existing matching rule. We use the canonical signature
|
package/dist/system-context.d.ts
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
* `RuntimeRunOptions.systemContext`. This module composes the daemon's
|
|
7
7
|
* system-context string from:
|
|
8
8
|
*
|
|
9
|
-
* 1. `[BotCord
|
|
10
|
-
* 2. `[BotCord
|
|
11
|
-
* 3. `[BotCord
|
|
12
|
-
* 4. `[BotCord
|
|
9
|
+
* 1. `[BotCord Identity]` (read fresh from workspace/identity.md each turn)
|
|
10
|
+
* 2. `[BotCord Scene: Owner Chat]` (owner-trust turns only)
|
|
11
|
+
* 3. `[BotCord Working Memory]`
|
|
12
|
+
* 4. `[BotCord Room Context]` (group rooms, via optional async fetcher)
|
|
13
|
+
* 5. `[BotCord Cross-Room Awareness]` (optional activity tracker)
|
|
13
14
|
*
|
|
14
15
|
* Behavior:
|
|
15
16
|
* - Working memory is loaded fresh per turn, so a `memory set` from another
|
package/dist/system-context.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { buildCrossRoomDigest } from "./cross-room.js";
|
|
2
2
|
import { buildWorkingMemoryPrompt, readWorkingMemory } from "./working-memory.js";
|
|
3
|
+
import { readIdentity } from "./agent-workspace.js";
|
|
3
4
|
import { classifyActivitySender } from "./sender-classify.js";
|
|
4
5
|
import { log } from "./log.js";
|
|
5
6
|
/**
|
|
@@ -25,6 +26,32 @@ function safeReadWorkingMemory(agentId) {
|
|
|
25
26
|
return null;
|
|
26
27
|
}
|
|
27
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Read identity.md and wrap it as a system-context block. Placed before
|
|
31
|
+
* every other block so the agent answers "who are you" from this file
|
|
32
|
+
* rather than from the underlying CLI's default persona ("I am Claude
|
|
33
|
+
* Code"). Re-read every turn so dashboard reconcile (`applyAgentIdentity`)
|
|
34
|
+
* and self-edits take effect immediately, mirroring working-memory
|
|
35
|
+
* semantics.
|
|
36
|
+
*/
|
|
37
|
+
function buildIdentityPrompt(agentId) {
|
|
38
|
+
let raw = null;
|
|
39
|
+
try {
|
|
40
|
+
raw = readIdentity(agentId);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
log.warn("identity read failed", { agentId, err: String(err) });
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
if (!raw)
|
|
47
|
+
return null;
|
|
48
|
+
return [
|
|
49
|
+
"[BotCord Identity]",
|
|
50
|
+
"Your persistent identity card. The fields below are the source of truth — when asked who you are, what you do, or what you will / will not do, answer from this block, not from the underlying CLI's default persona.",
|
|
51
|
+
"",
|
|
52
|
+
raw.trim(),
|
|
53
|
+
].join("\n");
|
|
54
|
+
}
|
|
28
55
|
/**
|
|
29
56
|
* Build a {@link SystemContextBuilder} for the gateway dispatcher.
|
|
30
57
|
*
|
|
@@ -34,6 +61,7 @@ function safeReadWorkingMemory(agentId) {
|
|
|
34
61
|
*/
|
|
35
62
|
export function createDaemonSystemContextBuilder(deps) {
|
|
36
63
|
const gatherSyncBlocks = (message) => {
|
|
64
|
+
const identity = buildIdentityPrompt(deps.agentId);
|
|
37
65
|
const ownerScene = classifyActivitySender(message).kind === "owner"
|
|
38
66
|
? buildOwnerChatSceneContext()
|
|
39
67
|
: null;
|
|
@@ -47,7 +75,7 @@ export function createDaemonSystemContextBuilder(deps) {
|
|
|
47
75
|
currentTopic: message.conversation.threadId ?? null,
|
|
48
76
|
}) || null
|
|
49
77
|
: null;
|
|
50
|
-
return { ownerScene, memory, digest };
|
|
78
|
+
return { identity, ownerScene, memory, digest };
|
|
51
79
|
};
|
|
52
80
|
const assemble = (parts) => {
|
|
53
81
|
const filtered = parts.filter((p) => typeof p === "string" && p.length > 0);
|
|
@@ -70,11 +98,12 @@ export function createDaemonSystemContextBuilder(deps) {
|
|
|
70
98
|
};
|
|
71
99
|
if (!deps.roomContextBuilder) {
|
|
72
100
|
const syncBuilder = (message) => {
|
|
73
|
-
const { ownerScene, memory, digest } = gatherSyncBlocks(message);
|
|
101
|
+
const { identity, ownerScene, memory, digest } = gatherSyncBlocks(message);
|
|
74
102
|
// Loop-risk sits at the end so its "reply NO_REPLY unless…" guidance
|
|
75
103
|
// is the last thing the model sees before the user turn body.
|
|
104
|
+
// Identity sits at the very front so it frames every other block.
|
|
76
105
|
const loopRisk = runLoopRisk(message);
|
|
77
|
-
return assemble([ownerScene, memory, digest, loopRisk]);
|
|
106
|
+
return assemble([identity, ownerScene, memory, digest, loopRisk]);
|
|
78
107
|
};
|
|
79
108
|
// Compile-time witness that the narrower sync signature still satisfies
|
|
80
109
|
// `SystemContextBuilder` (which allows async). Prevents the two contracts
|
|
@@ -85,11 +114,12 @@ export function createDaemonSystemContextBuilder(deps) {
|
|
|
85
114
|
}
|
|
86
115
|
const roomBuilder = deps.roomContextBuilder;
|
|
87
116
|
const asyncBuilder = async (message) => {
|
|
88
|
-
const { ownerScene, memory, digest } = gatherSyncBlocks(message);
|
|
117
|
+
const { identity, ownerScene, memory, digest } = gatherSyncBlocks(message);
|
|
89
118
|
// Room context landing order: after owner-scene / memory, before digest —
|
|
90
119
|
// "what room am I in" belongs with the session's own identity, while the
|
|
91
120
|
// cross-room digest deliberately describes OTHER rooms and should stay
|
|
92
121
|
// last so it doesn't get confused with the current room.
|
|
122
|
+
// Identity stays at the very front; see syncBuilder for rationale.
|
|
93
123
|
let roomBlock = null;
|
|
94
124
|
try {
|
|
95
125
|
roomBlock = await roomBuilder(message);
|
|
@@ -102,7 +132,7 @@ export function createDaemonSystemContextBuilder(deps) {
|
|
|
102
132
|
});
|
|
103
133
|
}
|
|
104
134
|
const loopRisk = runLoopRisk(message);
|
|
105
|
-
return assemble([ownerScene, memory, roomBlock, digest, loopRisk]);
|
|
135
|
+
return assemble([identity, ownerScene, memory, roomBlock, digest, loopRisk]);
|
|
106
136
|
};
|
|
107
137
|
const _typecheck = asyncBuilder;
|
|
108
138
|
void _typecheck;
|
package/dist/turn-text.js
CHANGED
|
@@ -4,6 +4,16 @@ const GROUP_HINT = '[In group chats, do NOT reply unless you are explicitly ment
|
|
|
4
4
|
'If no response is needed, reply with exactly "NO_REPLY" and nothing else.]';
|
|
5
5
|
const DIRECT_HINT = '[If the conversation has naturally concluded or no response is needed, ' +
|
|
6
6
|
'reply with exactly "NO_REPLY" and nothing else.]';
|
|
7
|
+
/**
|
|
8
|
+
* Reminder appended to every wrapped (non-owner-chat) inbound message. The
|
|
9
|
+
* dispatcher discards `result.text` for any room that is not `rm_oc_*`, so
|
|
10
|
+
* the agent must call the `botcord_send` tool (or the `botcord send` CLI
|
|
11
|
+
* via Bash) to actually deliver a reply. Plain assistant text in those
|
|
12
|
+
* rooms is logged and dropped.
|
|
13
|
+
*/
|
|
14
|
+
const NON_OWNER_REPLY_HINT = "[This room is NOT owner-chat. Plain text output WILL NOT be sent. " +
|
|
15
|
+
"To reply, call the `botcord_send` tool, or run " +
|
|
16
|
+
'`botcord send --room <room_id> --text "..."` via Bash.]';
|
|
7
17
|
/**
|
|
8
18
|
* Read the BotCord envelope type from a raw inbound message. Returns
|
|
9
19
|
* `undefined` when the message didn't come from the BotCord channel or the
|
|
@@ -116,6 +126,8 @@ export function composeBotCordUserTurn(msg) {
|
|
|
116
126
|
`</${tag}>`,
|
|
117
127
|
"",
|
|
118
128
|
hint,
|
|
129
|
+
"",
|
|
130
|
+
NON_OWNER_REPLY_HINT,
|
|
119
131
|
];
|
|
120
132
|
if (contactRequestHint) {
|
|
121
133
|
lines.push("", contactRequestHint);
|
|
@@ -160,7 +172,14 @@ function composeBatchedTurn(msg, batch) {
|
|
|
160
172
|
}
|
|
161
173
|
}
|
|
162
174
|
const hint = isGroup ? GROUP_HINT : DIRECT_HINT;
|
|
163
|
-
const lines = [
|
|
175
|
+
const lines = [
|
|
176
|
+
header.join(" | "),
|
|
177
|
+
blocks.join("\n"),
|
|
178
|
+
"",
|
|
179
|
+
hint,
|
|
180
|
+
"",
|
|
181
|
+
NON_OWNER_REPLY_HINT,
|
|
182
|
+
];
|
|
164
183
|
if (contactRequestSenders.length > 0) {
|
|
165
184
|
// Dedup + list — multiple distinct senders show as "A, B".
|
|
166
185
|
const unique = Array.from(new Set(contactRequestSenders));
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append a `next` query param to a URL. Used by the device-code flow to
|
|
3
|
+
* encode a post-auth redirect target into the Hub-issued verification URL,
|
|
4
|
+
* so the dashboard knows where to send the user after they click Authorize.
|
|
5
|
+
*
|
|
6
|
+
* Falls back to returning the original URL string if parsing fails — the
|
|
7
|
+
* device-code flow keeps working, just without the redirect convenience.
|
|
8
|
+
*/
|
|
9
|
+
export declare function appendNextParam(url: string, next: string): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append a `next` query param to a URL. Used by the device-code flow to
|
|
3
|
+
* encode a post-auth redirect target into the Hub-issued verification URL,
|
|
4
|
+
* so the dashboard knows where to send the user after they click Authorize.
|
|
5
|
+
*
|
|
6
|
+
* Falls back to returning the original URL string if parsing fails — the
|
|
7
|
+
* device-code flow keeps working, just without the redirect convenience.
|
|
8
|
+
*/
|
|
9
|
+
export function appendNextParam(url, next) {
|
|
10
|
+
try {
|
|
11
|
+
const u = new URL(url);
|
|
12
|
+
u.searchParams.set("next", next);
|
|
13
|
+
return u.toString();
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return url;
|
|
17
|
+
}
|
|
18
|
+
}
|
package/dist/user-auth.js
CHANGED
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
* `~/.botcord/credentials/*.json`), the user-auth record is singular —
|
|
6
6
|
* the daemon only logs in as *one* user at a time. Stored at
|
|
7
7
|
* `~/.botcord/daemon/user-auth.json` with `0600` permissions.
|
|
8
|
-
*
|
|
9
|
-
* See `docs/daemon-control-plane-plan.md` §6–§7.
|
|
10
8
|
*/
|
|
11
9
|
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
12
10
|
import path from "node:path";
|