nexus-agentd 0.4.2 → 0.5.0
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 +17 -0
- package/dist/artifact-store.js +9 -8
- package/dist/drivers/dsh.d.ts +2 -0
- package/dist/drivers/dsh.js +10 -0
- package/dist/drivers/grok.d.ts +2 -0
- package/dist/drivers/grok.js +12 -0
- package/dist/drivers/index.js +8 -0
- package/dist/session.d.ts +5 -0
- package/dist/session.js +11 -3
- package/dist/types.d.ts +1 -1
- package/dist/types.js +3 -1
- package/dist/workspace.d.ts +30 -5
- package/dist/workspace.js +39 -13
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -108,6 +108,23 @@ Agent 的编辑页单独设置,默认 60 秒、最大 30 分钟。
|
|
|
108
108
|
| `pi` | `pi-acp` |
|
|
109
109
|
| `openclaw` | `openclaw acp` |
|
|
110
110
|
| `hermes` | `hermes acp` |
|
|
111
|
+
| `dsh` | `dsh --profile acp` |
|
|
112
|
+
| `grok` | `grok --no-auto-update agent stdio` |
|
|
113
|
+
|
|
114
|
+
`dsh` 与 `grok` 使用各自 CLI 自带的 ACP 入口,不需要 Adapter。
|
|
115
|
+
|
|
116
|
+
`dsh`(DeepSeek Harness)默认执行 `dsh --profile acp`,要求 `dsh` 在 PATH 上。DeepSeek Harness
|
|
117
|
+
桌面版把启动器放在 `<安装目录>\resources\runtime\cli\bin\dsh.cmd`,但不会自动出现在已运行进程的
|
|
118
|
+
PATH 里;两种接法二选一:把该目录加入用户 PATH(桌面版的命令管理功能会写 HKCU 用户环境变量,新开的
|
|
119
|
+
进程才看得到),或在本机配置里用 `command` 覆盖成绝对路径,例如
|
|
120
|
+
`"command": "D:/DeepSeek/resources/runtime/cli/bin/dsh.cmd"`。
|
|
121
|
+
|
|
122
|
+
`grok`(Grok Build)默认执行 `grok --no-auto-update agent stdio`;`--no-auto-update` 用于跳过
|
|
123
|
+
自动化环境中的后台更新检查,可用本机配置的 `args` 覆盖。前置条件是 CLI 已完成认证,三选一:先跑
|
|
124
|
+
`grok login` 缓存 token、用 `"inheritEnv": ["XAI_API_KEY"]` 把守护进程环境里的变量透传给 grok
|
|
125
|
+
子进程、或用 `"env": { "XAI_API_KEY": "env:XAI_API_KEY" }` 显式赋值(`env:` 前缀从守护进程环境
|
|
126
|
+
取值,变量缺失时配置加载直接报错)。Gateway 的 ACP 客户端不做 `authenticate` 请求,未认证时 grok
|
|
127
|
+
会在握手后失败。DSH 是本地 agent,没有这个前置条件。
|
|
111
128
|
|
|
112
129
|
Claude Code、Codex、Pi 通常需要对应 Adapter:
|
|
113
130
|
|
package/dist/artifact-store.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { chmod as fsChmod, lstat as fsLstat, mkdir as fsMkdir, open as fsOpen, readdir as fsReaddir, readFile as fsReadFile, rename as fsRename, rm as fsRm, unlink as fsUnlink, writeFile as fsWriteFile } from 'node:fs/promises';
|
|
3
3
|
import { performance } from 'node:perf_hooks';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { isPathInside } from './workspace.js';
|
|
5
6
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
6
7
|
const DEFAULT_MAX_ARTIFACT_BYTES = 12 * 1024 * 1024;
|
|
7
8
|
const DEFAULT_MAX_ARTIFACTS_PER_RUN = 32;
|
|
@@ -354,7 +355,7 @@ export class ArtifactStore {
|
|
|
354
355
|
await this.ensureDirectory(runDir, false);
|
|
355
356
|
const runReal = await this.realDirectory(runDir);
|
|
356
357
|
const fileReal = await this.realPath(entry.filePath);
|
|
357
|
-
if (!
|
|
358
|
+
if (!isPathInside(runReal, fileReal))
|
|
358
359
|
return undefined;
|
|
359
360
|
const stat = await this.io.lstat(entry.filePath);
|
|
360
361
|
if (!stat.isFile() || stat.isSymbolicLink())
|
|
@@ -566,12 +567,16 @@ export class ArtifactStore {
|
|
|
566
567
|
await this.io.mkdir(runDir, { recursive: true, mode: 0o700 });
|
|
567
568
|
await this.ensureDirectory(runDir, false);
|
|
568
569
|
const runReal = await this.realDirectory(runDir);
|
|
569
|
-
if (!
|
|
570
|
+
if (!isPathInside(await this.realDirectory(this.rootDir), runReal)) {
|
|
570
571
|
throw new Error('Artifact run directory escaped the artifact root');
|
|
571
572
|
}
|
|
572
573
|
const filePath = this.payloadPath(item.runId, item.id);
|
|
573
|
-
|
|
574
|
-
|
|
574
|
+
// payloadPath() derives from the configured artifact root, which is not
|
|
575
|
+
// necessarily canonical (a Windows 8.3 short path or a symlinked parent),
|
|
576
|
+
// while runReal was resolved through realDirectory(). Compare the payload
|
|
577
|
+
// with the same canonical directory instead of the raw string.
|
|
578
|
+
const fileRealParent = await this.realDirectory(path.dirname(filePath));
|
|
579
|
+
if (!isPathInside(runReal, fileRealParent))
|
|
575
580
|
throw new Error('Invalid artifact payload path');
|
|
576
581
|
const delta = item.bytes.length - (entry.bytes || 0);
|
|
577
582
|
const previousView = entry.durableView;
|
|
@@ -1161,10 +1166,6 @@ function nonNegativeInteger(value, fallback) {
|
|
|
1161
1166
|
function nonNegativeNumber(value, fallback) {
|
|
1162
1167
|
return Number.isFinite(value) && Number(value) >= 0 ? Number(value) : fallback;
|
|
1163
1168
|
}
|
|
1164
|
-
function isContained(root, target) {
|
|
1165
|
-
const relative = path.relative(root, target);
|
|
1166
|
-
return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
1167
|
-
}
|
|
1168
1169
|
function isMissing(error) {
|
|
1169
1170
|
return error?.code === 'ENOENT';
|
|
1170
1171
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createStdioAcpDriver } from './stdio.js';
|
|
2
|
+
export function createDshDriver(id, config) {
|
|
3
|
+
return createStdioAcpDriver(id, config, {
|
|
4
|
+
name: 'DeepSeek Harness',
|
|
5
|
+
description: 'DeepSeek Harness through its native ACP stdio profile',
|
|
6
|
+
command: 'dsh',
|
|
7
|
+
args: ['--profile', 'acp'],
|
|
8
|
+
probeArgs: ['--version']
|
|
9
|
+
});
|
|
10
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createStdioAcpDriver } from './stdio.js';
|
|
2
|
+
export function createGrokDriver(id, config) {
|
|
3
|
+
return createStdioAcpDriver(id, config, {
|
|
4
|
+
name: 'Grok Build',
|
|
5
|
+
description: 'Grok Build through its native ACP stdio agent',
|
|
6
|
+
command: 'grok',
|
|
7
|
+
// The xAI CLI documents `--no-auto-update` for `grok agent stdio` in
|
|
8
|
+
// scripts and CI, which is exactly how this gateway launches it.
|
|
9
|
+
args: ['--no-auto-update', 'agent', 'stdio'],
|
|
10
|
+
probeArgs: ['version']
|
|
11
|
+
});
|
|
12
|
+
}
|
package/dist/drivers/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createClaudeDriver } from './claude.js';
|
|
2
2
|
import { createCodexDriver } from './codex.js';
|
|
3
|
+
import { createDshDriver } from './dsh.js';
|
|
4
|
+
import { createGrokDriver } from './grok.js';
|
|
3
5
|
import { createHermesDriver } from './hermes.js';
|
|
4
6
|
import { createOpenCodeDriver } from './opencode.js';
|
|
5
7
|
import { createOpenClawDriver } from './openclaw.js';
|
|
@@ -42,6 +44,12 @@ export function createDriverRegistry(config) {
|
|
|
42
44
|
case 'hermes':
|
|
43
45
|
drivers.set(id, createHermesDriver(id, driver));
|
|
44
46
|
break;
|
|
47
|
+
case 'dsh':
|
|
48
|
+
drivers.set(id, createDshDriver(id, driver));
|
|
49
|
+
break;
|
|
50
|
+
case 'grok':
|
|
51
|
+
drivers.set(id, createGrokDriver(id, driver));
|
|
52
|
+
break;
|
|
45
53
|
}
|
|
46
54
|
}
|
|
47
55
|
return drivers;
|
package/dist/session.d.ts
CHANGED
|
@@ -52,6 +52,11 @@ export declare class ManagedSession implements AgentSessionSink, AcpSessionSink
|
|
|
52
52
|
private runtime?;
|
|
53
53
|
private readonly artifactStore?;
|
|
54
54
|
private readonly onRunUpdate?;
|
|
55
|
+
/**
|
|
56
|
+
* `workspace` must be the canonical path the workspace policy resolved at
|
|
57
|
+
* session creation. Callers that hand over a raw path would make publish
|
|
58
|
+
* compare a non-canonical root against realpath'd targets.
|
|
59
|
+
*/
|
|
55
60
|
constructor(agentId: string, protocol: AgentdProtocol, workspace: string | undefined, ownerKeyId: string, maxEvents: number, maxOutputChars: number, agentName: string, runStore?: RunStore | undefined, instanceId?: string, artifactStore?: SessionArtifactStore, onRunUpdate?: (runId: string, state: AgentdSessionState) => void);
|
|
56
61
|
attach(runtime: AgentSessionRuntime): void;
|
|
57
62
|
runtimeAvailable(): boolean;
|
package/dist/session.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { realpath } from 'node:fs/promises';
|
|
3
2
|
import { extname } from 'node:path';
|
|
4
3
|
import { AcpProcessRuntime } from './acp/runtime.js';
|
|
5
4
|
import { A2AClientRuntime } from './a2a/runtime.js';
|
|
@@ -40,6 +39,11 @@ export class ManagedSession {
|
|
|
40
39
|
runtime;
|
|
41
40
|
artifactStore;
|
|
42
41
|
onRunUpdate;
|
|
42
|
+
/**
|
|
43
|
+
* `workspace` must be the canonical path the workspace policy resolved at
|
|
44
|
+
* session creation. Callers that hand over a raw path would make publish
|
|
45
|
+
* compare a non-canonical root against realpath'd targets.
|
|
46
|
+
*/
|
|
43
47
|
constructor(agentId, protocol, workspace, ownerKeyId, maxEvents, maxOutputChars, agentName, runStore, instanceId = randomUUID(), artifactStore, onRunUpdate) {
|
|
44
48
|
this.agentId = agentId;
|
|
45
49
|
this.protocol = protocol;
|
|
@@ -204,8 +208,12 @@ export class ManagedSession {
|
|
|
204
208
|
}
|
|
205
209
|
async publishSingleFile(requestedPath) {
|
|
206
210
|
try {
|
|
207
|
-
|
|
208
|
-
|
|
211
|
+
// `this.workspace` is the canonical path the session was created
|
|
212
|
+
// with, already resolved through the workspace policy. Resolving it
|
|
213
|
+
// again here would hand the trust decision back to the file system,
|
|
214
|
+
// so a workspace replaced by a link after creation would silently
|
|
215
|
+
// become the new root and publish would read outside the allowlist.
|
|
216
|
+
const file = await readContainedFile([this.workspace], requestedPath, {
|
|
209
217
|
maxBytes: MAX_PUBLISHED_FILE_BYTES
|
|
210
218
|
});
|
|
211
219
|
const artifact = {
|
package/dist/types.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface AgentdTurnCompletion extends AgentdTurnCompletionProof {
|
|
|
14
14
|
artifactCount: number;
|
|
15
15
|
completedAt: number;
|
|
16
16
|
}
|
|
17
|
-
export declare const agentdDriverKinds: readonly ["stdio", "opencode", "claude", "codex", "pi", "openclaw", "hermes"];
|
|
17
|
+
export declare const agentdDriverKinds: readonly ["stdio", "opencode", "claude", "codex", "pi", "openclaw", "hermes", "dsh", "grok"];
|
|
18
18
|
export type AgentdDriverKind = (typeof agentdDriverKinds)[number];
|
|
19
19
|
export interface AgentdDriverConfig {
|
|
20
20
|
protocol?: 'acp';
|
package/dist/types.js
CHANGED
package/dist/workspace.d.ts
CHANGED
|
@@ -5,7 +5,27 @@ export declare class WorkspacePolicy {
|
|
|
5
5
|
resolve(input: string): Promise<string>;
|
|
6
6
|
listRoots(): string[];
|
|
7
7
|
}
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Reports whether `target` is `root` itself or lives below it.
|
|
10
|
+
*
|
|
11
|
+
* Both arguments must already be canonical (realpath'd) absolute paths.
|
|
12
|
+
* `realpath` returns the spelling stored on disk, so on a normal
|
|
13
|
+
* case-insensitive volume two differently-cased spellings of one directory still
|
|
14
|
+
* compare equal here. A prefix comparison is used rather than `path.relative`,
|
|
15
|
+
* whose win32 implementation lower-cases both sides and would therefore read a
|
|
16
|
+
* case-variant sibling inside a case-sensitive directory as contained. The
|
|
17
|
+
* separator is required so that `\\server\share2` is not read as living inside
|
|
18
|
+
* `\\server\share`.
|
|
19
|
+
*
|
|
20
|
+
* `path.resolve` only folds lexical noise (`.`, `..`, duplicate separators) for
|
|
21
|
+
* callers that hand over a hand-written path, which the previous
|
|
22
|
+
* `path.relative` implementation also tolerated. It must never be replaced with
|
|
23
|
+
* `realpath`: resolving against the file system here would hand the trust
|
|
24
|
+
* decision back to whatever the directory currently points at.
|
|
25
|
+
*/
|
|
26
|
+
export declare function isPathInside(root: string, target: string): boolean;
|
|
27
|
+
/** @deprecated Use {@link isPathInside}. Kept because this module is re-exported. */
|
|
28
|
+
export declare const isWithin: typeof isPathInside;
|
|
9
29
|
export type ContainedReadFailure = 'outside' | 'swapped' | 'not-a-file' | 'too-large';
|
|
10
30
|
/**
|
|
11
31
|
* Reported as a kind rather than a finished message because each caller owns a
|
|
@@ -27,10 +47,15 @@ export interface ContainedFile {
|
|
|
27
47
|
/**
|
|
28
48
|
* Reads a file that must resolve inside one of `roots`.
|
|
29
49
|
*
|
|
30
|
-
* `roots` must
|
|
31
|
-
*
|
|
32
|
-
* containment
|
|
33
|
-
* (
|
|
50
|
+
* `roots` must be the canonical paths the caller decided to trust — not the
|
|
51
|
+
* result of a fresh `realpath()` of that same path. Re-resolving a root at call
|
|
52
|
+
* time hands the containment decision back to the file system, so a link
|
|
53
|
+
* planted after the decision (for example a workspace swapped for a junction)
|
|
54
|
+
* would move the trust boundary with it. `WorkspacePolicy.listRoots()` and a
|
|
55
|
+
* session's own canonical workspace both satisfy this. An unresolved root fails
|
|
56
|
+
* closed, because containment is checked against a realpath'd target.
|
|
57
|
+
* `requested` may be absolute (an agent-declared path) or relative to `roots[0]`
|
|
58
|
+
* (a client-supplied path).
|
|
34
59
|
*/
|
|
35
60
|
export declare function readContainedFile(roots: readonly string[], requested: string, options: {
|
|
36
61
|
maxBytes: number;
|
package/dist/workspace.js
CHANGED
|
@@ -13,7 +13,7 @@ export class WorkspacePolicy {
|
|
|
13
13
|
if (!input?.trim())
|
|
14
14
|
throw new Error('workspace is required');
|
|
15
15
|
const candidate = normalize(await realpath(input));
|
|
16
|
-
const allowed = this.roots.some((root) =>
|
|
16
|
+
const allowed = this.roots.some((root) => isPathInside(root, candidate));
|
|
17
17
|
if (!allowed)
|
|
18
18
|
throw new Error(`workspace is outside the configured allowlist: ${input}`);
|
|
19
19
|
return candidate;
|
|
@@ -22,13 +22,34 @@ export class WorkspacePolicy {
|
|
|
22
22
|
return [...this.roots];
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Reports whether `target` is `root` itself or lives below it.
|
|
27
|
+
*
|
|
28
|
+
* Both arguments must already be canonical (realpath'd) absolute paths.
|
|
29
|
+
* `realpath` returns the spelling stored on disk, so on a normal
|
|
30
|
+
* case-insensitive volume two differently-cased spellings of one directory still
|
|
31
|
+
* compare equal here. A prefix comparison is used rather than `path.relative`,
|
|
32
|
+
* whose win32 implementation lower-cases both sides and would therefore read a
|
|
33
|
+
* case-variant sibling inside a case-sensitive directory as contained. The
|
|
34
|
+
* separator is required so that `\\server\share2` is not read as living inside
|
|
35
|
+
* `\\server\share`.
|
|
36
|
+
*
|
|
37
|
+
* `path.resolve` only folds lexical noise (`.`, `..`, duplicate separators) for
|
|
38
|
+
* callers that hand over a hand-written path, which the previous
|
|
39
|
+
* `path.relative` implementation also tolerated. It must never be replaced with
|
|
40
|
+
* `realpath`: resolving against the file system here would hand the trust
|
|
41
|
+
* decision back to whatever the directory currently points at.
|
|
42
|
+
*/
|
|
43
|
+
export function isPathInside(root, target) {
|
|
44
|
+
const base = path.resolve(root);
|
|
45
|
+
const candidate = path.resolve(target);
|
|
46
|
+
if (candidate === base)
|
|
47
|
+
return true;
|
|
48
|
+
const prefix = base.endsWith(path.sep) ? base : base + path.sep;
|
|
49
|
+
return candidate.startsWith(prefix);
|
|
31
50
|
}
|
|
51
|
+
/** @deprecated Use {@link isPathInside}. Kept because this module is re-exported. */
|
|
52
|
+
export const isWithin = isPathInside;
|
|
32
53
|
/**
|
|
33
54
|
* Reported as a kind rather than a finished message because each caller owns a
|
|
34
55
|
* different error contract: workspace publish maps it to an HTTP status, while a
|
|
@@ -45,10 +66,15 @@ export class ContainedReadError extends Error {
|
|
|
45
66
|
/**
|
|
46
67
|
* Reads a file that must resolve inside one of `roots`.
|
|
47
68
|
*
|
|
48
|
-
* `roots` must
|
|
49
|
-
*
|
|
50
|
-
* containment
|
|
51
|
-
* (
|
|
69
|
+
* `roots` must be the canonical paths the caller decided to trust — not the
|
|
70
|
+
* result of a fresh `realpath()` of that same path. Re-resolving a root at call
|
|
71
|
+
* time hands the containment decision back to the file system, so a link
|
|
72
|
+
* planted after the decision (for example a workspace swapped for a junction)
|
|
73
|
+
* would move the trust boundary with it. `WorkspacePolicy.listRoots()` and a
|
|
74
|
+
* session's own canonical workspace both satisfy this. An unresolved root fails
|
|
75
|
+
* closed, because containment is checked against a realpath'd target.
|
|
76
|
+
* `requested` may be absolute (an agent-declared path) or relative to `roots[0]`
|
|
77
|
+
* (a client-supplied path).
|
|
52
78
|
*/
|
|
53
79
|
export async function readContainedFile(roots, requested, options) {
|
|
54
80
|
if (!roots.length) {
|
|
@@ -56,7 +82,7 @@ export async function readContainedFile(roots, requested, options) {
|
|
|
56
82
|
}
|
|
57
83
|
const unresolved = path.resolve(roots[0], requested);
|
|
58
84
|
const target = await realpath(unresolved);
|
|
59
|
-
const root = roots.find((candidate) =>
|
|
85
|
+
const root = roots.find((candidate) => isPathInside(candidate, target));
|
|
60
86
|
if (!root)
|
|
61
87
|
throw outsideError(requested);
|
|
62
88
|
let handle;
|
|
@@ -65,7 +91,7 @@ export async function readContainedFile(roots, requested, options) {
|
|
|
65
91
|
// Resolve again after opening so a swapped symlink cannot silently
|
|
66
92
|
// redirect the read outside the allowlist.
|
|
67
93
|
const verified = await realpath(unresolved);
|
|
68
|
-
if (!
|
|
94
|
+
if (!isPathInside(root, verified))
|
|
69
95
|
throw outsideError(requested);
|
|
70
96
|
if (verified !== target) {
|
|
71
97
|
throw new ContainedReadError('swapped', 'file changed while it was being opened');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexus-agentd",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Local Nexus Gateway daemon with an HTTP/SSE API and WebUI for allowlisted ACP and remote A2A agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "node scripts/generate-webui-sources.mjs && node scripts/clean-dist.mjs && tsc -p tsconfig.json",
|
|
38
38
|
"typecheck": "node scripts/generate-webui-sources.mjs && tsc -p tsconfig.json --noEmit && tsc -p tsconfig.webui.json",
|
|
39
|
-
"test": "
|
|
39
|
+
"test": "node scripts/run-tests.mjs",
|
|
40
40
|
"test:webui": "node scripts/generate-webui-sources.mjs && playwright test",
|
|
41
41
|
"prepack": "npm run build"
|
|
42
42
|
},
|