nexus-agentd 0.2.6 → 0.3.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/LICENSE +22 -22
- package/README.md +103 -14
- package/dist/a2a/runtime.d.ts +4 -1
- package/dist/a2a/runtime.js +213 -10
- package/dist/acp/runtime.js +13 -6
- package/dist/artifact-store.d.ts +162 -0
- package/dist/artifact-store.js +1081 -0
- package/dist/cli.js +13 -14
- package/dist/config.js +45 -5
- package/dist/control-plane-validation.d.ts +54 -0
- package/dist/control-plane-validation.js +152 -0
- package/dist/control-plane.d.ts +9 -32
- package/dist/control-plane.js +41 -157
- package/dist/drivers/index.js +12 -0
- package/dist/drivers/pi.js +1 -1
- package/dist/drivers/stdio.d.ts +4 -1
- package/dist/drivers/stdio.js +135 -19
- package/dist/events.d.ts +9 -0
- package/dist/events.js +17 -4
- package/dist/index.js +10 -1
- package/dist/process-tree.js +15 -6
- package/dist/run-store.d.ts +52 -7
- package/dist/run-store.js +178 -22
- package/dist/server/auth-policy.d.ts +27 -0
- package/dist/server/auth-policy.js +143 -0
- package/dist/server/connection.d.ts +8 -0
- package/dist/server/connection.js +67 -0
- package/dist/server/diagnostics.d.ts +40 -0
- package/dist/server/diagnostics.js +130 -0
- package/dist/server/metrics.d.ts +51 -0
- package/dist/server/metrics.js +56 -0
- package/dist/server/quota.d.ts +59 -0
- package/dist/server/quota.js +176 -0
- package/dist/server/validation.d.ts +29 -0
- package/dist/server/validation.js +221 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +682 -418
- package/dist/session/admin-runs.d.ts +73 -0
- package/dist/session/admin-runs.js +297 -0
- package/dist/session/admission.d.ts +32 -0
- package/dist/session/admission.js +62 -0
- package/dist/session/readiness.d.ts +33 -0
- package/dist/session/readiness.js +174 -0
- package/dist/session.d.ts +58 -6
- package/dist/session.js +244 -109
- package/dist/sse-writer.d.ts +7 -0
- package/dist/sse-writer.js +60 -0
- package/dist/types.d.ts +44 -2
- package/dist/types.js +1 -0
- package/dist/webui/app/sources.js +16 -8
- package/dist/webui/index.js +11 -11
- package/dist/webui/markup.js +2 -0
- package/dist/webui/styles.js +46 -1
- package/examples/client.mjs +102 -0
- package/nexus-agentd.example.json +53 -34
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -18,22 +18,21 @@ const label = typeof address === 'object' && address
|
|
|
18
18
|
console.log(`nexus-agentd listening on ${label}`);
|
|
19
19
|
for (const url of displayUrls(address))
|
|
20
20
|
console.log(`WebUI: ${url}`);
|
|
21
|
-
if (bootstrap.created)
|
|
21
|
+
if (bootstrap.created)
|
|
22
22
|
console.log(`Created config: ${configPath}`);
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
if (runtime.controlPlane.setupToken) {
|
|
24
|
+
console.log('Setup required: enter the following one-time setup token in the WebUI, then create the Console Password.');
|
|
25
|
+
console.log(`Setup token: ${runtime.controlPlane.setupToken}`);
|
|
26
|
+
console.warn('Keep this token private. It changes on restart. Use a trusted network or HTTPS for remote setup.');
|
|
27
27
|
}
|
|
28
|
-
let closing
|
|
29
|
-
const close =
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
process.once('
|
|
36
|
-
process.once('SIGTERM', () => void close().finally(() => process.exit(0)));
|
|
28
|
+
let closing;
|
|
29
|
+
const close = () => closing ??= runtime.close();
|
|
30
|
+
const shutdown = () => void close().then(() => process.exit(0), (error) => {
|
|
31
|
+
console.error(JSON.stringify({ level: 'error', event: 'shutdown_failed', message: error instanceof Error ? error.message : String(error) }));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
});
|
|
34
|
+
process.once('SIGINT', shutdown);
|
|
35
|
+
process.once('SIGTERM', shutdown);
|
|
37
36
|
function resolveConfigPath(args) {
|
|
38
37
|
const index = args.indexOf('--config');
|
|
39
38
|
const value = index >= 0 ? args[index + 1] : process.env.NEXUS_AGENTD_CONFIG;
|
package/dist/config.js
CHANGED
|
@@ -13,7 +13,7 @@ export async function ensureAgentdConfig(filePath, options = {}) {
|
|
|
13
13
|
}
|
|
14
14
|
const initial = {
|
|
15
15
|
listen: {
|
|
16
|
-
host: options.host?.trim() || '
|
|
16
|
+
host: options.host?.trim() || '0.0.0.0',
|
|
17
17
|
port: bootstrapNumber(options.port, 8787, 1, 65535)
|
|
18
18
|
},
|
|
19
19
|
initialized: false,
|
|
@@ -56,7 +56,7 @@ export async function loadAgentdConfig(filePath) {
|
|
|
56
56
|
if (!isRecord(value))
|
|
57
57
|
throw new Error('nexus-agentd config root must be an object');
|
|
58
58
|
const listen = optionalRecord(value.listen, 'listen') || {};
|
|
59
|
-
const host = optionalString(listen.host, 'listen.host') || '
|
|
59
|
+
const host = optionalString(listen.host, 'listen.host') || '0.0.0.0';
|
|
60
60
|
const port = integerValue(listen.port, 'listen.port', 8787, 1, 65535);
|
|
61
61
|
const initialized = optionalBoolean(value.initialized, 'initialized') ?? true;
|
|
62
62
|
const rawAuthToken = optionalString(value.authToken, 'authToken');
|
|
@@ -84,17 +84,45 @@ export async function loadAgentdConfig(filePath) {
|
|
|
84
84
|
throw new Error('nexus-agentd workspaceRoots must contain at least one path');
|
|
85
85
|
}
|
|
86
86
|
const rawAgents = requiredRecord(value.agents, 'agents');
|
|
87
|
-
const agents =
|
|
87
|
+
const agents = Object.create(null);
|
|
88
88
|
for (const [id, input] of Object.entries(rawAgents)) {
|
|
89
89
|
agents[id] = parseAgent(id, input);
|
|
90
90
|
}
|
|
91
|
+
const quotas = optionalRecord(value.quotas, 'quotas') || {};
|
|
92
|
+
const history = optionalRecord(value.history, 'history') || {};
|
|
93
|
+
const artifacts = optionalRecord(value.artifacts, 'artifacts') || {};
|
|
94
|
+
const maxSessions = integerValue(value.maxSessions, 'maxSessions', 64, 1, 10_000);
|
|
95
|
+
const maxSseConnections = integerValue(value.maxSseConnections, 'maxSseConnections', 128, 1, 10_000);
|
|
91
96
|
return {
|
|
92
97
|
listen: { host, port },
|
|
98
|
+
publicOrigins: value.publicOrigins === undefined ? [] : stringArray(value.publicOrigins, 'publicOrigins').map((origin) => {
|
|
99
|
+
validateHttpUrl(origin, 'publicOrigins');
|
|
100
|
+
if (new URL(origin).origin !== origin)
|
|
101
|
+
throw new Error('publicOrigins must contain exact origins without paths');
|
|
102
|
+
return origin;
|
|
103
|
+
}),
|
|
93
104
|
initialized,
|
|
94
105
|
authToken,
|
|
95
106
|
adminPasswordHash,
|
|
96
107
|
apiKeys,
|
|
97
108
|
workspaceRoots,
|
|
109
|
+
quotas: {
|
|
110
|
+
maxSessionsPerKey: integerValue(quotas.maxSessionsPerKey, 'quotas.maxSessionsPerKey', Math.min(16, maxSessions), 1, 10_000),
|
|
111
|
+
maxRunningRunsPerKey: integerValue(quotas.maxRunningRunsPerKey, 'quotas.maxRunningRunsPerKey', Math.min(4, maxSessions), 1, 10_000),
|
|
112
|
+
maxSsePerKey: integerValue(quotas.maxSsePerKey, 'quotas.maxSsePerKey', Math.min(8, maxSseConnections), 1, 10_000),
|
|
113
|
+
maxUploadBytesPerKey: integerValue(quotas.maxUploadBytesPerKey, 'quotas.maxUploadBytesPerKey', 32 * 1024 * 1024, 1024, 1024 * 1024 * 1024)
|
|
114
|
+
},
|
|
115
|
+
history: {
|
|
116
|
+
maxRuns: integerValue(history.maxRuns, 'history.maxRuns', 1000, 1, 100_000),
|
|
117
|
+
retentionDays: integerValue(history.retentionDays, 'history.retentionDays', 30, 1, 3650),
|
|
118
|
+
maxBytes: integerValue(history.maxBytes, 'history.maxBytes', 64 * 1024 * 1024, 1024, 1024 * 1024 * 1024)
|
|
119
|
+
},
|
|
120
|
+
artifacts: {
|
|
121
|
+
retentionDays: integerValue(artifacts.retentionDays, 'artifacts.retentionDays', 30, 1, 3650),
|
|
122
|
+
maxBytes: integerValue(artifacts.maxBytes, 'artifacts.maxBytes', 512 * 1024 * 1024, 1024, 1024 ** 4),
|
|
123
|
+
maxArtifactBytes: integerValue(artifacts.maxArtifactBytes, 'artifacts.maxArtifactBytes', 12 * 1024 * 1024, 1024, 12 * 1024 * 1024),
|
|
124
|
+
maxQueuedBytes: integerValue(artifacts.maxQueuedBytes, 'artifacts.maxQueuedBytes', 64 * 1024 * 1024, 1024, 512 * 1024 * 1024)
|
|
125
|
+
},
|
|
98
126
|
maxRequestBytes: integerValue(value.maxRequestBytes, 'maxRequestBytes', 1024 * 1024, 1024, 16 * 1024 * 1024),
|
|
99
127
|
maxAttachmentBytes: integerValue(value.maxAttachmentBytes, 'maxAttachmentBytes', 32 * 1024 * 1024, 1024, 64 * 1024 * 1024),
|
|
100
128
|
maxEventsPerSession: integerValue(value.maxEventsPerSession, 'maxEventsPerSession', 2048, 64, 100_000),
|
|
@@ -103,7 +131,7 @@ export async function loadAgentdConfig(filePath) {
|
|
|
103
131
|
cleanupIntervalMs: integerValue(value.cleanupIntervalMs, 'cleanupIntervalMs', 60_000, 5_000, 60 * 60 * 1000),
|
|
104
132
|
requestTimeoutMs: integerValue(value.requestTimeoutMs, 'requestTimeoutMs', 30_000, 1_000, 5 * 60_000),
|
|
105
133
|
promptTimeoutMs: integerValue(value.promptTimeoutMs, 'promptTimeoutMs', 30 * 60_000, 10_000, 24 * 60 * 60 * 1000),
|
|
106
|
-
maxSessions
|
|
134
|
+
maxSessions,
|
|
107
135
|
maxSseConnections: integerValue(value.maxSseConnections, 'maxSseConnections', 128, 1, 10_000),
|
|
108
136
|
maxConnections: integerValue(value.maxConnections, 'maxConnections', 256, 8, 100_000),
|
|
109
137
|
adminSessionTtlMs: integerValue(value.adminSessionTtlMs, 'adminSessionTtlMs', 12 * 60 * 60 * 1000, 5 * 60_000, 7 * 24 * 60 * 60 * 1000),
|
|
@@ -150,6 +178,9 @@ function parseAgent(id, value) {
|
|
|
150
178
|
args: input.args === undefined
|
|
151
179
|
? undefined
|
|
152
180
|
: stringArray(input.args, `agents.${id}.args`),
|
|
181
|
+
probeArgs: input.probeArgs === undefined
|
|
182
|
+
? undefined
|
|
183
|
+
: stringArray(input.probeArgs, `agents.${id}.probeArgs`),
|
|
153
184
|
inheritEnv: input.inheritEnv === undefined
|
|
154
185
|
? undefined
|
|
155
186
|
: stringArray(input.inheritEnv, `agents.${id}.inheritEnv`),
|
|
@@ -157,6 +188,9 @@ function parseAgent(id, value) {
|
|
|
157
188
|
permissionPolicy,
|
|
158
189
|
permissionTimeoutMs: integerValue(input.permissionTimeoutMs, `agents.${id}.permissionTimeoutMs`, 15 * 60 * 1000, 1000, 24 * 60 * 60 * 1000)
|
|
159
190
|
};
|
|
191
|
+
if (driver === 'stdio' && !result.command) {
|
|
192
|
+
throw new Error(`nexus-agentd agents.${id}.command is required for the stdio driver`);
|
|
193
|
+
}
|
|
160
194
|
return result;
|
|
161
195
|
}
|
|
162
196
|
function parseA2AAgent(id, input) {
|
|
@@ -204,7 +238,13 @@ function parseA2AAgent(id, input) {
|
|
|
204
238
|
agentUrl,
|
|
205
239
|
preferredTransport,
|
|
206
240
|
auth,
|
|
207
|
-
timeoutMs: integerValue(input.timeoutMs, `agents.${id}.timeoutMs`, 60_000, 1_000, 30 * 60_000)
|
|
241
|
+
timeoutMs: integerValue(input.timeoutMs, `agents.${id}.timeoutMs`, 60_000, 1_000, 30 * 60_000),
|
|
242
|
+
taskTimeoutMs: input.taskTimeoutMs === undefined
|
|
243
|
+
? undefined
|
|
244
|
+
: integerValue(input.taskTimeoutMs, `agents.${id}.taskTimeoutMs`, 30 * 60_000, 10_000, 24 * 60 * 60 * 1000),
|
|
245
|
+
streamIdleTimeoutMs: input.streamIdleTimeoutMs === undefined
|
|
246
|
+
? undefined
|
|
247
|
+
: integerValue(input.streamIdleTimeoutMs, `agents.${id}.streamIdleTimeoutMs`, 60_000, 1_000, 30 * 60_000)
|
|
208
248
|
};
|
|
209
249
|
}
|
|
210
250
|
function parseApiKeys(value) {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { AgentdProtocol, AgentdDriverKind, PermissionPolicy, A2ATransportPreference, A2AAuthType, AgentdApiKeyScope } from './types.js';
|
|
2
|
+
export interface AgentdAgentUpdate {
|
|
3
|
+
protocol?: AgentdProtocol;
|
|
4
|
+
driver?: AgentdDriverKind;
|
|
5
|
+
name?: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
workspace?: string;
|
|
9
|
+
permissionPolicy?: PermissionPolicy;
|
|
10
|
+
permissionTimeoutMs?: number;
|
|
11
|
+
agentCardUrl?: string;
|
|
12
|
+
agentUrl?: string;
|
|
13
|
+
preferredTransport?: A2ATransportPreference;
|
|
14
|
+
authType?: A2AAuthType;
|
|
15
|
+
authValue?: string;
|
|
16
|
+
authHeaderName?: string;
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
taskTimeoutMs?: number | null;
|
|
19
|
+
streamIdleTimeoutMs?: number | null;
|
|
20
|
+
}
|
|
21
|
+
export interface AgentdApiKeyUpdate {
|
|
22
|
+
name?: string;
|
|
23
|
+
enabled?: boolean;
|
|
24
|
+
scope?: AgentdApiKeyScope;
|
|
25
|
+
}
|
|
26
|
+
export interface AgentdRuntimeSettingsUpdate {
|
|
27
|
+
sessionTtlMs: number;
|
|
28
|
+
promptTimeoutMs: number;
|
|
29
|
+
cleanupIntervalMs: number;
|
|
30
|
+
}
|
|
31
|
+
export declare class ControlPlaneError extends Error {
|
|
32
|
+
readonly status: number;
|
|
33
|
+
constructor(status: number, message: string);
|
|
34
|
+
}
|
|
35
|
+
export declare function validateRuntimeSettings(update: AgentdRuntimeSettingsUpdate): void;
|
|
36
|
+
export declare function validateIntegerRange(value: number, min: number, max: number, name: string): void;
|
|
37
|
+
export declare function validateUpdate(update: AgentdAgentUpdate): void;
|
|
38
|
+
export declare function validateAgentId(value: string): string;
|
|
39
|
+
export declare function validateOpaqueId(value: string, label: string): string;
|
|
40
|
+
export declare function validatePassword(value: string, confirmation: string): string;
|
|
41
|
+
export declare function validateKeyName(value: string): string;
|
|
42
|
+
export declare function validateApiKeySecret(value: string): string;
|
|
43
|
+
export declare function validateA2ASecret(value: string): string;
|
|
44
|
+
export declare function validateAgentUrl(value: string): string;
|
|
45
|
+
export declare function defaultAgentCardUrl(agentUrl: string | undefined): string | undefined;
|
|
46
|
+
export declare function clampTimeout(value: number | undefined, fallback: number, min: number, max: number): number;
|
|
47
|
+
export declare function preservedAcpAdvanced(previous: Record<string, unknown>): Record<string, unknown>;
|
|
48
|
+
export declare function setOptionalString(target: Record<string, unknown>, key: string, value: string | undefined): void;
|
|
49
|
+
export declare function validHeaderName(value: string): boolean;
|
|
50
|
+
export declare function safeSecretEqual(left: string, right: string): boolean;
|
|
51
|
+
export declare function cleanString(value: unknown): string;
|
|
52
|
+
export declare function recordValue(value: unknown): Record<string, any>;
|
|
53
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
54
|
+
export declare function errorMessage(error: unknown): string;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
2
|
+
import { validateAdminPassword } from './auth.js';
|
|
3
|
+
export class ControlPlaneError extends Error {
|
|
4
|
+
status;
|
|
5
|
+
constructor(status, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.status = status;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function validateRuntimeSettings(update) {
|
|
11
|
+
validateIntegerRange(update.sessionTtlMs, 60_000, 30 * 24 * 60 * 60 * 1000, 'sessionTtlMs');
|
|
12
|
+
validateIntegerRange(update.promptTimeoutMs, 10_000, 24 * 60 * 60 * 1000, 'promptTimeoutMs');
|
|
13
|
+
validateIntegerRange(update.cleanupIntervalMs, 5_000, 60 * 60 * 1000, 'cleanupIntervalMs');
|
|
14
|
+
}
|
|
15
|
+
export function validateIntegerRange(value, min, max, name) {
|
|
16
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
17
|
+
throw new ControlPlaneError(400, `${name} must be between ${min} and ${max}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function validateUpdate(update) {
|
|
21
|
+
if (update.protocol !== undefined && update.protocol !== 'acp' && update.protocol !== 'a2a') {
|
|
22
|
+
throw new ControlPlaneError(400, 'protocol must be acp or a2a');
|
|
23
|
+
}
|
|
24
|
+
if (update.permissionPolicy !== undefined &&
|
|
25
|
+
update.permissionPolicy !== 'ask' &&
|
|
26
|
+
update.permissionPolicy !== 'allow' &&
|
|
27
|
+
update.permissionPolicy !== 'deny') {
|
|
28
|
+
throw new ControlPlaneError(400, 'permissionPolicy must be ask, allow, or deny');
|
|
29
|
+
}
|
|
30
|
+
if (update.preferredTransport !== undefined &&
|
|
31
|
+
!['auto', 'jsonrpc', 'http-json'].includes(update.preferredTransport)) {
|
|
32
|
+
throw new ControlPlaneError(400, 'preferredTransport must be auto, jsonrpc, or http-json');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function validateAgentId(value) {
|
|
36
|
+
const id = decodeURIComponent(value).trim().toLowerCase();
|
|
37
|
+
if (!/^[a-z0-9][a-z0-9._-]{0,63}$/.test(id)) {
|
|
38
|
+
throw new ControlPlaneError(400, 'Invalid Agent ID');
|
|
39
|
+
}
|
|
40
|
+
return id;
|
|
41
|
+
}
|
|
42
|
+
export function validateOpaqueId(value, label) {
|
|
43
|
+
const id = decodeURIComponent(value).trim();
|
|
44
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(id)) {
|
|
45
|
+
throw new ControlPlaneError(400, `Invalid ${label} ID`);
|
|
46
|
+
}
|
|
47
|
+
return id;
|
|
48
|
+
}
|
|
49
|
+
export function validatePassword(value, confirmation) {
|
|
50
|
+
if (value !== confirmation) {
|
|
51
|
+
throw new ControlPlaneError(400, 'Console Password confirmation does not match');
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
return validateAdminPassword(value);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
throw new ControlPlaneError(400, errorMessage(error));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export function validateKeyName(value) {
|
|
61
|
+
const name = cleanString(value);
|
|
62
|
+
if (!name)
|
|
63
|
+
throw new ControlPlaneError(400, 'API Key name is required');
|
|
64
|
+
if (name.length > 80)
|
|
65
|
+
throw new ControlPlaneError(400, 'API Key name is too long');
|
|
66
|
+
return name;
|
|
67
|
+
}
|
|
68
|
+
export function validateApiKeySecret(value) {
|
|
69
|
+
const secret = cleanString(value);
|
|
70
|
+
if (secret.length < 16) {
|
|
71
|
+
throw new ControlPlaneError(400, 'Custom API Key must contain at least 16 characters');
|
|
72
|
+
}
|
|
73
|
+
if (Buffer.byteLength(secret, 'utf8') > 512) {
|
|
74
|
+
throw new ControlPlaneError(400, 'Custom API Key must not exceed 512 bytes');
|
|
75
|
+
}
|
|
76
|
+
if (/^env:/i.test(secret) || /[\u0000-\u001f\u007f]/.test(secret)) {
|
|
77
|
+
throw new ControlPlaneError(400, 'Custom API Key contains an unsupported value');
|
|
78
|
+
}
|
|
79
|
+
return secret;
|
|
80
|
+
}
|
|
81
|
+
export function validateA2ASecret(value) {
|
|
82
|
+
const secret = cleanString(value);
|
|
83
|
+
if (!secret)
|
|
84
|
+
return '';
|
|
85
|
+
if (/^env:/i.test(secret) ||
|
|
86
|
+
Buffer.byteLength(secret, 'utf8') > 4096 ||
|
|
87
|
+
/[\u0000\r\n]/.test(secret)) {
|
|
88
|
+
throw new ControlPlaneError(400, 'A2A authentication value is invalid');
|
|
89
|
+
}
|
|
90
|
+
return secret;
|
|
91
|
+
}
|
|
92
|
+
export function validateAgentUrl(value) {
|
|
93
|
+
let url;
|
|
94
|
+
try {
|
|
95
|
+
url = new URL(value);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
throw new ControlPlaneError(400, 'A2A Agent Card URL is invalid');
|
|
99
|
+
}
|
|
100
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.hash) {
|
|
101
|
+
throw new ControlPlaneError(400, 'A2A Agent Card URL must use http(s) without credentials or a fragment');
|
|
102
|
+
}
|
|
103
|
+
return url.toString().replace(/\/$/, '');
|
|
104
|
+
}
|
|
105
|
+
export function defaultAgentCardUrl(agentUrl) {
|
|
106
|
+
if (!agentUrl)
|
|
107
|
+
return undefined;
|
|
108
|
+
return new URL('/.well-known/agent-card.json', agentUrl).toString();
|
|
109
|
+
}
|
|
110
|
+
export function clampTimeout(value, fallback, min, max) {
|
|
111
|
+
if (value === undefined)
|
|
112
|
+
return fallback;
|
|
113
|
+
if (!Number.isInteger(value) || value < min || value > max) {
|
|
114
|
+
throw new ControlPlaneError(400, `timeout must be between ${min} and ${max}`);
|
|
115
|
+
}
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
export function preservedAcpAdvanced(previous) {
|
|
119
|
+
const result = {};
|
|
120
|
+
for (const key of ['command', 'args', 'probeArgs', 'inheritEnv', 'env']) {
|
|
121
|
+
if (previous[key] !== undefined)
|
|
122
|
+
result[key] = structuredClone(previous[key]);
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
export function setOptionalString(target, key, value) {
|
|
127
|
+
const text = cleanString(value);
|
|
128
|
+
if (text)
|
|
129
|
+
target[key] = text;
|
|
130
|
+
else
|
|
131
|
+
delete target[key];
|
|
132
|
+
}
|
|
133
|
+
export function validHeaderName(value) {
|
|
134
|
+
return /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(value);
|
|
135
|
+
}
|
|
136
|
+
export function safeSecretEqual(left, right) {
|
|
137
|
+
const leftBuffer = Buffer.from(left);
|
|
138
|
+
const rightBuffer = Buffer.from(right);
|
|
139
|
+
return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
|
|
140
|
+
}
|
|
141
|
+
export function cleanString(value) {
|
|
142
|
+
return typeof value === 'string' ? value.trim() : '';
|
|
143
|
+
}
|
|
144
|
+
export function recordValue(value) {
|
|
145
|
+
return isRecord(value) ? { ...value } : {};
|
|
146
|
+
}
|
|
147
|
+
export function isRecord(value) {
|
|
148
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
149
|
+
}
|
|
150
|
+
export function errorMessage(error) {
|
|
151
|
+
return error instanceof Error ? error.message : String(error);
|
|
152
|
+
}
|
package/dist/control-plane.d.ts
CHANGED
|
@@ -1,39 +1,20 @@
|
|
|
1
|
+
import { type AgentdAgentUpdate, type AgentdApiKeyUpdate, type AgentdRuntimeSettingsUpdate } from './control-plane-validation.js';
|
|
2
|
+
export { ControlPlaneError, type AgentdAgentUpdate, type AgentdApiKeyUpdate, type AgentdRuntimeSettingsUpdate } from './control-plane-validation.js';
|
|
1
3
|
import type { SessionManager } from './session.js';
|
|
2
|
-
import { type
|
|
3
|
-
export interface AgentdAgentUpdate {
|
|
4
|
-
protocol?: AgentdProtocol;
|
|
5
|
-
driver?: AgentdDriverKind;
|
|
6
|
-
name?: string;
|
|
7
|
-
description?: string;
|
|
8
|
-
enabled?: boolean;
|
|
9
|
-
workspace?: string;
|
|
10
|
-
permissionPolicy?: PermissionPolicy;
|
|
11
|
-
permissionTimeoutMs?: number;
|
|
12
|
-
agentCardUrl?: string;
|
|
13
|
-
agentUrl?: string;
|
|
14
|
-
preferredTransport?: A2ATransportPreference;
|
|
15
|
-
authType?: A2AAuthType;
|
|
16
|
-
authValue?: string;
|
|
17
|
-
authHeaderName?: string;
|
|
18
|
-
timeoutMs?: number;
|
|
19
|
-
}
|
|
20
|
-
export interface AgentdApiKeyUpdate {
|
|
21
|
-
name?: string;
|
|
22
|
-
enabled?: boolean;
|
|
23
|
-
scope?: AgentdApiKeyScope;
|
|
24
|
-
}
|
|
25
|
-
export interface AgentdRuntimeSettingsUpdate {
|
|
26
|
-
sessionTtlMs: number;
|
|
27
|
-
promptTimeoutMs: number;
|
|
28
|
-
cleanupIntervalMs: number;
|
|
29
|
-
}
|
|
4
|
+
import { type AgentdApiKeyPrincipal, type AgentdApiKeyScope, type AgentdApiKeyView, type AgentdConfig, type AgentdControlPlaneView } from './types.js';
|
|
30
5
|
export declare class AgentdControlPlane {
|
|
31
6
|
private readonly configPath;
|
|
32
7
|
private config;
|
|
33
8
|
private readonly sessions;
|
|
9
|
+
/** Process-local proof, delivered only through the local CLI / embedding API. */
|
|
10
|
+
setupToken?: string;
|
|
34
11
|
private queue;
|
|
35
12
|
private usageTimer?;
|
|
13
|
+
private readonly keyWatchers;
|
|
14
|
+
/** Observe authorization without updating last-used time or retaining a stale scope. */
|
|
15
|
+
watchApiKey(keyId: string, secret: string, agentId: string, revoked: () => void): () => void;
|
|
36
16
|
constructor(configPath: string, config: AgentdConfig, sessions: SessionManager);
|
|
17
|
+
verifySetupToken(token: string): boolean;
|
|
37
18
|
snapshot(): AgentdControlPlaneView;
|
|
38
19
|
isInitialized(): boolean;
|
|
39
20
|
needsAdminSetup(): boolean;
|
|
@@ -85,7 +66,3 @@ export declare class AgentdControlPlane {
|
|
|
85
66
|
private agentView;
|
|
86
67
|
private exclusive;
|
|
87
68
|
}
|
|
88
|
-
export declare class ControlPlaneError extends Error {
|
|
89
|
-
readonly status: number;
|
|
90
|
-
constructor(status: number, message: string);
|
|
91
|
-
}
|
package/dist/control-plane.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ControlPlaneError, validateRuntimeSettings, validateUpdate, validateAgentId, validateOpaqueId, validatePassword, validateKeyName, validateApiKeySecret, validateA2ASecret, validateAgentUrl, defaultAgentCardUrl, clampTimeout, preservedAcpAdvanced, setOptionalString, validHeaderName, safeSecretEqual, cleanString, recordValue, isRecord, errorMessage } from './control-plane-validation.js';
|
|
2
|
+
export { ControlPlaneError } from './control-plane-validation.js';
|
|
3
|
+
import { randomBytes, randomUUID } from 'node:crypto';
|
|
2
4
|
import { chmod, open, readFile, rename, rm } from 'node:fs/promises';
|
|
3
5
|
import path from 'node:path';
|
|
4
|
-
import { hashAdminPassword,
|
|
6
|
+
import { hashAdminPassword, verifyAdminPassword } from './auth.js';
|
|
5
7
|
import { loadAgentdConfig } from './config.js';
|
|
6
8
|
import { createDriverRegistry } from './drivers/index.js';
|
|
7
9
|
import { agentdDriverKinds } from './types.js';
|
|
@@ -10,12 +12,33 @@ export class AgentdControlPlane {
|
|
|
10
12
|
configPath;
|
|
11
13
|
config;
|
|
12
14
|
sessions;
|
|
15
|
+
/** Process-local proof, delivered only through the local CLI / embedding API. */
|
|
16
|
+
setupToken;
|
|
13
17
|
queue = Promise.resolve();
|
|
14
18
|
usageTimer;
|
|
19
|
+
keyWatchers = new Set();
|
|
20
|
+
/** Observe authorization without updating last-used time or retaining a stale scope. */
|
|
21
|
+
watchApiKey(keyId, secret, agentId, revoked) {
|
|
22
|
+
const check = () => {
|
|
23
|
+
const key = this.config.apiKeys?.find((item) => item.id === keyId && item.enabled && safeSecretEqual(item.secret, secret));
|
|
24
|
+
if (!key || (!key.scope.allAgents && !key.scope.agentIds.includes(agentId))) {
|
|
25
|
+
this.keyWatchers.delete(check);
|
|
26
|
+
revoked();
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
this.keyWatchers.add(check);
|
|
30
|
+
check();
|
|
31
|
+
return () => { this.keyWatchers.delete(check); };
|
|
32
|
+
}
|
|
15
33
|
constructor(configPath, config, sessions) {
|
|
16
34
|
this.configPath = configPath;
|
|
17
35
|
this.config = config;
|
|
18
36
|
this.sessions = sessions;
|
|
37
|
+
if (!config.adminPasswordHash)
|
|
38
|
+
this.setupToken = randomBytes(32).toString('base64url');
|
|
39
|
+
}
|
|
40
|
+
verifySetupToken(token) {
|
|
41
|
+
return Boolean(this.setupToken && safeSecretEqual(token, this.setupToken));
|
|
19
42
|
}
|
|
20
43
|
snapshot() {
|
|
21
44
|
return {
|
|
@@ -54,6 +77,7 @@ export class AgentdControlPlane {
|
|
|
54
77
|
if (!Array.isArray(raw.apiKeys))
|
|
55
78
|
raw.apiKeys = [];
|
|
56
79
|
await this.persist(raw);
|
|
80
|
+
this.setupToken = undefined;
|
|
57
81
|
return { initialized: true, adminSetupRequired: false };
|
|
58
82
|
});
|
|
59
83
|
}
|
|
@@ -250,7 +274,7 @@ export class AgentdControlPlane {
|
|
|
250
274
|
const agents = recordValue(raw.agents);
|
|
251
275
|
const previous = recordValue(agents[id]);
|
|
252
276
|
let next;
|
|
253
|
-
const protocol = update.protocol || 'acp';
|
|
277
|
+
const protocol = update.protocol || previous.protocol || 'acp';
|
|
254
278
|
if (protocol === 'a2a') {
|
|
255
279
|
const requestedAgentCardUrl = cleanString(update.agentCardUrl);
|
|
256
280
|
const requestedAgentUrl = cleanString(update.agentUrl);
|
|
@@ -294,8 +318,14 @@ export class AgentdControlPlane {
|
|
|
294
318
|
...(authType !== 'none' ? { value: authValue } : {}),
|
|
295
319
|
...(authType === 'header' ? { headerName: authHeaderName } : {})
|
|
296
320
|
},
|
|
297
|
-
timeoutMs: clampTimeout(update.timeoutMs, 60_000, 1000, 30 * 60_000)
|
|
321
|
+
timeoutMs: clampTimeout(update.timeoutMs ?? previous.timeoutMs, 60_000, 1000, 30 * 60_000)
|
|
298
322
|
};
|
|
323
|
+
for (const field of ['taskTimeoutMs', 'streamIdleTimeoutMs']) {
|
|
324
|
+
const value = update[field] === undefined ? previous[field] : update[field];
|
|
325
|
+
if (value != null) {
|
|
326
|
+
next[field] = clampTimeout(value, 60_000, field === 'taskTimeoutMs' ? 10_000 : 1000, field === 'taskTimeoutMs' ? 24 * 60 * 60_000 : 30 * 60_000);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
299
329
|
}
|
|
300
330
|
else {
|
|
301
331
|
const driver = update.driver || cleanString(previous.driver);
|
|
@@ -314,7 +344,7 @@ export class AgentdControlPlane {
|
|
|
314
344
|
enabled: update.enabled ?? previous.enabled !== false,
|
|
315
345
|
workspace,
|
|
316
346
|
permissionPolicy: update.permissionPolicy || cleanString(previous.permissionPolicy) || 'ask',
|
|
317
|
-
permissionTimeoutMs: clampTimeout(update.permissionTimeoutMs, 15 * 60_000, 1000, 24 * 60 * 60 * 1000),
|
|
347
|
+
permissionTimeoutMs: clampTimeout(update.permissionTimeoutMs ?? previous.permissionTimeoutMs, 15 * 60_000, 1000, 24 * 60 * 60 * 1000),
|
|
318
348
|
...preservedAcpAdvanced(previous)
|
|
319
349
|
};
|
|
320
350
|
if (cleanString(update.name) || cleanString(previous.name)) {
|
|
@@ -333,7 +363,7 @@ export class AgentdControlPlane {
|
|
|
333
363
|
}
|
|
334
364
|
const agentIds = Array.from(new Set(input.agentIds.map(validateAgentId)));
|
|
335
365
|
for (const id of agentIds) {
|
|
336
|
-
if (!this.config.agents
|
|
366
|
+
if (!Object.hasOwn(this.config.agents, id)) {
|
|
337
367
|
throw new ControlPlaneError(400, `Configured agent not found: ${id}`);
|
|
338
368
|
}
|
|
339
369
|
}
|
|
@@ -445,6 +475,8 @@ export class AgentdControlPlane {
|
|
|
445
475
|
await chmod(target, 0o600).catch(() => undefined);
|
|
446
476
|
this.config = nextConfig;
|
|
447
477
|
this.sessions.reconfigure(nextConfig, workspacePolicy, drivers);
|
|
478
|
+
for (const check of this.keyWatchers)
|
|
479
|
+
check();
|
|
448
480
|
}
|
|
449
481
|
finally {
|
|
450
482
|
if (!moved)
|
|
@@ -467,7 +499,9 @@ export class AgentdControlPlane {
|
|
|
467
499
|
headerName: config.auth?.headerName,
|
|
468
500
|
configured: Boolean(config.auth?.value)
|
|
469
501
|
},
|
|
470
|
-
timeoutMs: config.timeoutMs || 60_000
|
|
502
|
+
timeoutMs: config.timeoutMs || 60_000,
|
|
503
|
+
taskTimeoutMs: config.taskTimeoutMs,
|
|
504
|
+
streamIdleTimeoutMs: config.streamIdleTimeoutMs
|
|
471
505
|
};
|
|
472
506
|
}
|
|
473
507
|
return {
|
|
@@ -488,153 +522,3 @@ export class AgentdControlPlane {
|
|
|
488
522
|
return result;
|
|
489
523
|
}
|
|
490
524
|
}
|
|
491
|
-
export class ControlPlaneError extends Error {
|
|
492
|
-
status;
|
|
493
|
-
constructor(status, message) {
|
|
494
|
-
super(message);
|
|
495
|
-
this.status = status;
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
function validateRuntimeSettings(update) {
|
|
499
|
-
validateIntegerRange(update.sessionTtlMs, 60_000, 30 * 24 * 60 * 60 * 1000, 'sessionTtlMs');
|
|
500
|
-
validateIntegerRange(update.promptTimeoutMs, 10_000, 24 * 60 * 60 * 1000, 'promptTimeoutMs');
|
|
501
|
-
validateIntegerRange(update.cleanupIntervalMs, 5_000, 60 * 60 * 1000, 'cleanupIntervalMs');
|
|
502
|
-
}
|
|
503
|
-
function validateIntegerRange(value, min, max, name) {
|
|
504
|
-
if (!Number.isInteger(value) || value < min || value > max) {
|
|
505
|
-
throw new ControlPlaneError(400, `${name} must be between ${min} and ${max}`);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
function validateUpdate(update) {
|
|
509
|
-
if (update.protocol !== undefined && update.protocol !== 'acp' && update.protocol !== 'a2a') {
|
|
510
|
-
throw new ControlPlaneError(400, 'protocol must be acp or a2a');
|
|
511
|
-
}
|
|
512
|
-
if (update.permissionPolicy !== undefined &&
|
|
513
|
-
update.permissionPolicy !== 'ask' &&
|
|
514
|
-
update.permissionPolicy !== 'allow' &&
|
|
515
|
-
update.permissionPolicy !== 'deny') {
|
|
516
|
-
throw new ControlPlaneError(400, 'permissionPolicy must be ask, allow, or deny');
|
|
517
|
-
}
|
|
518
|
-
if (update.preferredTransport !== undefined &&
|
|
519
|
-
!['auto', 'jsonrpc', 'http-json'].includes(update.preferredTransport)) {
|
|
520
|
-
throw new ControlPlaneError(400, 'preferredTransport must be auto, jsonrpc, or http-json');
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
function validateAgentId(value) {
|
|
524
|
-
const id = decodeURIComponent(value).trim().toLowerCase();
|
|
525
|
-
if (!/^[a-z0-9][a-z0-9._-]{0,63}$/.test(id)) {
|
|
526
|
-
throw new ControlPlaneError(400, 'Invalid Agent ID');
|
|
527
|
-
}
|
|
528
|
-
return id;
|
|
529
|
-
}
|
|
530
|
-
function validateOpaqueId(value, label) {
|
|
531
|
-
const id = decodeURIComponent(value).trim();
|
|
532
|
-
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(id)) {
|
|
533
|
-
throw new ControlPlaneError(400, `Invalid ${label} ID`);
|
|
534
|
-
}
|
|
535
|
-
return id;
|
|
536
|
-
}
|
|
537
|
-
function validatePassword(value, confirmation) {
|
|
538
|
-
if (value !== confirmation) {
|
|
539
|
-
throw new ControlPlaneError(400, 'Console Password confirmation does not match');
|
|
540
|
-
}
|
|
541
|
-
try {
|
|
542
|
-
return validateAdminPassword(value);
|
|
543
|
-
}
|
|
544
|
-
catch (error) {
|
|
545
|
-
throw new ControlPlaneError(400, errorMessage(error));
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
function validateKeyName(value) {
|
|
549
|
-
const name = cleanString(value);
|
|
550
|
-
if (!name)
|
|
551
|
-
throw new ControlPlaneError(400, 'API Key name is required');
|
|
552
|
-
if (name.length > 80)
|
|
553
|
-
throw new ControlPlaneError(400, 'API Key name is too long');
|
|
554
|
-
return name;
|
|
555
|
-
}
|
|
556
|
-
function validateApiKeySecret(value) {
|
|
557
|
-
const secret = cleanString(value);
|
|
558
|
-
if (secret.length < 16) {
|
|
559
|
-
throw new ControlPlaneError(400, 'Custom API Key must contain at least 16 characters');
|
|
560
|
-
}
|
|
561
|
-
if (Buffer.byteLength(secret, 'utf8') > 512) {
|
|
562
|
-
throw new ControlPlaneError(400, 'Custom API Key must not exceed 512 bytes');
|
|
563
|
-
}
|
|
564
|
-
if (/^env:/i.test(secret) || /[\u0000-\u001f\u007f]/.test(secret)) {
|
|
565
|
-
throw new ControlPlaneError(400, 'Custom API Key contains an unsupported value');
|
|
566
|
-
}
|
|
567
|
-
return secret;
|
|
568
|
-
}
|
|
569
|
-
function validateA2ASecret(value) {
|
|
570
|
-
const secret = cleanString(value);
|
|
571
|
-
if (!secret)
|
|
572
|
-
return '';
|
|
573
|
-
if (/^env:/i.test(secret) ||
|
|
574
|
-
Buffer.byteLength(secret, 'utf8') > 4096 ||
|
|
575
|
-
/[\u0000\r\n]/.test(secret)) {
|
|
576
|
-
throw new ControlPlaneError(400, 'A2A authentication value is invalid');
|
|
577
|
-
}
|
|
578
|
-
return secret;
|
|
579
|
-
}
|
|
580
|
-
function validateAgentUrl(value) {
|
|
581
|
-
let url;
|
|
582
|
-
try {
|
|
583
|
-
url = new URL(value);
|
|
584
|
-
}
|
|
585
|
-
catch {
|
|
586
|
-
throw new ControlPlaneError(400, 'A2A Agent Card URL is invalid');
|
|
587
|
-
}
|
|
588
|
-
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.hash) {
|
|
589
|
-
throw new ControlPlaneError(400, 'A2A Agent Card URL must use http(s) without credentials or a fragment');
|
|
590
|
-
}
|
|
591
|
-
return url.toString().replace(/\/$/, '');
|
|
592
|
-
}
|
|
593
|
-
function defaultAgentCardUrl(agentUrl) {
|
|
594
|
-
if (!agentUrl)
|
|
595
|
-
return undefined;
|
|
596
|
-
return new URL('/.well-known/agent-card.json', agentUrl).toString();
|
|
597
|
-
}
|
|
598
|
-
function clampTimeout(value, fallback, min, max) {
|
|
599
|
-
if (value === undefined)
|
|
600
|
-
return fallback;
|
|
601
|
-
if (!Number.isInteger(value) || value < min || value > max) {
|
|
602
|
-
throw new ControlPlaneError(400, `timeout must be between ${min} and ${max}`);
|
|
603
|
-
}
|
|
604
|
-
return value;
|
|
605
|
-
}
|
|
606
|
-
function preservedAcpAdvanced(previous) {
|
|
607
|
-
const result = {};
|
|
608
|
-
for (const key of ['command', 'args', 'inheritEnv', 'env']) {
|
|
609
|
-
if (previous[key] !== undefined)
|
|
610
|
-
result[key] = structuredClone(previous[key]);
|
|
611
|
-
}
|
|
612
|
-
return result;
|
|
613
|
-
}
|
|
614
|
-
function setOptionalString(target, key, value) {
|
|
615
|
-
const text = cleanString(value);
|
|
616
|
-
if (text)
|
|
617
|
-
target[key] = text;
|
|
618
|
-
else
|
|
619
|
-
delete target[key];
|
|
620
|
-
}
|
|
621
|
-
function validHeaderName(value) {
|
|
622
|
-
return /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(value);
|
|
623
|
-
}
|
|
624
|
-
function safeSecretEqual(left, right) {
|
|
625
|
-
const leftBuffer = Buffer.from(left);
|
|
626
|
-
const rightBuffer = Buffer.from(right);
|
|
627
|
-
return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
|
|
628
|
-
}
|
|
629
|
-
function cleanString(value) {
|
|
630
|
-
return typeof value === 'string' ? value.trim() : '';
|
|
631
|
-
}
|
|
632
|
-
function recordValue(value) {
|
|
633
|
-
return isRecord(value) ? { ...value } : {};
|
|
634
|
-
}
|
|
635
|
-
function isRecord(value) {
|
|
636
|
-
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
637
|
-
}
|
|
638
|
-
function errorMessage(error) {
|
|
639
|
-
return error instanceof Error ? error.message : String(error);
|
|
640
|
-
}
|