nexus-agentd 0.1.6 → 0.1.8

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/README.md CHANGED
@@ -11,22 +11,23 @@ Agent,并向 Koishi AgentNexus 或其他客户端提供 HTTP/SSE API。管理
11
11
 
12
12
  ## 快速开始
13
13
 
14
- 当前仓库代码建议按下面的源码部署流程启动;安装、首次初始化、systemd Agent 准备的完整说明见下文。
14
+ 当前稳定版本已发布到 npm,推荐按下面的 npm 流程启动;如果要运行 GitHub 上尚未发布的分支代码,
15
+ 再使用下方的源码部署方式。首次初始化、systemd 和 Agent 准备的完整说明见下文。
15
16
 
16
17
  ```bash
17
- git clone https://github.com/lumia1998/nexus-gateway.git
18
- cd nexus-gateway
18
+ NPM_CONFIG_PREFIX="$HOME/.local"
19
+ npm install --global --prefix "$NPM_CONFIG_PREFIX" nexus-agentd@latest
20
+ export PATH="$NPM_CONFIG_PREFIX/bin:$PATH"
19
21
  mkdir -p "$HOME/.config/agent-nexus" "$HOME/projects"
20
- npm ci
21
- npm run build
22
- node dist/cli.js \
23
- --config "$HOME/.config/agent-nexus/nexus-agentd.json" \
24
- --host 0.0.0.0 \
25
- --port 8787 \
26
- --workspace "$HOME/projects"
22
+ nexus-agentd \
23
+ --config "$HOME/.config/agent-nexus/nexus-agentd.json" \
24
+ --host 0.0.0.0 \
25
+ --port 8787 \
26
+ --workspace "$HOME/projects"
27
27
  ```
28
28
 
29
- 如果使用 npm 包,请先用 `npm view nexus-agentd version` 确认 registry 中的版本。当前仓库的 package.json 版本不代表该版本已经发布到 npm;未发布时不要把 `npm install nexus-agentd` 当成当前仓库的安装方式。
29
+ 当前仓库版本 `0.1.8` 已发布到 npm;可用 `npm view nexus-agentd version` 检查 registry latest,
30
+ 也可以将安装命令固定为 `nexus-agentd@0.1.8`。
30
31
 
31
32
  ## 安装与部署
32
33
 
@@ -41,15 +42,15 @@ Gateway 不会替你安装 Agent,也不会替你执行 OpenCode、Claude Code
41
42
 
42
43
  ### npm 包安装(已发布版本)
43
44
 
44
- 注意:只有当目标版本已经发布到 npm 时才使用此方式。否则请使用下面的源码部署,或将本仓库打包后的 tarball 安装到目标机器。
45
+ `0.1.8` 已发布到 npm。目标版本尚未发布时,请使用下面的源码部署,或将本仓库打包后的 tarball 安装到目标机器。
45
46
 
46
- 当前仓库的 package.json 版本可能高于 npm registry 的 latest;发布状态可用 `npm view nexus-agentd version` 检查。
47
+ 发布状态可用 `npm view nexus-agentd version` 检查;生产环境需要可复现部署时,建议固定为 `nexus-agentd@0.1.8`。
47
48
 
48
49
  生产环境建议使用专用的低权限系统用户,并把 npm 全局包安装到用户目录:
49
50
 
50
51
  ~~~bash
51
52
  NPM_CONFIG_PREFIX="$HOME/.local"
52
- npm install --global --prefix "$NPM_CONFIG_PREFIX" nexus-agentd
53
+ npm install --global --prefix "$NPM_CONFIG_PREFIX" nexus-agentd@0.1.8
53
54
  export PATH="$NPM_CONFIG_PREFIX/bin:$PATH"
54
55
  ~~~
55
56
 
@@ -129,7 +130,7 @@ curl http://127.0.0.1:8787/health
129
130
  绝对路径加上 `dist/cli.js`,例如:`/usr/bin/node %h/nexus-gateway/dist/cli.js --config
130
131
  %h/.config/agent-nexus/nexus-agentd.json`。不要在 systemd 中依赖交互式 Shell 的 nvm 初始化。
131
132
 
132
- ### 源码部署(当前仓库推荐)
133
+ ### 源码部署(开发分支或未发布版本)
133
134
 
134
135
  源码部署适合当前仓库和尚未发布到 npm 的版本;生产环境请使用外部进程管理器负责守护,
135
136
  不要把运行数据、Artifact 仓库或密钥放进 Git 工作树:
@@ -268,6 +269,17 @@ command -v hermes && hermes acp --check
268
269
 
269
270
  `workspace` 必须位于 `workspaceRoots` 之下;如果只通过 WebUI 配置,Gateway 会校验并保存这些字段。
270
271
 
272
+ ACP Agent 的 `permissionPolicy` 有三种取值:`ask`(默认,每次权限请求交给用户确认)、`allow`(始终允许,
273
+ 自动选择 ACP 返回的允许项,不创建等待确认的请求)和 `deny`(始终拒绝,自动选择拒绝项)。`allow` 会放行该
274
+ Agent 发起的所有 ACP 权限请求,请只对可信的 Agent 和工作区使用;没有明确的允许项时 Gateway 会安全地取消请求。
275
+ 该字段只适用于 ACP,A2A Agent 不使用本地 ACP 权限策略。
276
+
277
+ Gateway 会在每个 Agent Session 的首次请求前自动注入一段 Agent Nexus 交互规范,提醒 Agent 在需要
278
+ 用户选择、确认、支付或补充信息时使用 ACP elicitation/A2A `input-required`,不要只输出普通文本问题。
279
+ 这段规范不需要为 OpenCode、Claude Code 或 Hermes 手工重复配置。若某个 Agent 还需要额外规则,可在 Agent
280
+ 配置中增加 `instructions`;它会在内置规范之后追加,并限制为最多 32768 个字符。提示词注入只是行为约定,
281
+ Agent 仍必须实际支持相应的 ACP/A2A/MCP 用户输入机制,Gateway 不会把普通文本自动猜成等待状态。
282
+
271
283
  ### A2A
272
284
 
273
285
  A2A 使用官方 `@a2a-js/sdk` 客户端,通过完整的 Agent Card URL 发现名称、能力和实际调用地址,
@@ -279,6 +291,7 @@ A2A 使用官方 `@a2a-js/sdk` 客户端,通过完整的 Agent Card URL 发现
279
291
  {
280
292
  "protocol": "a2a",
281
293
  "name": "Research Agent",
294
+ "instructions": "需要用户确认时保持任务等待,并使用 Agent 的 input-required 能力。",
282
295
  "agentCardUrl": "http://192.168.1.20:8080/.well-known/agent-card.json",
283
296
  "preferredTransport": "auto",
284
297
  "auth": {
@@ -328,6 +341,34 @@ HTTP 上传总上限由 `maxAttachmentBytes` 控制,默认 32 MiB,允许调
328
341
  输出文件发布由 Gateway 自己的 `artifactStoragePath` 管理,默认单文件上限 512 MiB、链接有效期
329
342
  24 小时。上传和复制均使用流,不把文件编码进 JSON;公开 URL 使用 256 位随机 token,过期文件由
330
343
  后台清理。ACP/A2A 返回的内联二进制 Artifact 也会先落入该仓库,再在 Session 响应中改为 URL。
344
+ 客户端应使用 Artifact 的 `mediaType` 选择展示方式,而不是把所有产物都当作普通文件:音频使用原生
345
+ 音频元素、视频使用原生视频元素,其他内容使用文件元素。`koishi-plugin-agent-nexus` 会分别调用
346
+ Koishi 的 `h.audio`、`h.video`、`h.image` 和 `h.file`;URL 只作为底层传输地址,大文件不会在客户端
347
+ 重新编码成 Base64。
348
+
349
+ ### 多轮输入与确认
350
+
351
+ ACP elicitation、ACP permission request 和 A2A `input_required` 都会让 Session 进入等待状态,并在
352
+ Session 响应的 `pendingRequest` 中返回等待提示和可选项。客户端只需重复调用同一个
353
+ `POST /v1/sessions/:id/message`:
354
+
355
+ ~~~text
356
+ POST /v1/sessions/:id/message
357
+ {"message":"第一个"}
358
+ ~~~
359
+
360
+ Gateway 会复用原来的协议 Session/Task/Context,不会创建新任务。Agent 可以在下一轮再次进入
361
+ `input_required`,因此套餐选择、堂食方式、取餐时间和支付完成可以组成一条连续流程。需要表达业务
362
+ 授权时,客户端可以发送可选项的数字序号、ID 或名称;Gateway 也会将常见的 `同意`、`允许`、`可以`、
363
+ `确认`、`yes` 和 `allow` 映射到安全的允许项。无法识别的授权答案会返回 400,并保留等待状态,便于
364
+ 客户端根据 `pendingRequest.options` 重新提示用户。
365
+ 步骤时,可在 `pendingRequest` 中提供可选的 `step`、`inputType` 和 JSON-safe `metadata`;这些字段
366
+ 不应放入密钥或其他敏感信息。支付完成消息仍必须由上游 MCP 根据订单/支付状态核验,不能只信任用户文本。
367
+
368
+ 对 ACP Agent,Gateway 会在 Session 首次 prompt 前注入内置交互规范;对 A2A Agent,则把同一规范放在首个
369
+ 用户消息的前缀中。由于 A2A/ACP 的 system-message 能力在不同 Agent 实现中并不统一,这是一种兼容性更好的
370
+ 宿主提示方式。它不能替代 Agent 对 elicitation 或 `input-required` 的实现:Agent 必须真正发起协议级等待,
371
+ Gateway 才能暂停 Session 并在用户回复后继续。
331
372
 
332
373
  API Key 与 A2A 认证值支持 `env:VAR`。Console Password 哈希由 WebUI 管理,不要手工生成或把
333
374
  旧 `authToken` 复制到该字段。
@@ -10,6 +10,8 @@ export declare class A2AClientRuntime implements AgentSessionRuntime {
10
10
  private taskId;
11
11
  private contextId;
12
12
  private prompting;
13
+ private firstPrompt;
14
+ private readonly instructions;
13
15
  private disposed;
14
16
  private activeController?;
15
17
  private readonly artifactCache;
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { Role, TaskState } from '@a2a-js/sdk';
3
3
  import { ClientFactory, DefaultAgentCardResolver, JsonRpcTransportFactory, RestTransportFactory } from '@a2a-js/sdk/client';
4
+ import { buildAgentInstructions, composeInitialAgentPrompt } from '../agent-instructions.js';
4
5
  export async function probeA2AAgent(id, config) {
5
6
  const startedAt = Date.now();
6
7
  if (config.enabled === false)
@@ -43,6 +44,8 @@ export class A2AClientRuntime {
43
44
  taskId = '';
44
45
  contextId = '';
45
46
  prompting = false;
47
+ firstPrompt = true;
48
+ instructions;
46
49
  disposed = false;
47
50
  activeController;
48
51
  artifactCache = new Map();
@@ -51,6 +54,7 @@ export class A2AClientRuntime {
51
54
  this.config = config;
52
55
  this.sink = sink;
53
56
  this.promptTimeoutMs = promptTimeoutMs;
57
+ this.instructions = buildAgentInstructions(config.instructions);
54
58
  }
55
59
  async start() {
56
60
  const resolver = createResolver(this.config);
@@ -78,12 +82,16 @@ export class A2AClientRuntime {
78
82
  this.prompting = true;
79
83
  this.sink.clearPending();
80
84
  this.sink.setState('running');
85
+ const promptMessage = this.firstPrompt
86
+ ? composeInitialAgentPrompt(this.instructions, message)
87
+ : message;
88
+ this.firstPrompt = false;
81
89
  const controller = new AbortController();
82
90
  this.activeController = controller;
83
91
  const timer = setTimeout(() => controller.abort(new Error('A2A message timed out')), Math.min(this.promptTimeoutMs, this.config.timeoutMs || this.promptTimeoutMs));
84
92
  timer.unref?.();
85
93
  try {
86
- const request = this.messageRequest(message, attachments);
94
+ const request = this.messageRequest(promptMessage, attachments);
87
95
  for await (const response of this.client.sendMessageStream(request, {
88
96
  signal: controller.signal
89
97
  })) {
@@ -1,6 +1,9 @@
1
1
  import type { AgentDriver } from '../drivers/index.js';
2
2
  import type { AcpSessionSink } from '../session-contract.js';
3
3
  import type { AgentdInputAttachment } from '../types.js';
4
+ export declare class InvalidPermissionAnswerError extends Error {
5
+ constructor(message: string);
6
+ }
4
7
  export declare class AcpProcessRuntime {
5
8
  private readonly driver;
6
9
  private readonly sink;
@@ -12,9 +15,11 @@ export declare class AcpProcessRuntime {
12
15
  private prompting;
13
16
  private readonly maxStderrChunkChars;
14
17
  private readonly promptTimeoutMs;
18
+ private readonly instructions;
19
+ private firstPrompt;
15
20
  private promptCapabilities;
16
21
  private inputDirectory?;
17
- constructor(driver: AgentDriver, sink: AcpSessionSink, maxStderrChunkChars?: number, promptTimeoutMs?: number);
22
+ constructor(driver: AgentDriver, sink: AcpSessionSink, maxStderrChunkChars?: number, promptTimeoutMs?: number, customInstructions?: string);
18
23
  start(workspace: string): Promise<void>;
19
24
  prompt(message: string, attachments?: AgentdInputAttachment[]): Promise<void>;
20
25
  respondPending(message: string, _attachments?: AgentdInputAttachment[]): Promise<void>;
@@ -4,6 +4,13 @@ import path from 'node:path';
4
4
  import { Readable, Writable } from 'node:stream';
5
5
  import { pathToFileURL } from 'node:url';
6
6
  import * as acp from '@agentclientprotocol/sdk';
7
+ import { buildAgentInstructions, composeInitialAgentPrompt } from '../agent-instructions.js';
8
+ export class InvalidPermissionAnswerError extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = 'InvalidPermissionAnswerError';
12
+ }
13
+ }
7
14
  export class AcpProcessRuntime {
8
15
  driver;
9
16
  sink;
@@ -15,9 +22,11 @@ export class AcpProcessRuntime {
15
22
  prompting = false;
16
23
  maxStderrChunkChars;
17
24
  promptTimeoutMs;
25
+ instructions;
26
+ firstPrompt = true;
18
27
  promptCapabilities = {};
19
28
  inputDirectory;
20
- constructor(driver, sink, maxStderrChunkChars = 16 * 1024, promptTimeoutMs = 30 * 60 * 1000) {
29
+ constructor(driver, sink, maxStderrChunkChars = 16 * 1024, promptTimeoutMs = 30 * 60 * 1000, customInstructions) {
21
30
  this.driver = driver;
22
31
  this.sink = sink;
23
32
  const limit = Number.isFinite(maxStderrChunkChars)
@@ -25,6 +34,7 @@ export class AcpProcessRuntime {
25
34
  : 16 * 1024;
26
35
  this.maxStderrChunkChars = Math.max(1, Math.min(16 * 1024, limit));
27
36
  this.promptTimeoutMs = Math.max(10_000, Math.trunc(promptTimeoutMs));
37
+ this.instructions = buildAgentInstructions(customInstructions);
28
38
  }
29
39
  async start(workspace) {
30
40
  if (this.process)
@@ -86,10 +96,14 @@ export class AcpProcessRuntime {
86
96
  this.prompting = true;
87
97
  this.sink.clearPending();
88
98
  this.sink.setState('running');
99
+ const promptMessage = this.firstPrompt
100
+ ? composeInitialAgentPrompt(this.instructions, message)
101
+ : message;
102
+ this.firstPrompt = false;
89
103
  try {
90
104
  const response = await withTimeout(this.connection.agent.request(acp.methods.agent.session.prompt, {
91
105
  sessionId,
92
- prompt: await this.promptBlocks(message, attachments)
106
+ prompt: await this.promptBlocks(promptMessage, attachments)
93
107
  }), this.promptTimeoutMs, 'ACP prompt timed out');
94
108
  if (this.sink.state === 'canceled' || this.sink.state === 'failed') {
95
109
  return;
@@ -133,9 +147,9 @@ export class AcpProcessRuntime {
133
147
  const option = Number.isInteger(numeric) && numeric >= 1
134
148
  ? options[numeric - 1]
135
149
  : options.find((item) => item.id.toLowerCase() === normalized ||
136
- item.name.toLowerCase() === normalized);
150
+ item.name.toLowerCase() === normalized) || permissionApprovalOption(normalized, options);
137
151
  if (!option) {
138
- throw new Error(`Permission answer must be an option id/name or index: ${options
152
+ throw new InvalidPermissionAnswerError(`Permission answer must be an option id/name or index: ${options
139
153
  .map((item, index) => `${index + 1}. ${item.name} (${item.id})`)
140
154
  .join('; ')}`);
141
155
  }
@@ -207,6 +221,18 @@ export class AcpProcessRuntime {
207
221
  }
208
222
  : { outcome: { outcome: 'cancelled' } };
209
223
  }
224
+ if (this.driver.permissionPolicy === 'allow') {
225
+ const allow = params.options.find((option) => option.kind.toLowerCase() === 'allow_once') ||
226
+ params.options.find((option) => option.kind.toLowerCase().startsWith('allow'));
227
+ return allow
228
+ ? {
229
+ outcome: {
230
+ outcome: 'selected',
231
+ optionId: allow.optionId
232
+ }
233
+ }
234
+ : { outcome: { outcome: 'cancelled' } };
235
+ }
210
236
  if (this.pending) {
211
237
  return { outcome: { outcome: 'cancelled' } };
212
238
  }
@@ -451,6 +477,39 @@ export class AcpProcessRuntime {
451
477
  };
452
478
  }
453
479
  }
480
+ function permissionApprovalOption(answer, options) {
481
+ const approvals = new Set([
482
+ 'yes',
483
+ 'y',
484
+ 'ok',
485
+ 'okay',
486
+ 'approve',
487
+ 'approved',
488
+ 'accept',
489
+ 'allow',
490
+ 'proceed',
491
+ '同意',
492
+ '允许',
493
+ '允许一次',
494
+ '确认',
495
+ '好的',
496
+ '可以',
497
+ '继续',
498
+ '是',
499
+ '是的'
500
+ ]);
501
+ if (!approvals.has(answer))
502
+ return undefined;
503
+ return (options.find((item) => item.kind?.toLowerCase() === 'allow_once') ||
504
+ options.find((item) => isApprovalOption(item)));
505
+ }
506
+ function isApprovalOption(option) {
507
+ const kind = option.kind?.toLowerCase() || '';
508
+ if (kind.startsWith('allow'))
509
+ return true;
510
+ const values = [option.id, option.name].map((value) => value.trim().toLowerCase());
511
+ return values.some((value) => ['allow', 'approve', 'accept', 'permit', '允许', '同意'].some((word) => value === word || value.startsWith(`${word} `) || value.startsWith(`${word}_`)));
512
+ }
454
513
  class PromptTimeoutError extends Error {
455
514
  }
456
515
  function safeFilename(value) {
@@ -0,0 +1,11 @@
1
+ export declare const MAX_AGENT_INSTRUCTIONS_CHARS: number;
2
+ /**
3
+ * Baseline guidance for Agents running behind Agent Nexus.
4
+ *
5
+ * This is delivered as the first prompt because ACP and A2A do not expose a
6
+ * portable system-message field across all supported Agent implementations.
7
+ */
8
+ export declare const DEFAULT_AGENT_INSTRUCTIONS: string;
9
+ export declare function normalizeAgentInstructions(value: unknown, field?: string): string | undefined;
10
+ export declare function buildAgentInstructions(custom?: string): string;
11
+ export declare function composeInitialAgentPrompt(instructions: string, userMessage: string): string;
@@ -0,0 +1,44 @@
1
+ export const MAX_AGENT_INSTRUCTIONS_CHARS = 32 * 1024;
2
+ /**
3
+ * Baseline guidance for Agents running behind Agent Nexus.
4
+ *
5
+ * This is delivered as the first prompt because ACP and A2A do not expose a
6
+ * portable system-message field across all supported Agent implementations.
7
+ */
8
+ export const DEFAULT_AGENT_INSTRUCTIONS = [
9
+ 'You are running behind the Agent Nexus conversational host.',
10
+ 'When an external tool or operation needs the user to choose, confirm, authorize, pay, or provide more information before it can continue:',
11
+ '1. Do not present the task as completed.',
12
+ '2. Use the host\'s native user-input, elicitation, or permission request mechanism when available; do not only print a normal-text question and stop.',
13
+ '3. Include a concise prompt and structured options when the mechanism supports them. For payment, include the payment URL and stable order metadata.',
14
+ '4. After the user replies, validate the result with the upstream tool. Never treat a message such as "payment completed" as proof that payment succeeded.',
15
+ 'Only finish the task after the external operation has actually completed.'
16
+ ].join('\n');
17
+ export function normalizeAgentInstructions(value, field = 'instructions') {
18
+ if (value === undefined || value === null)
19
+ return undefined;
20
+ if (typeof value !== 'string')
21
+ throw new Error(`${field} must be a string`);
22
+ const text = value.trim();
23
+ if (text.length > MAX_AGENT_INSTRUCTIONS_CHARS) {
24
+ throw new Error(`${field} must be at most ${MAX_AGENT_INSTRUCTIONS_CHARS} characters`);
25
+ }
26
+ return text || undefined;
27
+ }
28
+ export function buildAgentInstructions(custom) {
29
+ const extra = normalizeAgentInstructions(custom);
30
+ return extra
31
+ ? `${DEFAULT_AGENT_INSTRUCTIONS}\n\nAdditional agent-specific instructions:\n${extra}`
32
+ : DEFAULT_AGENT_INSTRUCTIONS;
33
+ }
34
+ export function composeInitialAgentPrompt(instructions, userMessage) {
35
+ return [
36
+ '<agent-nexus-host-instructions>',
37
+ instructions,
38
+ '</agent-nexus-host-instructions>',
39
+ '',
40
+ '<user-request>',
41
+ userMessage,
42
+ '</user-request>'
43
+ ].join('\n');
44
+ }
package/dist/config.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { mkdir, open, readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { normalizeAgentInstructions } from './agent-instructions.js';
3
4
  import { agentdDriverKinds } from './types.js';
4
5
  export async function ensureAgentdConfig(filePath, options = {}) {
5
6
  const absolute = path.resolve(filePath);
@@ -140,7 +141,9 @@ function parseAgent(id, value) {
140
141
  throw new Error(`Unsupported nexus-agentd driver: ${driver || id}`);
141
142
  }
142
143
  const permissionPolicy = optionalString(input.permissionPolicy, `agents.${id}.permissionPolicy`) || 'ask';
143
- if (permissionPolicy !== 'ask' && permissionPolicy !== 'deny') {
144
+ if (permissionPolicy !== 'ask' &&
145
+ permissionPolicy !== 'allow' &&
146
+ permissionPolicy !== 'deny') {
144
147
  throw new Error(`Invalid permissionPolicy for ${id}`);
145
148
  }
146
149
  const result = {
@@ -148,6 +151,7 @@ function parseAgent(id, value) {
148
151
  driver,
149
152
  name: optionalString(input.name, `agents.${id}.name`),
150
153
  description: optionalString(input.description, `agents.${id}.description`),
154
+ instructions: normalizeAgentInstructions(input.instructions, `agents.${id}.instructions`),
151
155
  enabled: optionalBoolean(input.enabled, `agents.${id}.enabled`) ?? true,
152
156
  workspace: optionalString(input.workspace, `agents.${id}.workspace`),
153
157
  command: optionalString(input.command, `agents.${id}.command`),
@@ -203,6 +207,7 @@ function parseA2AAgent(id, input) {
203
207
  protocol: 'a2a',
204
208
  name: optionalString(input.name, `agents.${id}.name`),
205
209
  description: optionalString(input.description, `agents.${id}.description`),
210
+ instructions: normalizeAgentInstructions(input.instructions, `agents.${id}.instructions`),
206
211
  enabled: optionalBoolean(input.enabled, `agents.${id}.enabled`) ?? true,
207
212
  agentCardUrl,
208
213
  agentUrl,
@@ -5,6 +5,7 @@ export interface AgentdAgentUpdate {
5
5
  driver?: AgentdDriverKind;
6
6
  name?: string;
7
7
  description?: string;
8
+ instructions?: string;
8
9
  enabled?: boolean;
9
10
  workspace?: string;
10
11
  permissionPolicy?: PermissionPolicy;
@@ -2,6 +2,7 @@ import { randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
2
2
  import { chmod, open, readFile, rename, rm } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { hashAdminPassword, validateAdminPassword, verifyAdminPassword } from './auth.js';
5
+ import { normalizeAgentInstructions } from './agent-instructions.js';
5
6
  import { loadAgentdConfig } from './config.js';
6
7
  import { createDriverRegistry } from './drivers/index.js';
7
8
  import { agentdDriverKinds } from './types.js';
@@ -311,6 +312,7 @@ export class AgentdControlPlane {
311
312
  next.name = cleanString(update.name) || cleanString(previous.name);
312
313
  }
313
314
  }
315
+ setOptionalInstructions(next, update.instructions ?? cleanString(previous.instructions));
314
316
  setOptionalString(next, 'description', update.description ?? cleanString(previous.description));
315
317
  agents[id] = next;
316
318
  raw.agents = agents;
@@ -448,6 +450,7 @@ export class AgentdControlPlane {
448
450
  protocol: 'a2a',
449
451
  name: config.name || id,
450
452
  description: config.description,
453
+ instructions: config.instructions,
451
454
  enabled: config.enabled !== false,
452
455
  agentCardUrl: config.agentCardUrl || defaultAgentCardUrl(config.agentUrl),
453
456
  agentUrl: config.agentUrl,
@@ -466,6 +469,7 @@ export class AgentdControlPlane {
466
469
  driver: config.driver,
467
470
  name: config.name || id,
468
471
  description: config.description,
472
+ instructions: config.instructions,
469
473
  enabled: config.enabled !== false,
470
474
  workspace: config.workspace || this.config.workspaceRoots[0] || '',
471
475
  permissionPolicy: config.permissionPolicy || 'ask',
@@ -501,8 +505,9 @@ function validateUpdate(update) {
501
505
  }
502
506
  if (update.permissionPolicy !== undefined &&
503
507
  update.permissionPolicy !== 'ask' &&
508
+ update.permissionPolicy !== 'allow' &&
504
509
  update.permissionPolicy !== 'deny') {
505
- throw new ControlPlaneError(400, 'permissionPolicy must be ask or deny');
510
+ throw new ControlPlaneError(400, 'permissionPolicy must be ask, allow, or deny');
506
511
  }
507
512
  if (update.preferredTransport !== undefined &&
508
513
  !['auto', 'jsonrpc', 'http-json'].includes(update.preferredTransport)) {
@@ -607,6 +612,19 @@ function setOptionalString(target, key, value) {
607
612
  else
608
613
  delete target[key];
609
614
  }
615
+ function setOptionalInstructions(target, value) {
616
+ let text;
617
+ try {
618
+ text = normalizeAgentInstructions(value);
619
+ }
620
+ catch (error) {
621
+ throw new ControlPlaneError(400, errorMessage(error));
622
+ }
623
+ if (text)
624
+ target.instructions = text;
625
+ else
626
+ delete target.instructions;
627
+ }
610
628
  function validHeaderName(value) {
611
629
  return /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/.test(value);
612
630
  }
package/dist/server.js CHANGED
@@ -441,6 +441,7 @@ function readAgentUpdate(body) {
441
441
  'driver',
442
442
  'name',
443
443
  'description',
444
+ 'instructions',
444
445
  'enabled',
445
446
  'workspace',
446
447
  'permissionPolicy',
@@ -458,6 +459,7 @@ function readAgentUpdate(body) {
458
459
  driver: optionalString(body.driver),
459
460
  name: optionalString(body.name),
460
461
  description: optionalString(body.description),
462
+ instructions: optionalString(body.instructions),
461
463
  enabled: optionalBoolean(body.enabled, 'enabled'),
462
464
  workspace: optionalString(body.workspace),
463
465
  permissionPolicy: optionalString(body.permissionPolicy),
package/dist/session.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { AcpProcessRuntime } from './acp/runtime.js';
2
+ import { AcpProcessRuntime, InvalidPermissionAnswerError } from './acp/runtime.js';
3
3
  import { A2AClientRuntime, probeA2AAgent } from './a2a/runtime.js';
4
4
  import { SessionEventLog } from './events.js';
5
5
  const MAX_SESSION_ARTIFACTS = 64;
@@ -170,7 +170,15 @@ export class ManagedSession {
170
170
  this.syncRun({
171
171
  progress: { phase: '继续执行', message: '已提交补充信息' }
172
172
  });
173
- await this.runtime.respondPending(message, attachments);
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
+ }
174
182
  return;
175
183
  }
176
184
  if (this.state === 'running') {
@@ -374,7 +382,7 @@ export class SessionManager {
374
382
  const session = new ManagedSession(agentId, config.protocol === 'a2a' ? 'a2a' : 'acp', workspace, ownerKeyId, this.config.maxEventsPerSession, this.config.maxOutputChars, config.name || agentId, this.runStore);
375
383
  const runtime = config.protocol === 'a2a'
376
384
  ? new A2AClientRuntime(config, session, this.config.promptTimeoutMs || 30 * 60_000)
377
- : new AcpProcessRuntime(driver, session, this.config.maxOutputChars, this.config.promptTimeoutMs || 30 * 60_000);
385
+ : new AcpProcessRuntime(driver, session, this.config.maxOutputChars, this.config.promptTimeoutMs || 30 * 60_000, config.instructions);
378
386
  this.sessions.set(session.id, session);
379
387
  session.attach(runtime);
380
388
  try {
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' | 'deny';
3
+ export type PermissionPolicy = 'ask' | 'allow' | '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,6 +14,8 @@ 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;
17
19
  permissionPolicy?: PermissionPolicy;
18
20
  permissionTimeoutMs?: number;
19
21
  }
@@ -23,6 +25,8 @@ export interface AgentdA2AConfig {
23
25
  protocol: 'a2a';
24
26
  name?: string;
25
27
  description?: string;
28
+ /** Optional extra instructions sent with the first A2A user message. */
29
+ instructions?: string;
26
30
  enabled?: boolean;
27
31
  /** Full Agent Card JSON URL. Preferred for new configurations. */
28
32
  agentCardUrl?: string;
@@ -128,6 +132,7 @@ export interface AgentdAgentConfigView {
128
132
  protocol: AgentdProtocol;
129
133
  name: string;
130
134
  description?: string;
135
+ instructions?: string;
131
136
  enabled: boolean;
132
137
  driver?: AgentdDriverKind;
133
138
  workspace?: string;
@@ -181,11 +186,17 @@ export interface AgentdPendingRequest {
181
186
  id: string;
182
187
  kind: 'permission' | 'input';
183
188
  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';
184
193
  options?: Array<{
185
194
  id: string;
186
195
  name: string;
187
196
  kind?: string;
188
197
  }>;
198
+ /** JSON-safe metadata for the owning integration. Never put secrets here. */
199
+ metadata?: Record<string, unknown>;
189
200
  }
190
201
  export interface AgentdArtifact {
191
202
  id?: string;
package/dist/webui/app.js CHANGED
@@ -456,6 +456,7 @@ export const app = String.raw `
456
456
 
457
457
  function permissionLabel(value) {
458
458
  if (value === 'ask') return '询问'
459
+ if (value === 'allow') return '始终允许'
459
460
  if (value === 'deny') return '拒绝'
460
461
  return value || '—'
461
462
  }
@@ -672,7 +673,8 @@ export const app = String.raw `
672
673
  '<div class="field-row"><label>协议<select name="protocol"><option value="acp"' + selected(current.protocol, 'acp') + '>ACP</option><option value="a2a"' + selected(current.protocol, 'a2a') + '>A2A</option></select></label><label class="checkbox"><input name="enabled" type="checkbox"' + checked(current.enabled) + '>启用</label></div>' +
673
674
  '<label>名称<input name="name" value="' + escapeHtml(current.name || '') + '" required></label>' +
674
675
  '<label>描述<textarea name="description">' + escapeHtml(current.description || '') + '</textarea></label>' +
675
- '<div data-protocol-section="acp"><label>驱动<select name="driver">' + drivers + '</select></label><label>工作区<input name="workspace" list="workspace-roots" value="' + escapeHtml(current.workspace || state.config.workspaceRoots[0] || '') + '" required><datalist id="workspace-roots">' + roots + '</datalist></label><div class="field-row"><label>权限策略<select name="permissionPolicy"><option value="ask"' + selected(current.permissionPolicy, 'ask') + '>询问</option><option value="deny"' + selected(current.permissionPolicy, 'deny') + '>拒绝</option></select></label><label>权限确认超时(秒)<input name="permissionTimeoutMs" type="number" min="1" max="86400" step="1" value="' + escapeHtml(permissionTimeoutSeconds) + '"><small class="field-help">单位:秒;默认 900 秒,最长 24 小时。</small></label></div></div>' +
676
+ '<label>Agent 交互指令<textarea name="instructions" rows="8" placeholder="可选:补充这个 Agent 的专属规则。Gateway 会自动注入多轮确认和用户输入规范。">' + escapeHtml(current.instructions || '') + '</textarea><small class="field-help">可选补充规则,最多 32768 个字符;不要填写密钥等敏感信息。</small></label>' +
677
+ '<div data-protocol-section="acp"><label>驱动<select name="driver">' + drivers + '</select></label><label>工作区<input name="workspace" list="workspace-roots" value="' + escapeHtml(current.workspace || state.config.workspaceRoots[0] || '') + '" required><datalist id="workspace-roots">' + roots + '</datalist></label><div class="field-row"><label>权限策略<select name="permissionPolicy"><option value="ask"' + selected(current.permissionPolicy, 'ask') + '>询问</option><option value="allow"' + selected(current.permissionPolicy, 'allow') + '>始终允许</option><option value="deny"' + selected(current.permissionPolicy, 'deny') + '>拒绝</option></select></label><label>权限确认超时(秒)<input name="permissionTimeoutMs" type="number" min="1" max="86400" step="1" value="' + escapeHtml(permissionTimeoutSeconds) + '"><small class="field-help">单位:秒;默认 900 秒,最长 24 小时;“始终允许”不会等待确认。</small></label></div></div>' +
676
678
  '<div data-protocol-section="a2a"><label>Agent Card URL<input name="agentCardUrl" type="url" value="' + escapeHtml(current.agentCardUrl || '') + '" placeholder="http://agent.local:8080/.well-known/agent-card.json" required><small class="field-help">填写完整的 Agent Card JSON 地址;调用地址和能力将从 Card 自动发现。</small></label><div class="field-row"><label>首选传输<select name="preferredTransport"><option value="auto"' + selected(current.preferredTransport || 'auto', 'auto') + '>自动(按 Card)</option><option value="jsonrpc"' + selected(current.preferredTransport, 'jsonrpc') + '>JSON-RPC</option><option value="http-json"' + selected(current.preferredTransport, 'http-json') + '>HTTP+JSON</option></select></label><label>认证方式<select name="authType"><option value="none"' + selected(current.auth && current.auth.type || 'none', 'none') + '>无认证</option><option value="bearer"' + selected(current.auth && current.auth.type, 'bearer') + '>Bearer Token</option><option value="header"' + selected(current.auth && current.auth.type, 'header') + '>自定义请求头</option></select></label></div><label data-auth-header>请求头名称<input name="authHeaderName" value="' + escapeHtml(current.auth && current.auth.headerName || '') + '" placeholder="X-API-Key"></label><label data-auth-value><span data-auth-value-label>认证凭据</span><input name="authValue" type="password" autocomplete="off" placeholder="' + (editing && current.auth && current.auth.configured ? '留空以保留当前凭据' : '') + '"></label><label>请求超时(秒)<input name="timeoutMs" type="number" min="1" max="1800" step="1" value="' + escapeHtml(requestTimeoutSeconds) + '"><small class="field-help">单位:秒;默认 60 秒;用于限制这个 A2A Agent 的单次请求,最大 1800 秒(30 分钟)。</small></label></div>'
677
679
  openDrawer(editing ? '编辑智能体' : '添加智能体', body, editing ? '保存修改' : '添加智能体', async (form) => {
678
680
  const data = new FormData(form)
@@ -682,6 +684,7 @@ export const app = String.raw `
682
684
  protocol,
683
685
  name: String(data.get('name') || '').trim(),
684
686
  description: String(data.get('description') || '').trim(),
687
+ instructions: String(data.get('instructions') || '').trim(),
685
688
  enabled: data.get('enabled') === 'on'
686
689
  }
687
690
  if (protocol === 'acp') {
@@ -28,6 +28,7 @@
28
28
  "protocol": "acp",
29
29
  "driver": "codex",
30
30
  "name": "Codex",
31
+ "instructions": "如果外部工具需要用户选择、确认或支付,请使用宿主的用户输入机制并保持任务等待;不要只输出普通文本问题。",
31
32
  "command": "codex-acp",
32
33
  "workspace": "/data/repos/project",
33
34
  "permissionPolicy": "ask"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexus-agentd",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Local Nexus Gateway daemon that exposes allowlisted Coding Agents through HTTP/SSE and ACP",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",