nexus-agentd 0.1.7 → 0.1.9
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 +215 -439
- package/dist/a2a/runtime.d.ts +2 -4
- package/dist/a2a/runtime.js +11 -10
- package/dist/acp/runtime.d.ts +3 -5
- package/dist/acp/runtime.js +40 -16
- package/dist/config.js +0 -12
- package/dist/control-plane.d.ts +0 -1
- package/dist/control-plane.js +0 -17
- package/dist/server.js +63 -248
- package/dist/session-contract.d.ts +2 -2
- package/dist/session.d.ts +9 -2
- package/dist/session.js +149 -7
- package/dist/types.d.ts +8 -44
- package/dist/webui/app.js +912 -1073
- package/dist/webui/icons.d.ts +0 -1
- package/dist/webui/icons.js +0 -1
- package/dist/webui/index.js +10 -10
- package/dist/webui/markup.js +65 -66
- package/dist/webui/styles.js +256 -268
- package/nexus-agentd.example.json +40 -47
- package/package.json +52 -52
- package/dist/agent-instructions.d.ts +0 -11
- package/dist/agent-instructions.js +0 -44
- package/dist/artifact-store.d.ts +0 -41
- package/dist/artifact-store.js +0 -340
- package/dist/workspace-files.d.ts +0 -48
- package/dist/workspace-files.js +0 -269
package/dist/session.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { open, realpath } from 'node:fs/promises';
|
|
3
|
+
import { basename, extname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
2
4
|
import { AcpProcessRuntime } from './acp/runtime.js';
|
|
3
5
|
import { A2AClientRuntime, probeA2AAgent } from './a2a/runtime.js';
|
|
4
6
|
import { SessionEventLog } from './events.js';
|
|
@@ -9,6 +11,7 @@ const READINESS_CACHE_MS = 20_000;
|
|
|
9
11
|
const MAX_INPUT_ATTACHMENTS = 16;
|
|
10
12
|
const MAX_INPUT_ATTACHMENT_BYTES = 16 * 1024 * 1024;
|
|
11
13
|
const MAX_SESSION_INPUT_BYTES = 32 * 1024 * 1024;
|
|
14
|
+
const MAX_PUBLISHED_FILE_BYTES = 12 * 1024 * 1024;
|
|
12
15
|
export class ManagedSession {
|
|
13
16
|
agentId;
|
|
14
17
|
protocol;
|
|
@@ -17,6 +20,7 @@ export class ManagedSession {
|
|
|
17
20
|
maxOutputChars;
|
|
18
21
|
agentName;
|
|
19
22
|
runStore;
|
|
23
|
+
instanceId;
|
|
20
24
|
id = randomUUID();
|
|
21
25
|
createdAt = Date.now();
|
|
22
26
|
events;
|
|
@@ -30,7 +34,7 @@ export class ManagedSession {
|
|
|
30
34
|
artifacts = [];
|
|
31
35
|
inputAttachments = new Map();
|
|
32
36
|
runtime;
|
|
33
|
-
constructor(agentId, protocol, workspace, ownerKeyId, maxEvents, maxOutputChars, agentName, runStore) {
|
|
37
|
+
constructor(agentId, protocol, workspace, ownerKeyId, maxEvents, maxOutputChars, agentName, runStore, instanceId = randomUUID()) {
|
|
34
38
|
this.agentId = agentId;
|
|
35
39
|
this.protocol = protocol;
|
|
36
40
|
this.workspace = workspace;
|
|
@@ -38,6 +42,7 @@ export class ManagedSession {
|
|
|
38
42
|
this.maxOutputChars = maxOutputChars;
|
|
39
43
|
this.agentName = agentName;
|
|
40
44
|
this.runStore = runStore;
|
|
45
|
+
this.instanceId = instanceId;
|
|
41
46
|
this.events = new SessionEventLog(this.id, maxEvents);
|
|
42
47
|
this.events.append('session_state', { state: this.state, protocol });
|
|
43
48
|
}
|
|
@@ -116,6 +121,70 @@ export class ManagedSession {
|
|
|
116
121
|
artifactCount: this.artifacts.length
|
|
117
122
|
});
|
|
118
123
|
}
|
|
124
|
+
async publishFile(filePath) {
|
|
125
|
+
const requestedPath = String(filePath || '').trim();
|
|
126
|
+
if (!requestedPath) {
|
|
127
|
+
throw new SessionRequestError(400, 'path is required');
|
|
128
|
+
}
|
|
129
|
+
if (!this.workspace) {
|
|
130
|
+
throw new SessionRequestError(400, 'This session does not have a local workspace');
|
|
131
|
+
}
|
|
132
|
+
let handle;
|
|
133
|
+
try {
|
|
134
|
+
const workspaceRoot = await realpath(this.workspace);
|
|
135
|
+
const unresolved = resolve(workspaceRoot, requestedPath);
|
|
136
|
+
const target = await realpath(unresolved);
|
|
137
|
+
assertWorkspacePath(workspaceRoot, target);
|
|
138
|
+
handle = await open(target, 'r');
|
|
139
|
+
// Resolve again after opening so a swapped symlink cannot silently
|
|
140
|
+
// redirect a publish request outside the configured workspace.
|
|
141
|
+
const verifiedTarget = await realpath(unresolved);
|
|
142
|
+
assertWorkspacePath(workspaceRoot, verifiedTarget);
|
|
143
|
+
if (verifiedTarget !== target) {
|
|
144
|
+
throw new SessionRequestError(409, 'Published file changed while it was being opened');
|
|
145
|
+
}
|
|
146
|
+
const stats = await handle.stat();
|
|
147
|
+
if (!stats.isFile()) {
|
|
148
|
+
throw new SessionRequestError(400, 'Only regular files can be published');
|
|
149
|
+
}
|
|
150
|
+
if (stats.size > MAX_PUBLISHED_FILE_BYTES) {
|
|
151
|
+
throw new SessionRequestError(413, `Published file exceeds the ${MAX_PUBLISHED_FILE_BYTES} byte limit`);
|
|
152
|
+
}
|
|
153
|
+
const bytes = await handle.readFile();
|
|
154
|
+
const filename = basename(verifiedTarget);
|
|
155
|
+
const workspacePath = relative(workspaceRoot, verifiedTarget).split(sep).join('/');
|
|
156
|
+
const artifact = {
|
|
157
|
+
id: `published:${randomUUID()}`,
|
|
158
|
+
name: filename,
|
|
159
|
+
filename,
|
|
160
|
+
mediaType: mediaTypeForPath(filename),
|
|
161
|
+
bytesBase64: bytes.toString('base64'),
|
|
162
|
+
metadata: {
|
|
163
|
+
source: 'workspace_publish',
|
|
164
|
+
path: workspacePath,
|
|
165
|
+
size: bytes.length
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
this.addArtifact(artifact);
|
|
169
|
+
this.emit('artifact', runArtifact(artifact));
|
|
170
|
+
return artifact;
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
if (error instanceof SessionRequestError)
|
|
174
|
+
throw error;
|
|
175
|
+
const code = error?.code;
|
|
176
|
+
if (code === 'ENOENT') {
|
|
177
|
+
throw new SessionRequestError(404, 'Published file was not found');
|
|
178
|
+
}
|
|
179
|
+
if (code === 'EACCES' || code === 'EPERM') {
|
|
180
|
+
throw new SessionRequestError(403, 'Published file cannot be read');
|
|
181
|
+
}
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
await handle?.close();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
119
188
|
setPending(request) {
|
|
120
189
|
this.pendingRequest = structuredClone(request);
|
|
121
190
|
this.state = request.kind === 'permission' ? 'permission_required' : 'input_required';
|
|
@@ -167,10 +236,10 @@ export class ManagedSession {
|
|
|
167
236
|
throw new SessionRequestError(409, 'Agent runtime is unavailable');
|
|
168
237
|
const attachments = this.inputAttachmentsFor(attachmentIds);
|
|
169
238
|
if (this.pendingRequest) {
|
|
170
|
-
this.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
239
|
+
await this.resolvePending({
|
|
240
|
+
requestId: this.pendingRequest.id,
|
|
241
|
+
message
|
|
242
|
+
}, attachmentIds);
|
|
174
243
|
return;
|
|
175
244
|
}
|
|
176
245
|
if (this.state === 'running') {
|
|
@@ -196,6 +265,23 @@ export class ManagedSession {
|
|
|
196
265
|
this.setState('failed', errorMessage(error));
|
|
197
266
|
});
|
|
198
267
|
}
|
|
268
|
+
async resolvePending(response, attachmentIds = []) {
|
|
269
|
+
if (!this.runtime) {
|
|
270
|
+
throw new SessionRequestError(409, 'Agent runtime is unavailable');
|
|
271
|
+
}
|
|
272
|
+
const pending = this.pendingRequest;
|
|
273
|
+
if (!pending) {
|
|
274
|
+
throw new SessionRequestError(409, 'Session is not waiting for input');
|
|
275
|
+
}
|
|
276
|
+
if (pending.id !== response.requestId) {
|
|
277
|
+
throw new SessionRequestError(409, `Pending request no longer matches: ${response.requestId}`);
|
|
278
|
+
}
|
|
279
|
+
const attachments = this.inputAttachmentsFor(attachmentIds);
|
|
280
|
+
this.syncRun({
|
|
281
|
+
progress: { phase: '继续执行', message: '已提交补充信息' }
|
|
282
|
+
});
|
|
283
|
+
await this.runtime.respondPending(response, attachments);
|
|
284
|
+
}
|
|
199
285
|
async cancel() {
|
|
200
286
|
await this.runtime?.cancel();
|
|
201
287
|
}
|
|
@@ -206,6 +292,7 @@ export class ManagedSession {
|
|
|
206
292
|
snapshot() {
|
|
207
293
|
return {
|
|
208
294
|
id: this.id,
|
|
295
|
+
instanceId: this.instanceId,
|
|
209
296
|
runId: this.currentRunId,
|
|
210
297
|
protocol: this.protocol,
|
|
211
298
|
protocolSessionId: this.protocolSessionId,
|
|
@@ -257,6 +344,7 @@ export class SessionManager {
|
|
|
257
344
|
workspacePolicy;
|
|
258
345
|
drivers;
|
|
259
346
|
runStore;
|
|
347
|
+
instanceId = randomUUID();
|
|
260
348
|
sessions = new Map();
|
|
261
349
|
cleanupTimer;
|
|
262
350
|
readinessCache;
|
|
@@ -371,10 +459,10 @@ export class SessionManager {
|
|
|
371
459
|
}
|
|
372
460
|
workspace = await this.workspacePolicy.resolve(requestedWorkspace);
|
|
373
461
|
}
|
|
374
|
-
const session = new ManagedSession(agentId, config.protocol === 'a2a' ? 'a2a' : 'acp', workspace, ownerKeyId, this.config.maxEventsPerSession, this.config.maxOutputChars, config.name || agentId, this.runStore);
|
|
462
|
+
const session = new ManagedSession(agentId, config.protocol === 'a2a' ? 'a2a' : 'acp', workspace, ownerKeyId, this.config.maxEventsPerSession, this.config.maxOutputChars, config.name || agentId, this.runStore, this.instanceId);
|
|
375
463
|
const runtime = config.protocol === 'a2a'
|
|
376
464
|
? new A2AClientRuntime(config, session, this.config.promptTimeoutMs || 30 * 60_000)
|
|
377
|
-
: new AcpProcessRuntime(driver, session, this.config.maxOutputChars, this.config.promptTimeoutMs || 30 * 60_000
|
|
465
|
+
: new AcpProcessRuntime(driver, session, this.config.maxOutputChars, this.config.promptTimeoutMs || 30 * 60_000);
|
|
378
466
|
this.sessions.set(session.id, session);
|
|
379
467
|
session.attach(runtime);
|
|
380
468
|
try {
|
|
@@ -414,15 +502,34 @@ export class SessionManager {
|
|
|
414
502
|
await session.message(text, attachmentIds);
|
|
415
503
|
return session.snapshot();
|
|
416
504
|
}
|
|
505
|
+
async resolvePending(id, response, attachmentIds = []) {
|
|
506
|
+
const session = this.require(id);
|
|
507
|
+
await session.resolvePending(response, attachmentIds);
|
|
508
|
+
return session.snapshot();
|
|
509
|
+
}
|
|
417
510
|
addInputAttachment(id, name, mediaType, bytes) {
|
|
418
511
|
const session = this.require(id);
|
|
419
512
|
return session.addInputAttachment(name, mediaType, bytes);
|
|
420
513
|
}
|
|
514
|
+
async publishFile(id, path) {
|
|
515
|
+
const session = this.require(id);
|
|
516
|
+
await session.publishFile(path);
|
|
517
|
+
return session.snapshot();
|
|
518
|
+
}
|
|
421
519
|
async cancel(id) {
|
|
422
520
|
const session = this.require(id);
|
|
423
521
|
await session.cancel();
|
|
424
522
|
return session.snapshot();
|
|
425
523
|
}
|
|
524
|
+
async close(id) {
|
|
525
|
+
const session = this.require(id);
|
|
526
|
+
if (isActive(session.state))
|
|
527
|
+
await session.cancel();
|
|
528
|
+
const snapshot = session.snapshot();
|
|
529
|
+
await session.dispose();
|
|
530
|
+
this.sessions.delete(id);
|
|
531
|
+
return snapshot;
|
|
532
|
+
}
|
|
426
533
|
eventsAfter(id, after) {
|
|
427
534
|
return this.require(id).events.after(after);
|
|
428
535
|
}
|
|
@@ -534,6 +641,41 @@ function runArtifact(artifact) {
|
|
|
534
641
|
metadata: artifact.metadata ? structuredClone(artifact.metadata) : undefined
|
|
535
642
|
};
|
|
536
643
|
}
|
|
644
|
+
function assertWorkspacePath(workspaceRoot, target) {
|
|
645
|
+
const child = relative(workspaceRoot, target);
|
|
646
|
+
if (child === '..' || child.startsWith(`..${sep}`) || isAbsolute(child)) {
|
|
647
|
+
throw new SessionRequestError(403, 'Published files must stay inside the session workspace');
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
function mediaTypeForPath(path) {
|
|
651
|
+
switch (extname(path).toLowerCase()) {
|
|
652
|
+
case '.txt':
|
|
653
|
+
case '.log':
|
|
654
|
+
case '.md':
|
|
655
|
+
return 'text/plain';
|
|
656
|
+
case '.json':
|
|
657
|
+
return 'application/json';
|
|
658
|
+
case '.pdf':
|
|
659
|
+
return 'application/pdf';
|
|
660
|
+
case '.png':
|
|
661
|
+
return 'image/png';
|
|
662
|
+
case '.jpg':
|
|
663
|
+
case '.jpeg':
|
|
664
|
+
return 'image/jpeg';
|
|
665
|
+
case '.gif':
|
|
666
|
+
return 'image/gif';
|
|
667
|
+
case '.webp':
|
|
668
|
+
return 'image/webp';
|
|
669
|
+
case '.svg':
|
|
670
|
+
return 'image/svg+xml';
|
|
671
|
+
case '.csv':
|
|
672
|
+
return 'text/csv';
|
|
673
|
+
case '.zip':
|
|
674
|
+
return 'application/zip';
|
|
675
|
+
default:
|
|
676
|
+
return 'application/octet-stream';
|
|
677
|
+
}
|
|
678
|
+
}
|
|
537
679
|
function firstString(...values) {
|
|
538
680
|
for (const value of values) {
|
|
539
681
|
if (typeof value === 'string' && value.trim())
|
package/dist/types.d.ts
CHANGED
|
@@ -14,8 +14,6 @@ export interface AgentdDriverConfig {
|
|
|
14
14
|
args?: string[];
|
|
15
15
|
inheritEnv?: string[];
|
|
16
16
|
env?: Record<string, string>;
|
|
17
|
-
/** Optional extra instructions appended to the built-in Agent Nexus guidance. */
|
|
18
|
-
instructions?: string;
|
|
19
17
|
permissionPolicy?: PermissionPolicy;
|
|
20
18
|
permissionTimeoutMs?: number;
|
|
21
19
|
}
|
|
@@ -25,8 +23,6 @@ export interface AgentdA2AConfig {
|
|
|
25
23
|
protocol: 'a2a';
|
|
26
24
|
name?: string;
|
|
27
25
|
description?: string;
|
|
28
|
-
/** Optional extra instructions sent with the first A2A user message. */
|
|
29
|
-
instructions?: string;
|
|
30
26
|
enabled?: boolean;
|
|
31
27
|
/** Full Agent Card JSON URL. Preferred for new configurations. */
|
|
32
28
|
agentCardUrl?: string;
|
|
@@ -67,18 +63,6 @@ export interface AgentdConfig {
|
|
|
67
63
|
workspaceRoots: string[];
|
|
68
64
|
maxRequestBytes: number;
|
|
69
65
|
maxAttachmentBytes?: number;
|
|
70
|
-
/** Gateway-owned directory for temporary, anonymously downloadable artifacts. */
|
|
71
|
-
artifactStoragePath?: string;
|
|
72
|
-
/** Maximum size of one published artifact. */
|
|
73
|
-
maxArtifactBytes?: number;
|
|
74
|
-
/** Maximum combined size of all retained published artifacts. */
|
|
75
|
-
maxArtifactStorageBytes?: number;
|
|
76
|
-
/** Maximum number of retained published artifacts. */
|
|
77
|
-
maxPublishedArtifacts?: number;
|
|
78
|
-
/** Maximum number of simultaneous artifact copies. */
|
|
79
|
-
maxConcurrentArtifactPublishes?: number;
|
|
80
|
-
/** Lifetime of a published artifact URL. */
|
|
81
|
-
artifactTtlMs?: number;
|
|
82
66
|
maxEventsPerSession: number;
|
|
83
67
|
maxOutputChars: number;
|
|
84
68
|
sessionTtlMs: number;
|
|
@@ -92,27 +76,6 @@ export interface AgentdConfig {
|
|
|
92
76
|
secureAdminCookies?: boolean;
|
|
93
77
|
agents: Record<string, AgentdAgentConfig>;
|
|
94
78
|
}
|
|
95
|
-
export interface AgentdFileRootView {
|
|
96
|
-
id: string;
|
|
97
|
-
name: string;
|
|
98
|
-
path: string;
|
|
99
|
-
}
|
|
100
|
-
export interface AgentdFileEntryView {
|
|
101
|
-
name: string;
|
|
102
|
-
path: string;
|
|
103
|
-
type: 'file' | 'directory' | 'symlink' | 'other';
|
|
104
|
-
size?: number;
|
|
105
|
-
modifiedAt: number;
|
|
106
|
-
}
|
|
107
|
-
export interface AgentdPublishedFile {
|
|
108
|
-
id: string;
|
|
109
|
-
name: string;
|
|
110
|
-
url: string;
|
|
111
|
-
size: number;
|
|
112
|
-
mediaType: string;
|
|
113
|
-
sha256: string;
|
|
114
|
-
expiresAt: number;
|
|
115
|
-
}
|
|
116
79
|
export interface AgentdAgentView {
|
|
117
80
|
id: string;
|
|
118
81
|
name: string;
|
|
@@ -132,7 +95,6 @@ export interface AgentdAgentConfigView {
|
|
|
132
95
|
protocol: AgentdProtocol;
|
|
133
96
|
name: string;
|
|
134
97
|
description?: string;
|
|
135
|
-
instructions?: string;
|
|
136
98
|
enabled: boolean;
|
|
137
99
|
driver?: AgentdDriverKind;
|
|
138
100
|
workspace?: string;
|
|
@@ -186,17 +148,17 @@ export interface AgentdPendingRequest {
|
|
|
186
148
|
id: string;
|
|
187
149
|
kind: 'permission' | 'input';
|
|
188
150
|
prompt: string;
|
|
189
|
-
/** Optional business-facing step, for example `payment` or `pickup_time`. */
|
|
190
|
-
step?: string;
|
|
191
|
-
/** Optional hint for clients rendering the pending interaction. */
|
|
192
|
-
inputType?: 'text' | 'choice' | 'confirmation' | 'payment' | 'unknown';
|
|
193
151
|
options?: Array<{
|
|
194
152
|
id: string;
|
|
195
153
|
name: string;
|
|
196
154
|
kind?: string;
|
|
197
155
|
}>;
|
|
198
|
-
|
|
199
|
-
|
|
156
|
+
}
|
|
157
|
+
export interface AgentdPendingResponse {
|
|
158
|
+
requestId: string;
|
|
159
|
+
message?: string;
|
|
160
|
+
optionId?: string;
|
|
161
|
+
action?: 'accept' | 'decline' | 'cancel';
|
|
200
162
|
}
|
|
201
163
|
export interface AgentdArtifact {
|
|
202
164
|
id?: string;
|
|
@@ -212,6 +174,8 @@ export interface AgentdArtifact {
|
|
|
212
174
|
}
|
|
213
175
|
export interface AgentdSessionView {
|
|
214
176
|
id: string;
|
|
177
|
+
/** Identifies the Gateway process that owns this in-memory session. */
|
|
178
|
+
instanceId?: string;
|
|
215
179
|
runId?: string;
|
|
216
180
|
protocol: AgentdProtocol;
|
|
217
181
|
protocolSessionId?: string;
|