@agentvault/agentvault 0.14.30 → 0.15.0
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/README.md +3 -1
- package/dist/account-config.js +60 -0
- package/dist/account-config.js.map +1 -0
- package/dist/channel.d.ts +5 -0
- package/dist/channel.d.ts.map +1 -1
- package/dist/channel.js +3411 -0
- package/dist/channel.js.map +1 -0
- package/dist/cli.js +253 -72006
- package/dist/cli.js.map +1 -7
- package/dist/create-agent.js +314 -0
- package/dist/create-agent.js.map +1 -0
- package/dist/crypto-helpers.js +4 -0
- package/dist/crypto-helpers.js.map +1 -0
- package/dist/doctor.js +415 -0
- package/dist/doctor.js.map +1 -0
- package/dist/fetch-interceptor.js +213 -0
- package/dist/fetch-interceptor.js.map +1 -0
- package/dist/gateway-send.js +114 -0
- package/dist/gateway-send.js.map +1 -0
- package/dist/http-handlers.js +131 -0
- package/dist/http-handlers.js.map +1 -0
- package/dist/index.js +24 -76340
- package/dist/index.js.map +1 -7
- package/dist/mcp-handlers.js +48 -0
- package/dist/mcp-handlers.js.map +1 -0
- package/dist/mcp-server.js +192 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/openclaw-compat.js +94 -0
- package/dist/openclaw-compat.js.map +1 -0
- package/dist/openclaw-entry.d.ts.map +1 -1
- package/dist/openclaw-entry.js +1204 -1414
- package/dist/openclaw-entry.js.map +1 -7
- package/dist/openclaw-plugin.js +297 -0
- package/dist/openclaw-plugin.js.map +1 -0
- package/dist/openclaw-types.js +13 -0
- package/dist/openclaw-types.js.map +1 -0
- package/dist/setup.js +460 -0
- package/dist/setup.js.map +1 -0
- package/dist/skill-invoker.js +100 -0
- package/dist/skill-invoker.js.map +1 -0
- package/dist/skill-manifest.js +249 -0
- package/dist/skill-manifest.js.map +1 -0
- package/dist/skill-telemetry.js +146 -0
- package/dist/skill-telemetry.js.map +1 -0
- package/dist/skills-publish.js +133 -0
- package/dist/skills-publish.js.map +1 -0
- package/dist/state.js +178 -0
- package/dist/state.js.map +1 -0
- package/dist/transport.js +43 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/workspace-handlers.js +177 -0
- package/dist/workspace-handlers.js.map +1 -0
- package/package.json +1 -1
package/dist/openclaw-entry.js
CHANGED
|
@@ -1,1472 +1,1262 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
// src/openclaw-compat.ts
|
|
12
|
-
var openclaw_compat_exports = {};
|
|
13
|
-
__export(openclaw_compat_exports, {
|
|
14
|
-
onAgentEvent: () => onAgentEvent,
|
|
15
|
-
onSessionTranscriptUpdate: () => onSessionTranscriptUpdate,
|
|
16
|
-
requestHeartbeatNow: () => requestHeartbeatNow
|
|
17
|
-
});
|
|
18
|
-
async function requestHeartbeatNow(opts) {
|
|
19
|
-
if (_heartbeatFn === null) {
|
|
20
|
-
try {
|
|
21
|
-
const mod = await import("openclaw/dist/plugin-sdk/infra/heartbeat-wake.js");
|
|
22
|
-
_heartbeatFn = mod.requestHeartbeatNow ?? mod.default?.requestHeartbeatNow ?? false;
|
|
23
|
-
} catch {
|
|
24
|
-
_heartbeatFn = false;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
if (typeof _heartbeatFn === "function") {
|
|
28
|
-
try {
|
|
29
|
-
await _heartbeatFn(opts);
|
|
30
|
-
return true;
|
|
31
|
-
} catch {
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return false;
|
|
36
|
-
}
|
|
37
|
-
async function onAgentEvent(callback) {
|
|
38
|
-
if (_agentEventFn === null) {
|
|
39
|
-
try {
|
|
40
|
-
const mod = await import("openclaw/dist/plugin-sdk/infra/agent-events.js");
|
|
41
|
-
_agentEventFn = mod.onAgentEvent ?? mod.default?.onAgentEvent ?? false;
|
|
42
|
-
} catch {
|
|
43
|
-
_agentEventFn = false;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
if (typeof _agentEventFn === "function") {
|
|
47
|
-
try {
|
|
48
|
-
return _agentEventFn(callback);
|
|
49
|
-
} catch {
|
|
50
|
-
return () => {
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return () => {
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
async function onSessionTranscriptUpdate(callback) {
|
|
58
|
-
if (_transcriptFn === null) {
|
|
59
|
-
try {
|
|
60
|
-
const mod = await import("openclaw/dist/plugin-sdk/sessions/transcript-events.js");
|
|
61
|
-
_transcriptFn = mod.onSessionTranscriptUpdate ?? mod.default?.onSessionTranscriptUpdate ?? false;
|
|
62
|
-
} catch {
|
|
63
|
-
_transcriptFn = false;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
if (typeof _transcriptFn === "function") {
|
|
67
|
-
try {
|
|
68
|
-
return _transcriptFn(callback);
|
|
69
|
-
} catch {
|
|
70
|
-
return () => {
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return () => {
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
var _heartbeatFn, _agentEventFn, _transcriptFn;
|
|
78
|
-
var init_openclaw_compat = __esm({
|
|
79
|
-
"src/openclaw-compat.ts"() {
|
|
80
|
-
"use strict";
|
|
81
|
-
_heartbeatFn = null;
|
|
82
|
-
_agentEventFn = null;
|
|
83
|
-
_transcriptFn = null;
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
// src/openclaw-entry.ts
|
|
1
|
+
/**
|
|
2
|
+
* OpenClaw channel plugin entry point.
|
|
3
|
+
*
|
|
4
|
+
* Intentionally thin — no heavy imports (libsodium etc.) at module load time.
|
|
5
|
+
* SecureChannel is dynamically imported inside gateway.startAccount (already async)
|
|
6
|
+
* so libsodium's top-level await never runs during plugin registration.
|
|
7
|
+
*
|
|
8
|
+
* Loaded by OpenClaw via the `openclaw.extensions` field in package.json.
|
|
9
|
+
*/
|
|
88
10
|
import { resolve } from "node:path";
|
|
89
11
|
import { randomBytes } from "node:crypto";
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
agentName: id,
|
|
113
|
-
httpPort: DEFAULT_HTTP_PORT,
|
|
114
|
-
configured: false
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
let httpPort = acct.httpPort;
|
|
118
|
-
if (httpPort == null) {
|
|
119
|
-
const keys = Object.keys(av.accounts);
|
|
120
|
-
const index = keys.indexOf(id);
|
|
121
|
-
httpPort = DEFAULT_HTTP_PORT + (index >= 0 ? index : 0);
|
|
122
|
-
}
|
|
123
|
-
return {
|
|
124
|
-
accountId: id,
|
|
125
|
-
dataDir: acct.dataDir ?? "",
|
|
126
|
-
apiUrl: acct.apiUrl ?? av.apiUrl ?? DEFAULT_API_URL,
|
|
127
|
-
agentName: acct.agentName ?? id,
|
|
128
|
-
httpPort,
|
|
129
|
-
configured: Boolean(acct.dataDir)
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
return {
|
|
133
|
-
accountId: id,
|
|
134
|
-
dataDir: av.dataDir ?? "~/.openclaw/agentvault",
|
|
135
|
-
apiUrl: av.apiUrl ?? DEFAULT_API_URL,
|
|
136
|
-
agentName: av.agentName ?? "OpenClaw Agent",
|
|
137
|
-
httpPort: av.httpPort ?? DEFAULT_HTTP_PORT,
|
|
138
|
-
configured: Boolean(av.dataDir)
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// src/fetch-interceptor.ts
|
|
143
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
144
|
-
import diagnosticsChannel from "node:diagnostics_channel";
|
|
145
|
-
var traceStore = new AsyncLocalStorage();
|
|
146
|
-
var DEFAULT_SKIP_PATTERNS = [
|
|
147
|
-
/^https?:\/\/localhost(:\d+)?/,
|
|
148
|
-
/^https?:\/\/127\.0\.0\.1(:\d+)?/,
|
|
149
|
-
/\.agentvault\.chat/,
|
|
150
|
-
/\.agentvault\.dev/,
|
|
151
|
-
/\.clerk\./
|
|
152
|
-
];
|
|
153
|
-
var installed = false;
|
|
154
|
-
var originalFetch;
|
|
155
|
-
var inflightRequests = /* @__PURE__ */ new WeakMap();
|
|
156
|
-
var unsubCreate;
|
|
157
|
-
var unsubHeaders;
|
|
158
|
-
var unsubError;
|
|
159
|
-
function shouldSkip(url, extraPatterns) {
|
|
160
|
-
const allPatterns = [...DEFAULT_SKIP_PATTERNS, ...extraPatterns];
|
|
161
|
-
return allPatterns.some((p) => p.test(url));
|
|
162
|
-
}
|
|
163
|
-
function extractUrl(origin, path) {
|
|
164
|
-
return `${origin}${path}`;
|
|
165
|
-
}
|
|
166
|
-
function installFetchInterceptor(opts) {
|
|
167
|
-
if (installed) return;
|
|
168
|
-
installed = true;
|
|
169
|
-
const skipPatterns = [
|
|
170
|
-
...DEFAULT_SKIP_PATTERNS,
|
|
171
|
-
...opts.skipPatterns ?? []
|
|
172
|
-
];
|
|
173
|
-
try {
|
|
174
|
-
const createChannel = diagnosticsChannel.channel("undici:request:create");
|
|
175
|
-
const headersChannel = diagnosticsChannel.channel("undici:request:headers");
|
|
176
|
-
const errorChannel = diagnosticsChannel.channel("undici:request:error");
|
|
177
|
-
const onRequestCreate = (message) => {
|
|
178
|
-
try {
|
|
179
|
-
const msg = message;
|
|
180
|
-
const req = msg.request;
|
|
181
|
-
if (!req) return;
|
|
182
|
-
const origin = String(req.origin ?? "");
|
|
183
|
-
const path = String(req.path ?? "/");
|
|
184
|
-
const url = extractUrl(origin, path);
|
|
185
|
-
if (shouldSkip(url, skipPatterns)) return;
|
|
186
|
-
inflightRequests.set(req, {
|
|
187
|
-
url,
|
|
188
|
-
method: String(req.method ?? "GET").toUpperCase(),
|
|
189
|
-
startTime: Date.now()
|
|
190
|
-
});
|
|
191
|
-
} catch {
|
|
192
|
-
}
|
|
193
|
-
};
|
|
194
|
-
const onRequestHeaders = (message) => {
|
|
195
|
-
try {
|
|
196
|
-
const msg = message;
|
|
197
|
-
const tracked = inflightRequests.get(msg.request);
|
|
198
|
-
if (!tracked) return;
|
|
199
|
-
inflightRequests.delete(msg.request);
|
|
200
|
-
const latencyMs = Date.now() - tracked.startTime;
|
|
201
|
-
const ctx = traceStore.getStore();
|
|
202
|
-
opts.onHttpCall({
|
|
203
|
-
method: tracked.method,
|
|
204
|
-
url: tracked.url,
|
|
205
|
-
statusCode: msg.response?.statusCode ?? 0,
|
|
206
|
-
latencyMs,
|
|
207
|
-
traceId: ctx?.traceId,
|
|
208
|
-
parentSpanId: ctx?.parentSpanId
|
|
209
|
-
});
|
|
210
|
-
} catch {
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
|
-
const onRequestError = (message) => {
|
|
214
|
-
try {
|
|
215
|
-
const msg = message;
|
|
216
|
-
const tracked = inflightRequests.get(msg.request);
|
|
217
|
-
if (!tracked) return;
|
|
218
|
-
inflightRequests.delete(msg.request);
|
|
219
|
-
const latencyMs = Date.now() - tracked.startTime;
|
|
220
|
-
const ctx = traceStore.getStore();
|
|
221
|
-
opts.onHttpCall({
|
|
222
|
-
method: tracked.method,
|
|
223
|
-
url: tracked.url,
|
|
224
|
-
statusCode: 0,
|
|
225
|
-
latencyMs,
|
|
226
|
-
traceId: ctx?.traceId,
|
|
227
|
-
parentSpanId: ctx?.parentSpanId
|
|
228
|
-
});
|
|
229
|
-
} catch {
|
|
230
|
-
}
|
|
231
|
-
};
|
|
232
|
-
createChannel.subscribe(onRequestCreate);
|
|
233
|
-
headersChannel.subscribe(onRequestHeaders);
|
|
234
|
-
errorChannel.subscribe(onRequestError);
|
|
235
|
-
unsubCreate = () => createChannel.unsubscribe(onRequestCreate);
|
|
236
|
-
unsubHeaders = () => headersChannel.unsubscribe(onRequestHeaders);
|
|
237
|
-
unsubError = () => errorChannel.unsubscribe(onRequestError);
|
|
238
|
-
} catch {
|
|
239
|
-
}
|
|
240
|
-
originalFetch = globalThis.fetch;
|
|
241
|
-
const savedOriginal = originalFetch;
|
|
242
|
-
globalThis.fetch = async (input, init) => {
|
|
243
|
-
const url = input instanceof Request ? input.url : input instanceof URL ? input.href : String(input);
|
|
244
|
-
if (shouldSkip(url, skipPatterns)) {
|
|
245
|
-
return savedOriginal(input, init);
|
|
246
|
-
}
|
|
247
|
-
const method = input instanceof Request ? input.method : init?.method ?? "GET";
|
|
248
|
-
const ctx = traceStore.getStore();
|
|
249
|
-
const start = Date.now();
|
|
250
|
-
try {
|
|
251
|
-
const response = await savedOriginal(input, init);
|
|
252
|
-
const latencyMs = Date.now() - start;
|
|
253
|
-
try {
|
|
254
|
-
opts.onHttpCall({
|
|
255
|
-
method,
|
|
256
|
-
url,
|
|
257
|
-
statusCode: response.status,
|
|
258
|
-
latencyMs,
|
|
259
|
-
traceId: ctx?.traceId,
|
|
260
|
-
parentSpanId: ctx?.parentSpanId
|
|
261
|
-
});
|
|
262
|
-
} catch {
|
|
263
|
-
}
|
|
264
|
-
return response;
|
|
265
|
-
} catch (err) {
|
|
266
|
-
const latencyMs = Date.now() - start;
|
|
267
|
-
try {
|
|
268
|
-
opts.onHttpCall({
|
|
269
|
-
method,
|
|
270
|
-
url,
|
|
271
|
-
statusCode: 0,
|
|
272
|
-
latencyMs,
|
|
273
|
-
traceId: ctx?.traceId,
|
|
274
|
-
parentSpanId: ctx?.parentSpanId
|
|
275
|
-
});
|
|
276
|
-
} catch {
|
|
277
|
-
}
|
|
278
|
-
throw err;
|
|
279
|
-
}
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
function runWithTraceContext(ctx, fn) {
|
|
283
|
-
return traceStore.run(ctx, fn);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// src/http-handlers.ts
|
|
287
|
-
async function handleSendRequest(parsed, channel) {
|
|
288
|
-
const text = parsed.text;
|
|
289
|
-
if (!text || typeof text !== "string") {
|
|
290
|
-
return { status: 400, body: { ok: false, error: "Missing 'text' field" } };
|
|
291
|
-
}
|
|
292
|
-
try {
|
|
293
|
-
let a2aTarget = parsed.hub_address ?? parsed.a2a_address ?? parsed.a2aAddress;
|
|
294
|
-
if (!a2aTarget && parsed.channel_id && typeof parsed.channel_id === "string") {
|
|
295
|
-
const hubAddr = channel.resolveA2AChannelHub(parsed.channel_id);
|
|
296
|
-
if (hubAddr) a2aTarget = hubAddr;
|
|
297
|
-
}
|
|
298
|
-
const roomId = parsed.room_id ?? channel.lastInboundRoomId;
|
|
299
|
-
if (a2aTarget && typeof a2aTarget === "string") {
|
|
300
|
-
await channel.sendToAgent(a2aTarget, text);
|
|
301
|
-
} else if (roomId) {
|
|
302
|
-
await channel.sendToRoom(roomId, text, {
|
|
303
|
-
messageType: parsed.message_type,
|
|
304
|
-
priority: parsed.priority,
|
|
305
|
-
metadata: parsed.metadata
|
|
306
|
-
});
|
|
307
|
-
} else if (parsed.file_path && typeof parsed.file_path === "string") {
|
|
308
|
-
await channel.sendWithAttachment(text, parsed.file_path, {
|
|
309
|
-
topicId: parsed.topicId
|
|
310
|
-
});
|
|
311
|
-
} else {
|
|
312
|
-
await channel.send(text, {
|
|
313
|
-
topicId: parsed.topicId,
|
|
314
|
-
messageType: parsed.message_type,
|
|
315
|
-
metadata: parsed.metadata
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
return { status: 200, body: { ok: true } };
|
|
319
|
-
} catch (err) {
|
|
320
|
-
return { status: 500, body: { ok: false, error: String(err) } };
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
async function handleActionRequest(parsed, channel) {
|
|
324
|
-
if (!parsed.action || typeof parsed.action !== "string") {
|
|
325
|
-
return { status: 400, body: { ok: false, error: "Missing 'action' field" } };
|
|
326
|
-
}
|
|
327
|
-
try {
|
|
328
|
-
const confirmation = {
|
|
329
|
-
action: parsed.action,
|
|
330
|
-
status: parsed.status ?? "completed",
|
|
331
|
-
decisionId: parsed.decision_id,
|
|
332
|
-
detail: parsed.detail,
|
|
333
|
-
estimated_cost: parsed.estimated_cost
|
|
334
|
-
};
|
|
335
|
-
if (parsed.room_id && typeof parsed.room_id === "string") {
|
|
336
|
-
await channel.sendActionConfirmationToRoom(parsed.room_id, confirmation);
|
|
337
|
-
} else {
|
|
338
|
-
await channel.sendActionConfirmation(confirmation);
|
|
339
|
-
}
|
|
340
|
-
return { status: 200, body: { ok: true } };
|
|
341
|
-
} catch (err) {
|
|
342
|
-
return { status: 500, body: { ok: false, error: String(err) } };
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
async function handleDecisionRequest(parsed, channel) {
|
|
346
|
-
const title = parsed.title;
|
|
347
|
-
if (!title || typeof title !== "string") {
|
|
348
|
-
return { status: 400, body: { ok: false, error: "Missing 'title' field" } };
|
|
349
|
-
}
|
|
350
|
-
const options = parsed.options;
|
|
351
|
-
if (!Array.isArray(options) || options.length < 2) {
|
|
352
|
-
return { status: 400, body: { ok: false, error: "'options' must be an array with at least 2 items" } };
|
|
353
|
-
}
|
|
354
|
-
for (const opt of options) {
|
|
355
|
-
if (!opt || typeof opt !== "object" || !opt.option_id || !opt.label) {
|
|
356
|
-
return { status: 400, body: { ok: false, error: "Each option must have 'option_id' and 'label'" } };
|
|
12
|
+
import { listAccountIds, resolveAccount } from "./account-config.js";
|
|
13
|
+
import { installFetchInterceptor, runWithTraceContext } from "./fetch-interceptor.js";
|
|
14
|
+
import { handleSendRequest, handleActionRequest, handleDecisionRequest, handleStatusRequest } from "./http-handlers.js";
|
|
15
|
+
import { requestHeartbeatNow } from "./openclaw-compat.js";
|
|
16
|
+
// --- Runtime and active channels (set during register) ---
|
|
17
|
+
let _ocRuntime = null;
|
|
18
|
+
const _channels = new Map();
|
|
19
|
+
const _messageQueue = [];
|
|
20
|
+
// --- A2A conversation loop prevention ---
|
|
21
|
+
// Tracks recent A2A reply timestamps per channel to prevent infinite loops.
|
|
22
|
+
// Max replies per channel within the window, then cooldown.
|
|
23
|
+
const A2A_MAX_REPLIES_PER_WINDOW = 4;
|
|
24
|
+
const A2A_WINDOW_MS = 60_000; // 1-minute sliding window
|
|
25
|
+
const A2A_COOLDOWN_MS = 120_000; // 2-minute cooldown after hitting limit
|
|
26
|
+
const _a2aReplyTimestamps = new Map();
|
|
27
|
+
const _a2aCooldownUntil = new Map();
|
|
28
|
+
function _a2aCanReply(channelId) {
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
// Check cooldown
|
|
31
|
+
const cooldownEnd = _a2aCooldownUntil.get(channelId) ?? 0;
|
|
32
|
+
if (now < cooldownEnd) {
|
|
33
|
+
return false;
|
|
357
34
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
return { status: 200, body: { ok: true, decision_id } };
|
|
369
|
-
} catch (err) {
|
|
370
|
-
return { status: 500, body: { ok: false, error: String(err) } };
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
function handleStatusRequest(channel) {
|
|
374
|
-
return {
|
|
375
|
-
status: 200,
|
|
376
|
-
body: {
|
|
377
|
-
ok: true,
|
|
378
|
-
state: channel.state,
|
|
379
|
-
deviceId: channel.deviceId ?? void 0,
|
|
380
|
-
sessions: channel.sessionCount
|
|
35
|
+
// Get timestamps within window
|
|
36
|
+
const timestamps = _a2aReplyTimestamps.get(channelId) ?? [];
|
|
37
|
+
const recent = timestamps.filter((t) => now - t < A2A_WINDOW_MS);
|
|
38
|
+
if (recent.length >= A2A_MAX_REPLIES_PER_WINDOW) {
|
|
39
|
+
// Hit limit — enter cooldown
|
|
40
|
+
_a2aCooldownUntil.set(channelId, now + A2A_COOLDOWN_MS);
|
|
41
|
+
_a2aReplyTimestamps.set(channelId, []);
|
|
42
|
+
console.warn(`[AgentVault] A2A rate limit hit for channel ${channelId.slice(0, 8)}... — ` +
|
|
43
|
+
`${A2A_MAX_REPLIES_PER_WINDOW} replies in ${A2A_WINDOW_MS / 1000}s, cooling down ${A2A_COOLDOWN_MS / 1000}s`);
|
|
44
|
+
return false;
|
|
381
45
|
}
|
|
382
|
-
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// src/openclaw-entry.ts
|
|
386
|
-
init_openclaw_compat();
|
|
387
|
-
var _ocRuntime = null;
|
|
388
|
-
var _channels = /* @__PURE__ */ new Map();
|
|
389
|
-
var _messageQueue = [];
|
|
390
|
-
var A2A_MAX_REPLIES_PER_WINDOW = 4;
|
|
391
|
-
var A2A_WINDOW_MS = 6e4;
|
|
392
|
-
var A2A_COOLDOWN_MS = 12e4;
|
|
393
|
-
var _a2aReplyTimestamps = /* @__PURE__ */ new Map();
|
|
394
|
-
var _a2aCooldownUntil = /* @__PURE__ */ new Map();
|
|
395
|
-
function _a2aCanReply(channelId) {
|
|
396
|
-
const now = Date.now();
|
|
397
|
-
const cooldownEnd = _a2aCooldownUntil.get(channelId) ?? 0;
|
|
398
|
-
if (now < cooldownEnd) {
|
|
399
|
-
return false;
|
|
400
|
-
}
|
|
401
|
-
const timestamps = _a2aReplyTimestamps.get(channelId) ?? [];
|
|
402
|
-
const recent = timestamps.filter((t) => now - t < A2A_WINDOW_MS);
|
|
403
|
-
if (recent.length >= A2A_MAX_REPLIES_PER_WINDOW) {
|
|
404
|
-
_a2aCooldownUntil.set(channelId, now + A2A_COOLDOWN_MS);
|
|
405
|
-
_a2aReplyTimestamps.set(channelId, []);
|
|
406
|
-
console.warn(
|
|
407
|
-
`[AgentVault] A2A rate limit hit for channel ${channelId.slice(0, 8)}... \u2014 ${A2A_MAX_REPLIES_PER_WINDOW} replies in ${A2A_WINDOW_MS / 1e3}s, cooling down ${A2A_COOLDOWN_MS / 1e3}s`
|
|
408
|
-
);
|
|
409
|
-
return false;
|
|
410
|
-
}
|
|
411
|
-
return true;
|
|
46
|
+
return true;
|
|
412
47
|
}
|
|
413
48
|
function _a2aRecordReply(channelId) {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
channelId,
|
|
419
|
-
timestamps.filter((t) => now - t < A2A_WINDOW_MS)
|
|
420
|
-
);
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
const timestamps = _a2aReplyTimestamps.get(channelId) ?? [];
|
|
51
|
+
timestamps.push(now);
|
|
52
|
+
// Keep only timestamps within window
|
|
53
|
+
_a2aReplyTimestamps.set(channelId, timestamps.filter((t) => now - t < A2A_WINDOW_MS));
|
|
421
54
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
55
|
+
/** Whether OpenClaw managed HTTP routes are active (vs self-managed server). */
|
|
56
|
+
export let isUsingManagedRoutes = false;
|
|
57
|
+
/**
|
|
58
|
+
* Shared mutable targets array for OpenClaw outbound routing.
|
|
59
|
+
* OpenClaw validates `to` against this list before calling sendText/sendPayload.
|
|
60
|
+
* We mutate it at runtime to add A2A channel targets dynamically.
|
|
61
|
+
*/
|
|
62
|
+
const _outboundTargets = [
|
|
63
|
+
{ id: "owner", label: "AgentVault Owner", accountId: "default" },
|
|
64
|
+
{ id: "default", label: "AgentVault Owner (default)", accountId: "default" },
|
|
426
65
|
];
|
|
66
|
+
/** Register a room as a valid outbound target (idempotent). */
|
|
67
|
+
function _registerRoomTarget(roomId, roomName, accountId) {
|
|
68
|
+
const targetId = `room:${roomId}`;
|
|
69
|
+
if (_outboundTargets.some((t) => t.id === targetId))
|
|
70
|
+
return;
|
|
71
|
+
_outboundTargets.push({ id: targetId, label: `Room: ${roomName}`, accountId });
|
|
72
|
+
console.log(`[AgentVault] Registered room target: ${roomName} (${roomId.slice(0, 8)}...)`);
|
|
73
|
+
}
|
|
74
|
+
/** Register an A2A peer as a valid outbound target (idempotent). */
|
|
427
75
|
function _registerA2ATarget(hubAddress, accountId) {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
76
|
+
const targetId = `a2a:${hubAddress}`;
|
|
77
|
+
if (_outboundTargets.some((t) => t.id === targetId))
|
|
78
|
+
return;
|
|
79
|
+
_outboundTargets.push({
|
|
80
|
+
id: targetId,
|
|
81
|
+
label: `A2A: ${hubAddress}`,
|
|
82
|
+
accountId,
|
|
83
|
+
});
|
|
84
|
+
console.log(`[AgentVault] Registered A2A target: ${targetId}`);
|
|
436
85
|
}
|
|
437
86
|
function _setRuntime(rt) {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
87
|
+
_ocRuntime = rt;
|
|
88
|
+
// Flush any messages that arrived before runtime was ready
|
|
89
|
+
if (_messageQueue.length > 0) {
|
|
90
|
+
const pending = _messageQueue.splice(0);
|
|
91
|
+
for (const fn of pending) {
|
|
92
|
+
fn().catch(() => { });
|
|
93
|
+
}
|
|
444
94
|
}
|
|
445
|
-
}
|
|
446
95
|
}
|
|
96
|
+
// --- @mention filtering for multi-agent rooms ---
|
|
97
|
+
/** Extract @mention names from plaintext. Returns lowercased names. */
|
|
447
98
|
function _parseMentions(text) {
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
99
|
+
const mentions = [];
|
|
100
|
+
// Match @word at word boundary — supports multi-word via sequential matching
|
|
101
|
+
const re = /@(\w[\w]*)/gi;
|
|
102
|
+
let match;
|
|
103
|
+
while ((match = re.exec(text)) !== null) {
|
|
104
|
+
mentions.push(match[1].toLowerCase());
|
|
105
|
+
}
|
|
106
|
+
return mentions;
|
|
455
107
|
}
|
|
108
|
+
/** Determine whether this agent should process a room message based on @mentions. */
|
|
456
109
|
function _shouldProcessRoomMessage(plaintext, agentName, accountId) {
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
110
|
+
const mentions = _parseMentions(plaintext);
|
|
111
|
+
// No mentions → broadcast to all agents (current behavior)
|
|
112
|
+
if (mentions.length === 0)
|
|
113
|
+
return true;
|
|
114
|
+
// @all / @everyone → all agents process
|
|
115
|
+
if (mentions.includes("all") || mentions.includes("everyone"))
|
|
116
|
+
return true;
|
|
117
|
+
// Check if this agent is mentioned (by name or accountId)
|
|
118
|
+
const nameLower = agentName.toLowerCase();
|
|
119
|
+
const idLower = accountId.toLowerCase();
|
|
120
|
+
// Match first word of agent name (e.g., "Cortina" from "Cortina (Coder)")
|
|
121
|
+
const firstWord = nameLower.split(/[\s(]/)[0];
|
|
122
|
+
return mentions.some((m) => m === nameLower || m === firstWord || m === idLower);
|
|
466
123
|
}
|
|
124
|
+
/** Strip the matching @mention prefix from plaintext so the agent sees clean text. */
|
|
467
125
|
function _stripMentions(text, agentName, accountId) {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
126
|
+
const nameLower = agentName.toLowerCase();
|
|
127
|
+
const firstWord = nameLower.split(/[\s(]/)[0];
|
|
128
|
+
const idLower = accountId.toLowerCase();
|
|
129
|
+
// Remove all @mentions that match this agent (case-insensitive)
|
|
130
|
+
return text
|
|
131
|
+
.replace(/@(\w[\w]*)/gi, (full, name) => {
|
|
132
|
+
const lower = name.toLowerCase();
|
|
133
|
+
if (lower === nameLower || lower === firstWord || lower === idLower) {
|
|
134
|
+
return "";
|
|
135
|
+
}
|
|
136
|
+
// Also strip @all/@everyone since they're routing directives
|
|
137
|
+
if (lower === "all" || lower === "everyone")
|
|
138
|
+
return "";
|
|
139
|
+
return full;
|
|
140
|
+
})
|
|
141
|
+
.replace(/^\s+/, "") // trim leading whitespace left by stripped mention
|
|
142
|
+
.trim();
|
|
479
143
|
}
|
|
144
|
+
// --- Inbound message dispatch ---
|
|
480
145
|
async function handleInbound(params) {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
if (
|
|
486
|
-
|
|
146
|
+
const { plaintext: rawPlaintext, metadata, channel, account, cfg } = params;
|
|
147
|
+
const isRoomMessage = Boolean(metadata.roomId);
|
|
148
|
+
const isA2AMessage = Boolean(metadata.a2aChannelId);
|
|
149
|
+
// @mention filtering: only for room messages
|
|
150
|
+
if (isRoomMessage) {
|
|
151
|
+
if (!_shouldProcessRoomMessage(rawPlaintext, account.agentName ?? "", account.accountId ?? "")) {
|
|
152
|
+
return; // This agent is not mentioned — skip silently
|
|
153
|
+
}
|
|
487
154
|
}
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
155
|
+
// Strip @mentions from plaintext so the agent sees clean text
|
|
156
|
+
const plaintext = isRoomMessage
|
|
157
|
+
? _stripMentions(rawPlaintext, account.agentName ?? "", account.accountId ?? "")
|
|
158
|
+
: rawPlaintext;
|
|
159
|
+
// Telemetry: hierarchical spans for the full message lifecycle
|
|
160
|
+
const startTime = Date.now();
|
|
161
|
+
const traceId = randomBytes(16).toString("hex");
|
|
162
|
+
const rootSpanId = randomBytes(8).toString("hex");
|
|
163
|
+
const inferenceSpanId = randomBytes(8).toString("hex");
|
|
164
|
+
// Instrumentation context for agent code to report LLM/tool/error spans
|
|
165
|
+
/** Helper: send an activity span over WS for real-time owner display. */
|
|
166
|
+
const _sendActivity = (spanData) => {
|
|
167
|
+
try {
|
|
168
|
+
channel.sendActivitySpan({ ...spanData, trace_id: traceId, parent_span_id: inferenceSpanId });
|
|
169
|
+
}
|
|
170
|
+
catch { }
|
|
171
|
+
};
|
|
172
|
+
const _instrument = {
|
|
173
|
+
reportLlm: (opts) => {
|
|
174
|
+
const enriched = { ...opts, traceId, parentSpanId: inferenceSpanId };
|
|
175
|
+
if (opts.skillName || _instrument.skillName)
|
|
176
|
+
enriched.skillName = opts.skillName ?? _instrument.skillName;
|
|
177
|
+
try {
|
|
178
|
+
channel.telemetry?.reportLlmCall(enriched);
|
|
179
|
+
}
|
|
180
|
+
catch { }
|
|
181
|
+
const activityAttrs = { "ai.agent.llm.model": opts.model ?? "" };
|
|
182
|
+
if (enriched.skillName)
|
|
183
|
+
activityAttrs["ai.agent.skill.name"] = enriched.skillName;
|
|
184
|
+
_sendActivity({
|
|
185
|
+
span_id: randomBytes(8).toString("hex"), span_type: "llm", span_name: opts.model ?? "LLM",
|
|
186
|
+
status: opts.status === "error" ? "error" : "ok",
|
|
187
|
+
start_time: new Date(Date.now() - (opts.latencyMs ?? 0)).toISOString(),
|
|
188
|
+
end_time: new Date().toISOString(), duration_ms: opts.latencyMs ?? 0,
|
|
189
|
+
attributes: activityAttrs,
|
|
190
|
+
tokens_input: opts.tokensInput, tokens_output: opts.tokensOutput,
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
reportTool: (opts) => {
|
|
194
|
+
const enriched = { ...opts, traceId, parentSpanId: inferenceSpanId };
|
|
195
|
+
if (opts.skillName || _instrument.skillName)
|
|
196
|
+
enriched.skillName = opts.skillName ?? _instrument.skillName;
|
|
197
|
+
try {
|
|
198
|
+
channel.telemetry?.reportToolCall(enriched);
|
|
199
|
+
}
|
|
200
|
+
catch { }
|
|
201
|
+
const activityAttrs = { "ai.agent.tool.name": opts.toolName ?? "" };
|
|
202
|
+
if (enriched.skillName)
|
|
203
|
+
activityAttrs["ai.agent.skill.name"] = enriched.skillName;
|
|
204
|
+
_sendActivity({
|
|
205
|
+
span_id: randomBytes(8).toString("hex"), span_type: "tool", span_name: opts.toolName ?? "tool",
|
|
206
|
+
status: opts.success === false ? "error" : "ok",
|
|
207
|
+
start_time: new Date(Date.now() - (opts.latencyMs ?? 0)).toISOString(),
|
|
208
|
+
end_time: new Date().toISOString(), duration_ms: opts.latencyMs ?? 0,
|
|
209
|
+
attributes: activityAttrs,
|
|
210
|
+
tool_success: opts.success,
|
|
211
|
+
});
|
|
212
|
+
},
|
|
213
|
+
reportError: (opts) => {
|
|
214
|
+
const enriched = { ...opts, traceId, parentSpanId: inferenceSpanId };
|
|
215
|
+
if (opts.skillName || _instrument.skillName)
|
|
216
|
+
enriched.skillName = opts.skillName ?? _instrument.skillName;
|
|
217
|
+
try {
|
|
218
|
+
channel.telemetry?.reportError(enriched);
|
|
219
|
+
}
|
|
220
|
+
catch { }
|
|
221
|
+
const activityAttrs = { "ai.agent.error.type": opts.errorType ?? "" };
|
|
222
|
+
if (enriched.skillName)
|
|
223
|
+
activityAttrs["ai.agent.skill.name"] = enriched.skillName;
|
|
224
|
+
_sendActivity({
|
|
225
|
+
span_id: randomBytes(8).toString("hex"), span_type: "error", span_name: opts.errorType ?? "error",
|
|
226
|
+
status: "error",
|
|
227
|
+
start_time: new Date().toISOString(), end_time: new Date().toISOString(), duration_ms: 0,
|
|
228
|
+
attributes: activityAttrs,
|
|
229
|
+
error_type: opts.errorType, error_message: opts.errorMessage,
|
|
230
|
+
});
|
|
231
|
+
},
|
|
232
|
+
reportHttp: (opts) => {
|
|
233
|
+
const enriched = { ...opts, traceId, parentSpanId: inferenceSpanId };
|
|
234
|
+
if (opts.skillName || _instrument.skillName)
|
|
235
|
+
enriched.skillName = opts.skillName ?? _instrument.skillName;
|
|
236
|
+
try {
|
|
237
|
+
channel.telemetry?.reportHttpCall(enriched);
|
|
238
|
+
}
|
|
239
|
+
catch { }
|
|
240
|
+
const activityAttrs = { "ai.agent.http.method": opts.method ?? "", "ai.agent.http.url": opts.url ?? "" };
|
|
241
|
+
if (enriched.skillName)
|
|
242
|
+
activityAttrs["ai.agent.skill.name"] = enriched.skillName;
|
|
243
|
+
_sendActivity({
|
|
244
|
+
span_id: randomBytes(8).toString("hex"), span_type: "http",
|
|
245
|
+
span_name: (() => { try {
|
|
246
|
+
return new URL(opts.url).hostname;
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return opts.url?.slice(0, 40) ?? "http";
|
|
250
|
+
} })(),
|
|
251
|
+
status: (opts.statusCode ?? 200) >= 400 ? "error" : "ok",
|
|
252
|
+
start_time: new Date(Date.now() - (opts.latencyMs ?? 0)).toISOString(),
|
|
253
|
+
end_time: new Date().toISOString(), duration_ms: opts.latencyMs ?? 0,
|
|
254
|
+
attributes: activityAttrs,
|
|
255
|
+
http_method: opts.method, http_status_code: opts.statusCode, http_url: opts.url,
|
|
256
|
+
});
|
|
257
|
+
},
|
|
258
|
+
reportAction: (opts) => {
|
|
259
|
+
const enriched = { ...opts, traceId, parentSpanId: inferenceSpanId };
|
|
260
|
+
if (opts.skillName || _instrument.skillName)
|
|
261
|
+
enriched.skillName = opts.skillName ?? _instrument.skillName;
|
|
262
|
+
try {
|
|
263
|
+
channel.telemetry?.reportActionCall(enriched);
|
|
264
|
+
}
|
|
265
|
+
catch { }
|
|
266
|
+
const activityAttrs = { "ai.agent.action.type": opts.actionType ?? "", "ai.agent.action.target": opts.target ?? "" };
|
|
267
|
+
if (enriched.skillName)
|
|
268
|
+
activityAttrs["ai.agent.skill.name"] = enriched.skillName;
|
|
269
|
+
_sendActivity({
|
|
270
|
+
span_id: randomBytes(8).toString("hex"), span_type: "action",
|
|
271
|
+
span_name: `${opts.actionType ?? "action"}: ${opts.target ?? ""}`,
|
|
272
|
+
status: opts.success === false ? "error" : "ok",
|
|
273
|
+
start_time: new Date(Date.now() - (opts.latencyMs ?? 0)).toISOString(),
|
|
274
|
+
end_time: new Date().toISOString(), duration_ms: opts.latencyMs ?? 0,
|
|
275
|
+
attributes: activityAttrs,
|
|
276
|
+
action_type: opts.actionType, action_target: opts.target,
|
|
277
|
+
});
|
|
278
|
+
},
|
|
279
|
+
reportNav: (opts) => {
|
|
280
|
+
const enriched = { ...opts, traceId, parentSpanId: inferenceSpanId };
|
|
281
|
+
if (opts.skillName || _instrument.skillName)
|
|
282
|
+
enriched.skillName = opts.skillName ?? _instrument.skillName;
|
|
283
|
+
try {
|
|
284
|
+
channel.telemetry?.reportNavCall(enriched);
|
|
285
|
+
}
|
|
286
|
+
catch { }
|
|
287
|
+
const activityAttrs = { "ai.agent.nav.to_url": opts.toUrl ?? "" };
|
|
288
|
+
if (enriched.skillName)
|
|
289
|
+
activityAttrs["ai.agent.skill.name"] = enriched.skillName;
|
|
290
|
+
_sendActivity({
|
|
291
|
+
span_id: randomBytes(8).toString("hex"), span_type: "nav",
|
|
292
|
+
span_name: opts.toUrl?.slice(0, 40) ?? "navigate",
|
|
293
|
+
status: opts.status === "error" ? "error" : "ok",
|
|
294
|
+
start_time: new Date(Date.now() - (opts.latencyMs ?? 0)).toISOString(),
|
|
295
|
+
end_time: new Date().toISOString(), duration_ms: opts.latencyMs ?? 0,
|
|
296
|
+
attributes: activityAttrs,
|
|
297
|
+
});
|
|
298
|
+
},
|
|
299
|
+
skillName: undefined,
|
|
300
|
+
traceId,
|
|
301
|
+
parentSpanId: inferenceSpanId,
|
|
302
|
+
};
|
|
303
|
+
let replyCount = 0;
|
|
304
|
+
let totalReplyChars = 0;
|
|
305
|
+
let firstReplyTime = null;
|
|
306
|
+
const replySpans = [];
|
|
307
|
+
// Emit "receive" child span immediately — marks inbound arrival
|
|
308
|
+
_emitChildSpan(channel, {
|
|
309
|
+
traceId,
|
|
310
|
+
parentSpanId: rootSpanId,
|
|
311
|
+
name: "ai.agent.message.receive",
|
|
312
|
+
startTime,
|
|
313
|
+
endTime: startTime + 1, // instant
|
|
314
|
+
attributes: {
|
|
315
|
+
"ai.agent.message.input_chars": plaintext.length,
|
|
316
|
+
"ai.agent.message.type": isA2AMessage ? "a2a" : metadata.roomId ? "room" : "direct",
|
|
317
|
+
},
|
|
318
|
+
status: "ok",
|
|
319
|
+
});
|
|
320
|
+
// Send typing indicator to owner (non-critical, best-effort)
|
|
495
321
|
try {
|
|
496
|
-
|
|
497
|
-
} catch {
|
|
322
|
+
channel.sendTyping();
|
|
498
323
|
}
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
const enriched = { ...opts, traceId, parentSpanId: inferenceSpanId };
|
|
503
|
-
if (opts.skillName || _instrument.skillName) enriched.skillName = opts.skillName ?? _instrument.skillName;
|
|
504
|
-
try {
|
|
505
|
-
channel.telemetry?.reportLlmCall(enriched);
|
|
506
|
-
} catch {
|
|
507
|
-
}
|
|
508
|
-
const activityAttrs = { "ai.agent.llm.model": opts.model ?? "" };
|
|
509
|
-
if (enriched.skillName) activityAttrs["ai.agent.skill.name"] = enriched.skillName;
|
|
510
|
-
_sendActivity({
|
|
511
|
-
span_id: randomBytes(8).toString("hex"),
|
|
512
|
-
span_type: "llm",
|
|
513
|
-
span_name: opts.model ?? "LLM",
|
|
514
|
-
status: opts.status === "error" ? "error" : "ok",
|
|
515
|
-
start_time: new Date(Date.now() - (opts.latencyMs ?? 0)).toISOString(),
|
|
516
|
-
end_time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
517
|
-
duration_ms: opts.latencyMs ?? 0,
|
|
518
|
-
attributes: activityAttrs,
|
|
519
|
-
tokens_input: opts.tokensInput,
|
|
520
|
-
tokens_output: opts.tokensOutput
|
|
521
|
-
});
|
|
522
|
-
},
|
|
523
|
-
reportTool: (opts) => {
|
|
524
|
-
const enriched = { ...opts, traceId, parentSpanId: inferenceSpanId };
|
|
525
|
-
if (opts.skillName || _instrument.skillName) enriched.skillName = opts.skillName ?? _instrument.skillName;
|
|
526
|
-
try {
|
|
527
|
-
channel.telemetry?.reportToolCall(enriched);
|
|
528
|
-
} catch {
|
|
529
|
-
}
|
|
530
|
-
const activityAttrs = { "ai.agent.tool.name": opts.toolName ?? "" };
|
|
531
|
-
if (enriched.skillName) activityAttrs["ai.agent.skill.name"] = enriched.skillName;
|
|
532
|
-
_sendActivity({
|
|
533
|
-
span_id: randomBytes(8).toString("hex"),
|
|
534
|
-
span_type: "tool",
|
|
535
|
-
span_name: opts.toolName ?? "tool",
|
|
536
|
-
status: opts.success === false ? "error" : "ok",
|
|
537
|
-
start_time: new Date(Date.now() - (opts.latencyMs ?? 0)).toISOString(),
|
|
538
|
-
end_time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
539
|
-
duration_ms: opts.latencyMs ?? 0,
|
|
540
|
-
attributes: activityAttrs,
|
|
541
|
-
tool_success: opts.success
|
|
542
|
-
});
|
|
543
|
-
},
|
|
544
|
-
reportError: (opts) => {
|
|
545
|
-
const enriched = { ...opts, traceId, parentSpanId: inferenceSpanId };
|
|
546
|
-
if (opts.skillName || _instrument.skillName) enriched.skillName = opts.skillName ?? _instrument.skillName;
|
|
547
|
-
try {
|
|
548
|
-
channel.telemetry?.reportError(enriched);
|
|
549
|
-
} catch {
|
|
550
|
-
}
|
|
551
|
-
const activityAttrs = { "ai.agent.error.type": opts.errorType ?? "" };
|
|
552
|
-
if (enriched.skillName) activityAttrs["ai.agent.skill.name"] = enriched.skillName;
|
|
553
|
-
_sendActivity({
|
|
554
|
-
span_id: randomBytes(8).toString("hex"),
|
|
555
|
-
span_type: "error",
|
|
556
|
-
span_name: opts.errorType ?? "error",
|
|
557
|
-
status: "error",
|
|
558
|
-
start_time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
559
|
-
end_time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
560
|
-
duration_ms: 0,
|
|
561
|
-
attributes: activityAttrs,
|
|
562
|
-
error_type: opts.errorType,
|
|
563
|
-
error_message: opts.errorMessage
|
|
564
|
-
});
|
|
565
|
-
},
|
|
566
|
-
reportHttp: (opts) => {
|
|
567
|
-
const enriched = { ...opts, traceId, parentSpanId: inferenceSpanId };
|
|
568
|
-
if (opts.skillName || _instrument.skillName) enriched.skillName = opts.skillName ?? _instrument.skillName;
|
|
569
|
-
try {
|
|
570
|
-
channel.telemetry?.reportHttpCall(enriched);
|
|
571
|
-
} catch {
|
|
572
|
-
}
|
|
573
|
-
const activityAttrs = { "ai.agent.http.method": opts.method ?? "", "ai.agent.http.url": opts.url ?? "" };
|
|
574
|
-
if (enriched.skillName) activityAttrs["ai.agent.skill.name"] = enriched.skillName;
|
|
575
|
-
_sendActivity({
|
|
576
|
-
span_id: randomBytes(8).toString("hex"),
|
|
577
|
-
span_type: "http",
|
|
578
|
-
span_name: (() => {
|
|
579
|
-
try {
|
|
580
|
-
return new URL(opts.url).hostname;
|
|
581
|
-
} catch {
|
|
582
|
-
return opts.url?.slice(0, 40) ?? "http";
|
|
583
|
-
}
|
|
584
|
-
})(),
|
|
585
|
-
status: (opts.statusCode ?? 200) >= 400 ? "error" : "ok",
|
|
586
|
-
start_time: new Date(Date.now() - (opts.latencyMs ?? 0)).toISOString(),
|
|
587
|
-
end_time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
588
|
-
duration_ms: opts.latencyMs ?? 0,
|
|
589
|
-
attributes: activityAttrs,
|
|
590
|
-
http_method: opts.method,
|
|
591
|
-
http_status_code: opts.statusCode,
|
|
592
|
-
http_url: opts.url
|
|
593
|
-
});
|
|
594
|
-
},
|
|
595
|
-
reportAction: (opts) => {
|
|
596
|
-
const enriched = { ...opts, traceId, parentSpanId: inferenceSpanId };
|
|
597
|
-
if (opts.skillName || _instrument.skillName) enriched.skillName = opts.skillName ?? _instrument.skillName;
|
|
598
|
-
try {
|
|
599
|
-
channel.telemetry?.reportActionCall(enriched);
|
|
600
|
-
} catch {
|
|
601
|
-
}
|
|
602
|
-
const activityAttrs = { "ai.agent.action.type": opts.actionType ?? "", "ai.agent.action.target": opts.target ?? "" };
|
|
603
|
-
if (enriched.skillName) activityAttrs["ai.agent.skill.name"] = enriched.skillName;
|
|
604
|
-
_sendActivity({
|
|
605
|
-
span_id: randomBytes(8).toString("hex"),
|
|
606
|
-
span_type: "action",
|
|
607
|
-
span_name: `${opts.actionType ?? "action"}: ${opts.target ?? ""}`,
|
|
608
|
-
status: opts.success === false ? "error" : "ok",
|
|
609
|
-
start_time: new Date(Date.now() - (opts.latencyMs ?? 0)).toISOString(),
|
|
610
|
-
end_time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
611
|
-
duration_ms: opts.latencyMs ?? 0,
|
|
612
|
-
attributes: activityAttrs,
|
|
613
|
-
action_type: opts.actionType,
|
|
614
|
-
action_target: opts.target
|
|
615
|
-
});
|
|
616
|
-
},
|
|
617
|
-
reportNav: (opts) => {
|
|
618
|
-
const enriched = { ...opts, traceId, parentSpanId: inferenceSpanId };
|
|
619
|
-
if (opts.skillName || _instrument.skillName) enriched.skillName = opts.skillName ?? _instrument.skillName;
|
|
620
|
-
try {
|
|
621
|
-
channel.telemetry?.reportNavCall(enriched);
|
|
622
|
-
} catch {
|
|
623
|
-
}
|
|
624
|
-
const activityAttrs = { "ai.agent.nav.to_url": opts.toUrl ?? "" };
|
|
625
|
-
if (enriched.skillName) activityAttrs["ai.agent.skill.name"] = enriched.skillName;
|
|
626
|
-
_sendActivity({
|
|
627
|
-
span_id: randomBytes(8).toString("hex"),
|
|
628
|
-
span_type: "nav",
|
|
629
|
-
span_name: opts.toUrl?.slice(0, 40) ?? "navigate",
|
|
630
|
-
status: opts.status === "error" ? "error" : "ok",
|
|
631
|
-
start_time: new Date(Date.now() - (opts.latencyMs ?? 0)).toISOString(),
|
|
632
|
-
end_time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
633
|
-
duration_ms: opts.latencyMs ?? 0,
|
|
634
|
-
attributes: activityAttrs
|
|
635
|
-
});
|
|
636
|
-
},
|
|
637
|
-
skillName: void 0,
|
|
638
|
-
traceId,
|
|
639
|
-
parentSpanId: inferenceSpanId
|
|
640
|
-
};
|
|
641
|
-
let replyCount = 0;
|
|
642
|
-
let totalReplyChars = 0;
|
|
643
|
-
let firstReplyTime = null;
|
|
644
|
-
const replySpans = [];
|
|
645
|
-
_emitChildSpan(channel, {
|
|
646
|
-
traceId,
|
|
647
|
-
parentSpanId: rootSpanId,
|
|
648
|
-
name: "ai.agent.message.receive",
|
|
649
|
-
startTime,
|
|
650
|
-
endTime: startTime + 1,
|
|
651
|
-
// instant
|
|
652
|
-
attributes: {
|
|
653
|
-
"ai.agent.message.input_chars": plaintext.length,
|
|
654
|
-
"ai.agent.message.type": isA2AMessage ? "a2a" : metadata.roomId ? "room" : "direct"
|
|
655
|
-
},
|
|
656
|
-
status: "ok"
|
|
657
|
-
});
|
|
658
|
-
try {
|
|
659
|
-
channel.sendTyping();
|
|
660
|
-
} catch {
|
|
661
|
-
}
|
|
662
|
-
const core = _ocRuntime;
|
|
663
|
-
const route = core.channel.routing.resolveAgentRoute({
|
|
664
|
-
cfg,
|
|
665
|
-
channel: "agentvault",
|
|
666
|
-
accountId: account.accountId,
|
|
667
|
-
peer: {
|
|
668
|
-
kind: isA2AMessage ? "a2a" : "direct",
|
|
669
|
-
id: isA2AMessage ? `agentvault:a2a:${metadata.fromHubAddress}` : isRoomMessage ? `agentvault:room:${metadata.roomId}` : "agentvault:owner"
|
|
670
|
-
}
|
|
671
|
-
});
|
|
672
|
-
const storePath = core.channel.session.resolveStorePath(cfg?.session?.store, {
|
|
673
|
-
agentId: route.agentId
|
|
674
|
-
});
|
|
675
|
-
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
|
676
|
-
const previousTimestamp = core.channel.session.readSessionUpdatedAt({
|
|
677
|
-
storePath,
|
|
678
|
-
sessionKey: route.sessionKey
|
|
679
|
-
});
|
|
680
|
-
const body = core.channel.reply.formatAgentEnvelope({
|
|
681
|
-
channel: "AgentVault",
|
|
682
|
-
from: isA2AMessage ? `Agent (${metadata.fromHubAddress})` : isRoomMessage ? "Room" : "Owner",
|
|
683
|
-
timestamp: new Date(metadata.timestamp).getTime(),
|
|
684
|
-
previousTimestamp,
|
|
685
|
-
envelope: envelopeOptions,
|
|
686
|
-
body: plaintext
|
|
687
|
-
});
|
|
688
|
-
const attachmentFields = {};
|
|
689
|
-
if (metadata.attachment) {
|
|
690
|
-
attachmentFields.AttachmentPath = metadata.attachment.filePath;
|
|
691
|
-
attachmentFields.AttachmentFilename = metadata.attachment.filename;
|
|
692
|
-
attachmentFields.AttachmentMime = metadata.attachment.mime;
|
|
693
|
-
if (metadata.attachment.base64) {
|
|
694
|
-
attachmentFields.MediaUrl = metadata.attachment.base64;
|
|
695
|
-
attachmentFields.NumMedia = "1";
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
699
|
-
Body: body,
|
|
700
|
-
RawBody: plaintext,
|
|
701
|
-
CommandBody: plaintext,
|
|
702
|
-
From: isA2AMessage ? `a2a:${metadata.fromHubAddress}` : isRoomMessage ? `agentvault:room:${metadata.roomId}` : "agentvault:owner",
|
|
703
|
-
To: `agentvault:agent:${account.accountId}`,
|
|
704
|
-
SessionKey: route.sessionKey,
|
|
705
|
-
AccountId: account.accountId,
|
|
706
|
-
ChatType: isA2AMessage ? "a2a" : isRoomMessage ? "room" : "direct",
|
|
707
|
-
ConversationLabel: isA2AMessage ? `A2A: ${metadata.fromHubAddress}` : isRoomMessage ? "AgentVault Room" : "AgentVault",
|
|
708
|
-
SenderName: isA2AMessage ? metadata.fromHubAddress : isRoomMessage ? "Room" : "Owner",
|
|
709
|
-
SenderId: isA2AMessage ? `a2a:${metadata.fromHubAddress}` : isRoomMessage ? `agentvault:room:${metadata.roomId}` : "agentvault:owner",
|
|
710
|
-
Provider: "agentvault",
|
|
711
|
-
Surface: "agentvault",
|
|
712
|
-
MessageSid: metadata.messageId,
|
|
713
|
-
Timestamp: new Date(metadata.timestamp).getTime(),
|
|
714
|
-
OriginatingChannel: "agentvault",
|
|
715
|
-
OriginatingTo: `agentvault:agent:${account.accountId}`,
|
|
716
|
-
CommandAuthorized: true,
|
|
717
|
-
Instrument: _instrument,
|
|
718
|
-
// OpenClaw ctx convention
|
|
719
|
-
AgentVaultTelemetry: _instrument,
|
|
720
|
-
// AgentVault-specific accessor
|
|
721
|
-
...attachmentFields
|
|
722
|
-
});
|
|
723
|
-
await core.channel.session.recordInboundSession({
|
|
724
|
-
storePath,
|
|
725
|
-
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
|
726
|
-
ctx: ctxPayload,
|
|
727
|
-
onRecordError: (err) => {
|
|
728
|
-
core.error?.(`[AgentVault] session record failed: ${String(err)}`);
|
|
729
|
-
}
|
|
730
|
-
});
|
|
731
|
-
try {
|
|
732
|
-
await runWithTraceContext(
|
|
733
|
-
{ traceId, parentSpanId: inferenceSpanId },
|
|
734
|
-
() => core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
735
|
-
ctx: ctxPayload,
|
|
324
|
+
catch { /* ignore */ }
|
|
325
|
+
const core = _ocRuntime; // Non-null: caller guards with `if (!_ocRuntime)` check
|
|
326
|
+
const route = core.channel.routing.resolveAgentRoute({
|
|
736
327
|
cfg,
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
if (!firstReplyTime) firstReplyTime = replyStart;
|
|
747
|
-
if (isA2AMessage) {
|
|
748
|
-
if (!_a2aCanReply(metadata.a2aChannelId)) {
|
|
749
|
-
console.warn(`[AgentVault] A2A reply suppressed (rate limit) for channel ${metadata.a2aChannelId?.slice(0, 8)}...`);
|
|
750
|
-
return;
|
|
751
|
-
}
|
|
752
|
-
await channel.sendToAgent(metadata.fromHubAddress, text, {
|
|
753
|
-
parentSpanId: inferenceSpanId
|
|
754
|
-
});
|
|
755
|
-
_a2aRecordReply(metadata.a2aChannelId);
|
|
756
|
-
} else if (isRoomMessage) {
|
|
757
|
-
await channel.sendToRoom(metadata.roomId, text, {
|
|
758
|
-
metadata: { content_length: text.length }
|
|
759
|
-
});
|
|
760
|
-
} else {
|
|
761
|
-
await channel.send(text, {
|
|
762
|
-
conversationId: metadata.conversationId,
|
|
763
|
-
topicId: metadata.topicId
|
|
764
|
-
});
|
|
765
|
-
}
|
|
766
|
-
const replyEnd = Date.now();
|
|
767
|
-
replySpans.push({ startTime: replyStart, endTime: replyEnd, chars: text.length, index: replyCount });
|
|
768
|
-
},
|
|
769
|
-
onError: (err, info) => {
|
|
770
|
-
core.error?.(`[AgentVault] ${info?.kind ?? "reply"} error: ${String(err)}`);
|
|
771
|
-
}
|
|
328
|
+
channel: "agentvault",
|
|
329
|
+
accountId: account.accountId,
|
|
330
|
+
peer: {
|
|
331
|
+
kind: isA2AMessage ? "a2a" : "direct",
|
|
332
|
+
id: isA2AMessage
|
|
333
|
+
? `agentvault:a2a:${metadata.fromHubAddress}`
|
|
334
|
+
: isRoomMessage
|
|
335
|
+
? `agentvault:room:${metadata.roomId}`
|
|
336
|
+
: "agentvault:owner",
|
|
772
337
|
},
|
|
773
|
-
replyOptions: {}
|
|
774
|
-
})
|
|
775
|
-
);
|
|
776
|
-
const endTime = Date.now();
|
|
777
|
-
_emitHierarchicalSpans(channel, {
|
|
778
|
-
traceId,
|
|
779
|
-
rootSpanId,
|
|
780
|
-
inferenceSpanId,
|
|
781
|
-
startTime,
|
|
782
|
-
endTime,
|
|
783
|
-
firstReplyTime,
|
|
784
|
-
replySpans,
|
|
785
|
-
inputChars: plaintext.length,
|
|
786
|
-
replyCount,
|
|
787
|
-
totalReplyChars,
|
|
788
|
-
isRoom: isRoomMessage,
|
|
789
|
-
status: "ok"
|
|
790
338
|
});
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
_emitHierarchicalSpans(channel, {
|
|
794
|
-
traceId,
|
|
795
|
-
rootSpanId,
|
|
796
|
-
inferenceSpanId,
|
|
797
|
-
startTime,
|
|
798
|
-
endTime,
|
|
799
|
-
firstReplyTime,
|
|
800
|
-
replySpans,
|
|
801
|
-
inputChars: plaintext.length,
|
|
802
|
-
replyCount,
|
|
803
|
-
totalReplyChars,
|
|
804
|
-
isRoom: isRoomMessage,
|
|
805
|
-
status: "error",
|
|
806
|
-
statusMessage: String(err)
|
|
339
|
+
const storePath = core.channel.session.resolveStorePath(cfg?.session?.store, {
|
|
340
|
+
agentId: route.agentId,
|
|
807
341
|
});
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
startTime: endTime,
|
|
813
|
-
endTime,
|
|
814
|
-
attributes: {
|
|
815
|
-
"ai.agent.error.type": err?.constructor?.name ?? "Error",
|
|
816
|
-
"ai.agent.error.message": String(err)
|
|
817
|
-
},
|
|
818
|
-
status: "error",
|
|
819
|
-
statusMessage: String(err)
|
|
342
|
+
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(cfg);
|
|
343
|
+
const previousTimestamp = core.channel.session.readSessionUpdatedAt({
|
|
344
|
+
storePath,
|
|
345
|
+
sessionKey: route.sessionKey,
|
|
820
346
|
});
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
if (name.startsWith("http.") || name.includes("http")) return "http";
|
|
829
|
-
if (name.startsWith("nav.") || name.includes("nav")) return "nav";
|
|
830
|
-
if (name.startsWith("action.") || name.includes("action")) return "action";
|
|
831
|
-
return "action";
|
|
832
|
-
}
|
|
833
|
-
function _extractSpanName(name, attrs) {
|
|
834
|
-
if (attrs["ai.agent.llm.model"]) return String(attrs["ai.agent.llm.model"]);
|
|
835
|
-
if (attrs["ai.agent.tool.name"]) return String(attrs["ai.agent.tool.name"]);
|
|
836
|
-
if (attrs["ai.agent.http.url"]) {
|
|
837
|
-
try {
|
|
838
|
-
return new URL(String(attrs["ai.agent.http.url"])).hostname;
|
|
839
|
-
} catch {
|
|
840
|
-
}
|
|
841
|
-
return String(attrs["ai.agent.http.url"]).slice(0, 40);
|
|
842
|
-
}
|
|
843
|
-
if (attrs["ai.agent.action.type"]) return `${attrs["ai.agent.action.type"]}: ${attrs["ai.agent.action.target"] ?? ""}`;
|
|
844
|
-
if (attrs["ai.agent.nav.to_url"]) return String(attrs["ai.agent.nav.to_url"]).slice(0, 40);
|
|
845
|
-
if (attrs["ai.agent.error.type"]) return String(attrs["ai.agent.error.type"]);
|
|
846
|
-
return name;
|
|
847
|
-
}
|
|
848
|
-
function _emitChildSpan(channel, opts) {
|
|
849
|
-
try {
|
|
850
|
-
const reporter = channel.telemetry;
|
|
851
|
-
if (!reporter) return;
|
|
852
|
-
const spanId = opts.spanId ?? randomBytes(8).toString("hex");
|
|
853
|
-
reporter.reportCustomSpan({
|
|
854
|
-
traceId: opts.traceId,
|
|
855
|
-
spanId,
|
|
856
|
-
parentSpanId: opts.parentSpanId,
|
|
857
|
-
name: opts.name,
|
|
858
|
-
kind: "internal",
|
|
859
|
-
startTime: opts.startTime,
|
|
860
|
-
endTime: opts.endTime,
|
|
861
|
-
attributes: opts.attributes,
|
|
862
|
-
status: {
|
|
863
|
-
code: opts.status === "ok" ? 1 : 2,
|
|
864
|
-
message: opts.statusMessage
|
|
865
|
-
}
|
|
347
|
+
const body = core.channel.reply.formatAgentEnvelope({
|
|
348
|
+
channel: "AgentVault",
|
|
349
|
+
from: isA2AMessage ? `Agent (${metadata.fromHubAddress})` : isRoomMessage ? "Room" : "Owner",
|
|
350
|
+
timestamp: new Date(metadata.timestamp).getTime(),
|
|
351
|
+
previousTimestamp,
|
|
352
|
+
envelope: envelopeOptions,
|
|
353
|
+
body: plaintext,
|
|
866
354
|
});
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
end_time: new Date(opts.endTime).toISOString(),
|
|
880
|
-
duration_ms: durationMs,
|
|
881
|
-
attributes: opts.attributes,
|
|
882
|
-
...spanType === "llm" ? {
|
|
883
|
-
tokens_input: opts.attributes["ai.agent.llm.tokens_input"],
|
|
884
|
-
tokens_output: opts.attributes["ai.agent.llm.tokens_output"]
|
|
885
|
-
} : {},
|
|
886
|
-
...spanType === "http" ? {
|
|
887
|
-
http_method: opts.attributes["ai.agent.http.method"],
|
|
888
|
-
http_status_code: opts.attributes["ai.agent.http.status_code"],
|
|
889
|
-
http_url: opts.attributes["ai.agent.http.url"]
|
|
890
|
-
} : {},
|
|
891
|
-
...spanType === "tool" ? {
|
|
892
|
-
tool_success: opts.attributes["ai.agent.tool.success"]
|
|
893
|
-
} : {},
|
|
894
|
-
...spanType === "error" ? {
|
|
895
|
-
error_type: opts.attributes["ai.agent.error.type"],
|
|
896
|
-
error_message: opts.attributes["ai.agent.error.message"]
|
|
897
|
-
} : {}
|
|
898
|
-
});
|
|
899
|
-
} catch {
|
|
355
|
+
// Build attachment fields for the context payload
|
|
356
|
+
const attachmentFields = {};
|
|
357
|
+
if (metadata.attachment) {
|
|
358
|
+
attachmentFields.AttachmentPath = metadata.attachment.filePath;
|
|
359
|
+
attachmentFields.AttachmentFilename = metadata.attachment.filename;
|
|
360
|
+
attachmentFields.AttachmentMime = metadata.attachment.mime;
|
|
361
|
+
// For images: include as MediaUrl so the LLM can see the visual content
|
|
362
|
+
if (metadata.attachment.base64) {
|
|
363
|
+
attachmentFields.MediaUrl = metadata.attachment.base64;
|
|
364
|
+
attachmentFields.NumMedia = "1";
|
|
365
|
+
}
|
|
366
|
+
// For text files: content is already inlined in plaintext body
|
|
900
367
|
}
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
368
|
+
const ctxPayload = core.channel.reply.finalizeInboundContext({
|
|
369
|
+
Body: body,
|
|
370
|
+
RawBody: plaintext,
|
|
371
|
+
CommandBody: plaintext,
|
|
372
|
+
From: isA2AMessage ? `a2a:${metadata.fromHubAddress}` : isRoomMessage ? `agentvault:room:${metadata.roomId}` : "agentvault:owner",
|
|
373
|
+
To: `agentvault:agent:${account.accountId}`,
|
|
374
|
+
SessionKey: route.sessionKey,
|
|
375
|
+
AccountId: account.accountId,
|
|
376
|
+
ChatType: isA2AMessage ? "a2a" : isRoomMessage ? "room" : "direct",
|
|
377
|
+
ConversationLabel: isA2AMessage ? `A2A: ${metadata.fromHubAddress}` : isRoomMessage ? "AgentVault Room" : "AgentVault",
|
|
378
|
+
SenderName: isA2AMessage ? metadata.fromHubAddress : isRoomMessage ? "Room" : "Owner",
|
|
379
|
+
SenderId: isA2AMessage ? `a2a:${metadata.fromHubAddress}` : isRoomMessage ? `agentvault:room:${metadata.roomId}` : "agentvault:owner",
|
|
380
|
+
Provider: "agentvault",
|
|
381
|
+
Surface: "agentvault",
|
|
382
|
+
MessageSid: metadata.messageId,
|
|
383
|
+
Timestamp: new Date(metadata.timestamp).getTime(),
|
|
384
|
+
OriginatingChannel: "agentvault",
|
|
385
|
+
OriginatingTo: `agentvault:agent:${account.accountId}`,
|
|
386
|
+
CommandAuthorized: true,
|
|
387
|
+
Instrument: _instrument, // OpenClaw ctx convention
|
|
388
|
+
AgentVaultTelemetry: _instrument, // AgentVault-specific accessor
|
|
389
|
+
...attachmentFields,
|
|
923
390
|
});
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
endTime: reply.endTime,
|
|
931
|
-
attributes: {
|
|
932
|
-
"ai.agent.reply.index": reply.index,
|
|
933
|
-
"ai.agent.reply.chars": reply.chars
|
|
391
|
+
await core.channel.session.recordInboundSession({
|
|
392
|
+
storePath,
|
|
393
|
+
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
|
394
|
+
ctx: ctxPayload,
|
|
395
|
+
onRecordError: (err) => {
|
|
396
|
+
core.error?.(`[AgentVault] session record failed: ${String(err)}`);
|
|
934
397
|
},
|
|
935
|
-
status: "ok"
|
|
936
|
-
});
|
|
937
|
-
}
|
|
938
|
-
reporter.reportCustomSpan({
|
|
939
|
-
traceId: opts.traceId,
|
|
940
|
-
spanId: opts.rootSpanId,
|
|
941
|
-
name: "ai.agent.message",
|
|
942
|
-
kind: "server",
|
|
943
|
-
startTime: opts.startTime,
|
|
944
|
-
endTime: opts.endTime,
|
|
945
|
-
attributes: {
|
|
946
|
-
"ai.agent.message.input_chars": opts.inputChars,
|
|
947
|
-
"ai.agent.message.reply_count": opts.replyCount,
|
|
948
|
-
"ai.agent.message.reply_chars": opts.totalReplyChars,
|
|
949
|
-
"ai.agent.message.latency_ms": opts.endTime - opts.startTime,
|
|
950
|
-
"ai.agent.message.type": opts.isRoom ? "room" : "direct"
|
|
951
|
-
},
|
|
952
|
-
status: {
|
|
953
|
-
code: opts.status === "ok" ? 1 : 2,
|
|
954
|
-
message: opts.statusMessage
|
|
955
|
-
}
|
|
956
398
|
});
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
}
|
|
1007
|
-
const dataDir = resolve(account.dataDir.replace(/^~/, process.env.HOME ?? "~"));
|
|
1008
|
-
_log?.(`[AgentVault] starting (dataDir=${dataDir})`);
|
|
1009
|
-
await new Promise((resolve2, reject) => {
|
|
1010
|
-
let channel;
|
|
1011
|
-
const onAbort = async () => {
|
|
1012
|
-
await channel?.stop();
|
|
1013
|
-
_channels.delete(account.accountId);
|
|
1014
|
-
resolve2();
|
|
1015
|
-
};
|
|
1016
|
-
abortSignal?.addEventListener("abort", () => void onAbort());
|
|
1017
|
-
import("./index.js").then(({ SecureChannel }) => {
|
|
1018
|
-
channel = new SecureChannel({
|
|
1019
|
-
inviteToken: "",
|
|
1020
|
-
dataDir,
|
|
1021
|
-
apiUrl: account.apiUrl,
|
|
1022
|
-
agentName: account.agentName,
|
|
1023
|
-
onMessage: async (plaintext, metadata) => {
|
|
1024
|
-
if (!_ocRuntime) {
|
|
1025
|
-
_log?.("[AgentVault] runtime not ready, queuing message");
|
|
1026
|
-
_messageQueue.push(async () => {
|
|
1027
|
-
await handleInbound({ plaintext, metadata, channel, account, cfg });
|
|
1028
|
-
});
|
|
1029
|
-
return;
|
|
1030
|
-
}
|
|
1031
|
-
try {
|
|
1032
|
-
await handleInbound({ plaintext, metadata, channel, account, cfg });
|
|
1033
|
-
} catch (err) {
|
|
1034
|
-
_log?.(`[AgentVault] inbound error: ${String(err)}`);
|
|
1035
|
-
}
|
|
1036
|
-
},
|
|
1037
|
-
onA2AMessage: async (msg) => {
|
|
1038
|
-
const a2aMetadata = {
|
|
1039
|
-
a2aChannelId: msg.channelId,
|
|
1040
|
-
fromHubAddress: msg.fromHubAddress,
|
|
1041
|
-
conversationId: msg.conversationId,
|
|
1042
|
-
timestamp: msg.timestamp,
|
|
1043
|
-
parentSpanId: msg.parentSpanId,
|
|
1044
|
-
messageId: `a2a:${msg.channelId}:${Date.now()}`
|
|
1045
|
-
};
|
|
1046
|
-
if (!_ocRuntime) {
|
|
1047
|
-
_log?.("[AgentVault] runtime not ready, queuing A2A message");
|
|
1048
|
-
_messageQueue.push(async () => {
|
|
1049
|
-
await handleInbound({ plaintext: msg.text, metadata: a2aMetadata, channel, account, cfg });
|
|
1050
|
-
});
|
|
1051
|
-
return;
|
|
1052
|
-
}
|
|
1053
|
-
try {
|
|
1054
|
-
await handleInbound({ plaintext: msg.text, metadata: a2aMetadata, channel, account, cfg });
|
|
1055
|
-
} catch (err) {
|
|
1056
|
-
_log?.(`[AgentVault] A2A inbound error: ${String(err)}`);
|
|
1057
|
-
}
|
|
1058
|
-
},
|
|
1059
|
-
onA2AChannelReady: async (info) => {
|
|
1060
|
-
_registerA2ATarget(info.peerHubAddress, account.accountId);
|
|
1061
|
-
if (info.role !== "initiator") {
|
|
1062
|
-
_log?.(`[AgentVault] A2A channel ready (responder) \u2014 waiting for initiator: ${info.peerHubAddress}`);
|
|
1063
|
-
return;
|
|
1064
|
-
}
|
|
1065
|
-
_log?.(`[AgentVault] A2A channel ready (initiator) \u2014 sending seed to ${info.peerHubAddress}${info.topic ? ` [topic: ${info.topic}]` : ""}`);
|
|
1066
|
-
const seedText = info.topic ? `[System] A new A2A channel has been established with agent ${info.peerHubAddress}. Topic: "${info.topic}". Introduce yourself briefly and begin discussing this topic.` : `[System] A new A2A channel has been established with agent ${info.peerHubAddress}. Introduce yourself briefly and state what you can help with.`;
|
|
1067
|
-
const seedMetadata = {
|
|
1068
|
-
a2aChannelId: info.channelId,
|
|
1069
|
-
fromHubAddress: info.peerHubAddress,
|
|
1070
|
-
conversationId: info.conversationId,
|
|
1071
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1072
|
-
messageId: `a2a-seed:${info.channelId}:${Date.now()}`
|
|
1073
|
-
};
|
|
1074
|
-
if (!_ocRuntime) {
|
|
1075
|
-
_log?.("[AgentVault] runtime not ready, queuing A2A seed");
|
|
1076
|
-
_messageQueue.push(async () => {
|
|
1077
|
-
await handleInbound({ plaintext: seedText, metadata: seedMetadata, channel, account, cfg });
|
|
1078
|
-
});
|
|
1079
|
-
return;
|
|
1080
|
-
}
|
|
1081
|
-
try {
|
|
1082
|
-
await handleInbound({ plaintext: seedText, metadata: seedMetadata, channel, account, cfg });
|
|
1083
|
-
} catch (err) {
|
|
1084
|
-
_log?.(`[AgentVault] A2A seed error: ${String(err)}`);
|
|
1085
|
-
}
|
|
399
|
+
try {
|
|
400
|
+
await runWithTraceContext({ traceId, parentSpanId: inferenceSpanId }, () => core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
401
|
+
ctx: ctxPayload,
|
|
402
|
+
cfg,
|
|
403
|
+
dispatcherOptions: {
|
|
404
|
+
deliver: async (payload) => {
|
|
405
|
+
// Filter out thinking/reasoning blocks — only deliver actual responses
|
|
406
|
+
if (payload.kind === "thinking" || payload.kind === "reasoning")
|
|
407
|
+
return;
|
|
408
|
+
const text = (payload.text ?? "").trim();
|
|
409
|
+
if (!text)
|
|
410
|
+
return;
|
|
411
|
+
// Heuristic: skip blocks that look like leaked chain-of-thought
|
|
412
|
+
if (/^(Reasoning|Thinking|Let me think|I need to|Let me check):/i.test(text))
|
|
413
|
+
return;
|
|
414
|
+
const replyStart = Date.now();
|
|
415
|
+
replyCount++;
|
|
416
|
+
totalReplyChars += text.length;
|
|
417
|
+
if (!firstReplyTime)
|
|
418
|
+
firstReplyTime = replyStart;
|
|
419
|
+
// Route reply: A2A → sendToAgent, room → sendToRoom, 1:1 → send
|
|
420
|
+
if (isA2AMessage) {
|
|
421
|
+
// Loop prevention: rate limit A2A replies per channel
|
|
422
|
+
if (!_a2aCanReply(metadata.a2aChannelId)) {
|
|
423
|
+
console.warn(`[AgentVault] A2A reply suppressed (rate limit) for channel ${metadata.a2aChannelId?.slice(0, 8)}...`);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
await channel.sendToAgent(metadata.fromHubAddress, text, {
|
|
427
|
+
parentSpanId: inferenceSpanId,
|
|
428
|
+
});
|
|
429
|
+
_a2aRecordReply(metadata.a2aChannelId);
|
|
430
|
+
}
|
|
431
|
+
else if (isRoomMessage) {
|
|
432
|
+
await channel.sendToRoom(metadata.roomId, text, {
|
|
433
|
+
metadata: { content_length: text.length },
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
await channel.send(text, {
|
|
438
|
+
conversationId: metadata.conversationId,
|
|
439
|
+
topicId: metadata.topicId,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
const replyEnd = Date.now();
|
|
443
|
+
replySpans.push({ startTime: replyStart, endTime: replyEnd, chars: text.length, index: replyCount });
|
|
444
|
+
},
|
|
445
|
+
onError: (err, info) => {
|
|
446
|
+
core.error?.(`[AgentVault] ${info?.kind ?? "reply"} error: ${String(err)}`);
|
|
447
|
+
},
|
|
1086
448
|
},
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
_registerA2ATarget(peerAddress, account.accountId);
|
|
1105
|
-
}
|
|
1106
|
-
});
|
|
1107
|
-
channel.start().catch(reject);
|
|
1108
|
-
}).catch(reject);
|
|
1109
|
-
});
|
|
1110
|
-
return { stop: async () => {
|
|
1111
|
-
} };
|
|
1112
|
-
}
|
|
1113
|
-
},
|
|
1114
|
-
outbound: {
|
|
1115
|
-
deliveryMode: "direct",
|
|
1116
|
-
// Register valid send targets so OpenClaw's `message` tool can route
|
|
1117
|
-
// proactive (agent-initiated) sends — not just replies to inbound messages.
|
|
1118
|
-
targets: _outboundTargets,
|
|
1119
|
-
sendText: async ({ to, text, accountId }) => {
|
|
1120
|
-
const resolvedId = accountId ?? "default";
|
|
1121
|
-
const ch = _channels.get(resolvedId);
|
|
1122
|
-
if (!ch) return { ok: false, error: "AgentVault channel not connected" };
|
|
1123
|
-
try {
|
|
1124
|
-
const wasReady = ch.state === "ready";
|
|
1125
|
-
if (to.startsWith("a2a:")) {
|
|
1126
|
-
const hubAddress = to.slice(4);
|
|
1127
|
-
await ch.sendToAgent(hubAddress, text);
|
|
1128
|
-
} else {
|
|
1129
|
-
await ch.send(text);
|
|
1130
|
-
}
|
|
1131
|
-
return { ok: true, queued: !wasReady };
|
|
1132
|
-
} catch (err) {
|
|
1133
|
-
return { ok: false, error: String(err) };
|
|
1134
|
-
}
|
|
1135
|
-
},
|
|
1136
|
-
sendMedia: async ({ to, text, mediaUrl, accountId }) => {
|
|
1137
|
-
const resolvedId = accountId ?? "default";
|
|
1138
|
-
const ch = _channels.get(resolvedId);
|
|
1139
|
-
if (!ch) return { ok: false, error: "AgentVault channel not connected" };
|
|
1140
|
-
try {
|
|
1141
|
-
const message = text ? `${text}
|
|
1142
|
-
${mediaUrl}` : mediaUrl;
|
|
1143
|
-
const wasReady = ch.state === "ready";
|
|
1144
|
-
if (to.startsWith("a2a:")) {
|
|
1145
|
-
await ch.sendToAgent(to.slice(4), message);
|
|
1146
|
-
} else {
|
|
1147
|
-
await ch.send(message);
|
|
1148
|
-
}
|
|
1149
|
-
return { ok: true, queued: !wasReady };
|
|
1150
|
-
} catch (err) {
|
|
1151
|
-
return { ok: false, error: String(err) };
|
|
1152
|
-
}
|
|
1153
|
-
},
|
|
1154
|
-
/** Rich payload delivery — OpenClaw v2026.3.2+ calls this for structured messages. */
|
|
1155
|
-
sendPayload: async (ctx) => {
|
|
1156
|
-
const { payload, accountId } = ctx;
|
|
1157
|
-
const resolvedId = accountId ?? "default";
|
|
1158
|
-
const ch = _channels.get(resolvedId);
|
|
1159
|
-
if (!ch) return { ok: false, error: "AgentVault channel not connected" };
|
|
1160
|
-
try {
|
|
1161
|
-
if (payload.isReasoning) return { ok: true };
|
|
1162
|
-
requestHeartbeatNow({ reason: "outbound-payload" }).catch(() => {
|
|
449
|
+
replyOptions: {},
|
|
450
|
+
}));
|
|
451
|
+
const endTime = Date.now();
|
|
452
|
+
// Emit child spans for the completed message lifecycle
|
|
453
|
+
_emitHierarchicalSpans(channel, {
|
|
454
|
+
traceId,
|
|
455
|
+
rootSpanId,
|
|
456
|
+
inferenceSpanId,
|
|
457
|
+
startTime,
|
|
458
|
+
endTime,
|
|
459
|
+
firstReplyTime,
|
|
460
|
+
replySpans,
|
|
461
|
+
inputChars: plaintext.length,
|
|
462
|
+
replyCount,
|
|
463
|
+
totalReplyChars,
|
|
464
|
+
isRoom: isRoomMessage,
|
|
465
|
+
status: "ok",
|
|
1163
466
|
});
|
|
1164
|
-
const text = (payload.text ?? "").trim();
|
|
1165
|
-
if (!text && !payload.mediaUrls?.length) return { ok: true };
|
|
1166
|
-
const _send = async (msg) => {
|
|
1167
|
-
const target = ctx.to;
|
|
1168
|
-
if (target && typeof target === "string" && target.startsWith("a2a:")) {
|
|
1169
|
-
await ch.sendToAgent(target.slice(4), msg);
|
|
1170
|
-
} else {
|
|
1171
|
-
await ch.send(msg);
|
|
1172
|
-
}
|
|
1173
|
-
};
|
|
1174
|
-
if (text) {
|
|
1175
|
-
await _send(text);
|
|
1176
|
-
}
|
|
1177
|
-
if (payload.mediaUrls?.length) {
|
|
1178
|
-
for (const url of payload.mediaUrls) {
|
|
1179
|
-
await _send(url);
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
if (payload.suggestedActions?.length) {
|
|
1183
|
-
const actionsText = payload.suggestedActions.map((a) => `- ${a.label}: ${a.action}`).join("\n");
|
|
1184
|
-
await _send(`Suggested actions:
|
|
1185
|
-
${actionsText}`);
|
|
1186
|
-
}
|
|
1187
|
-
return { ok: true };
|
|
1188
|
-
} catch (err) {
|
|
1189
|
-
return { ok: false, error: String(err) };
|
|
1190
|
-
}
|
|
1191
467
|
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
try {
|
|
1218
|
-
const hostname = (() => {
|
|
1219
|
-
try {
|
|
1220
|
-
return new URL(report.url).hostname;
|
|
1221
|
-
} catch {
|
|
1222
|
-
return report.url.slice(0, 40);
|
|
1223
|
-
}
|
|
1224
|
-
})();
|
|
1225
|
-
ch.sendActivitySpan({
|
|
1226
|
-
trace_id: report.traceId ?? "",
|
|
1227
|
-
parent_span_id: report.parentSpanId ?? "",
|
|
1228
|
-
span_id: randomBytes(8).toString("hex"),
|
|
1229
|
-
span_type: "http",
|
|
1230
|
-
span_name: hostname,
|
|
1231
|
-
status: report.statusCode >= 400 || report.statusCode === 0 ? "error" : "ok",
|
|
1232
|
-
start_time: new Date(Date.now() - report.latencyMs).toISOString(),
|
|
1233
|
-
end_time: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1234
|
-
duration_ms: report.latencyMs,
|
|
468
|
+
catch (err) {
|
|
469
|
+
const endTime = Date.now();
|
|
470
|
+
// Emit spans even on error — shows where failure occurred
|
|
471
|
+
_emitHierarchicalSpans(channel, {
|
|
472
|
+
traceId,
|
|
473
|
+
rootSpanId,
|
|
474
|
+
inferenceSpanId,
|
|
475
|
+
startTime,
|
|
476
|
+
endTime,
|
|
477
|
+
firstReplyTime,
|
|
478
|
+
replySpans,
|
|
479
|
+
inputChars: plaintext.length,
|
|
480
|
+
replyCount,
|
|
481
|
+
totalReplyChars,
|
|
482
|
+
isRoom: isRoomMessage,
|
|
483
|
+
status: "error",
|
|
484
|
+
statusMessage: String(err),
|
|
485
|
+
});
|
|
486
|
+
// Dedicated error span for the Errors tab
|
|
487
|
+
_emitChildSpan(channel, {
|
|
488
|
+
traceId,
|
|
489
|
+
parentSpanId: rootSpanId,
|
|
490
|
+
name: "error",
|
|
491
|
+
startTime: endTime,
|
|
492
|
+
endTime: endTime,
|
|
1235
493
|
attributes: {
|
|
1236
|
-
|
|
1237
|
-
|
|
494
|
+
"ai.agent.error.type": err?.constructor?.name ?? "Error",
|
|
495
|
+
"ai.agent.error.message": String(err),
|
|
1238
496
|
},
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
http_url: report.url
|
|
1242
|
-
});
|
|
1243
|
-
} catch {
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
});
|
|
1247
|
-
api.registerChannel({ plugin: agentVaultPlugin });
|
|
1248
|
-
if (typeof api.registerHttpRoute === "function") {
|
|
1249
|
-
try {
|
|
1250
|
-
api.registerHttpRoute({
|
|
1251
|
-
path: "/agentvault/send",
|
|
1252
|
-
method: "POST",
|
|
1253
|
-
handler: async (req) => {
|
|
1254
|
-
const ch = _channels.values().next().value;
|
|
1255
|
-
if (!ch) return { status: 503, body: { ok: false, error: "Channel not started" } };
|
|
1256
|
-
const parsed = typeof req.body === "string" ? JSON.parse(req.body) : req.body;
|
|
1257
|
-
const result = await handleSendRequest(parsed, ch);
|
|
1258
|
-
return { status: result.status, body: result.body };
|
|
1259
|
-
}
|
|
1260
|
-
});
|
|
1261
|
-
api.registerHttpRoute({
|
|
1262
|
-
path: "/agentvault/action",
|
|
1263
|
-
method: "POST",
|
|
1264
|
-
handler: async (req) => {
|
|
1265
|
-
const ch = _channels.values().next().value;
|
|
1266
|
-
if (!ch) return { status: 503, body: { ok: false, error: "Channel not started" } };
|
|
1267
|
-
const parsed = typeof req.body === "string" ? JSON.parse(req.body) : req.body;
|
|
1268
|
-
const result = await handleActionRequest(parsed, ch);
|
|
1269
|
-
return { status: result.status, body: result.body };
|
|
1270
|
-
}
|
|
1271
|
-
});
|
|
1272
|
-
api.registerHttpRoute({
|
|
1273
|
-
path: "/agentvault/decision",
|
|
1274
|
-
method: "POST",
|
|
1275
|
-
handler: async (req) => {
|
|
1276
|
-
const ch = _channels.values().next().value;
|
|
1277
|
-
if (!ch) return { status: 503, body: { ok: false, error: "Channel not started" } };
|
|
1278
|
-
const parsed = typeof req.body === "string" ? JSON.parse(req.body) : req.body;
|
|
1279
|
-
const result = await handleDecisionRequest(parsed, ch);
|
|
1280
|
-
return { status: result.status, body: result.body };
|
|
1281
|
-
}
|
|
497
|
+
status: "error",
|
|
498
|
+
statusMessage: String(err),
|
|
1282
499
|
});
|
|
1283
|
-
|
|
1284
|
-
path: "/agentvault/status",
|
|
1285
|
-
method: "GET",
|
|
1286
|
-
handler: async () => {
|
|
1287
|
-
const ch = _channels.values().next().value;
|
|
1288
|
-
if (!ch) return { status: 503, body: { ok: false, error: "Channel not started" } };
|
|
1289
|
-
const result = handleStatusRequest(ch);
|
|
1290
|
-
return { status: result.status, body: result.body };
|
|
1291
|
-
}
|
|
1292
|
-
});
|
|
1293
|
-
isUsingManagedRoutes = true;
|
|
1294
|
-
} catch {
|
|
1295
|
-
}
|
|
500
|
+
throw err;
|
|
1296
501
|
}
|
|
1297
|
-
|
|
1298
|
-
|
|
502
|
+
}
|
|
503
|
+
/** Infer a span type from the span name for activity stream display. */
|
|
504
|
+
function _inferSpanType(name) {
|
|
505
|
+
if (name === "error" || name.includes("error"))
|
|
506
|
+
return "error";
|
|
507
|
+
if (name.startsWith("llm.") || name.includes("inference"))
|
|
508
|
+
return "llm";
|
|
509
|
+
if (name.startsWith("tool.") || name.includes("tool"))
|
|
510
|
+
return "tool";
|
|
511
|
+
if (name.startsWith("http.") || name.includes("http"))
|
|
512
|
+
return "http";
|
|
513
|
+
if (name.startsWith("nav.") || name.includes("nav"))
|
|
514
|
+
return "nav";
|
|
515
|
+
if (name.startsWith("action.") || name.includes("action"))
|
|
516
|
+
return "action";
|
|
517
|
+
return "action"; // default fallback
|
|
518
|
+
}
|
|
519
|
+
/** Extract a human-readable span name from span attributes. */
|
|
520
|
+
function _extractSpanName(name, attrs) {
|
|
521
|
+
if (attrs["ai.agent.llm.model"])
|
|
522
|
+
return String(attrs["ai.agent.llm.model"]);
|
|
523
|
+
if (attrs["ai.agent.tool.name"])
|
|
524
|
+
return String(attrs["ai.agent.tool.name"]);
|
|
525
|
+
if (attrs["ai.agent.http.url"]) {
|
|
1299
526
|
try {
|
|
1300
|
-
|
|
1301
|
-
if (!ch?.telemetry) return;
|
|
1302
|
-
ch.telemetry.reportCustomSpan({
|
|
1303
|
-
traceId: randomBytes(16).toString("hex"),
|
|
1304
|
-
spanId: randomBytes(8).toString("hex"),
|
|
1305
|
-
name: "agentvault.message.delivered",
|
|
1306
|
-
kind: "internal",
|
|
1307
|
-
startTime: event.timestamp,
|
|
1308
|
-
endTime: event.timestamp + 1,
|
|
1309
|
-
attributes: {
|
|
1310
|
-
"agentvault.message.id": event.messageId,
|
|
1311
|
-
"agentvault.delivery.status": event.deliveryStatus,
|
|
1312
|
-
"agentvault.channel.id": event.channelId
|
|
1313
|
-
},
|
|
1314
|
-
status: { code: event.deliveryStatus === "failed" ? 2 : 1 }
|
|
1315
|
-
});
|
|
1316
|
-
} catch {
|
|
527
|
+
return new URL(String(attrs["ai.agent.http.url"])).hostname;
|
|
1317
528
|
}
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
529
|
+
catch { /* use raw */ }
|
|
530
|
+
return String(attrs["ai.agent.http.url"]).slice(0, 40);
|
|
531
|
+
}
|
|
532
|
+
if (attrs["ai.agent.action.type"])
|
|
533
|
+
return `${attrs["ai.agent.action.type"]}: ${attrs["ai.agent.action.target"] ?? ""}`;
|
|
534
|
+
if (attrs["ai.agent.nav.to_url"])
|
|
535
|
+
return String(attrs["ai.agent.nav.to_url"]).slice(0, 40);
|
|
536
|
+
if (attrs["ai.agent.error.type"])
|
|
537
|
+
return String(attrs["ai.agent.error.type"]);
|
|
538
|
+
return name;
|
|
539
|
+
}
|
|
540
|
+
/** Emit a single child span via the channel's telemetry reporter + WS activity stream. */
|
|
541
|
+
function _emitChildSpan(channel, opts) {
|
|
542
|
+
try {
|
|
543
|
+
const reporter = channel.telemetry;
|
|
544
|
+
if (!reporter)
|
|
545
|
+
return;
|
|
546
|
+
const spanId = opts.spanId ?? randomBytes(8).toString("hex");
|
|
547
|
+
reporter.reportCustomSpan({
|
|
548
|
+
traceId: opts.traceId,
|
|
549
|
+
spanId,
|
|
550
|
+
parentSpanId: opts.parentSpanId,
|
|
551
|
+
name: opts.name,
|
|
1327
552
|
kind: "internal",
|
|
1328
|
-
startTime:
|
|
1329
|
-
endTime:
|
|
1330
|
-
attributes:
|
|
1331
|
-
|
|
1332
|
-
|
|
553
|
+
startTime: opts.startTime,
|
|
554
|
+
endTime: opts.endTime,
|
|
555
|
+
attributes: opts.attributes,
|
|
556
|
+
status: {
|
|
557
|
+
code: opts.status === "ok" ? 1 : 2,
|
|
558
|
+
message: opts.statusMessage,
|
|
1333
559
|
},
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
} catch {
|
|
1337
|
-
}
|
|
1338
|
-
});
|
|
1339
|
-
api.on("session_end", async (event) => {
|
|
560
|
+
});
|
|
561
|
+
// Dual-path: also send via WS for real-time activity streaming
|
|
1340
562
|
try {
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
563
|
+
const spanType = _inferSpanType(opts.name);
|
|
564
|
+
const spanName = _extractSpanName(opts.name, opts.attributes);
|
|
565
|
+
const durationMs = opts.endTime - opts.startTime;
|
|
566
|
+
channel.sendActivitySpan({
|
|
567
|
+
trace_id: opts.traceId,
|
|
568
|
+
span_id: spanId,
|
|
569
|
+
parent_span_id: opts.parentSpanId,
|
|
570
|
+
span_type: spanType,
|
|
571
|
+
span_name: spanName,
|
|
572
|
+
status: opts.status,
|
|
573
|
+
start_time: new Date(opts.startTime).toISOString(),
|
|
574
|
+
end_time: new Date(opts.endTime).toISOString(),
|
|
575
|
+
duration_ms: durationMs,
|
|
576
|
+
attributes: opts.attributes,
|
|
577
|
+
...(spanType === "llm" ? {
|
|
578
|
+
tokens_input: opts.attributes["ai.agent.llm.tokens_input"],
|
|
579
|
+
tokens_output: opts.attributes["ai.agent.llm.tokens_output"],
|
|
580
|
+
} : {}),
|
|
581
|
+
...(spanType === "http" ? {
|
|
582
|
+
http_method: opts.attributes["ai.agent.http.method"],
|
|
583
|
+
http_status_code: opts.attributes["ai.agent.http.status_code"],
|
|
584
|
+
http_url: opts.attributes["ai.agent.http.url"],
|
|
585
|
+
} : {}),
|
|
586
|
+
...(spanType === "tool" ? {
|
|
587
|
+
tool_success: opts.attributes["ai.agent.tool.success"],
|
|
588
|
+
} : {}),
|
|
589
|
+
...(spanType === "error" ? {
|
|
590
|
+
error_type: opts.attributes["ai.agent.error.type"],
|
|
591
|
+
error_message: opts.attributes["ai.agent.error.message"],
|
|
592
|
+
} : {}),
|
|
593
|
+
});
|
|
1358
594
|
}
|
|
1359
|
-
|
|
595
|
+
catch { /* WS activity is best-effort */ }
|
|
1360
596
|
}
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
597
|
+
catch { /* telemetry is best-effort */ }
|
|
598
|
+
}
|
|
599
|
+
/** Emit the full hierarchical span tree for a message lifecycle. */
|
|
600
|
+
function _emitHierarchicalSpans(channel, opts) {
|
|
601
|
+
try {
|
|
602
|
+
const reporter = channel.telemetry;
|
|
603
|
+
if (!reporter)
|
|
604
|
+
return;
|
|
605
|
+
// 1. Inference span: from message arrival to first reply (or end if no replies)
|
|
606
|
+
const inferenceEnd = opts.firstReplyTime ?? opts.endTime;
|
|
607
|
+
_emitChildSpan(channel, {
|
|
608
|
+
traceId: opts.traceId,
|
|
609
|
+
parentSpanId: opts.rootSpanId,
|
|
610
|
+
spanId: opts.inferenceSpanId,
|
|
611
|
+
name: "ai.agent.inference",
|
|
612
|
+
startTime: opts.startTime + 1, // just after receive
|
|
613
|
+
endTime: inferenceEnd,
|
|
1373
614
|
attributes: {
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
...event.data ? Object.fromEntries(
|
|
1377
|
-
Object.entries(event.data).map(([k, v]) => [`ai.agent.event.${k}`, String(v)])
|
|
1378
|
-
) : {}
|
|
615
|
+
"ai.agent.inference.latency_ms": inferenceEnd - opts.startTime,
|
|
616
|
+
"ai.agent.message.input_chars": opts.inputChars,
|
|
1379
617
|
},
|
|
1380
|
-
status:
|
|
1381
|
-
|
|
1382
|
-
}
|
|
618
|
+
status: opts.status,
|
|
619
|
+
statusMessage: opts.status === "error" ? opts.statusMessage : undefined,
|
|
620
|
+
});
|
|
621
|
+
// 2. Individual reply spans
|
|
622
|
+
for (const reply of opts.replySpans) {
|
|
623
|
+
_emitChildSpan(channel, {
|
|
624
|
+
traceId: opts.traceId,
|
|
625
|
+
parentSpanId: opts.rootSpanId,
|
|
626
|
+
name: "ai.agent.reply",
|
|
627
|
+
startTime: reply.startTime,
|
|
628
|
+
endTime: reply.endTime,
|
|
629
|
+
attributes: {
|
|
630
|
+
"ai.agent.reply.index": reply.index,
|
|
631
|
+
"ai.agent.reply.chars": reply.chars,
|
|
632
|
+
},
|
|
633
|
+
status: "ok",
|
|
634
|
+
});
|
|
1383
635
|
}
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
spanId: randomBytes(8).toString("hex"),
|
|
1393
|
-
name: "ai.agent.transcript.update",
|
|
1394
|
-
kind: "internal",
|
|
1395
|
-
startTime: update.timestamp,
|
|
1396
|
-
endTime: update.timestamp + 1,
|
|
636
|
+
// 3. Root span (wraps everything) — emitted last
|
|
637
|
+
reporter.reportCustomSpan({
|
|
638
|
+
traceId: opts.traceId,
|
|
639
|
+
spanId: opts.rootSpanId,
|
|
640
|
+
name: "ai.agent.message",
|
|
641
|
+
kind: "server",
|
|
642
|
+
startTime: opts.startTime,
|
|
643
|
+
endTime: opts.endTime,
|
|
1397
644
|
attributes: {
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
645
|
+
"ai.agent.message.input_chars": opts.inputChars,
|
|
646
|
+
"ai.agent.message.reply_count": opts.replyCount,
|
|
647
|
+
"ai.agent.message.reply_chars": opts.totalReplyChars,
|
|
648
|
+
"ai.agent.message.latency_ms": opts.endTime - opts.startTime,
|
|
649
|
+
"ai.agent.message.type": opts.isRoom ? "room" : "direct",
|
|
1401
650
|
},
|
|
1402
|
-
status: {
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
651
|
+
status: {
|
|
652
|
+
code: opts.status === "ok" ? 1 : 2,
|
|
653
|
+
message: opts.statusMessage,
|
|
654
|
+
},
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
catch { /* telemetry is best-effort */ }
|
|
658
|
+
}
|
|
659
|
+
// --- Channel plugin definition ---
|
|
660
|
+
const agentVaultPlugin = {
|
|
661
|
+
id: "agentvault",
|
|
662
|
+
meta: {
|
|
663
|
+
id: "agentvault",
|
|
664
|
+
label: "AgentVault",
|
|
665
|
+
selectionLabel: "AgentVault (E2E Encrypted)",
|
|
666
|
+
docsPath: "https://agentvault.chat/docs",
|
|
667
|
+
blurb: "Zero-knowledge, end-to-end encrypted messaging between owners and their AI agents.",
|
|
668
|
+
aliases: ["av", "agent-vault"],
|
|
669
|
+
},
|
|
670
|
+
capabilities: { chatTypes: ["direct", "room"] },
|
|
671
|
+
config: { listAccountIds, resolveAccount },
|
|
672
|
+
gateway: {
|
|
673
|
+
/** Health probe for `openclaw channels status --probe` */
|
|
674
|
+
probe: async (ctx) => {
|
|
675
|
+
const accountId = ctx?.accountId ?? "default";
|
|
676
|
+
const ch = _channels.get(accountId);
|
|
677
|
+
if (!ch)
|
|
678
|
+
return { ok: false, status: "disconnected", error: "Channel not started" };
|
|
679
|
+
const state = ch.state;
|
|
680
|
+
return {
|
|
681
|
+
ok: state === "ready",
|
|
682
|
+
status: state,
|
|
683
|
+
deviceId: ch.deviceId ?? undefined,
|
|
684
|
+
sessions: ch.sessionCount,
|
|
685
|
+
};
|
|
1422
686
|
},
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
content: [{
|
|
1431
|
-
type: "text",
|
|
1432
|
-
text: JSON.stringify({
|
|
687
|
+
/** Status for `openclaw health --json` per-channel summary */
|
|
688
|
+
status: (ctx) => {
|
|
689
|
+
const accountId = ctx?.accountId ?? "default";
|
|
690
|
+
const ch = _channels.get(accountId);
|
|
691
|
+
if (!ch)
|
|
692
|
+
return { connected: false, status: "not_started" };
|
|
693
|
+
return {
|
|
1433
694
|
connected: ch.state === "ready",
|
|
1434
|
-
|
|
1435
|
-
deviceId: ch.deviceId,
|
|
695
|
+
status: ch.state,
|
|
696
|
+
deviceId: ch.deviceId ?? undefined,
|
|
1436
697
|
sessions: ch.sessionCount,
|
|
1437
|
-
encrypted: true
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
try {
|
|
1446
|
-
api.registerCommand?.({
|
|
1447
|
-
name: "agentvault",
|
|
1448
|
-
description: "Show AgentVault encrypted channel status",
|
|
1449
|
-
handler: () => {
|
|
1450
|
-
const statuses = [];
|
|
1451
|
-
if (_channels.size === 0) {
|
|
1452
|
-
statuses.push("AgentVault: no active channels");
|
|
1453
|
-
} else {
|
|
1454
|
-
for (const [id, ch] of _channels) {
|
|
1455
|
-
statuses.push(`AgentVault [${id}]: ${ch.state} | sessions: ${ch.sessionCount} | encrypted: yes`);
|
|
698
|
+
encrypted: true,
|
|
699
|
+
};
|
|
700
|
+
},
|
|
701
|
+
startAccount: async (ctx) => {
|
|
702
|
+
const { account, cfg, log, abortSignal } = ctx;
|
|
703
|
+
const _log = typeof log === "function" ? log : log?.info?.bind(log);
|
|
704
|
+
if (!account.configured) {
|
|
705
|
+
throw new Error("AgentVault channel not configured. Run: npx @agentvault/agentvault setup --token=av_tok_...\nThen restart OpenClaw.");
|
|
1456
706
|
}
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
707
|
+
const dataDir = resolve(account.dataDir.replace(/^~/, process.env.HOME ?? "~"));
|
|
708
|
+
_log?.(`[AgentVault] starting (dataDir=${dataDir})`);
|
|
709
|
+
// startAccount must STAY PENDING while the channel is running.
|
|
710
|
+
// Resolving signals "channel stopped" to the gateway health monitor,
|
|
711
|
+
// which triggers auto-restart. We block here until the abortSignal
|
|
712
|
+
// fires (gateway shutdown / config reload), then clean up.
|
|
713
|
+
await new Promise((resolve, reject) => {
|
|
714
|
+
let channel;
|
|
715
|
+
const onAbort = async () => {
|
|
716
|
+
await channel?.stop();
|
|
717
|
+
_channels.delete(account.accountId);
|
|
718
|
+
resolve();
|
|
719
|
+
};
|
|
720
|
+
abortSignal?.addEventListener("abort", () => void onAbort());
|
|
721
|
+
// Lazy import — defers libsodium initialization until channel starts
|
|
722
|
+
import("./index.js").then(({ SecureChannel }) => {
|
|
723
|
+
channel = new SecureChannel({
|
|
724
|
+
inviteToken: "",
|
|
725
|
+
dataDir,
|
|
726
|
+
apiUrl: account.apiUrl,
|
|
727
|
+
agentName: account.agentName,
|
|
728
|
+
onMessage: async (plaintext, metadata) => {
|
|
729
|
+
if (!_ocRuntime) {
|
|
730
|
+
_log?.("[AgentVault] runtime not ready, queuing message");
|
|
731
|
+
_messageQueue.push(async () => {
|
|
732
|
+
await handleInbound({ plaintext, metadata, channel, account, cfg });
|
|
733
|
+
});
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
try {
|
|
737
|
+
await handleInbound({ plaintext, metadata, channel, account, cfg });
|
|
738
|
+
}
|
|
739
|
+
catch (err) {
|
|
740
|
+
_log?.(`[AgentVault] inbound error: ${String(err)}`);
|
|
741
|
+
}
|
|
742
|
+
},
|
|
743
|
+
onA2AMessage: async (msg) => {
|
|
744
|
+
const a2aMetadata = {
|
|
745
|
+
a2aChannelId: msg.channelId,
|
|
746
|
+
fromHubAddress: msg.fromHubAddress,
|
|
747
|
+
conversationId: msg.conversationId,
|
|
748
|
+
timestamp: msg.timestamp,
|
|
749
|
+
parentSpanId: msg.parentSpanId,
|
|
750
|
+
messageId: `a2a:${msg.channelId}:${Date.now()}`,
|
|
751
|
+
};
|
|
752
|
+
if (!_ocRuntime) {
|
|
753
|
+
_log?.("[AgentVault] runtime not ready, queuing A2A message");
|
|
754
|
+
_messageQueue.push(async () => {
|
|
755
|
+
await handleInbound({ plaintext: msg.text, metadata: a2aMetadata, channel, account, cfg });
|
|
756
|
+
});
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
try {
|
|
760
|
+
await handleInbound({ plaintext: msg.text, metadata: a2aMetadata, channel, account, cfg });
|
|
761
|
+
}
|
|
762
|
+
catch (err) {
|
|
763
|
+
_log?.(`[AgentVault] A2A inbound error: ${String(err)}`);
|
|
764
|
+
}
|
|
765
|
+
},
|
|
766
|
+
onA2AChannelReady: async (info) => {
|
|
767
|
+
// Register peer as valid outbound target (both roles)
|
|
768
|
+
_registerA2ATarget(info.peerHubAddress, account.accountId);
|
|
769
|
+
// Only initiator sends the seed message
|
|
770
|
+
if (info.role !== "initiator") {
|
|
771
|
+
_log?.(`[AgentVault] A2A channel ready (responder) — waiting for initiator: ${info.peerHubAddress}`);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
_log?.(`[AgentVault] A2A channel ready (initiator) — sending seed to ${info.peerHubAddress}${info.topic ? ` [topic: ${info.topic}]` : ""}`);
|
|
775
|
+
const seedText = info.topic
|
|
776
|
+
? `[System] A new A2A channel has been established with agent ${info.peerHubAddress}. ` +
|
|
777
|
+
`Topic: "${info.topic}". ` +
|
|
778
|
+
`Introduce yourself briefly and begin discussing this topic.`
|
|
779
|
+
: `[System] A new A2A channel has been established with agent ${info.peerHubAddress}. ` +
|
|
780
|
+
`Introduce yourself briefly and state what you can help with.`;
|
|
781
|
+
const seedMetadata = {
|
|
782
|
+
a2aChannelId: info.channelId,
|
|
783
|
+
fromHubAddress: info.peerHubAddress,
|
|
784
|
+
conversationId: info.conversationId,
|
|
785
|
+
timestamp: new Date().toISOString(),
|
|
786
|
+
messageId: `a2a-seed:${info.channelId}:${Date.now()}`,
|
|
787
|
+
};
|
|
788
|
+
if (!_ocRuntime) {
|
|
789
|
+
_log?.("[AgentVault] runtime not ready, queuing A2A seed");
|
|
790
|
+
_messageQueue.push(async () => {
|
|
791
|
+
await handleInbound({ plaintext: seedText, metadata: seedMetadata, channel, account, cfg });
|
|
792
|
+
});
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
try {
|
|
796
|
+
await handleInbound({ plaintext: seedText, metadata: seedMetadata, channel, account, cfg });
|
|
797
|
+
}
|
|
798
|
+
catch (err) {
|
|
799
|
+
_log?.(`[AgentVault] A2A seed error: ${String(err)}`);
|
|
800
|
+
}
|
|
801
|
+
},
|
|
802
|
+
onStateChange: (state) => {
|
|
803
|
+
_log?.(`[AgentVault] → ${state}`);
|
|
804
|
+
// "error" is a permanent failure — reject so gateway can restart
|
|
805
|
+
if (state === "error")
|
|
806
|
+
reject(new Error("AgentVault channel permanent error"));
|
|
807
|
+
// All other states (connecting/ready/disconnected) are handled
|
|
808
|
+
// internally by SecureChannel's reconnect logic — do NOT resolve.
|
|
809
|
+
},
|
|
810
|
+
});
|
|
811
|
+
_channels.set(account.accountId, channel);
|
|
812
|
+
// Prevent unhandled "error" events from crashing the process.
|
|
813
|
+
// Without this handler, Node.js EventEmitter throws on emit("error").
|
|
814
|
+
channel.on("error", (err) => {
|
|
815
|
+
_log?.(`[AgentVault] channel error (non-fatal): ${String(err)}`);
|
|
816
|
+
});
|
|
817
|
+
// Always start local HTTP server — managed routes are an overlay, not a replacement
|
|
818
|
+
const httpPort = account.httpPort ?? 18790;
|
|
819
|
+
channel.on("ready", () => {
|
|
820
|
+
channel.startHttpServer(httpPort);
|
|
821
|
+
_log?.(`[AgentVault] HTTP send server listening on http://127.0.0.1:${httpPort}`);
|
|
822
|
+
if (isUsingManagedRoutes) {
|
|
823
|
+
_log?.(`[AgentVault] OpenClaw managed routes also registered`);
|
|
824
|
+
}
|
|
825
|
+
// Register persisted A2A peers as valid outbound targets
|
|
826
|
+
for (const peerAddress of channel.a2aPeerAddresses) {
|
|
827
|
+
_registerA2ATarget(peerAddress, account.accountId);
|
|
828
|
+
}
|
|
829
|
+
// Register persisted rooms as valid outbound targets
|
|
830
|
+
for (const room of channel.roomIds) {
|
|
831
|
+
_registerRoomTarget(room.roomId, room.name, account.accountId);
|
|
832
|
+
}
|
|
833
|
+
});
|
|
834
|
+
// Register new rooms as outbound targets when agent joins
|
|
835
|
+
channel.on("room_joined", (info) => {
|
|
836
|
+
_registerRoomTarget(info.roomId, info.name, account.accountId);
|
|
837
|
+
});
|
|
838
|
+
channel.start().catch(reject);
|
|
839
|
+
}).catch(reject);
|
|
840
|
+
});
|
|
841
|
+
return { stop: async () => { } }; // Channel already stopped via abortSignal by this point
|
|
842
|
+
},
|
|
843
|
+
},
|
|
844
|
+
outbound: {
|
|
845
|
+
deliveryMode: "direct",
|
|
846
|
+
// Register valid send targets so OpenClaw's `message` tool can route
|
|
847
|
+
// proactive (agent-initiated) sends — not just replies to inbound messages.
|
|
848
|
+
targets: _outboundTargets,
|
|
849
|
+
sendText: async ({ to, text, accountId }) => {
|
|
850
|
+
const resolvedId = accountId ?? "default";
|
|
851
|
+
const ch = _channels.get(resolvedId);
|
|
852
|
+
if (!ch)
|
|
853
|
+
return { ok: false, error: "AgentVault channel not connected" };
|
|
854
|
+
try {
|
|
855
|
+
const wasReady = ch.state === "ready";
|
|
856
|
+
if (to.startsWith("a2a:")) {
|
|
857
|
+
await ch.sendToAgent(to.slice(4), text);
|
|
858
|
+
}
|
|
859
|
+
else if (to.startsWith("room:")) {
|
|
860
|
+
await ch.sendToRoom(to.slice(5), text);
|
|
861
|
+
}
|
|
862
|
+
else {
|
|
863
|
+
// "owner"/"default" — auto-route to room if context exists
|
|
864
|
+
const roomId = ch.lastInboundRoomId;
|
|
865
|
+
if (roomId) {
|
|
866
|
+
await ch.sendToRoom(roomId, text);
|
|
867
|
+
}
|
|
868
|
+
else {
|
|
869
|
+
await ch.send(text);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return { ok: true, queued: !wasReady };
|
|
873
|
+
}
|
|
874
|
+
catch (err) {
|
|
875
|
+
return { ok: false, error: String(err) };
|
|
876
|
+
}
|
|
877
|
+
},
|
|
878
|
+
sendMedia: async ({ to, text, mediaUrl, accountId }) => {
|
|
879
|
+
const resolvedId = accountId ?? "default";
|
|
880
|
+
const ch = _channels.get(resolvedId);
|
|
881
|
+
if (!ch)
|
|
882
|
+
return { ok: false, error: "AgentVault channel not connected" };
|
|
883
|
+
try {
|
|
884
|
+
// For now, send media URL as text — AgentVault handles attachments separately
|
|
885
|
+
const message = text ? `${text}\n${mediaUrl}` : mediaUrl;
|
|
886
|
+
const wasReady = ch.state === "ready";
|
|
887
|
+
if (to.startsWith("a2a:")) {
|
|
888
|
+
await ch.sendToAgent(to.slice(4), message);
|
|
889
|
+
}
|
|
890
|
+
else if (to.startsWith("room:")) {
|
|
891
|
+
await ch.sendToRoom(to.slice(5), message);
|
|
892
|
+
}
|
|
893
|
+
else {
|
|
894
|
+
// "owner"/"default" — auto-route to room if context exists
|
|
895
|
+
const roomId = ch.lastInboundRoomId;
|
|
896
|
+
if (roomId) {
|
|
897
|
+
await ch.sendToRoom(roomId, message);
|
|
898
|
+
}
|
|
899
|
+
else {
|
|
900
|
+
await ch.send(message);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
return { ok: true, queued: !wasReady };
|
|
904
|
+
}
|
|
905
|
+
catch (err) {
|
|
906
|
+
return { ok: false, error: String(err) };
|
|
907
|
+
}
|
|
908
|
+
},
|
|
909
|
+
/** Rich payload delivery — OpenClaw v2026.3.2+ calls this for structured messages. */
|
|
910
|
+
sendPayload: async (ctx) => {
|
|
911
|
+
const { payload, accountId } = ctx;
|
|
912
|
+
const resolvedId = accountId ?? "default";
|
|
913
|
+
const ch = _channels.get(resolvedId);
|
|
914
|
+
if (!ch)
|
|
915
|
+
return { ok: false, error: "AgentVault channel not connected" };
|
|
916
|
+
try {
|
|
917
|
+
// Suppress reasoning blocks — E2E channel should only deliver actual responses
|
|
918
|
+
if (payload.isReasoning)
|
|
919
|
+
return { ok: true };
|
|
920
|
+
// Wake agent proactively before sending (fire-and-forget)
|
|
921
|
+
requestHeartbeatNow({ reason: "outbound-payload" }).catch(() => { });
|
|
922
|
+
const text = (payload.text ?? "").trim();
|
|
923
|
+
if (!text && !payload.mediaUrls?.length)
|
|
924
|
+
return { ok: true };
|
|
925
|
+
// Helper: route through A2A, room, or direct depending on target
|
|
926
|
+
const _send = async (msg) => {
|
|
927
|
+
const target = ctx.to;
|
|
928
|
+
if (target && typeof target === "string" && target.startsWith("a2a:")) {
|
|
929
|
+
await ch.sendToAgent(target.slice(4), msg);
|
|
930
|
+
}
|
|
931
|
+
else if (target && typeof target === "string" && target.startsWith("room:")) {
|
|
932
|
+
await ch.sendToRoom(target.slice(5), msg);
|
|
933
|
+
}
|
|
934
|
+
else {
|
|
935
|
+
// "owner"/"default" — auto-route to room if context exists
|
|
936
|
+
const roomId = ch.lastInboundRoomId;
|
|
937
|
+
if (roomId) {
|
|
938
|
+
await ch.sendToRoom(roomId, msg);
|
|
939
|
+
}
|
|
940
|
+
else {
|
|
941
|
+
await ch.send(msg);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
// Encrypt and deliver text content
|
|
946
|
+
if (text) {
|
|
947
|
+
await _send(text);
|
|
948
|
+
}
|
|
949
|
+
// Deliver media URLs as text messages (E2E encrypted)
|
|
950
|
+
if (payload.mediaUrls?.length) {
|
|
951
|
+
for (const url of payload.mediaUrls) {
|
|
952
|
+
await _send(url);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
// Forward suggested actions as a structured message
|
|
956
|
+
if (payload.suggestedActions?.length) {
|
|
957
|
+
const actionsText = payload.suggestedActions
|
|
958
|
+
.map((a) => `- ${a.label}: ${a.action}`)
|
|
959
|
+
.join("\n");
|
|
960
|
+
await _send(`Suggested actions:\n${actionsText}`);
|
|
961
|
+
}
|
|
962
|
+
return { ok: true };
|
|
963
|
+
}
|
|
964
|
+
catch (err) {
|
|
965
|
+
return { ok: false, error: String(err) };
|
|
966
|
+
}
|
|
967
|
+
},
|
|
968
|
+
},
|
|
1464
969
|
};
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
970
|
+
// --- Exported for testing ---
|
|
971
|
+
export { _parseMentions, _shouldProcessRoomMessage, _stripMentions };
|
|
972
|
+
// --- Plugin export ---
|
|
973
|
+
export default {
|
|
974
|
+
id: "agentvault",
|
|
975
|
+
name: "AgentVault",
|
|
976
|
+
description: "End-to-end encrypted, zero-knowledge messaging between AI agent owners and their agents.",
|
|
977
|
+
register(api) {
|
|
978
|
+
_setRuntime(api.runtime);
|
|
979
|
+
// Install fetch interceptor to capture all outbound HTTP from the gateway process.
|
|
980
|
+
installFetchInterceptor({
|
|
981
|
+
onHttpCall: (report) => {
|
|
982
|
+
// Find an active channel to emit through
|
|
983
|
+
const ch = _channels.values().next().value;
|
|
984
|
+
if (!ch)
|
|
985
|
+
return;
|
|
986
|
+
// Telemetry reporter is optional — may not be initialized
|
|
987
|
+
if (ch.telemetry) {
|
|
988
|
+
try {
|
|
989
|
+
ch.telemetry.reportHttpCall({
|
|
990
|
+
method: report.method,
|
|
991
|
+
url: report.url,
|
|
992
|
+
statusCode: report.statusCode,
|
|
993
|
+
latencyMs: report.latencyMs,
|
|
994
|
+
...(report.traceId ? { traceId: report.traceId } : {}),
|
|
995
|
+
...(report.parentSpanId ? { parentSpanId: report.parentSpanId } : {}),
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
catch { /* best-effort */ }
|
|
999
|
+
}
|
|
1000
|
+
// Activity span always fires (uses WS, not telemetry reporter)
|
|
1001
|
+
try {
|
|
1002
|
+
const hostname = (() => { try {
|
|
1003
|
+
return new URL(report.url).hostname;
|
|
1004
|
+
}
|
|
1005
|
+
catch {
|
|
1006
|
+
return report.url.slice(0, 40);
|
|
1007
|
+
} })();
|
|
1008
|
+
ch.sendActivitySpan({
|
|
1009
|
+
trace_id: report.traceId ?? "",
|
|
1010
|
+
parent_span_id: report.parentSpanId ?? "",
|
|
1011
|
+
span_id: randomBytes(8).toString("hex"),
|
|
1012
|
+
span_type: "http",
|
|
1013
|
+
span_name: hostname,
|
|
1014
|
+
status: report.statusCode >= 400 || report.statusCode === 0 ? "error" : "ok",
|
|
1015
|
+
start_time: new Date(Date.now() - report.latencyMs).toISOString(),
|
|
1016
|
+
end_time: new Date().toISOString(),
|
|
1017
|
+
duration_ms: report.latencyMs,
|
|
1018
|
+
attributes: {
|
|
1019
|
+
"ai.agent.http.method": report.method,
|
|
1020
|
+
"ai.agent.http.url": report.url,
|
|
1021
|
+
},
|
|
1022
|
+
http_method: report.method,
|
|
1023
|
+
http_status_code: report.statusCode,
|
|
1024
|
+
http_url: report.url,
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
catch { /* best-effort */ }
|
|
1028
|
+
},
|
|
1029
|
+
});
|
|
1030
|
+
api.registerChannel({ plugin: agentVaultPlugin });
|
|
1031
|
+
// --- Managed HTTP routes (OpenClaw v2026.3.2+) ---
|
|
1032
|
+
if (typeof api.registerHttpRoute === "function") {
|
|
1033
|
+
try {
|
|
1034
|
+
api.registerHttpRoute({
|
|
1035
|
+
path: "/agentvault/send",
|
|
1036
|
+
method: "POST",
|
|
1037
|
+
handler: async (req) => {
|
|
1038
|
+
const ch = _channels.values().next().value;
|
|
1039
|
+
if (!ch)
|
|
1040
|
+
return { status: 503, body: { ok: false, error: "Channel not started" } };
|
|
1041
|
+
const parsed = (typeof req.body === "string" ? JSON.parse(req.body) : req.body);
|
|
1042
|
+
const result = await handleSendRequest(parsed, ch);
|
|
1043
|
+
return { status: result.status, body: result.body };
|
|
1044
|
+
},
|
|
1045
|
+
});
|
|
1046
|
+
api.registerHttpRoute({
|
|
1047
|
+
path: "/agentvault/action",
|
|
1048
|
+
method: "POST",
|
|
1049
|
+
handler: async (req) => {
|
|
1050
|
+
const ch = _channels.values().next().value;
|
|
1051
|
+
if (!ch)
|
|
1052
|
+
return { status: 503, body: { ok: false, error: "Channel not started" } };
|
|
1053
|
+
const parsed = (typeof req.body === "string" ? JSON.parse(req.body) : req.body);
|
|
1054
|
+
const result = await handleActionRequest(parsed, ch);
|
|
1055
|
+
return { status: result.status, body: result.body };
|
|
1056
|
+
},
|
|
1057
|
+
});
|
|
1058
|
+
api.registerHttpRoute({
|
|
1059
|
+
path: "/agentvault/decision",
|
|
1060
|
+
method: "POST",
|
|
1061
|
+
handler: async (req) => {
|
|
1062
|
+
const ch = _channels.values().next().value;
|
|
1063
|
+
if (!ch)
|
|
1064
|
+
return { status: 503, body: { ok: false, error: "Channel not started" } };
|
|
1065
|
+
const parsed = (typeof req.body === "string" ? JSON.parse(req.body) : req.body);
|
|
1066
|
+
const result = await handleDecisionRequest(parsed, ch);
|
|
1067
|
+
return { status: result.status, body: result.body };
|
|
1068
|
+
},
|
|
1069
|
+
});
|
|
1070
|
+
api.registerHttpRoute({
|
|
1071
|
+
path: "/agentvault/status",
|
|
1072
|
+
method: "GET",
|
|
1073
|
+
handler: async () => {
|
|
1074
|
+
const ch = _channels.values().next().value;
|
|
1075
|
+
if (!ch)
|
|
1076
|
+
return { status: 503, body: { ok: false, error: "Channel not started" } };
|
|
1077
|
+
const result = handleStatusRequest(ch);
|
|
1078
|
+
return { status: result.status, body: result.body };
|
|
1079
|
+
},
|
|
1080
|
+
});
|
|
1081
|
+
isUsingManagedRoutes = true;
|
|
1082
|
+
}
|
|
1083
|
+
catch { /* registerHttpRoute failed — fall back to self-managed server */ }
|
|
1084
|
+
}
|
|
1085
|
+
// --- Event hooks (OpenClaw v2026.3.2+) ---
|
|
1086
|
+
if (typeof api.on === "function") {
|
|
1087
|
+
// Phase 4: message_sent — delivery confirmation tracking
|
|
1088
|
+
api.on("message_sent", async (event) => {
|
|
1089
|
+
try {
|
|
1090
|
+
const ch = _channels.values().next().value;
|
|
1091
|
+
if (!ch?.telemetry)
|
|
1092
|
+
return;
|
|
1093
|
+
ch.telemetry.reportCustomSpan({
|
|
1094
|
+
traceId: randomBytes(16).toString("hex"),
|
|
1095
|
+
spanId: randomBytes(8).toString("hex"),
|
|
1096
|
+
name: "agentvault.message.delivered",
|
|
1097
|
+
kind: "internal",
|
|
1098
|
+
startTime: event.timestamp,
|
|
1099
|
+
endTime: event.timestamp + 1,
|
|
1100
|
+
attributes: {
|
|
1101
|
+
"agentvault.message.id": event.messageId,
|
|
1102
|
+
"agentvault.delivery.status": event.deliveryStatus,
|
|
1103
|
+
"agentvault.channel.id": event.channelId,
|
|
1104
|
+
},
|
|
1105
|
+
status: { code: event.deliveryStatus === "failed" ? 2 : 1 },
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
catch { /* best-effort */ }
|
|
1109
|
+
});
|
|
1110
|
+
// Phase 6: session lifecycle hooks
|
|
1111
|
+
api.on("session_start", async (event) => {
|
|
1112
|
+
try {
|
|
1113
|
+
const ch = _channels.values().next().value;
|
|
1114
|
+
if (!ch?.telemetry)
|
|
1115
|
+
return;
|
|
1116
|
+
ch.telemetry.reportCustomSpan({
|
|
1117
|
+
traceId: randomBytes(16).toString("hex"),
|
|
1118
|
+
spanId: randomBytes(8).toString("hex"),
|
|
1119
|
+
name: "agentvault.session.start",
|
|
1120
|
+
kind: "internal",
|
|
1121
|
+
startTime: event.timestamp,
|
|
1122
|
+
endTime: event.timestamp + 1,
|
|
1123
|
+
attributes: {
|
|
1124
|
+
"agentvault.session.key": event.sessionKey,
|
|
1125
|
+
"agentvault.agent.id": event.agentId,
|
|
1126
|
+
},
|
|
1127
|
+
status: { code: 1 },
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
catch { /* best-effort */ }
|
|
1131
|
+
});
|
|
1132
|
+
api.on("session_end", async (event) => {
|
|
1133
|
+
try {
|
|
1134
|
+
const ch = _channels.values().next().value;
|
|
1135
|
+
if (!ch?.telemetry)
|
|
1136
|
+
return;
|
|
1137
|
+
ch.telemetry.reportCustomSpan({
|
|
1138
|
+
traceId: randomBytes(16).toString("hex"),
|
|
1139
|
+
spanId: randomBytes(8).toString("hex"),
|
|
1140
|
+
name: "agentvault.session.end",
|
|
1141
|
+
kind: "internal",
|
|
1142
|
+
startTime: event.timestamp,
|
|
1143
|
+
endTime: event.timestamp + 1,
|
|
1144
|
+
attributes: {
|
|
1145
|
+
"agentvault.session.key": event.sessionKey,
|
|
1146
|
+
"agentvault.agent.id": event.agentId,
|
|
1147
|
+
...(event.reason ? { "agentvault.session.end_reason": event.reason } : {}),
|
|
1148
|
+
},
|
|
1149
|
+
status: { code: 1 },
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
catch { /* best-effort */ }
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
// --- Phase 7: Agent events for trust telemetry ---
|
|
1156
|
+
import("./openclaw-compat.js").then(async ({ onAgentEvent, onSessionTranscriptUpdate }) => {
|
|
1157
|
+
onAgentEvent((event) => {
|
|
1158
|
+
try {
|
|
1159
|
+
const ch = _channels.values().next().value;
|
|
1160
|
+
if (!ch?.telemetry)
|
|
1161
|
+
return;
|
|
1162
|
+
ch.telemetry.reportCustomSpan({
|
|
1163
|
+
traceId: randomBytes(16).toString("hex"),
|
|
1164
|
+
spanId: randomBytes(8).toString("hex"),
|
|
1165
|
+
name: `ai.agent.event.${event.type}`,
|
|
1166
|
+
kind: "internal",
|
|
1167
|
+
startTime: event.timestamp,
|
|
1168
|
+
endTime: event.timestamp + 1,
|
|
1169
|
+
attributes: {
|
|
1170
|
+
"ai.agent.event.type": event.type,
|
|
1171
|
+
"ai.agent.id": event.agentId,
|
|
1172
|
+
...(event.data ? Object.fromEntries(Object.entries(event.data).map(([k, v]) => [`ai.agent.event.${k}`, String(v)])) : {}),
|
|
1173
|
+
},
|
|
1174
|
+
status: { code: 1 },
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
catch { /* best-effort */ }
|
|
1178
|
+
}).catch(() => { });
|
|
1179
|
+
onSessionTranscriptUpdate((update) => {
|
|
1180
|
+
try {
|
|
1181
|
+
const ch = _channels.values().next().value;
|
|
1182
|
+
if (!ch?.telemetry)
|
|
1183
|
+
return;
|
|
1184
|
+
ch.telemetry.reportCustomSpan({
|
|
1185
|
+
traceId: randomBytes(16).toString("hex"),
|
|
1186
|
+
spanId: randomBytes(8).toString("hex"),
|
|
1187
|
+
name: "ai.agent.transcript.update",
|
|
1188
|
+
kind: "internal",
|
|
1189
|
+
startTime: update.timestamp,
|
|
1190
|
+
endTime: update.timestamp + 1,
|
|
1191
|
+
attributes: {
|
|
1192
|
+
"agentvault.session.key": update.sessionKey,
|
|
1193
|
+
"ai.agent.transcript.role": update.role ?? "unknown",
|
|
1194
|
+
"ai.agent.transcript.delta_chars": update.delta.length,
|
|
1195
|
+
},
|
|
1196
|
+
status: { code: 1 },
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
catch { /* best-effort */ }
|
|
1200
|
+
}).catch(() => { });
|
|
1201
|
+
}).catch(() => { });
|
|
1202
|
+
// --- Agent tool: agentvault_status ---
|
|
1203
|
+
// Lets the agent check its own encrypted channel status
|
|
1204
|
+
try {
|
|
1205
|
+
api.registerTool?.({
|
|
1206
|
+
name: "agentvault_status",
|
|
1207
|
+
description: "Check the AgentVault encrypted channel status, connection state, and session count.",
|
|
1208
|
+
parameters: {
|
|
1209
|
+
type: "object",
|
|
1210
|
+
properties: {
|
|
1211
|
+
accountId: {
|
|
1212
|
+
type: "string",
|
|
1213
|
+
description: "Account ID to check (default: 'default')",
|
|
1214
|
+
},
|
|
1215
|
+
},
|
|
1216
|
+
},
|
|
1217
|
+
execute: async (_id, params) => {
|
|
1218
|
+
const id = params.accountId ?? "default";
|
|
1219
|
+
const ch = _channels.get(id);
|
|
1220
|
+
if (!ch) {
|
|
1221
|
+
return { content: [{ type: "text", text: JSON.stringify({ connected: false, error: "Channel not started" }) }] };
|
|
1222
|
+
}
|
|
1223
|
+
return {
|
|
1224
|
+
content: [{
|
|
1225
|
+
type: "text",
|
|
1226
|
+
text: JSON.stringify({
|
|
1227
|
+
connected: ch.state === "ready",
|
|
1228
|
+
state: ch.state,
|
|
1229
|
+
deviceId: ch.deviceId,
|
|
1230
|
+
sessions: ch.sessionCount,
|
|
1231
|
+
encrypted: true,
|
|
1232
|
+
}),
|
|
1233
|
+
}],
|
|
1234
|
+
};
|
|
1235
|
+
},
|
|
1236
|
+
}, { optional: true });
|
|
1237
|
+
}
|
|
1238
|
+
catch { /* registerTool may not be available in older OpenClaw versions */ }
|
|
1239
|
+
// --- Auto-reply command: /agentvault ---
|
|
1240
|
+
// Quick status check without AI involvement
|
|
1241
|
+
try {
|
|
1242
|
+
api.registerCommand?.({
|
|
1243
|
+
name: "agentvault",
|
|
1244
|
+
description: "Show AgentVault encrypted channel status",
|
|
1245
|
+
handler: () => {
|
|
1246
|
+
const statuses = [];
|
|
1247
|
+
if (_channels.size === 0) {
|
|
1248
|
+
statuses.push("AgentVault: no active channels");
|
|
1249
|
+
}
|
|
1250
|
+
else {
|
|
1251
|
+
for (const [id, ch] of _channels) {
|
|
1252
|
+
statuses.push(`AgentVault [${id}]: ${ch.state} | sessions: ${ch.sessionCount} | encrypted: yes`);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
return { text: statuses.join("\n") };
|
|
1256
|
+
},
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
catch { /* registerCommand may not be available in older OpenClaw versions */ }
|
|
1260
|
+
},
|
|
1471
1261
|
};
|
|
1472
|
-
//# sourceMappingURL=openclaw-entry.js.map
|
|
1262
|
+
//# sourceMappingURL=openclaw-entry.js.map
|