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/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.syncRun({
171
- progress: { phase: '继续执行', message: '已提交补充信息' }
172
- });
173
- await this.runtime.respondPending(message, attachments);
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, config.instructions);
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
- /** JSON-safe metadata for the owning integration. Never put secrets here. */
199
- metadata?: Record<string, unknown>;
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;