nexus-agentd 0.1.0 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +351 -179
  3. package/dist/a2a/runtime.d.ts +30 -0
  4. package/dist/a2a/runtime.js +374 -0
  5. package/dist/acp/runtime.d.ts +12 -3
  6. package/dist/acp/runtime.js +248 -17
  7. package/dist/artifact-store.d.ts +41 -0
  8. package/dist/artifact-store.js +340 -0
  9. package/dist/auth.d.ts +13 -0
  10. package/dist/auth.js +77 -0
  11. package/dist/cli.js +58 -1
  12. package/dist/config.d.ts +10 -0
  13. package/dist/config.js +281 -53
  14. package/dist/control-plane.d.ts +91 -0
  15. package/dist/control-plane.js +629 -0
  16. package/dist/drivers/hermes.d.ts +2 -0
  17. package/dist/drivers/hermes.js +10 -0
  18. package/dist/drivers/index.js +6 -0
  19. package/dist/events.js +13 -2
  20. package/dist/index.d.ts +6 -0
  21. package/dist/index.js +15 -3
  22. package/dist/run-store.d.ts +49 -0
  23. package/dist/run-store.js +291 -0
  24. package/dist/server.d.ts +3 -2
  25. package/dist/server.js +911 -41
  26. package/dist/session-contract.d.ts +16 -4
  27. package/dist/session.d.ts +74 -8
  28. package/dist/session.js +433 -38
  29. package/dist/types.d.ts +193 -6
  30. package/dist/types.js +2 -1
  31. package/dist/webui/app.d.ts +1 -0
  32. package/dist/webui/app.js +1072 -0
  33. package/dist/webui/icons.d.ts +9 -0
  34. package/dist/webui/icons.js +10 -0
  35. package/dist/webui/index.d.ts +3 -0
  36. package/dist/webui/index.js +32 -0
  37. package/dist/webui/markup.d.ts +1 -0
  38. package/dist/webui/markup.js +68 -0
  39. package/dist/webui/styles.d.ts +1 -0
  40. package/dist/webui/styles.js +269 -0
  41. package/dist/workspace-files.d.ts +48 -0
  42. package/dist/workspace-files.js +269 -0
  43. package/nexus-agentd.example.json +30 -27
  44. package/package.json +6 -6
@@ -0,0 +1,629 @@
1
+ import { randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
2
+ import { chmod, open, readFile, rename, rm } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { hashAdminPassword, validateAdminPassword, verifyAdminPassword } from './auth.js';
5
+ import { loadAgentdConfig } from './config.js';
6
+ import { createDriverRegistry } from './drivers/index.js';
7
+ import { agentdDriverKinds } from './types.js';
8
+ import { WorkspacePolicy } from './workspace.js';
9
+ export class AgentdControlPlane {
10
+ configPath;
11
+ config;
12
+ sessions;
13
+ queue = Promise.resolve();
14
+ usageTimer;
15
+ constructor(configPath, config, sessions) {
16
+ this.configPath = configPath;
17
+ this.config = config;
18
+ this.sessions = sessions;
19
+ }
20
+ snapshot() {
21
+ return {
22
+ workspaceRoots: [...this.config.workspaceRoots],
23
+ driverKinds: [...agentdDriverKinds],
24
+ sessionTtlMs: this.config.sessionTtlMs,
25
+ promptTimeoutMs: this.config.promptTimeoutMs || 30 * 60 * 1000,
26
+ cleanupIntervalMs: this.config.cleanupIntervalMs || 60_000,
27
+ agents: Object.entries(this.config.agents)
28
+ .map(([id, config]) => this.agentView(id, config))
29
+ .sort((left, right) => left.name.localeCompare(right.name))
30
+ };
31
+ }
32
+ isInitialized() {
33
+ return this.config.initialized;
34
+ }
35
+ needsAdminSetup() {
36
+ return !this.config.adminPasswordHash;
37
+ }
38
+ /** @deprecated Legacy data-plane key accessor. */
39
+ accessKey() {
40
+ return this.config.authToken;
41
+ }
42
+ initializeAdminPassword(passwordInput, confirmationInput) {
43
+ return this.exclusive(async () => {
44
+ if (this.config.adminPasswordHash) {
45
+ throw new ControlPlaneError(409, 'Console setup is already complete');
46
+ }
47
+ const password = validatePassword(passwordInput, confirmationInput);
48
+ const raw = await this.readRawConfig();
49
+ if (cleanString(raw.adminPasswordHash)) {
50
+ throw new ControlPlaneError(409, 'Console setup is already complete');
51
+ }
52
+ raw.initialized = true;
53
+ raw.adminPasswordHash = await hashAdminPassword(password);
54
+ if (!Array.isArray(raw.apiKeys))
55
+ raw.apiKeys = [];
56
+ await this.persist(raw);
57
+ return { initialized: true, adminSetupRequired: false };
58
+ });
59
+ }
60
+ /** @deprecated Kept as a clear error for integrations using the old bootstrap API. */
61
+ initializeAccessKey() {
62
+ throw new ControlPlaneError(410, 'Access Key bootstrap was replaced by Console Password setup');
63
+ }
64
+ verifyAdminPassword(password) {
65
+ return verifyAdminPassword(password, this.config.adminPasswordHash);
66
+ }
67
+ changeAdminPassword(current, next, confirmation) {
68
+ return this.exclusive(async () => {
69
+ if (!(await this.verifyAdminPassword(current))) {
70
+ throw new ControlPlaneError(403, 'Current Console Password is incorrect');
71
+ }
72
+ const password = validatePassword(next, confirmation);
73
+ const raw = await this.readRawConfig();
74
+ raw.adminPasswordHash = await hashAdminPassword(password);
75
+ await this.persist(raw);
76
+ return { changed: true };
77
+ });
78
+ }
79
+ authenticateApiKey(secret) {
80
+ for (const key of this.config.apiKeys || []) {
81
+ if (!key.enabled || !safeSecretEqual(secret, key.secret))
82
+ continue;
83
+ this.markApiKeyUsed(key.id);
84
+ return { id: key.id, scope: structuredClone(key.scope) };
85
+ }
86
+ return undefined;
87
+ }
88
+ listApiKeys() {
89
+ return (this.config.apiKeys || [])
90
+ .map((key) => this.apiKeyView(key))
91
+ .sort((left, right) => right.createdAt - left.createdAt);
92
+ }
93
+ createApiKey(nameInput, scopeInput, customSecret) {
94
+ return this.exclusive(async () => {
95
+ const name = validateKeyName(nameInput);
96
+ const scope = this.validateScope(scopeInput);
97
+ const secret = customSecret
98
+ ? validateApiKeySecret(customSecret)
99
+ : `nx_sk_${randomBytes(24).toString('base64url')}`;
100
+ if ((this.config.apiKeys || []).some((key) => safeSecretEqual(key.secret, secret))) {
101
+ throw new ControlPlaneError(409, 'API Key already exists');
102
+ }
103
+ const raw = await this.readRawConfig();
104
+ const keys = this.rawApiKeys(raw);
105
+ const key = {
106
+ id: randomUUID(),
107
+ name,
108
+ secret,
109
+ enabled: true,
110
+ scope,
111
+ createdAt: Date.now()
112
+ };
113
+ keys.push(key);
114
+ raw.apiKeys = keys;
115
+ await this.persist(raw);
116
+ return { key: this.apiKeyView(this.requireApiKey(key.id)), secret };
117
+ });
118
+ }
119
+ updateApiKey(idInput, update) {
120
+ const id = validateOpaqueId(idInput, 'API Key');
121
+ return this.exclusive(async () => {
122
+ const raw = await this.readRawConfig();
123
+ const keys = this.rawApiKeys(raw);
124
+ const index = keys.findIndex((item) => cleanString(item.id) === id);
125
+ if (index < 0)
126
+ throw new ControlPlaneError(404, 'API Key not found');
127
+ const current = recordValue(keys[index]);
128
+ if (update.name !== undefined)
129
+ current.name = validateKeyName(update.name);
130
+ if (update.enabled !== undefined)
131
+ current.enabled = update.enabled;
132
+ if (update.scope !== undefined)
133
+ current.scope = this.validateScope(update.scope);
134
+ keys[index] = current;
135
+ raw.apiKeys = keys;
136
+ await this.persist(raw);
137
+ return this.apiKeyView(this.requireApiKey(id));
138
+ });
139
+ }
140
+ deleteApiKey(idInput) {
141
+ const id = validateOpaqueId(idInput, 'API Key');
142
+ return this.exclusive(async () => {
143
+ const raw = await this.readRawConfig();
144
+ const keys = this.rawApiKeys(raw);
145
+ const next = keys.filter((item) => cleanString(item.id) !== id);
146
+ if (next.length === keys.length) {
147
+ throw new ControlPlaneError(404, 'API Key not found');
148
+ }
149
+ raw.apiKeys = next;
150
+ if (id === 'legacy')
151
+ delete raw.authToken;
152
+ await this.persist(raw);
153
+ return { deleted: true };
154
+ });
155
+ }
156
+ revealApiKey(idInput) {
157
+ const id = validateOpaqueId(idInput, 'API Key');
158
+ return { secret: this.requireApiKey(id).secret };
159
+ }
160
+ regenerateApiKey(idInput) {
161
+ const id = validateOpaqueId(idInput, 'API Key');
162
+ return this.exclusive(async () => {
163
+ const raw = await this.readRawConfig();
164
+ const keys = this.rawApiKeys(raw);
165
+ const index = keys.findIndex((item) => cleanString(item.id) === id);
166
+ if (index < 0)
167
+ throw new ControlPlaneError(404, 'API Key not found');
168
+ const secret = `nx_sk_${randomBytes(24).toString('base64url')}`;
169
+ keys[index] = { ...recordValue(keys[index]), secret };
170
+ raw.apiKeys = keys;
171
+ if (id === 'legacy')
172
+ raw.authToken = secret;
173
+ await this.persist(raw);
174
+ return { key: this.apiKeyView(this.requireApiKey(id)), secret };
175
+ });
176
+ }
177
+ /** @deprecated Rotates the migrated legacy key when present. */
178
+ async rotateAccessKey() {
179
+ const key = (this.config.apiKeys || []).find((item) => item.id === 'legacy');
180
+ if (!key)
181
+ throw new ControlPlaneError(404, 'Legacy Access Key not found');
182
+ const result = await this.regenerateApiKey('legacy');
183
+ return { accessKey: result.secret };
184
+ }
185
+ putWorkspaceRoots(values) {
186
+ return this.exclusive(async () => {
187
+ const workspaceRoots = Array.from(new Set(values.map(cleanString).filter(Boolean)));
188
+ if (!workspaceRoots.length) {
189
+ throw new ControlPlaneError(400, 'workspaceRoots must contain at least one path');
190
+ }
191
+ await WorkspacePolicy.create(workspaceRoots);
192
+ const raw = await this.readRawConfig();
193
+ raw.workspaceRoots = workspaceRoots;
194
+ await this.persist(raw);
195
+ return this.snapshot();
196
+ });
197
+ }
198
+ putRuntimeSettings(update) {
199
+ return this.exclusive(async () => {
200
+ validateRuntimeSettings(update);
201
+ const raw = await this.readRawConfig();
202
+ raw.sessionTtlMs = update.sessionTtlMs;
203
+ raw.promptTimeoutMs = update.promptTimeoutMs;
204
+ raw.cleanupIntervalMs = update.cleanupIntervalMs;
205
+ await this.persist(raw);
206
+ return this.snapshot();
207
+ });
208
+ }
209
+ putAgent(idInput, update) {
210
+ const id = validateAgentId(idInput);
211
+ return this.exclusive(() => this.updateAgents(id, update));
212
+ }
213
+ deleteAgent(idInput) {
214
+ const id = validateAgentId(idInput);
215
+ return this.exclusive(async () => {
216
+ const raw = await this.readRawConfig();
217
+ const agents = recordValue(raw.agents);
218
+ if (!Object.hasOwn(agents, id)) {
219
+ throw new ControlPlaneError(404, `Configured agent not found: ${id}`);
220
+ }
221
+ delete agents[id];
222
+ raw.agents = agents;
223
+ const keys = this.rawApiKeys(raw).map((entry) => {
224
+ const key = recordValue(entry);
225
+ const scope = recordValue(key.scope);
226
+ if (Array.isArray(scope.agentIds)) {
227
+ scope.agentIds = scope.agentIds.filter((agentId) => agentId !== id);
228
+ key.scope = scope;
229
+ }
230
+ return key;
231
+ });
232
+ raw.apiKeys = keys;
233
+ await this.persist(raw);
234
+ return this.snapshot();
235
+ });
236
+ }
237
+ async updateAgents(id, update) {
238
+ validateUpdate(update);
239
+ const raw = await this.readRawConfig();
240
+ const agents = recordValue(raw.agents);
241
+ const previous = recordValue(agents[id]);
242
+ let next;
243
+ const protocol = update.protocol || 'acp';
244
+ if (protocol === 'a2a') {
245
+ const requestedAgentCardUrl = cleanString(update.agentCardUrl);
246
+ const requestedAgentUrl = cleanString(update.agentUrl);
247
+ const previousAgentCardUrl = cleanString(previous.agentCardUrl);
248
+ const previousAgentUrl = cleanString(previous.agentUrl);
249
+ const selectedAgentCardUrl = requestedAgentCardUrl || (!requestedAgentUrl ? previousAgentCardUrl : '');
250
+ const selectedAgentUrl = requestedAgentUrl || (!selectedAgentCardUrl ? previousAgentUrl : '');
251
+ const agentCardUrl = selectedAgentCardUrl
252
+ ? validateAgentUrl(selectedAgentCardUrl)
253
+ : '';
254
+ const agentUrl = selectedAgentUrl ? validateAgentUrl(selectedAgentUrl) : '';
255
+ if (!agentCardUrl && !agentUrl) {
256
+ throw new ControlPlaneError(400, 'A2A Agent Card URL is required');
257
+ }
258
+ const preferredTransport = update.preferredTransport ||
259
+ cleanString(previous.preferredTransport) ||
260
+ 'auto';
261
+ const previousAuth = recordValue(previous.auth);
262
+ const authType = update.authType || cleanString(previousAuth.type) || 'none';
263
+ if (!['none', 'bearer', 'header'].includes(authType)) {
264
+ throw new ControlPlaneError(400, 'authType must be none, bearer, or header');
265
+ }
266
+ const authValue = update.authValue !== undefined
267
+ ? validateA2ASecret(update.authValue)
268
+ : cleanString(previousAuth.value);
269
+ const authHeaderName = cleanString(update.authHeaderName) || cleanString(previousAuth.headerName);
270
+ if (authType !== 'none' && !authValue) {
271
+ throw new ControlPlaneError(400, 'A2A authentication value is required');
272
+ }
273
+ if (authType === 'header' && !validHeaderName(authHeaderName)) {
274
+ throw new ControlPlaneError(400, 'A valid A2A header name is required');
275
+ }
276
+ next = {
277
+ protocol: 'a2a',
278
+ name: cleanString(update.name) || cleanString(previous.name) || id,
279
+ enabled: update.enabled ?? previous.enabled !== false,
280
+ ...(agentCardUrl ? { agentCardUrl } : { agentUrl }),
281
+ preferredTransport,
282
+ auth: {
283
+ type: authType,
284
+ ...(authType !== 'none' ? { value: authValue } : {}),
285
+ ...(authType === 'header' ? { headerName: authHeaderName } : {})
286
+ },
287
+ timeoutMs: clampTimeout(update.timeoutMs, 60_000, 1000, 30 * 60_000)
288
+ };
289
+ }
290
+ else {
291
+ const driver = update.driver || cleanString(previous.driver);
292
+ if (!agentdDriverKinds.includes(driver)) {
293
+ throw new ControlPlaneError(400, `Unsupported nexus-agentd driver: ${driver}`);
294
+ }
295
+ const workspace = cleanString(update.workspace) ||
296
+ cleanString(previous.workspace) ||
297
+ this.config.workspaceRoots[0];
298
+ if (!workspace)
299
+ throw new ControlPlaneError(400, 'workspace is required');
300
+ await (await WorkspacePolicy.create(this.config.workspaceRoots)).resolve(workspace);
301
+ next = {
302
+ protocol: 'acp',
303
+ driver,
304
+ enabled: update.enabled ?? previous.enabled !== false,
305
+ workspace,
306
+ permissionPolicy: update.permissionPolicy || cleanString(previous.permissionPolicy) || 'ask',
307
+ permissionTimeoutMs: clampTimeout(update.permissionTimeoutMs, 15 * 60_000, 1000, 24 * 60 * 60 * 1000),
308
+ ...preservedAcpAdvanced(previous)
309
+ };
310
+ if (cleanString(update.name) || cleanString(previous.name)) {
311
+ next.name = cleanString(update.name) || cleanString(previous.name);
312
+ }
313
+ }
314
+ setOptionalString(next, 'description', update.description ?? cleanString(previous.description));
315
+ agents[id] = next;
316
+ raw.agents = agents;
317
+ await this.persist(raw);
318
+ return this.snapshot();
319
+ }
320
+ validateScope(input) {
321
+ if (!input || typeof input.allAgents !== 'boolean' || !Array.isArray(input.agentIds)) {
322
+ throw new ControlPlaneError(400, 'scope must include allAgents and agentIds');
323
+ }
324
+ const agentIds = Array.from(new Set(input.agentIds.map(validateAgentId)));
325
+ for (const id of agentIds) {
326
+ if (!this.config.agents[id]) {
327
+ throw new ControlPlaneError(400, `Configured agent not found: ${id}`);
328
+ }
329
+ }
330
+ return { allAgents: input.allAgents, agentIds: input.allAgents ? [] : agentIds };
331
+ }
332
+ requireApiKey(id) {
333
+ const key = (this.config.apiKeys || []).find((item) => item.id === id);
334
+ if (!key)
335
+ throw new ControlPlaneError(404, 'API Key not found');
336
+ return key;
337
+ }
338
+ apiKeyView(key) {
339
+ return {
340
+ id: key.id,
341
+ name: key.name,
342
+ enabled: key.enabled,
343
+ scope: structuredClone(key.scope),
344
+ suffix: key.secret.slice(-4),
345
+ createdAt: key.createdAt,
346
+ lastUsedAt: key.lastUsedAt,
347
+ legacy: key.id === 'legacy'
348
+ };
349
+ }
350
+ markApiKeyUsed(id) {
351
+ const key = (this.config.apiKeys || []).find((item) => item.id === id);
352
+ if (!key)
353
+ return;
354
+ const now = Date.now();
355
+ if (key.lastUsedAt && now - key.lastUsedAt < 60_000)
356
+ return;
357
+ key.lastUsedAt = now;
358
+ if (this.usageTimer)
359
+ return;
360
+ this.usageTimer = setTimeout(() => {
361
+ this.usageTimer = undefined;
362
+ void this.exclusive(async () => {
363
+ const raw = await this.readRawConfig();
364
+ const keys = this.rawApiKeys(raw);
365
+ for (const entry of keys) {
366
+ const currentId = cleanString(entry.id);
367
+ const current = (this.config.apiKeys || []).find((item) => item.id === currentId);
368
+ if (current?.lastUsedAt)
369
+ entry.lastUsedAt = current.lastUsedAt;
370
+ }
371
+ raw.apiKeys = keys;
372
+ await this.persist(raw);
373
+ }).catch((error) => console.error(JSON.stringify({
374
+ level: 'error',
375
+ event: 'api_key_usage_persist_failed',
376
+ message: errorMessage(error)
377
+ })));
378
+ }, 5_000);
379
+ this.usageTimer.unref?.();
380
+ }
381
+ rawApiKeys(raw) {
382
+ const keys = Array.isArray(raw.apiKeys)
383
+ ? raw.apiKeys.map((item) => recordValue(item))
384
+ : [];
385
+ const legacySecret = cleanString(raw.authToken);
386
+ if (legacySecret && !keys.some((item) => cleanString(item.id) === 'legacy')) {
387
+ keys.unshift({
388
+ id: 'legacy',
389
+ name: 'Legacy Access Key',
390
+ secret: legacySecret,
391
+ enabled: true,
392
+ scope: { allAgents: true, agentIds: [] },
393
+ createdAt: 0
394
+ });
395
+ }
396
+ return keys;
397
+ }
398
+ async readRawConfig() {
399
+ let value;
400
+ try {
401
+ value = JSON.parse(await readFile(path.resolve(this.configPath), 'utf8'));
402
+ }
403
+ catch (error) {
404
+ throw new ControlPlaneError(500, `Unable to read config: ${errorMessage(error)}`);
405
+ }
406
+ if (!isRecord(value))
407
+ throw new ControlPlaneError(500, 'Config root must be an object');
408
+ return structuredClone(value);
409
+ }
410
+ async persist(raw) {
411
+ const target = path.resolve(this.configPath);
412
+ const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`;
413
+ let moved = false;
414
+ try {
415
+ const handle = await open(temporary, 'wx', 0o600);
416
+ try {
417
+ await handle.writeFile(`${JSON.stringify(raw, null, 2)}\n`, 'utf8');
418
+ await handle.sync();
419
+ }
420
+ finally {
421
+ await handle.close();
422
+ }
423
+ const nextConfig = await loadAgentdConfig(temporary);
424
+ const workspacePolicy = await WorkspacePolicy.create(nextConfig.workspaceRoots);
425
+ for (const agent of Object.values(nextConfig.agents)) {
426
+ if (agent.protocol === 'a2a')
427
+ continue;
428
+ const workspace = agent.workspace || nextConfig.workspaceRoots[0];
429
+ if (workspace)
430
+ await workspacePolicy.resolve(workspace);
431
+ }
432
+ const drivers = createDriverRegistry(nextConfig);
433
+ await rename(temporary, target);
434
+ moved = true;
435
+ await chmod(target, 0o600).catch(() => undefined);
436
+ this.config = nextConfig;
437
+ this.sessions.reconfigure(nextConfig, workspacePolicy, drivers);
438
+ }
439
+ finally {
440
+ if (!moved)
441
+ await rm(temporary, { force: true }).catch(() => undefined);
442
+ }
443
+ }
444
+ agentView(id, config) {
445
+ if (config.protocol === 'a2a') {
446
+ return {
447
+ id,
448
+ protocol: 'a2a',
449
+ name: config.name || id,
450
+ description: config.description,
451
+ enabled: config.enabled !== false,
452
+ agentCardUrl: config.agentCardUrl || defaultAgentCardUrl(config.agentUrl),
453
+ agentUrl: config.agentUrl,
454
+ preferredTransport: config.preferredTransport || 'auto',
455
+ auth: {
456
+ type: config.auth?.type || 'none',
457
+ headerName: config.auth?.headerName,
458
+ configured: Boolean(config.auth?.value)
459
+ },
460
+ timeoutMs: config.timeoutMs || 60_000
461
+ };
462
+ }
463
+ return {
464
+ id,
465
+ protocol: 'acp',
466
+ driver: config.driver,
467
+ name: config.name || id,
468
+ description: config.description,
469
+ enabled: config.enabled !== false,
470
+ workspace: config.workspace || this.config.workspaceRoots[0] || '',
471
+ permissionPolicy: config.permissionPolicy || 'ask',
472
+ permissionTimeoutMs: config.permissionTimeoutMs || 15 * 60 * 1000
473
+ };
474
+ }
475
+ exclusive(task) {
476
+ const result = this.queue.then(task, task);
477
+ this.queue = result.then(() => undefined, () => undefined);
478
+ return result;
479
+ }
480
+ }
481
+ export class ControlPlaneError extends Error {
482
+ status;
483
+ constructor(status, message) {
484
+ super(message);
485
+ this.status = status;
486
+ }
487
+ }
488
+ function validateRuntimeSettings(update) {
489
+ validateIntegerRange(update.sessionTtlMs, 60_000, 30 * 24 * 60 * 60 * 1000, 'sessionTtlMs');
490
+ validateIntegerRange(update.promptTimeoutMs, 10_000, 24 * 60 * 60 * 1000, 'promptTimeoutMs');
491
+ validateIntegerRange(update.cleanupIntervalMs, 5_000, 60 * 60 * 1000, 'cleanupIntervalMs');
492
+ }
493
+ function validateIntegerRange(value, min, max, name) {
494
+ if (!Number.isInteger(value) || value < min || value > max) {
495
+ throw new ControlPlaneError(400, `${name} must be between ${min} and ${max}`);
496
+ }
497
+ }
498
+ function validateUpdate(update) {
499
+ if (update.protocol !== undefined && update.protocol !== 'acp' && update.protocol !== 'a2a') {
500
+ throw new ControlPlaneError(400, 'protocol must be acp or a2a');
501
+ }
502
+ if (update.permissionPolicy !== undefined &&
503
+ update.permissionPolicy !== 'ask' &&
504
+ update.permissionPolicy !== 'deny') {
505
+ throw new ControlPlaneError(400, 'permissionPolicy must be ask or deny');
506
+ }
507
+ if (update.preferredTransport !== undefined &&
508
+ !['auto', 'jsonrpc', 'http-json'].includes(update.preferredTransport)) {
509
+ throw new ControlPlaneError(400, 'preferredTransport must be auto, jsonrpc, or http-json');
510
+ }
511
+ }
512
+ function validateAgentId(value) {
513
+ const id = decodeURIComponent(value).trim().toLowerCase();
514
+ if (!/^[a-z0-9][a-z0-9._-]{0,63}$/.test(id)) {
515
+ throw new ControlPlaneError(400, 'Invalid Agent ID');
516
+ }
517
+ return id;
518
+ }
519
+ function validateOpaqueId(value, label) {
520
+ const id = decodeURIComponent(value).trim();
521
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(id)) {
522
+ throw new ControlPlaneError(400, `Invalid ${label} ID`);
523
+ }
524
+ return id;
525
+ }
526
+ function validatePassword(value, confirmation) {
527
+ if (value !== confirmation) {
528
+ throw new ControlPlaneError(400, 'Console Password confirmation does not match');
529
+ }
530
+ try {
531
+ return validateAdminPassword(value);
532
+ }
533
+ catch (error) {
534
+ throw new ControlPlaneError(400, errorMessage(error));
535
+ }
536
+ }
537
+ function validateKeyName(value) {
538
+ const name = cleanString(value);
539
+ if (!name)
540
+ throw new ControlPlaneError(400, 'API Key name is required');
541
+ if (name.length > 80)
542
+ throw new ControlPlaneError(400, 'API Key name is too long');
543
+ return name;
544
+ }
545
+ function validateApiKeySecret(value) {
546
+ const secret = cleanString(value);
547
+ if (secret.length < 16) {
548
+ throw new ControlPlaneError(400, 'Custom API Key must contain at least 16 characters');
549
+ }
550
+ if (Buffer.byteLength(secret, 'utf8') > 512) {
551
+ throw new ControlPlaneError(400, 'Custom API Key must not exceed 512 bytes');
552
+ }
553
+ if (/^env:/i.test(secret) || /[\u0000-\u001f\u007f]/.test(secret)) {
554
+ throw new ControlPlaneError(400, 'Custom API Key contains an unsupported value');
555
+ }
556
+ return secret;
557
+ }
558
+ function validateA2ASecret(value) {
559
+ const secret = cleanString(value);
560
+ if (!secret)
561
+ return '';
562
+ if (/^env:/i.test(secret) ||
563
+ Buffer.byteLength(secret, 'utf8') > 4096 ||
564
+ /[\u0000\r\n]/.test(secret)) {
565
+ throw new ControlPlaneError(400, 'A2A authentication value is invalid');
566
+ }
567
+ return secret;
568
+ }
569
+ function validateAgentUrl(value) {
570
+ let url;
571
+ try {
572
+ url = new URL(value);
573
+ }
574
+ catch {
575
+ throw new ControlPlaneError(400, 'A2A Agent Card URL is invalid');
576
+ }
577
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.hash) {
578
+ throw new ControlPlaneError(400, 'A2A Agent Card URL must use http(s) without credentials or a fragment');
579
+ }
580
+ return url.toString().replace(/\/$/, '');
581
+ }
582
+ function defaultAgentCardUrl(agentUrl) {
583
+ if (!agentUrl)
584
+ return undefined;
585
+ return new URL('/.well-known/agent-card.json', agentUrl).toString();
586
+ }
587
+ function clampTimeout(value, fallback, min, max) {
588
+ if (value === undefined)
589
+ return fallback;
590
+ if (!Number.isInteger(value) || value < min || value > max) {
591
+ throw new ControlPlaneError(400, `timeout must be between ${min} and ${max}`);
592
+ }
593
+ return value;
594
+ }
595
+ function preservedAcpAdvanced(previous) {
596
+ const result = {};
597
+ for (const key of ['command', 'args', 'inheritEnv', 'env']) {
598
+ if (previous[key] !== undefined)
599
+ result[key] = structuredClone(previous[key]);
600
+ }
601
+ return result;
602
+ }
603
+ function setOptionalString(target, key, value) {
604
+ const text = cleanString(value);
605
+ if (text)
606
+ target[key] = text;
607
+ else
608
+ delete target[key];
609
+ }
610
+ function validHeaderName(value) {
611
+ return /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(value);
612
+ }
613
+ function safeSecretEqual(left, right) {
614
+ const leftBuffer = Buffer.from(left);
615
+ const rightBuffer = Buffer.from(right);
616
+ return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
617
+ }
618
+ function cleanString(value) {
619
+ return typeof value === 'string' ? value.trim() : '';
620
+ }
621
+ function recordValue(value) {
622
+ return isRecord(value) ? { ...value } : {};
623
+ }
624
+ function isRecord(value) {
625
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
626
+ }
627
+ function errorMessage(error) {
628
+ return error instanceof Error ? error.message : String(error);
629
+ }
@@ -0,0 +1,2 @@
1
+ import type { AgentdDriverConfig } from '../types.js';
2
+ export declare function createHermesDriver(id: string, config: AgentdDriverConfig): import("./types.js").AgentDriver;
@@ -0,0 +1,10 @@
1
+ import { createStdioAcpDriver } from './stdio.js';
2
+ export function createHermesDriver(id, config) {
3
+ return createStdioAcpDriver(id, config, {
4
+ name: 'Hermes Agent',
5
+ description: 'Hermes Agent through its native ACP stdio server',
6
+ command: 'hermes',
7
+ args: ['acp'],
8
+ probeArgs: ['acp', '--check']
9
+ });
10
+ }
@@ -1,11 +1,14 @@
1
1
  import { createClaudeDriver } from './claude.js';
2
2
  import { createCodexDriver } from './codex.js';
3
+ import { createHermesDriver } from './hermes.js';
3
4
  import { createOpenCodeDriver } from './opencode.js';
4
5
  import { createOpenClawDriver } from './openclaw.js';
5
6
  import { createPiDriver } from './pi.js';
6
7
  export function createDriverRegistry(config) {
7
8
  const drivers = new Map();
8
9
  for (const [id, driver] of Object.entries(config.agents)) {
10
+ if (driver.protocol === 'a2a')
11
+ continue;
9
12
  if (driver.enabled === false)
10
13
  continue;
11
14
  switch (driver.driver) {
@@ -24,6 +27,9 @@ export function createDriverRegistry(config) {
24
27
  case 'openclaw':
25
28
  drivers.set(id, createOpenClawDriver(id, driver));
26
29
  break;
30
+ case 'hermes':
31
+ drivers.set(id, createHermesDriver(id, driver));
32
+ break;
27
33
  }
28
34
  }
29
35
  return drivers;
package/dist/events.js CHANGED
@@ -20,8 +20,19 @@ export class SessionEventLog {
20
20
  if (this.events.length > this.maxEvents) {
21
21
  this.events.splice(0, this.events.length - this.maxEvents);
22
22
  }
23
- for (const listener of this.listeners)
24
- listener(structuredClone(event));
23
+ for (const listener of this.listeners) {
24
+ try {
25
+ listener(structuredClone(event));
26
+ }
27
+ catch (error) {
28
+ console.error(JSON.stringify({
29
+ level: 'error',
30
+ event: 'session_event_listener_failed',
31
+ sessionId: this.sessionId,
32
+ message: error instanceof Error ? error.message : String(error)
33
+ }));
34
+ }
35
+ }
25
36
  return event;
26
37
  }
27
38
  after(id) {