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/dist/session.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { AcpProcessRuntime, InvalidPermissionAnswerError } from './acp/runtime.js';
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.syncRun({
171
- progress: { phase: '继续执行', message: '已提交补充信息' }
172
- });
173
- try {
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, config.instructions);
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' | 'allow' | 'deny';
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
- /** 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;