nexus-agentd 0.1.7 → 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
@@ -26,8 +26,8 @@ nexus-agentd \
26
26
  --workspace "$HOME/projects"
27
27
  ```
28
28
 
29
- 当前仓库版本 `0.1.6` 已发布到 npm;可用 `npm view nexus-agentd version` 检查 registry 的 latest,
30
- 也可以将安装命令固定为 `nexus-agentd@0.1.6`。
29
+ 当前仓库版本 `0.1.8` 已发布到 npm;可用 `npm view nexus-agentd version` 检查 registry 的 latest,
30
+ 也可以将安装命令固定为 `nexus-agentd@0.1.8`。
31
31
 
32
32
  ## 安装与部署
33
33
 
@@ -42,15 +42,15 @@ Gateway 不会替你安装 Agent,也不会替你执行 OpenCode、Claude Code
42
42
 
43
43
  ### npm 包安装(已发布版本)
44
44
 
45
- `0.1.6` 已发布到 npm。目标版本尚未发布时,请使用下面的源码部署,或将本仓库打包后的 tarball 安装到目标机器。
45
+ `0.1.8` 已发布到 npm。目标版本尚未发布时,请使用下面的源码部署,或将本仓库打包后的 tarball 安装到目标机器。
46
46
 
47
- 发布状态可用 `npm view nexus-agentd version` 检查;生产环境需要可复现部署时,建议固定为 `nexus-agentd@0.1.6`。
47
+ 发布状态可用 `npm view nexus-agentd version` 检查;生产环境需要可复现部署时,建议固定为 `nexus-agentd@0.1.8`。
48
48
 
49
49
  生产环境建议使用专用的低权限系统用户,并把 npm 全局包安装到用户目录:
50
50
 
51
51
  ~~~bash
52
52
  NPM_CONFIG_PREFIX="$HOME/.local"
53
- npm install --global --prefix "$NPM_CONFIG_PREFIX" nexus-agentd@0.1.6
53
+ npm install --global --prefix "$NPM_CONFIG_PREFIX" nexus-agentd@0.1.8
54
54
  export PATH="$NPM_CONFIG_PREFIX/bin:$PATH"
55
55
  ~~~
56
56
 
@@ -269,6 +269,11 @@ command -v hermes && hermes acp --check
269
269
 
270
270
  `workspace` 必须位于 `workspaceRoots` 之下;如果只通过 WebUI 配置,Gateway 会校验并保存这些字段。
271
271
 
272
+ ACP Agent 的 `permissionPolicy` 有三种取值:`ask`(默认,每次权限请求交给用户确认)、`allow`(始终允许,
273
+ 自动选择 ACP 返回的允许项,不创建等待确认的请求)和 `deny`(始终拒绝,自动选择拒绝项)。`allow` 会放行该
274
+ Agent 发起的所有 ACP 权限请求,请只对可信的 Agent 和工作区使用;没有明确的允许项时 Gateway 会安全地取消请求。
275
+ 该字段只适用于 ACP,A2A Agent 不使用本地 ACP 权限策略。
276
+
272
277
  Gateway 会在每个 Agent Session 的首次请求前自动注入一段 Agent Nexus 交互规范,提醒 Agent 在需要
273
278
  用户选择、确认、支付或补充信息时使用 ACP elicitation/A2A `input-required`,不要只输出普通文本问题。
274
279
  这段规范不需要为 OpenCode、Claude Code 或 Hermes 手工重复配置。若某个 Agent 还需要额外规则,可在 Agent
@@ -336,6 +341,10 @@ HTTP 上传总上限由 `maxAttachmentBytes` 控制,默认 32 MiB,允许调
336
341
  输出文件发布由 Gateway 自己的 `artifactStoragePath` 管理,默认单文件上限 512 MiB、链接有效期
337
342
  24 小时。上传和复制均使用流,不把文件编码进 JSON;公开 URL 使用 256 位随机 token,过期文件由
338
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。
339
348
 
340
349
  ### 多轮输入与确认
341
350
 
@@ -350,6 +359,9 @@ POST /v1/sessions/:id/message
350
359
 
351
360
  Gateway 会复用原来的协议 Session/Task/Context,不会创建新任务。Agent 可以在下一轮再次进入
352
361
  `input_required`,因此套餐选择、堂食方式、取餐时间和支付完成可以组成一条连续流程。需要表达业务
362
+ 授权时,客户端可以发送可选项的数字序号、ID 或名称;Gateway 也会将常见的 `同意`、`允许`、`可以`、
363
+ `确认`、`yes` 和 `allow` 映射到安全的允许项。无法识别的授权答案会返回 400,并保留等待状态,便于
364
+ 客户端根据 `pendingRequest.options` 重新提示用户。
353
365
  步骤时,可在 `pendingRequest` 中提供可选的 `step`、`inputType` 和 JSON-safe `metadata`;这些字段
354
366
  不应放入密钥或其他敏感信息。支付完成消息仍必须由上游 MCP 根据订单/支付状态核验,不能只信任用户文本。
355
367
 
@@ -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;
@@ -5,6 +5,12 @@ import { Readable, Writable } from 'node:stream';
5
5
  import { pathToFileURL } from 'node:url';
6
6
  import * as acp from '@agentclientprotocol/sdk';
7
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
+ }
8
14
  export class AcpProcessRuntime {
9
15
  driver;
10
16
  sink;
@@ -141,9 +147,9 @@ export class AcpProcessRuntime {
141
147
  const option = Number.isInteger(numeric) && numeric >= 1
142
148
  ? options[numeric - 1]
143
149
  : options.find((item) => item.id.toLowerCase() === normalized ||
144
- item.name.toLowerCase() === normalized);
150
+ item.name.toLowerCase() === normalized) || permissionApprovalOption(normalized, options);
145
151
  if (!option) {
146
- 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
147
153
  .map((item, index) => `${index + 1}. ${item.name} (${item.id})`)
148
154
  .join('; ')}`);
149
155
  }
@@ -215,6 +221,18 @@ export class AcpProcessRuntime {
215
221
  }
216
222
  : { outcome: { outcome: 'cancelled' } };
217
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
+ }
218
236
  if (this.pending) {
219
237
  return { outcome: { outcome: 'cancelled' } };
220
238
  }
@@ -459,6 +477,39 @@ export class AcpProcessRuntime {
459
477
  };
460
478
  }
461
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
+ }
462
513
  class PromptTimeoutError extends Error {
463
514
  }
464
515
  function safeFilename(value) {
package/dist/config.js CHANGED
@@ -141,7 +141,9 @@ function parseAgent(id, value) {
141
141
  throw new Error(`Unsupported nexus-agentd driver: ${driver || id}`);
142
142
  }
143
143
  const permissionPolicy = optionalString(input.permissionPolicy, `agents.${id}.permissionPolicy`) || 'ask';
144
- if (permissionPolicy !== 'ask' && permissionPolicy !== 'deny') {
144
+ if (permissionPolicy !== 'ask' &&
145
+ permissionPolicy !== 'allow' &&
146
+ permissionPolicy !== 'deny') {
145
147
  throw new Error(`Invalid permissionPolicy for ${id}`);
146
148
  }
147
149
  const result = {
@@ -505,8 +505,9 @@ function validateUpdate(update) {
505
505
  }
506
506
  if (update.permissionPolicy !== undefined &&
507
507
  update.permissionPolicy !== 'ask' &&
508
+ update.permissionPolicy !== 'allow' &&
508
509
  update.permissionPolicy !== 'deny') {
509
- throw new ControlPlaneError(400, 'permissionPolicy must be ask or deny');
510
+ throw new ControlPlaneError(400, 'permissionPolicy must be ask, allow, or deny');
510
511
  }
511
512
  if (update.preferredTransport !== undefined &&
512
513
  !['auto', 'jsonrpc', 'http-json'].includes(update.preferredTransport)) {
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') {
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 {
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
  }
@@ -673,7 +674,7 @@ export const app = String.raw `
673
674
  '<label>名称<input name="name" value="' + escapeHtml(current.name || '') + '" required></label>' +
674
675
  '<label>描述<textarea name="description">' + escapeHtml(current.description || '') + '</textarea></label>' +
675
676
  '<label>Agent 交互指令<textarea name="instructions" rows="8" placeholder="可选:补充这个 Agent 的专属规则。Gateway 会自动注入多轮确认和用户输入规范。">' + escapeHtml(current.instructions || '') + '</textarea><small class="field-help">可选补充规则,最多 32768 个字符;不要填写密钥等敏感信息。</small></label>' +
676
- '<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>' +
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>' +
677
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>'
678
679
  openDrawer(editing ? '编辑智能体' : '添加智能体', body, editing ? '保存修改' : '添加智能体', async (form) => {
679
680
  const data = new FormData(form)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexus-agentd",
3
- "version": "0.1.7",
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",