nexus-agentd 0.2.0 → 0.2.2
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 +8 -3
- package/dist/acp/runtime.js +21 -6
- package/dist/config.js +3 -1
- package/dist/control-plane.js +2 -1
- package/dist/drivers/pi.d.ts +1 -0
- package/dist/drivers/stdio.js +2 -0
- package/dist/drivers/types.d.ts +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +18 -4
- package/dist/process-tree.d.ts +5 -0
- package/dist/process-tree.js +105 -0
- package/dist/types.d.ts +1 -1
- package/dist/webui/app.js +5 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,8 +97,12 @@ npm install -g \
|
|
|
97
97
|
pi-acp
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
`command`、`args`、`inheritEnv` 和 `env` 是仅可在本机配置文件修改的高级字段,WebUI 不接受这些
|
|
101
|
-
字段。Workspace 在启动进程前经过 `realpath` 边界校验。
|
|
100
|
+
`command`、`args`、`inheritEnv` 和 `env` 是仅可在本机配置文件修改的高级字段,WebUI 不接受这些
|
|
101
|
+
字段。Workspace 在启动进程前经过 `realpath` 边界校验。
|
|
102
|
+
|
|
103
|
+
ACP 权限策略支持 `ask`(询问)、`allow`(始终允许)和 `deny`(拒绝)。`allow` 会优先选择
|
|
104
|
+
Agent 提供的 `allow_once`,不创建待确认请求;它适合管理员明确授权、工作区边界可信的自动化 Agent。
|
|
105
|
+
默认仍为 `ask`。
|
|
102
106
|
|
|
103
107
|
### A2A
|
|
104
108
|
|
|
@@ -215,7 +219,8 @@ GET /v1/sessions/:id/events
|
|
|
215
219
|
`/v1/meta` 和 Session 响应包含 Gateway `instanceId`;完成的 Session 还包含与当前 Run 绑定的
|
|
216
220
|
`completion` 证明,客户端可识别进程重启和迟到/伪造的完成状态。授权与输入通过精确的
|
|
217
221
|
`requestId` 解析;过期 ID 返回 `409`,不会误答后续请求。`DELETE /v1/sessions/:id` 会取消活动任务、
|
|
218
|
-
释放 Agent
|
|
222
|
+
释放 Agent 整个进程组并移除内存 Session。Gateway 停止时会先终止 Session,再在有限宽限期后关闭残留
|
|
223
|
+
HTTP/SSE 连接,避免长连接或 Agent 孙进程阻塞服务重启。
|
|
219
224
|
|
|
220
225
|
API Key 的 Agent scope 在 Agent inventory、Session 创建和后续 Session 操作上都会检查;Session
|
|
221
226
|
还绑定创建它的 Key,其他 Key 即使拥有同一 Agent scope 也不能读取或控制该 Session。
|
package/dist/acp/runtime.js
CHANGED
|
@@ -4,6 +4,7 @@ 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 { terminateProcessTree } from '../process-tree.js';
|
|
7
8
|
import { TURN_COMPLETION_CONTRACT } from '../completion-contract.js';
|
|
8
9
|
export class AcpProcessRuntime {
|
|
9
10
|
driver;
|
|
@@ -61,7 +62,7 @@ export class AcpProcessRuntime {
|
|
|
61
62
|
},
|
|
62
63
|
clientInfo: {
|
|
63
64
|
name: 'nexus-agentd',
|
|
64
|
-
version: '0.2.
|
|
65
|
+
version: '0.2.2'
|
|
65
66
|
}
|
|
66
67
|
});
|
|
67
68
|
if (initialize.protocolVersion !== acp.PROTOCOL_VERSION) {
|
|
@@ -224,15 +225,17 @@ export class AcpProcessRuntime {
|
|
|
224
225
|
}
|
|
225
226
|
this.connection?.close();
|
|
226
227
|
this.connection = undefined;
|
|
228
|
+
const child = this.process;
|
|
229
|
+
this.process = undefined;
|
|
230
|
+
if (child) {
|
|
231
|
+
await terminateProcessTree(child, {
|
|
232
|
+
ownsProcessGroup: this.driver.ownsProcessGroup
|
|
233
|
+
});
|
|
234
|
+
}
|
|
227
235
|
if (this.inputDirectory) {
|
|
228
236
|
await rm(this.inputDirectory, { recursive: true, force: true }).catch(() => undefined);
|
|
229
237
|
this.inputDirectory = undefined;
|
|
230
238
|
}
|
|
231
|
-
const child = this.process;
|
|
232
|
-
this.process = undefined;
|
|
233
|
-
if (child && child.exitCode === null && child.signalCode === null) {
|
|
234
|
-
child.kill();
|
|
235
|
-
}
|
|
236
239
|
}
|
|
237
240
|
requestPermission(params) {
|
|
238
241
|
if (this.driver.permissionPolicy === 'deny') {
|
|
@@ -246,6 +249,18 @@ export class AcpProcessRuntime {
|
|
|
246
249
|
}
|
|
247
250
|
: { outcome: { outcome: 'cancelled' } };
|
|
248
251
|
}
|
|
252
|
+
if (this.driver.permissionPolicy === 'allow') {
|
|
253
|
+
const allow = params.options.find((option) => option.kind.toLowerCase() === 'allow_once') ||
|
|
254
|
+
params.options.find((option) => option.kind.toLowerCase().startsWith('allow'));
|
|
255
|
+
return allow
|
|
256
|
+
? {
|
|
257
|
+
outcome: {
|
|
258
|
+
outcome: 'selected',
|
|
259
|
+
optionId: allow.optionId
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
: { outcome: { outcome: 'cancelled' } };
|
|
263
|
+
}
|
|
249
264
|
if (this.pending) {
|
|
250
265
|
return { outcome: { outcome: 'cancelled' } };
|
|
251
266
|
}
|
package/dist/config.js
CHANGED
|
@@ -134,7 +134,9 @@ function parseAgent(id, value) {
|
|
|
134
134
|
throw new Error(`Unsupported nexus-agentd driver: ${driver || id}`);
|
|
135
135
|
}
|
|
136
136
|
const permissionPolicy = optionalString(input.permissionPolicy, `agents.${id}.permissionPolicy`) || 'ask';
|
|
137
|
-
if (permissionPolicy !== 'ask' &&
|
|
137
|
+
if (permissionPolicy !== 'ask' &&
|
|
138
|
+
permissionPolicy !== 'allow' &&
|
|
139
|
+
permissionPolicy !== 'deny') {
|
|
138
140
|
throw new Error(`Invalid permissionPolicy for ${id}`);
|
|
139
141
|
}
|
|
140
142
|
const result = {
|
package/dist/control-plane.js
CHANGED
|
@@ -501,8 +501,9 @@ function validateUpdate(update) {
|
|
|
501
501
|
}
|
|
502
502
|
if (update.permissionPolicy !== undefined &&
|
|
503
503
|
update.permissionPolicy !== 'ask' &&
|
|
504
|
+
update.permissionPolicy !== 'allow' &&
|
|
504
505
|
update.permissionPolicy !== 'deny') {
|
|
505
|
-
throw new ControlPlaneError(400, 'permissionPolicy must be ask or deny');
|
|
506
|
+
throw new ControlPlaneError(400, 'permissionPolicy must be ask, allow, or deny');
|
|
506
507
|
}
|
|
507
508
|
if (update.preferredTransport !== undefined &&
|
|
508
509
|
!['auto', 'jsonrpc', 'http-json'].includes(update.preferredTransport)) {
|
package/dist/drivers/pi.d.ts
CHANGED
|
@@ -9,5 +9,6 @@ export declare function createPiDriver(id: string, config: AgentdDriverConfig):
|
|
|
9
9
|
env: NodeJS.ProcessEnv;
|
|
10
10
|
permissionPolicy: import("../types.js").PermissionPolicy;
|
|
11
11
|
permissionTimeoutMs: number;
|
|
12
|
+
ownsProcessGroup?: boolean;
|
|
12
13
|
spawn(workspace: string): import("child_process").ChildProcessWithoutNullStreams;
|
|
13
14
|
};
|
package/dist/drivers/stdio.js
CHANGED
|
@@ -28,6 +28,7 @@ export function createStdioAcpDriver(id, config, defaults) {
|
|
|
28
28
|
env,
|
|
29
29
|
permissionPolicy: config.permissionPolicy || 'ask',
|
|
30
30
|
permissionTimeoutMs: config.permissionTimeoutMs || 15 * 60 * 1000,
|
|
31
|
+
ownsProcessGroup: process.platform !== 'win32',
|
|
31
32
|
async probe() {
|
|
32
33
|
try {
|
|
33
34
|
const version = await probeCommand(command, defaults.probeArgs || ['--version'], env, name);
|
|
@@ -55,6 +56,7 @@ export function createStdioAcpDriver(id, config, defaults) {
|
|
|
55
56
|
return spawn(command, args, {
|
|
56
57
|
cwd: workspace,
|
|
57
58
|
env,
|
|
59
|
+
detached: process.platform !== 'win32',
|
|
58
60
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
59
61
|
});
|
|
60
62
|
}
|
package/dist/drivers/types.d.ts
CHANGED
|
@@ -9,6 +9,8 @@ export interface AgentDriver {
|
|
|
9
9
|
env: NodeJS.ProcessEnv;
|
|
10
10
|
permissionPolicy: PermissionPolicy;
|
|
11
11
|
permissionTimeoutMs: number;
|
|
12
|
+
/** The spawned Agent owns a dedicated process group that can be terminated as a tree. */
|
|
13
|
+
ownsProcessGroup?: boolean;
|
|
12
14
|
probe(): Promise<AgentdAgentView>;
|
|
13
15
|
spawn(workspace: string): ChildProcessWithoutNullStreams;
|
|
14
16
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export declare function startAgentd(configPath: string): Promise<{
|
|
|
10
10
|
controlPlane: AgentdControlPlane;
|
|
11
11
|
close(): Promise<void>;
|
|
12
12
|
}>;
|
|
13
|
+
export declare function closeServer(server: Server, graceMs?: number): Promise<void>;
|
|
13
14
|
export * from './config.js';
|
|
14
15
|
export * from './control-plane.js';
|
|
15
16
|
export * from './server.js';
|
package/dist/index.js
CHANGED
|
@@ -24,8 +24,7 @@ export async function startAgentd(configPath) {
|
|
|
24
24
|
runStore,
|
|
25
25
|
controlPlane,
|
|
26
26
|
async close() {
|
|
27
|
-
await closeServer(server);
|
|
28
|
-
await sessions.shutdown();
|
|
27
|
+
await Promise.all([closeServer(server), sessions.shutdown()]);
|
|
29
28
|
await runStore.flush();
|
|
30
29
|
}
|
|
31
30
|
};
|
|
@@ -45,9 +44,24 @@ function listen(server, port, host) {
|
|
|
45
44
|
server.listen(port, host);
|
|
46
45
|
});
|
|
47
46
|
}
|
|
48
|
-
function closeServer(server) {
|
|
47
|
+
export function closeServer(server, graceMs = 2_000) {
|
|
49
48
|
return new Promise((resolve, reject) => {
|
|
50
|
-
|
|
49
|
+
let settled = false;
|
|
50
|
+
const finish = (error) => {
|
|
51
|
+
if (settled)
|
|
52
|
+
return;
|
|
53
|
+
settled = true;
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
if (error)
|
|
56
|
+
reject(error);
|
|
57
|
+
else
|
|
58
|
+
resolve();
|
|
59
|
+
};
|
|
60
|
+
const timer = setTimeout(() => {
|
|
61
|
+
server.closeAllConnections?.();
|
|
62
|
+
finish();
|
|
63
|
+
}, Math.max(0, graceMs));
|
|
64
|
+
server.close((error) => finish(error || undefined));
|
|
51
65
|
server.closeIdleConnections?.();
|
|
52
66
|
});
|
|
53
67
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
const DEFAULT_GRACE_MS = 2_000;
|
|
3
|
+
const FORCE_KILL_WAIT_MS = 1_000;
|
|
4
|
+
export async function terminateProcessTree(child, options = {}) {
|
|
5
|
+
const pid = child.pid;
|
|
6
|
+
if (!pid || hasExited(child))
|
|
7
|
+
return;
|
|
8
|
+
const graceMs = Math.max(0, options.graceMs ?? DEFAULT_GRACE_MS);
|
|
9
|
+
if (process.platform === 'win32') {
|
|
10
|
+
await taskkill(pid, false);
|
|
11
|
+
if (await waitForExit(child, graceMs))
|
|
12
|
+
return;
|
|
13
|
+
await taskkill(pid, true);
|
|
14
|
+
await waitForExit(child, FORCE_KILL_WAIT_MS);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (options.ownsProcessGroup === true) {
|
|
18
|
+
signalUnixGroup(pid, 'SIGTERM');
|
|
19
|
+
if (await waitForUnixGroupExit(pid, graceMs))
|
|
20
|
+
return;
|
|
21
|
+
signalUnixGroup(pid, 'SIGKILL');
|
|
22
|
+
await waitForUnixGroupExit(pid, FORCE_KILL_WAIT_MS);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
signalUnix(child, 'SIGTERM');
|
|
26
|
+
if (await waitForExit(child, graceMs))
|
|
27
|
+
return;
|
|
28
|
+
signalUnix(child, 'SIGKILL');
|
|
29
|
+
await waitForExit(child, FORCE_KILL_WAIT_MS);
|
|
30
|
+
}
|
|
31
|
+
function signalUnix(child, signal) {
|
|
32
|
+
try {
|
|
33
|
+
child.kill(signal);
|
|
34
|
+
}
|
|
35
|
+
catch { }
|
|
36
|
+
}
|
|
37
|
+
function signalUnixGroup(pid, signal) {
|
|
38
|
+
try {
|
|
39
|
+
process.kill(-pid, signal);
|
|
40
|
+
}
|
|
41
|
+
catch { }
|
|
42
|
+
}
|
|
43
|
+
async function waitForUnixGroupExit(pid, timeoutMs) {
|
|
44
|
+
const deadline = Date.now() + timeoutMs;
|
|
45
|
+
while (unixGroupExists(pid)) {
|
|
46
|
+
const remaining = deadline - Date.now();
|
|
47
|
+
if (remaining <= 0)
|
|
48
|
+
return false;
|
|
49
|
+
await delay(Math.min(25, remaining));
|
|
50
|
+
}
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
function unixGroupExists(pid) {
|
|
54
|
+
try {
|
|
55
|
+
process.kill(-pid, 0);
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
return error.code !== 'ESRCH';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function waitForExit(child, timeoutMs) {
|
|
63
|
+
if (hasExited(child))
|
|
64
|
+
return Promise.resolve(true);
|
|
65
|
+
return new Promise((resolve) => {
|
|
66
|
+
let settled = false;
|
|
67
|
+
const finish = (exited) => {
|
|
68
|
+
if (settled)
|
|
69
|
+
return;
|
|
70
|
+
settled = true;
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
child.off('exit', onExit);
|
|
73
|
+
resolve(exited);
|
|
74
|
+
};
|
|
75
|
+
const onExit = () => finish(true);
|
|
76
|
+
const timer = setTimeout(() => finish(hasExited(child)), timeoutMs);
|
|
77
|
+
child.once('exit', onExit);
|
|
78
|
+
if (hasExited(child))
|
|
79
|
+
finish(true);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
function taskkill(pid, force) {
|
|
83
|
+
return new Promise((resolve) => {
|
|
84
|
+
const args = ['/PID', String(pid), '/T'];
|
|
85
|
+
if (force)
|
|
86
|
+
args.push('/F');
|
|
87
|
+
const killer = spawn('taskkill.exe', args, { stdio: 'ignore', windowsHide: true });
|
|
88
|
+
const timer = setTimeout(() => {
|
|
89
|
+
killer.kill();
|
|
90
|
+
resolve();
|
|
91
|
+
}, FORCE_KILL_WAIT_MS);
|
|
92
|
+
const finish = () => {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
resolve();
|
|
95
|
+
};
|
|
96
|
+
killer.once('error', finish);
|
|
97
|
+
killer.once('exit', finish);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function delay(ms) {
|
|
101
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
102
|
+
}
|
|
103
|
+
function hasExited(child) {
|
|
104
|
+
return child.exitCode !== null || child.signalCode !== null;
|
|
105
|
+
}
|
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 type AgentdTurnCompletionSource = 'acp_prompt_response' | 'a2a_task_status' | 'a2a_message_stream';
|
|
5
5
|
export interface AgentdTurnCompletionProof {
|
|
6
6
|
source: AgentdTurnCompletionSource;
|
package/dist/webui/app.js
CHANGED
|
@@ -441,9 +441,10 @@ export const app = String.raw `
|
|
|
441
441
|
'</div></td><td><span class="badge">' + escapeHtml(agent.protocol.toUpperCase()) + '</span></td><td>' + escapeHtml(agent.driver || '—') + '</td><td>' + statusMarkup(statusAgent) + '</td><td>' + escapeHtml(agent.workspace || '—') + '</td><td>' + escapeHtml(permissionLabel(agent.permissionPolicy)) + '</td><td><div class="row-actions"><button class="button small" data-agent-edit="' + escapeHtml(agent.id) + '">编辑</button><button class="button small danger" data-agent-delete="' + escapeHtml(agent.id) + '">删除</button></div></td></tr>'
|
|
442
442
|
}
|
|
443
443
|
|
|
444
|
-
function permissionLabel(value) {
|
|
445
|
-
if (value === 'ask') return '询问'
|
|
446
|
-
if (value === '
|
|
444
|
+
function permissionLabel(value) {
|
|
445
|
+
if (value === 'ask') return '询问'
|
|
446
|
+
if (value === 'allow') return '始终允许'
|
|
447
|
+
if (value === 'deny') return '拒绝'
|
|
447
448
|
return value || '—'
|
|
448
449
|
}
|
|
449
450
|
|
|
@@ -545,7 +546,7 @@ export const app = String.raw `
|
|
|
545
546
|
'<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>' +
|
|
546
547
|
'<label>名称<input name="name" value="' + escapeHtml(current.name || '') + '" required></label>' +
|
|
547
548
|
'<label>描述<textarea name="description">' + escapeHtml(current.description || '') + '</textarea></label>' +
|
|
548
|
-
'<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
|
|
549
|
+
'<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>' +
|
|
549
550
|
'<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>'
|
|
550
551
|
openDrawer(editing ? '编辑智能体' : '添加智能体', body, editing ? '保存修改' : '添加智能体', async (form) => {
|
|
551
552
|
const data = new FormData(form)
|