@gt-fe/eap-runtime 0.0.30 → 0.0.31
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/api/handlers/invoke.handler.js +9 -5
- package/dist/api/handlers/stream.handler.js +15 -4
- package/dist/common/index.d.ts +1 -0
- package/dist/common/index.js +1 -0
- package/dist/common/user-root.d.ts +8 -0
- package/dist/common/user-root.js +19 -0
- package/dist/graph/adapters/langgraph.adapter.js +30 -3
- package/dist/graph/adapters/skill-read.js +13 -4
- package/dist/graph/contracts/executable-graph.d.ts +7 -1
- package/dist/graph/nodes/tool-node.d.ts +4 -1
- package/dist/graph/nodes/tool-node.js +15 -1
- package/dist/registry/deployment-snapshot.d.ts +1 -1
- package/dist/registry/deployment-snapshot.js +6 -3
- package/dist/registry/skill-tree.d.ts +1 -1
- package/dist/registry/skill-tree.js +11 -6
- package/dist/runtime/agent-runtime.js +36 -5
- package/dist/session/persist-turn-messages.d.ts +13 -6
- package/dist/session/persist-turn-messages.js +4 -0
- package/dist/sse/events/tool-events.d.ts +29 -0
- package/dist/tool/harness-command.d.ts +1 -1
- package/dist/tool/harness-command.js +8 -7
- package/dist/tool/harness-primitives.d.ts +3 -3
- package/dist/tool/harness-primitives.js +185 -60
- package/dist/tool/index.d.ts +2 -0
- package/dist/tool/index.js +2 -0
- package/dist/tool/inject-public-artifact-links.d.ts +32 -0
- package/dist/tool/inject-public-artifact-links.js +142 -0
- package/dist/tool/static-artifact-url.d.ts +66 -0
- package/dist/tool/static-artifact-url.js +561 -0
- package/dist/tool/system-prompt-builder.js +8 -4
- package/dist/tool/tool-event-emitter.d.ts +2 -1
- package/dist/tool/tool-event-emitter.js +30 -18
- package/dist/tool/tool-executor.interface.d.ts +8 -1
- package/dist/tool/tool-result-injector.d.ts +3 -3
- package/dist/tool/tool-result-injector.js +7 -3
- package/package.json +2 -2
|
@@ -30,6 +30,19 @@ export interface ToolResultPayload {
|
|
|
30
30
|
content: unknown;
|
|
31
31
|
/** 是否执行成功 */
|
|
32
32
|
success: boolean;
|
|
33
|
+
/** 与 ToolCallResult.file 同形;SSE / persist / 注入共用 */
|
|
34
|
+
file?: {
|
|
35
|
+
fileName: string;
|
|
36
|
+
fileType: string;
|
|
37
|
+
size: number;
|
|
38
|
+
url: string;
|
|
39
|
+
};
|
|
40
|
+
files?: Array<{
|
|
41
|
+
fileName: string;
|
|
42
|
+
fileType: string;
|
|
43
|
+
size: number;
|
|
44
|
+
url: string;
|
|
45
|
+
}>;
|
|
33
46
|
}
|
|
34
47
|
/** 工具执行进度 */
|
|
35
48
|
export type ToolProgressStatus = 'started' | 'completed' | 'error' | (string & {});
|
|
@@ -52,6 +65,8 @@ export interface ToolProgressPayload {
|
|
|
52
65
|
content?: unknown;
|
|
53
66
|
/** status=error 时可选:错误信息 */
|
|
54
67
|
error?: string;
|
|
68
|
+
file?: ToolResultPayload['file'];
|
|
69
|
+
files?: ToolResultPayload['files'];
|
|
55
70
|
/** 执行耗时(毫秒),status=completed|error 时可选 */
|
|
56
71
|
executionTime?: number;
|
|
57
72
|
}
|
|
@@ -65,4 +80,18 @@ export interface ToolErrorPayload {
|
|
|
65
80
|
error: string;
|
|
66
81
|
/** 是否会自动重试 */
|
|
67
82
|
willRetry: boolean;
|
|
83
|
+
/** 失败且有交付物时保留原正文(与 ToolCallResult.content 同形) */
|
|
84
|
+
content?: unknown;
|
|
85
|
+
file?: {
|
|
86
|
+
fileName: string;
|
|
87
|
+
fileType: string;
|
|
88
|
+
size: number;
|
|
89
|
+
url: string;
|
|
90
|
+
};
|
|
91
|
+
files?: Array<{
|
|
92
|
+
fileName: string;
|
|
93
|
+
fileType: string;
|
|
94
|
+
size: number;
|
|
95
|
+
url: string;
|
|
96
|
+
}>;
|
|
68
97
|
}
|
|
@@ -27,6 +27,6 @@ export declare function ensurePythonShims(workdir: string): string | undefined;
|
|
|
27
27
|
* spawn 前替换成绝对路径,不依赖壳去展开。
|
|
28
28
|
*/
|
|
29
29
|
export declare function substituteSkillDir(command: string, skillDir?: string): string;
|
|
30
|
-
export declare function sandboxEnv(workdir: string, cwd: string, allowlist: string[], shimDir?: string, skillDir?: string): NodeJS.ProcessEnv;
|
|
30
|
+
export declare function sandboxEnv(workdir: string, cwd: string, allowlist: string[], shimDir?: string, skillDir?: string, homeDir?: string): NodeJS.ProcessEnv;
|
|
31
31
|
export declare function formatSpawnError(err: unknown, shellFile: string): Error;
|
|
32
32
|
export declare function killProcessTree(child: ChildProcess): void;
|
|
@@ -21,7 +21,7 @@ export function commandDialectLines(platform = process.platform) {
|
|
|
21
21
|
else {
|
|
22
22
|
lines.push('基础命令:当前目录 `pwd`;列目录 `ls`;环境变量 `$VAR`;串命令用 `&&`。');
|
|
23
23
|
}
|
|
24
|
-
lines.push('解释器:`python`/`python3`、`node`/`nodejs`、`sh`/`bash`(Windows 上 `sh` 需本机有 Git Bash 等)。', '可执行文件:用绝对路径或相对于 cwd(Windows `.exe`/`.bat`/`.cmd`;Unix 二进制需可执行或用 `sh`)。', '`execute_command` 的 cwd
|
|
24
|
+
lines.push('解释器:`python`/`python3`、`node`/`nodejs`、`sh`/`bash`(Windows 上 `sh` 需本机有 Git Bash 等)。', '可执行文件:用绝对路径或相对于 cwd(Windows `.exe`/`.bat`/`.cmd`;Unix 二进制需可执行或用 `sh`)。', '`execute_command` 的 cwd 默认是用户目录下的 `files/`。技能包只读。包内任意文件用 `EAP_SKILL_DIR` 加相对路径(不限 `scripts/`)。读到的 SKILL.md 里本包存在的相对路径已展开为绝对路径。不要把 cwd 设成技能目录、`work/` 或 `public/`。相对写出在 `files/`。');
|
|
25
25
|
return lines;
|
|
26
26
|
}
|
|
27
27
|
export function resolveShellSpawn(command, platform = process.platform) {
|
|
@@ -179,22 +179,23 @@ export function substituteSkillDir(command, skillDir) {
|
|
|
179
179
|
.replace(/\$EAP_SKILL_DIR\b/g, dir)
|
|
180
180
|
.replace(/%EAP_SKILL_DIR%/gi, dir);
|
|
181
181
|
}
|
|
182
|
-
export function sandboxEnv(workdir, cwd, allowlist, shimDir, skillDir) {
|
|
182
|
+
export function sandboxEnv(workdir, cwd, allowlist, shimDir, skillDir, homeDir) {
|
|
183
183
|
const win = process.platform === 'win32';
|
|
184
184
|
const hostPath = process.env.PATH
|
|
185
185
|
?? (win
|
|
186
186
|
? (process.env.SystemRoot ? path.join(process.env.SystemRoot, 'System32') : 'C:\\Windows\\System32')
|
|
187
187
|
: '/usr/bin:/bin:/usr/local/bin');
|
|
188
188
|
const pathValue = shimDir ? `${shimDir}${path.delimiter}${hostPath}` : hostPath;
|
|
189
|
+
const home = homeDir?.trim() ? path.resolve(homeDir) : workdir;
|
|
189
190
|
const env = {
|
|
190
191
|
PATH: pathValue,
|
|
191
192
|
LANG: process.env.LANG ?? 'C.UTF-8',
|
|
192
193
|
LC_ALL: process.env.LC_ALL ?? process.env.LANG ?? 'C.UTF-8',
|
|
193
|
-
HOME:
|
|
194
|
-
USERPROFILE:
|
|
195
|
-
TMPDIR:
|
|
196
|
-
TMP:
|
|
197
|
-
TEMP:
|
|
194
|
+
HOME: home,
|
|
195
|
+
USERPROFILE: home,
|
|
196
|
+
TMPDIR: home,
|
|
197
|
+
TMP: home,
|
|
198
|
+
TEMP: home,
|
|
198
199
|
PWD: cwd,
|
|
199
200
|
PYTHONUTF8: '1',
|
|
200
201
|
PYTHONIOENCODING: 'utf-8',
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* 平台默认 Harness 原语:read_file / write_file / execute_command。
|
|
3
3
|
*
|
|
4
4
|
* 不写进 agent.manifest.toolsets。同名业务工具优先(问数 file-io 等)。
|
|
5
|
-
* 写:`users/<userId>/`(含用户自己的 skills
|
|
5
|
+
* 写:`dirname(agentDir)/users/<userId>/`(含用户自己的 skills)。产物默认 `files/`,公开副本 `public/`。
|
|
6
6
|
* 读:写根 + 已装配 `preinstall/tools`、`preinstall/skills`。
|
|
7
|
-
* 命令:cwd
|
|
7
|
+
* 命令:cwd 默认 `files/`。技能包只读,EAP_SKILL_DIR 为整包绝对根(不限 scripts/)。不把预装拷进用户装配槽。
|
|
8
8
|
* 开发态 overlay:`EAP_LOCAL_SKILLS_DIR` / `EAP_LOCAL_TOOLS_DIR`(只读)。在 `createDevRuntime` /
|
|
9
9
|
* `createToolExecutor` 时钉进执行器 extraRoots;SDK `createAgent` 构造完会恢复 process.env,
|
|
10
10
|
* 请求时不再读这两项。相对 `read_file` 会把 overlay 根下直接子目录里带 SKILL.md 的包纳入唯一命中。
|
|
@@ -19,7 +19,7 @@ export interface HarnessJailOptions {
|
|
|
19
19
|
workdir?: string;
|
|
20
20
|
extraRoots?: string[];
|
|
21
21
|
}
|
|
22
|
-
/** `
|
|
22
|
+
/** `dirname(agentDir)/users/<userId>`(写根;含用户 skills) */
|
|
23
23
|
export declare function resolveUserDir(agentDir: string, userId: string): string;
|
|
24
24
|
export declare function collectToolBundlePaths(loadedTools?: LoadedToolMap): string[];
|
|
25
25
|
export declare function resolveJailFromState(state: {
|
|
@@ -2,20 +2,24 @@
|
|
|
2
2
|
* 平台默认 Harness 原语:read_file / write_file / execute_command。
|
|
3
3
|
*
|
|
4
4
|
* 不写进 agent.manifest.toolsets。同名业务工具优先(问数 file-io 等)。
|
|
5
|
-
* 写:`users/<userId>/`(含用户自己的 skills
|
|
5
|
+
* 写:`dirname(agentDir)/users/<userId>/`(含用户自己的 skills)。产物默认 `files/`,公开副本 `public/`。
|
|
6
6
|
* 读:写根 + 已装配 `preinstall/tools`、`preinstall/skills`。
|
|
7
|
-
* 命令:cwd
|
|
7
|
+
* 命令:cwd 默认 `files/`。技能包只读,EAP_SKILL_DIR 为整包绝对根(不限 scripts/)。不把预装拷进用户装配槽。
|
|
8
8
|
* 开发态 overlay:`EAP_LOCAL_SKILLS_DIR` / `EAP_LOCAL_TOOLS_DIR`(只读)。在 `createDevRuntime` /
|
|
9
9
|
* `createToolExecutor` 时钉进执行器 extraRoots;SDK `createAgent` 构造完会恢复 process.env,
|
|
10
10
|
* 请求时不再读这两项。相对 `read_file` 会把 overlay 根下直接子目录里带 SKILL.md 的包纳入唯一命中。
|
|
11
11
|
*/
|
|
12
12
|
import { spawn } from 'node:child_process';
|
|
13
13
|
import { constants as fsConstants, existsSync, readdirSync, realpathSync } from 'node:fs';
|
|
14
|
-
import { mkdir, open, readFile } from 'node:fs/promises';
|
|
14
|
+
import { chmod, mkdir, open, readFile, stat } from 'node:fs/promises';
|
|
15
15
|
import path from 'node:path';
|
|
16
|
+
import { MISSING_USER_DIR, userRoot } from '../common/user-root.js';
|
|
17
|
+
import { MAX_SNAPSHOT_ENUM, MAX_SNAPSHOT_URLS, PUBLIC_FILE_MODE, ensureUserSlots, maxWriteBytesFromEnv, publishFromFiles, resolveUserProductPath, snapshotCandidates, walkFilesTree, } from './static-artifact-url.js';
|
|
16
18
|
import { BUILTIN_TOOL_SOURCE, DEFAULT_BUILTIN_TIMEOUT_MS, MAX_BUILTIN_TIMEOUT_MS, clampBuiltinTimeout, isBuiltinToolExecution, resolveBuiltinToolsConfig, } from '@gt-fe/eap-contracts';
|
|
17
19
|
import { ensureInterpreterShims, formatSpawnError, killProcessTree, resolveShellSpawn, sandboxEnv, spawnOptionsFor, substituteSkillDir, } from './harness-command.js';
|
|
18
20
|
import { expandSkillScriptRefs, isSkillEntryFile } from './skill-script-refs.js';
|
|
21
|
+
/** 超时杀进程或 spawn 失败后等 `close`;未到则最多再等此时长再快照。 */
|
|
22
|
+
const SPAWN_CLOSE_GRACE_MS = 1000;
|
|
19
23
|
function builtinConfig() {
|
|
20
24
|
return resolveBuiltinToolsConfig(process.env);
|
|
21
25
|
}
|
|
@@ -66,7 +70,7 @@ function harnessDefinitions() {
|
|
|
66
70
|
manifest: {
|
|
67
71
|
id: 'builtin:write_file',
|
|
68
72
|
name: 'write_file',
|
|
69
|
-
description: '
|
|
73
|
+
description: '把 UTF-8 文本整份写入当前用户的 files/(装配槽用 skills/ 前缀)。没有该文件则创建;已有文件会整份换成 content,不是改某一行。只要在末尾加内容请用 append_to_file。不能写 public/ 或 work/,不能写预装 tools/skills,不能写 .assembly.json 等平台文件。png/xlsx/docx/zip/pdf 等二进制请用 execute_command 写。成功时工具结果是 JSON:{"ok":true,"path":"工作稿原名","bytesWritten":字节数,"url":"公开副本链接,未配基址则无此字段"}。路径越权或禁写文件名会失败。',
|
|
70
74
|
...common,
|
|
71
75
|
},
|
|
72
76
|
inputSchema: {
|
|
@@ -77,11 +81,11 @@ function harnessDefinitions() {
|
|
|
77
81
|
path: {
|
|
78
82
|
type: 'string',
|
|
79
83
|
minLength: 1,
|
|
80
|
-
description: '
|
|
84
|
+
description: '相对 files/ 的路径,例如 notes/todo.md 或 page/index.html。装配槽用 skills/ 前缀。不能写预装 tools/skills、public/、work/。',
|
|
81
85
|
},
|
|
82
86
|
content: {
|
|
83
87
|
type: 'string',
|
|
84
|
-
description: '
|
|
88
|
+
description: '文件的完整新内容(UTF-8 文本)。会覆盖原文件全部内容。',
|
|
85
89
|
},
|
|
86
90
|
},
|
|
87
91
|
},
|
|
@@ -92,7 +96,7 @@ function harnessDefinitions() {
|
|
|
92
96
|
manifest: {
|
|
93
97
|
id: 'builtin:append_to_file',
|
|
94
98
|
name: 'append_to_file',
|
|
95
|
-
description: '
|
|
99
|
+
description: '在当前用户 files/ 的文件末尾追加 UTF-8 文本,不覆盖已有内容。没有该文件则创建。不要用它整份改写文件,那是 write_file。不能写 public/ 或 work/,不能写预装 tools/skills,不能写 .assembly.json 等平台文件。成功时工具结果是 JSON:{"ok":true,"path":"工作稿原名","bytesWritten":本次追加字节数,"url":"公开副本链接,未配基址则无此字段"}。路径越权或禁写文件名会失败。',
|
|
96
100
|
...common,
|
|
97
101
|
},
|
|
98
102
|
inputSchema: {
|
|
@@ -103,11 +107,11 @@ function harnessDefinitions() {
|
|
|
103
107
|
path: {
|
|
104
108
|
type: 'string',
|
|
105
109
|
minLength: 1,
|
|
106
|
-
description: '
|
|
110
|
+
description: '相对 files/ 的路径,例如 notes/todo.md。装配槽用 skills/ 前缀。不能写预装 tools/skills、public/、work/。',
|
|
107
111
|
},
|
|
108
112
|
content: {
|
|
109
113
|
type: 'string',
|
|
110
|
-
description: '
|
|
114
|
+
description: '接到文件末尾的 UTF-8 文本。不会删除或替换原有内容。',
|
|
111
115
|
},
|
|
112
116
|
},
|
|
113
117
|
},
|
|
@@ -118,7 +122,7 @@ function harnessDefinitions() {
|
|
|
118
122
|
manifest: {
|
|
119
123
|
id: 'builtin:execute_command',
|
|
120
124
|
name: 'execute_command',
|
|
121
|
-
description: `用本机 shell
|
|
125
|
+
description: `用本机 shell 在用户 files/ 执行命令(Windows cmd.exe,其它 /bin/sh)。技能包只读,用 EAP_SKILL_DIR 加包内相对路径跑文件(不限 scripts/)。python/python3、node/nodejs、sh/bash 经 shim。默认超时 ${DEFAULT_BUILTIN_TIMEOUT_MS}ms,可用 timeoutMs 延长,上限 ${MAX_BUILTIN_TIMEOUT_MS}ms。不继承密钥。HOME/TMPDIR 在 work/。子进程 close 后(含超时杀进程)把 files/ 里新的或有改动的公开类型文件做成 public/ 时间戳副本。`,
|
|
122
126
|
...common,
|
|
123
127
|
},
|
|
124
128
|
inputSchema: {
|
|
@@ -126,7 +130,7 @@ function harnessDefinitions() {
|
|
|
126
130
|
required: ['command'],
|
|
127
131
|
properties: {
|
|
128
132
|
command: { type: 'string' },
|
|
129
|
-
cwd: { type: 'string', description: '
|
|
133
|
+
cwd: { type: 'string', description: 'files/ 内的子目录,可选;省略则 files/。不能是 work/、public/ 或预装技能/工具包' },
|
|
130
134
|
timeoutMs: {
|
|
131
135
|
type: 'integer',
|
|
132
136
|
description: `超时毫秒。省略则 ${DEFAULT_BUILTIN_TIMEOUT_MS}。上限 ${MAX_BUILTIN_TIMEOUT_MS}。`,
|
|
@@ -151,19 +155,10 @@ export function mergeHarnessPrimitives(loadedTools) {
|
|
|
151
155
|
export function assembleLoadedTools(loadedTools) {
|
|
152
156
|
return mergeHarnessPrimitives(loadedTools);
|
|
153
157
|
}
|
|
154
|
-
const MISSING_USER_DIR = '缺少用户目录,拒绝该操作';
|
|
155
158
|
const PROTECTED_WRITE_NAMES = new Set(['.assembly.json', 'assembly.json', '.ready']);
|
|
156
|
-
/** `
|
|
159
|
+
/** `dirname(agentDir)/users/<userId>`(写根;含用户 skills) */
|
|
157
160
|
export function resolveUserDir(agentDir, userId) {
|
|
158
|
-
|
|
159
|
-
const uid = userId.trim();
|
|
160
|
-
if (!dir || !uid) {
|
|
161
|
-
throw new Error(MISSING_USER_DIR);
|
|
162
|
-
}
|
|
163
|
-
if (uid === '.' || uid === '..' || /[\\/]/.test(uid)) {
|
|
164
|
-
throw new Error('非法用户标识');
|
|
165
|
-
}
|
|
166
|
-
return path.resolve(dir, 'users', uid);
|
|
161
|
+
return userRoot(agentDir, userId);
|
|
167
162
|
}
|
|
168
163
|
export function collectToolBundlePaths(loadedTools) {
|
|
169
164
|
const roots = [];
|
|
@@ -270,6 +265,17 @@ async function writeFileNoFollow(target, content, append = false) {
|
|
|
270
265
|
await handle.close();
|
|
271
266
|
}
|
|
272
267
|
}
|
|
268
|
+
function maxWriteBytes(config) {
|
|
269
|
+
const n = config.maxWriteBytes;
|
|
270
|
+
if (typeof n === 'number' && Number.isFinite(n) && n > 0)
|
|
271
|
+
return Math.floor(n);
|
|
272
|
+
return maxWriteBytesFromEnv();
|
|
273
|
+
}
|
|
274
|
+
function toolFileFromPublished(p) {
|
|
275
|
+
if (!p.url)
|
|
276
|
+
return undefined;
|
|
277
|
+
return { fileName: p.fileName, fileType: p.fileType, size: p.size, url: p.url };
|
|
278
|
+
}
|
|
273
279
|
function isInsideRoot(candidate, root) {
|
|
274
280
|
const rel = path.relative(root, candidate);
|
|
275
281
|
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
@@ -473,10 +479,27 @@ export class HarnessPrimitiveExecutor {
|
|
|
473
479
|
const filePath = String(request.arguments.path ?? '');
|
|
474
480
|
const workdir = this.workdirFor(request);
|
|
475
481
|
const located = locateSkillRelativeRead(filePath, workdir, request.skillDir, this.extraRootsFor(request));
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
482
|
+
const userBase = userReadBase(workdir);
|
|
483
|
+
let target;
|
|
484
|
+
if (path.isAbsolute(filePath.trim())) {
|
|
485
|
+
target = resolveJailedPath(filePath, this.rootsFor(request), { mustExist: true });
|
|
486
|
+
}
|
|
487
|
+
else if (located.relativeBase && located.relativeBase !== userBase) {
|
|
488
|
+
target = resolveJailedPath(located.path, this.rootsFor(request), {
|
|
489
|
+
mustExist: true,
|
|
490
|
+
relativeBase: located.relativeBase,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
else if (workdir) {
|
|
494
|
+
const product = resolveUserProductPath(filePath, workdir, 'read');
|
|
495
|
+
target = resolveJailedPath(product.abs, this.rootsFor(request), { mustExist: true });
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
target = resolveJailedPath(located.path, this.rootsFor(request), {
|
|
499
|
+
mustExist: true,
|
|
500
|
+
relativeBase: located.relativeBase,
|
|
501
|
+
});
|
|
502
|
+
}
|
|
480
503
|
const buf = await readFile(target);
|
|
481
504
|
const config = builtinConfig();
|
|
482
505
|
const lines = buf.toString('utf8').split('\n');
|
|
@@ -513,38 +536,75 @@ export class HarnessPrimitiveExecutor {
|
|
|
513
536
|
const filePath = String(request.arguments.path ?? '');
|
|
514
537
|
const content = String(request.arguments.content ?? '');
|
|
515
538
|
const workdir = this.requireWorkdir(request);
|
|
516
|
-
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
539
|
+
await ensureUserSlots(workdir);
|
|
540
|
+
const resolved = resolveUserProductPath(filePath, workdir, 'write');
|
|
541
|
+
if (PROTECTED_WRITE_NAMES.has(path.basename(resolved.abs))) {
|
|
542
|
+
throw new Error(`禁止写入平台文件:${path.basename(resolved.abs)}`);
|
|
543
|
+
}
|
|
544
|
+
const incomingBytes = Buffer.byteLength(content, 'utf8');
|
|
545
|
+
const limit = maxWriteBytes(builtinConfig());
|
|
546
|
+
if (append) {
|
|
547
|
+
const existing = await stat(resolved.abs)
|
|
548
|
+
.then((s) => s.size)
|
|
549
|
+
.catch(() => 0);
|
|
550
|
+
if (existing + incomingBytes > limit) {
|
|
551
|
+
throw new Error(`文件超过 ${limit} 字节上限`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
else if (incomingBytes > limit) {
|
|
555
|
+
throw new Error(`文件超过 ${limit} 字节上限`);
|
|
521
556
|
}
|
|
522
|
-
await mkdir(path.dirname(
|
|
523
|
-
const recast =
|
|
524
|
-
if (PROTECTED_WRITE_NAMES.has(path.basename(recast))) {
|
|
525
|
-
throw new Error(`禁止写入平台文件:${path.basename(recast)}`);
|
|
557
|
+
await mkdir(path.dirname(resolved.abs), { recursive: true });
|
|
558
|
+
const recast = resolveUserProductPath(filePath, workdir, 'write');
|
|
559
|
+
if (PROTECTED_WRITE_NAMES.has(path.basename(recast.abs))) {
|
|
560
|
+
throw new Error(`禁止写入平台文件:${path.basename(recast.abs)}`);
|
|
526
561
|
}
|
|
527
|
-
const bytesWritten = await writeFileNoFollow(recast, content, append);
|
|
528
|
-
|
|
562
|
+
const bytesWritten = await writeFileNoFollow(recast.abs, content, append);
|
|
563
|
+
await chmod(recast.abs, PUBLIC_FILE_MODE).catch(() => undefined);
|
|
564
|
+
const payload = {
|
|
565
|
+
ok: true,
|
|
566
|
+
path: recast.jsonPath,
|
|
567
|
+
bytesWritten,
|
|
568
|
+
};
|
|
569
|
+
let file;
|
|
570
|
+
if (recast.slot === 'files' && recast.relFromFiles) {
|
|
571
|
+
const published = await publishFromFiles(workdir, [recast.relFromFiles]);
|
|
572
|
+
const first = published[0];
|
|
573
|
+
if (first?.url) {
|
|
574
|
+
payload.url = first.url;
|
|
575
|
+
file = toolFileFromPublished(first);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return { ...ok(request, JSON.stringify(payload)), ...(file ? { file } : {}) };
|
|
529
579
|
}
|
|
530
580
|
async executeCommand(request) {
|
|
531
581
|
const command = substituteSkillDir(String(request.arguments.command ?? '').trim(), request.skillDir);
|
|
532
582
|
if (!command)
|
|
533
583
|
return fail(request, 'command 不能为空');
|
|
534
584
|
const workdir = this.requireWorkdir(request);
|
|
535
|
-
await
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
585
|
+
const slots = await ensureUserSlots(workdir);
|
|
586
|
+
let cwd;
|
|
587
|
+
if (request.arguments.cwd != null && String(request.arguments.cwd).trim() !== '') {
|
|
588
|
+
const resolvedCwd = resolveUserProductPath(String(request.arguments.cwd), workdir, 'cwd');
|
|
589
|
+
if (resolvedCwd.slot !== 'files') {
|
|
590
|
+
throw new Error(`路径超出允许目录:${String(request.arguments.cwd)}`);
|
|
591
|
+
}
|
|
592
|
+
cwd = resolvedCwd.abs;
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
cwd = slots.filesDir;
|
|
596
|
+
}
|
|
597
|
+
await mkdir(cwd, { recursive: true });
|
|
541
598
|
const config = builtinConfig();
|
|
542
599
|
const timeoutMs = clampBuiltinTimeout(request.arguments.timeoutMs, config);
|
|
543
600
|
const maxOutput = config.maxOutputBytes;
|
|
601
|
+
const writeLimit = maxWriteBytes(config);
|
|
602
|
+
const before = await walkFilesTree(slots.filesDir);
|
|
544
603
|
const shell = resolveShellSpawn(command);
|
|
545
|
-
const shimDir = ensureInterpreterShims(
|
|
546
|
-
const env = sandboxEnv(workdir, cwd, config.envAllowlist, shimDir, request.skillDir);
|
|
547
|
-
|
|
604
|
+
const shimDir = ensureInterpreterShims(slots.workDir);
|
|
605
|
+
const env = sandboxEnv(workdir, cwd, config.envAllowlist, shimDir, request.skillDir, slots.workDir);
|
|
606
|
+
// 子进程 close 后再快照 files/(超时先杀进程再等 close;close 未到则最多再等 SPAWN_CLOSE_GRACE_MS)。
|
|
607
|
+
const output = await new Promise((resolve) => {
|
|
548
608
|
const child = spawn(shell.file, shell.args, {
|
|
549
609
|
cwd,
|
|
550
610
|
env,
|
|
@@ -555,9 +615,36 @@ export class HarnessPrimitiveExecutor {
|
|
|
555
615
|
let stderr = Buffer.alloc(0);
|
|
556
616
|
let stdoutTruncated = false;
|
|
557
617
|
let stderrTruncated = false;
|
|
618
|
+
let settled = false;
|
|
619
|
+
let timedOut = false;
|
|
620
|
+
let spawnError;
|
|
621
|
+
let closeFallback;
|
|
622
|
+
const finish = (value) => {
|
|
623
|
+
if (settled)
|
|
624
|
+
return;
|
|
625
|
+
settled = true;
|
|
626
|
+
clearTimeout(timer);
|
|
627
|
+
if (closeFallback !== undefined)
|
|
628
|
+
clearTimeout(closeFallback);
|
|
629
|
+
resolve(value);
|
|
630
|
+
};
|
|
631
|
+
const snapshotStdio = () => ({
|
|
632
|
+
stdout: stdout.toString('utf8'),
|
|
633
|
+
stderr: stderr.toString('utf8'),
|
|
634
|
+
stdoutTruncated,
|
|
635
|
+
stderrTruncated,
|
|
636
|
+
});
|
|
637
|
+
const waitForCloseThenFinish = () => {
|
|
638
|
+
if (closeFallback !== undefined)
|
|
639
|
+
clearTimeout(closeFallback);
|
|
640
|
+
closeFallback = setTimeout(() => {
|
|
641
|
+
finish({ ...snapshotStdio(), code: null, timedOut, spawnError });
|
|
642
|
+
}, SPAWN_CLOSE_GRACE_MS);
|
|
643
|
+
};
|
|
558
644
|
const timer = setTimeout(() => {
|
|
645
|
+
timedOut = true;
|
|
559
646
|
killProcessTree(child);
|
|
560
|
-
|
|
647
|
+
waitForCloseThenFinish();
|
|
561
648
|
}, timeoutMs);
|
|
562
649
|
child.stdout?.on('data', (chunk) => {
|
|
563
650
|
if (stdout.length >= maxOutput) {
|
|
@@ -578,33 +665,71 @@ export class HarnessPrimitiveExecutor {
|
|
|
578
665
|
stderrTruncated = true;
|
|
579
666
|
});
|
|
580
667
|
child.on('error', (err) => {
|
|
668
|
+
spawnError = formatSpawnError(err, shell.file);
|
|
581
669
|
clearTimeout(timer);
|
|
582
|
-
|
|
670
|
+
waitForCloseThenFinish();
|
|
583
671
|
});
|
|
584
672
|
child.on('close', (code) => {
|
|
585
|
-
|
|
586
|
-
resolve({
|
|
587
|
-
stdout: stdout.toString('utf8'),
|
|
588
|
-
stderr: stderr.toString('utf8'),
|
|
589
|
-
code,
|
|
590
|
-
stdoutTruncated,
|
|
591
|
-
stderrTruncated,
|
|
592
|
-
});
|
|
673
|
+
finish({ ...snapshotStdio(), code, timedOut, spawnError });
|
|
593
674
|
});
|
|
594
675
|
});
|
|
595
|
-
const
|
|
676
|
+
const after = await walkFilesTree(slots.filesDir);
|
|
677
|
+
const { rels, skippedOversize } = snapshotCandidates(before.entries, after.entries, writeLimit);
|
|
678
|
+
const limited = rels.slice(0, MAX_SNAPSHOT_URLS);
|
|
679
|
+
const published = limited.length > 0 ? await publishFromFiles(workdir, limited) : [];
|
|
680
|
+
const publicFiles = published.map((p) => ({
|
|
681
|
+
path: p.sourceRel ?? p.fileName,
|
|
682
|
+
fileName: p.fileName,
|
|
683
|
+
size: p.size,
|
|
684
|
+
...(p.url ? { url: p.url } : {}),
|
|
685
|
+
}));
|
|
686
|
+
const notes = [];
|
|
687
|
+
if (before.truncated || after.truncated) {
|
|
688
|
+
notes.push(`快照枚举达到 ${MAX_SNAPSHOT_ENUM} 上限`);
|
|
689
|
+
}
|
|
690
|
+
if (skippedOversize > 0) {
|
|
691
|
+
notes.push(`跳过 ${skippedOversize} 个超过大小上限的文件`);
|
|
692
|
+
}
|
|
693
|
+
if (rels.length > MAX_SNAPSHOT_URLS) {
|
|
694
|
+
notes.push(`快照 url 最多 ${MAX_SNAPSHOT_URLS} 个`);
|
|
695
|
+
}
|
|
696
|
+
const withUrl = published
|
|
697
|
+
.map(toolFileFromPublished)
|
|
698
|
+
.filter((f) => Boolean(f));
|
|
699
|
+
const file = withUrl[0];
|
|
700
|
+
const files = withUrl.slice(1);
|
|
701
|
+
const ended = output.timedOut
|
|
702
|
+
? `命令超时(${timeoutMs}ms)`
|
|
703
|
+
: output.spawnError
|
|
704
|
+
? output.spawnError.message
|
|
705
|
+
: `exit ${output.code ?? -1}`;
|
|
706
|
+
const commandBody = [
|
|
596
707
|
output.stdout.trimEnd(),
|
|
597
708
|
output.stdoutTruncated ? `[stdout truncated at ${maxOutput} bytes]` : '',
|
|
598
709
|
output.stderr.trim() ? `stderr:\n${output.stderr.trimEnd()}` : '',
|
|
599
710
|
output.stderrTruncated ? `[stderr truncated at ${maxOutput} bytes]` : '',
|
|
600
|
-
|
|
711
|
+
ended,
|
|
712
|
+
...notes,
|
|
601
713
|
]
|
|
602
714
|
.filter(Boolean)
|
|
603
715
|
.join('\n');
|
|
716
|
+
const body = publicFiles.length > 0
|
|
717
|
+
? `${JSON.stringify({ publicFiles })}\n${commandBody}`
|
|
718
|
+
: commandBody;
|
|
719
|
+
const extra = {
|
|
720
|
+
...(file ? { file } : {}),
|
|
721
|
+
...(files.length > 0 ? { files } : {}),
|
|
722
|
+
};
|
|
723
|
+
if (output.spawnError) {
|
|
724
|
+
return { ...fail(request, output.spawnError.message), content: body, ...extra };
|
|
725
|
+
}
|
|
726
|
+
if (output.timedOut) {
|
|
727
|
+
return { ...fail(request, `命令超时(${timeoutMs}ms)`), content: body, ...extra };
|
|
728
|
+
}
|
|
604
729
|
if (output.code !== 0) {
|
|
605
|
-
return { ...fail(request, `命令退出码 ${output.code}`), content: body };
|
|
730
|
+
return { ...fail(request, `命令退出码 ${output.code}`), content: body, ...extra };
|
|
606
731
|
}
|
|
607
|
-
return ok(request, body);
|
|
732
|
+
return { ...ok(request, body), ...extra };
|
|
608
733
|
}
|
|
609
734
|
}
|
|
610
735
|
export function withHarnessPrimitives(inner, options) {
|
package/dist/tool/index.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export { formatMcpOutput, toModelVisibleToolContent, MAX_MODEL_RESULT_ROWS, MAX_
|
|
|
15
15
|
export { ToolExecutionRouter, type DeploymentContext, type ToolExecutionRouterOptions, } from './tool-execution-router.js';
|
|
16
16
|
export { createToolExecutor, EmptyInstallStore, type CreateToolExecutorOptions, } from './create-tool-executor.js';
|
|
17
17
|
export { assembleLoadedTools, mergeHarnessPrimitives as mergeBuiltinTools, withHarnessPrimitives as withBuiltinTools, HarnessPrimitiveExecutor as BuiltinToolExecutor, resolveUserDir, resolveJailFromState, captureOverlayJailOptions, collectToolBundlePaths, type HarnessJailOptions as BuiltinJailOptions, } from './harness-primitives.js';
|
|
18
|
+
export { injectPublicArtifactLinks, collectPublicArtifactLinks, applyInjectedAssistant, } from './inject-public-artifact-links.js';
|
|
19
|
+
export { publishFromFiles, ensureUserSlots, publicArtifactUrl, PUBLIC_EXTENSIONS, } from './static-artifact-url.js';
|
|
18
20
|
export { resolveShellSpawn, shellNameFor, commandDialectLines, findPythonInterpreter, findNodeInterpreter, ensureInterpreterShims, substituteSkillDir, } from './harness-command.js';
|
|
19
21
|
export { expandSkillScriptRefs, isSkillEntryFile, EAP_SKILL_DIR_ENV, } from './skill-script-refs.js';
|
|
20
22
|
export { ToolResultInjector, type ToolMessage, } from './tool-result-injector.js';
|
package/dist/tool/index.js
CHANGED
|
@@ -14,6 +14,8 @@ export { formatMcpOutput, toModelVisibleToolContent, MAX_MODEL_RESULT_ROWS, MAX_
|
|
|
14
14
|
export { ToolExecutionRouter, } from './tool-execution-router.js';
|
|
15
15
|
export { createToolExecutor, EmptyInstallStore, } from './create-tool-executor.js';
|
|
16
16
|
export { assembleLoadedTools, mergeHarnessPrimitives as mergeBuiltinTools, withHarnessPrimitives as withBuiltinTools, HarnessPrimitiveExecutor as BuiltinToolExecutor, resolveUserDir, resolveJailFromState, captureOverlayJailOptions, collectToolBundlePaths, } from './harness-primitives.js';
|
|
17
|
+
export { injectPublicArtifactLinks, collectPublicArtifactLinks, applyInjectedAssistant, } from './inject-public-artifact-links.js';
|
|
18
|
+
export { publishFromFiles, ensureUserSlots, publicArtifactUrl, PUBLIC_EXTENSIONS, } from './static-artifact-url.js';
|
|
17
19
|
export { resolveShellSpawn, shellNameFor, commandDialectLines, findPythonInterpreter, findNodeInterpreter, ensureInterpreterShims, substituteSkillDir, } from './harness-command.js';
|
|
18
20
|
export { expandSkillScriptRefs, isSkillEntryFile, EAP_SKILL_DIR_ENV, } from './skill-script-refs.js';
|
|
19
21
|
export { ToolResultInjector, } from './tool-result-injector.js';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本轮带 url 的公开副本 → 助手正文末尾 GFM 链接。
|
|
3
|
+
* stream / streamResumeHitl / stream.handler / invoke.handler 共用。
|
|
4
|
+
*/
|
|
5
|
+
export type ArtifactLink = {
|
|
6
|
+
fileName: string;
|
|
7
|
+
url: string;
|
|
8
|
+
};
|
|
9
|
+
export type ToolResultLike = {
|
|
10
|
+
content?: unknown;
|
|
11
|
+
file?: {
|
|
12
|
+
fileName?: string;
|
|
13
|
+
url?: string;
|
|
14
|
+
};
|
|
15
|
+
files?: Array<{
|
|
16
|
+
fileName?: string;
|
|
17
|
+
url?: string;
|
|
18
|
+
}>;
|
|
19
|
+
};
|
|
20
|
+
export declare function mergeArtifactLinks(...groups: ArtifactLink[][]): ArtifactLink[];
|
|
21
|
+
export declare function collectPublicArtifactLinks(results: ToolResultLike[]): ArtifactLink[];
|
|
22
|
+
export declare function collectPublicArtifactLinksFromEvent(eventType: string, payload: unknown, bucket: ArtifactLink[]): void;
|
|
23
|
+
export declare function toolResultHasArtifacts(result: ToolResultLike): boolean;
|
|
24
|
+
export declare function applyInjectedAssistant<T extends {
|
|
25
|
+
role?: string;
|
|
26
|
+
content: string;
|
|
27
|
+
toolCalls?: unknown[];
|
|
28
|
+
}>(messages: T[], injected: string, links: ArtifactLink[]): T[];
|
|
29
|
+
export declare function injectPublicArtifactLinks(visibleText: string, artifacts: ArtifactLink[]): {
|
|
30
|
+
injected: string;
|
|
31
|
+
suffix: string;
|
|
32
|
+
};
|