nexus-agentd 0.1.8 → 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 -451
- package/dist/a2a/runtime.d.ts +2 -4
- package/dist/a2a/runtime.js +11 -10
- package/dist/acp/runtime.d.ts +3 -8
- package/dist/acp/runtime.js +42 -69
- package/dist/config.js +1 -15
- package/dist/control-plane.d.ts +0 -1
- package/dist/control-plane.js +1 -19
- 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 +150 -16
- package/dist/types.d.ts +9 -45
- package/dist/webui/app.js +912 -1074
- 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,5 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { open, realpath } from 'node:fs/promises';
|
|
3
|
+
import { basename, extname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { AcpProcessRuntime } from './acp/runtime.js';
|
|
3
5
|
import { A2AClientRuntime, probeA2AAgent } from './a2a/runtime.js';
|
|
4
6
|
import { SessionEventLog } from './events.js';
|
|
5
7
|
const MAX_SESSION_ARTIFACTS = 64;
|
|
@@ -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,18 +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
|
-
|
|
174
|
-
await this.runtime.respondPending(message, attachments);
|
|
175
|
-
}
|
|
176
|
-
catch (error) {
|
|
177
|
-
if (error instanceof InvalidPermissionAnswerError) {
|
|
178
|
-
throw new SessionRequestError(400, error.message);
|
|
179
|
-
}
|
|
180
|
-
throw error;
|
|
181
|
-
}
|
|
239
|
+
await this.resolvePending({
|
|
240
|
+
requestId: this.pendingRequest.id,
|
|
241
|
+
message
|
|
242
|
+
}, attachmentIds);
|
|
182
243
|
return;
|
|
183
244
|
}
|
|
184
245
|
if (this.state === 'running') {
|
|
@@ -204,6 +265,23 @@ export class ManagedSession {
|
|
|
204
265
|
this.setState('failed', errorMessage(error));
|
|
205
266
|
});
|
|
206
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
|
+
}
|
|
207
285
|
async cancel() {
|
|
208
286
|
await this.runtime?.cancel();
|
|
209
287
|
}
|
|
@@ -214,6 +292,7 @@ export class ManagedSession {
|
|
|
214
292
|
snapshot() {
|
|
215
293
|
return {
|
|
216
294
|
id: this.id,
|
|
295
|
+
instanceId: this.instanceId,
|
|
217
296
|
runId: this.currentRunId,
|
|
218
297
|
protocol: this.protocol,
|
|
219
298
|
protocolSessionId: this.protocolSessionId,
|
|
@@ -265,6 +344,7 @@ export class SessionManager {
|
|
|
265
344
|
workspacePolicy;
|
|
266
345
|
drivers;
|
|
267
346
|
runStore;
|
|
347
|
+
instanceId = randomUUID();
|
|
268
348
|
sessions = new Map();
|
|
269
349
|
cleanupTimer;
|
|
270
350
|
readinessCache;
|
|
@@ -379,10 +459,10 @@ export class SessionManager {
|
|
|
379
459
|
}
|
|
380
460
|
workspace = await this.workspacePolicy.resolve(requestedWorkspace);
|
|
381
461
|
}
|
|
382
|
-
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);
|
|
383
463
|
const runtime = config.protocol === 'a2a'
|
|
384
464
|
? new A2AClientRuntime(config, session, this.config.promptTimeoutMs || 30 * 60_000)
|
|
385
|
-
: 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);
|
|
386
466
|
this.sessions.set(session.id, session);
|
|
387
467
|
session.attach(runtime);
|
|
388
468
|
try {
|
|
@@ -422,15 +502,34 @@ export class SessionManager {
|
|
|
422
502
|
await session.message(text, attachmentIds);
|
|
423
503
|
return session.snapshot();
|
|
424
504
|
}
|
|
505
|
+
async resolvePending(id, response, attachmentIds = []) {
|
|
506
|
+
const session = this.require(id);
|
|
507
|
+
await session.resolvePending(response, attachmentIds);
|
|
508
|
+
return session.snapshot();
|
|
509
|
+
}
|
|
425
510
|
addInputAttachment(id, name, mediaType, bytes) {
|
|
426
511
|
const session = this.require(id);
|
|
427
512
|
return session.addInputAttachment(name, mediaType, bytes);
|
|
428
513
|
}
|
|
514
|
+
async publishFile(id, path) {
|
|
515
|
+
const session = this.require(id);
|
|
516
|
+
await session.publishFile(path);
|
|
517
|
+
return session.snapshot();
|
|
518
|
+
}
|
|
429
519
|
async cancel(id) {
|
|
430
520
|
const session = this.require(id);
|
|
431
521
|
await session.cancel();
|
|
432
522
|
return session.snapshot();
|
|
433
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
|
+
}
|
|
434
533
|
eventsAfter(id, after) {
|
|
435
534
|
return this.require(id).events.after(after);
|
|
436
535
|
}
|
|
@@ -542,6 +641,41 @@ function runArtifact(artifact) {
|
|
|
542
641
|
metadata: artifact.metadata ? structuredClone(artifact.metadata) : undefined
|
|
543
642
|
};
|
|
544
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
|
+
}
|
|
545
679
|
function firstString(...values) {
|
|
546
680
|
for (const value of values) {
|
|
547
681
|
if (typeof value === 'string' && value.trim())
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type AgentdSessionState = 'created' | 'running' | 'input_required' | 'permission_required' | 'completed' | 'failed' | 'canceled';
|
|
2
2
|
export type AgentdProtocol = 'acp' | 'a2a';
|
|
3
|
-
export type PermissionPolicy = 'ask' | '
|
|
3
|
+
export type PermissionPolicy = 'ask' | 'deny';
|
|
4
4
|
export declare const agentdDriverKinds: readonly ["opencode", "claude", "codex", "pi", "openclaw", "hermes"];
|
|
5
5
|
export type AgentdDriverKind = (typeof agentdDriverKinds)[number];
|
|
6
6
|
export interface AgentdDriverConfig {
|
|
@@ -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;
|