@gt-fe/eap-runtime 0.0.10 → 0.0.12
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 +3 -3
- package/dist/api/handlers/invoke.handler.js +4 -0
- package/dist/api/handlers/stream.handler.js +4 -0
- package/dist/contracts/trace-collector.d.ts +1 -1
- package/dist/governance/governance-client.js +40 -16
- package/dist/governance/index.d.ts +1 -1
- package/dist/governance/index.js +1 -1
- package/dist/governance/permission-guard.d.ts +1 -0
- package/dist/governance/permission-guard.js +13 -4
- package/dist/graph/adapters/langgraph.adapter.js +11 -6
- package/dist/graph/contracts/executable-graph.d.ts +2 -0
- package/dist/graph/hitl/hitl-handler.d.ts +3 -0
- package/dist/graph/hitl/hitl-handler.js +4 -1
- package/dist/graph/nodes/tool-node.js +27 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/providers/echo-llm-provider.js +2 -2
- package/dist/registry/deployment-snapshot.d.ts +14 -2
- package/dist/registry/deployment-snapshot.js +56 -30
- package/dist/registry/graph-loader.js +4 -4
- package/dist/registry/index.d.ts +1 -1
- package/dist/registry/index.js +1 -1
- package/dist/runtime/agent-runtime.js +18 -4
- package/dist/runtime/dev-runtime.d.ts +1 -1
- package/dist/runtime/dev-runtime.js +6 -10
- package/dist/tool/dev-http-executor.js +9 -6
- package/dist/tool/dev-mcp-executor.js +5 -3
- package/dist/tool/dev-tool-executor.d.ts +1 -1
- package/dist/tool/dev-tool-executor.js +2 -2
- package/dist/tool/harness-command.d.ts +32 -0
- package/dist/tool/harness-command.js +276 -0
- package/dist/tool/harness-primitives.d.ts +25 -3
- package/dist/tool/harness-primitives.js +156 -49
- package/dist/tool/http-tool-executor.js +15 -20
- package/dist/tool/index.d.ts +3 -2
- package/dist/tool/index.js +3 -2
- package/dist/tool/local-mcp-client.js +54 -23
- package/dist/tool/local-tool-executor.js +13 -9
- package/dist/tool/mcp-tool-executor.js +14 -6
- package/dist/tool/skill-script-refs.d.ts +20 -0
- package/dist/tool/skill-script-refs.js +62 -0
- package/dist/tool/system-prompt-builder.d.ts +2 -2
- package/dist/tool/system-prompt-builder.js +7 -4
- package/dist/tool/timeout.d.ts +14 -0
- package/dist/tool/timeout.js +36 -0
- package/dist/tool/tool-confirmation.js +2 -1
- package/dist/tool/tool-executor.interface.d.ts +13 -3
- package/dist/tool/tool-orchestrator.d.ts +1 -1
- package/dist/tool/tool-orchestrator.js +6 -3
- package/package.json +3 -3
- package/dist/common/concurrency.d.ts +0 -79
- package/dist/common/concurrency.js +0 -121
- package/dist/common/queue-consumer.d.ts +0 -125
- package/dist/common/queue-consumer.js +0 -159
- package/dist/tool/mock-tool-executor.d.ts +0 -28
- package/dist/tool/mock-tool-executor.js +0 -65
|
@@ -2,15 +2,18 @@
|
|
|
2
2
|
* 平台默认 Harness 原语:read_file / write_file / execute_command。
|
|
3
3
|
*
|
|
4
4
|
* 不写进 agent.manifest.toolsets。同名业务工具优先(问数 file-io 等)。
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* 命令:cwd
|
|
5
|
+
* 写:`users/<userId>/`(含用户自己的 skills)。
|
|
6
|
+
* 读:写根 + 已装配 `preinstall/tools`、`preinstall/skills`。
|
|
7
|
+
* 命令:cwd 仅写根。技能包只读,EAP_SKILL_DIR 为整包绝对根(不限 scripts/)。不把预装拷进用户装配槽。
|
|
8
|
+
* 开发态可选 overlay:`EAP_LOCAL_SKILLS_DIR` / `EAP_LOCAL_TOOLS_DIR`(只读)。
|
|
8
9
|
*/
|
|
9
10
|
import { spawn } from 'node:child_process';
|
|
10
11
|
import { constants as fsConstants, realpathSync } from 'node:fs';
|
|
11
12
|
import { mkdir, open, readFile } from 'node:fs/promises';
|
|
12
13
|
import path from 'node:path';
|
|
13
14
|
import { BUILTIN_TOOL_SOURCE, DEFAULT_BUILTIN_TIMEOUT_MS, MAX_BUILTIN_TIMEOUT_MS, clampBuiltinTimeout, isBuiltinToolExecution, resolveBuiltinToolsConfig, } from '@gt-fe/eap-contracts';
|
|
15
|
+
import { ensureInterpreterShims, formatSpawnError, killProcessTree, resolveShellSpawn, sandboxEnv, spawnOptionsFor, substituteSkillDir, } from './harness-command.js';
|
|
16
|
+
import { expandSkillScriptRefs, isSkillEntryFile } from './skill-script-refs.js';
|
|
14
17
|
function builtinConfig() {
|
|
15
18
|
return resolveBuiltinToolsConfig(process.env);
|
|
16
19
|
}
|
|
@@ -29,7 +32,7 @@ function harnessDefinitions() {
|
|
|
29
32
|
manifest: {
|
|
30
33
|
id: 'builtin:read_file',
|
|
31
34
|
name: 'read_file',
|
|
32
|
-
description: '
|
|
35
|
+
description: '读取当前用户目录或预装 tools/skills 内的文本。要用某个技能时读该技能文件夹下的 SKILL.md。超限会截断并注明。',
|
|
33
36
|
...common,
|
|
34
37
|
},
|
|
35
38
|
inputSchema: {
|
|
@@ -48,7 +51,7 @@ function harnessDefinitions() {
|
|
|
48
51
|
manifest: {
|
|
49
52
|
id: 'builtin:write_file',
|
|
50
53
|
name: 'write_file',
|
|
51
|
-
description: '
|
|
54
|
+
description: '写入当前用户目录(含用户自己的 skills)。预装 tools / skills 只读。',
|
|
52
55
|
...common,
|
|
53
56
|
},
|
|
54
57
|
inputSchema: {
|
|
@@ -66,7 +69,7 @@ function harnessDefinitions() {
|
|
|
66
69
|
manifest: {
|
|
67
70
|
id: 'builtin:execute_command',
|
|
68
71
|
name: 'execute_command',
|
|
69
|
-
description:
|
|
72
|
+
description: `用本机 shell 在用户目录执行命令(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。不继承密钥。`,
|
|
70
73
|
...common,
|
|
71
74
|
},
|
|
72
75
|
inputSchema: {
|
|
@@ -74,7 +77,7 @@ function harnessDefinitions() {
|
|
|
74
77
|
required: ['command'],
|
|
75
78
|
properties: {
|
|
76
79
|
command: { type: 'string' },
|
|
77
|
-
cwd: { type: 'string', description: '
|
|
80
|
+
cwd: { type: 'string', description: '用户目录内的子目录,可选;省略则用户目录。不能是预装技能/工具包' },
|
|
78
81
|
timeoutMs: {
|
|
79
82
|
type: 'integer',
|
|
80
83
|
description: `超时毫秒。省略则 ${DEFAULT_BUILTIN_TIMEOUT_MS}。上限 ${MAX_BUILTIN_TIMEOUT_MS}。`,
|
|
@@ -99,9 +102,63 @@ export function mergeHarnessPrimitives(loadedTools) {
|
|
|
99
102
|
export function assembleLoadedTools(loadedTools) {
|
|
100
103
|
return mergeHarnessPrimitives(loadedTools);
|
|
101
104
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
const MISSING_USER_DIR = '缺少用户目录,拒绝该操作';
|
|
106
|
+
const PROTECTED_WRITE_NAMES = new Set(['.assembly.json', 'assembly.json', '.ready']);
|
|
107
|
+
/** `{agentDir}/users/<userId>`(写根;含用户 skills) */
|
|
108
|
+
export function resolveUserDir(agentDir, userId) {
|
|
109
|
+
const dir = agentDir.trim();
|
|
110
|
+
const uid = userId.trim();
|
|
111
|
+
if (!dir || !uid) {
|
|
112
|
+
throw new Error(MISSING_USER_DIR);
|
|
113
|
+
}
|
|
114
|
+
if (uid === '.' || uid === '..' || /[\\/]/.test(uid)) {
|
|
115
|
+
throw new Error('非法用户标识');
|
|
116
|
+
}
|
|
117
|
+
return path.resolve(dir, 'users', uid);
|
|
118
|
+
}
|
|
119
|
+
export function collectToolBundlePaths(loadedTools) {
|
|
120
|
+
const roots = [];
|
|
121
|
+
for (const def of Object.values(loadedTools ?? {})) {
|
|
122
|
+
const execution = def.manifest.execution;
|
|
123
|
+
if (!execution || typeof execution !== 'object' || Array.isArray(execution))
|
|
124
|
+
continue;
|
|
125
|
+
const bundlePath = execution.bundlePath;
|
|
126
|
+
if (typeof bundlePath !== 'string' || !bundlePath.trim())
|
|
127
|
+
continue;
|
|
128
|
+
if (!roots.includes(bundlePath))
|
|
129
|
+
roots.push(bundlePath);
|
|
130
|
+
}
|
|
131
|
+
return roots;
|
|
132
|
+
}
|
|
133
|
+
export function resolveJailFromState(state) {
|
|
134
|
+
const execUser = state.executionContext?.userId;
|
|
135
|
+
const userId = (typeof execUser === 'string' && execUser.trim()) ||
|
|
136
|
+
(typeof state.userId === 'string' && state.userId.trim()) ||
|
|
137
|
+
'';
|
|
138
|
+
const agentDir = typeof state.agentDir === 'string' ? state.agentDir.trim() : '';
|
|
139
|
+
const extraRoots = [];
|
|
140
|
+
for (const skill of Object.values(state.skillDefinitions ?? {})) {
|
|
141
|
+
if (skill.bundlePath)
|
|
142
|
+
extraRoots.push(skill.bundlePath);
|
|
143
|
+
}
|
|
144
|
+
extraRoots.push(...collectToolBundlePaths(state.loadedTools));
|
|
145
|
+
const scoped = agentDir
|
|
146
|
+
? extraRoots.filter((root) => isPreinstallExtraRoot(agentDir, root))
|
|
147
|
+
: [];
|
|
148
|
+
return {
|
|
149
|
+
workdir: agentDir && userId ? resolveUserDir(agentDir, userId) : undefined,
|
|
150
|
+
extraRoots: scoped,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function deploymentAllowsLocalOverlay() {
|
|
154
|
+
const ctx = process.env.EAP_DEPLOYMENT_CONTEXT?.trim();
|
|
155
|
+
return ctx === 'dev' || ctx === 'desktop';
|
|
156
|
+
}
|
|
157
|
+
function devOverlayRoots() {
|
|
158
|
+
if (!deploymentAllowsLocalOverlay())
|
|
159
|
+
return [];
|
|
160
|
+
const extra = [];
|
|
161
|
+
for (const key of ['EAP_LOCAL_SKILLS_DIR', 'EAP_LOCAL_TOOLS_DIR']) {
|
|
105
162
|
const value = process.env[key]?.trim();
|
|
106
163
|
if (value)
|
|
107
164
|
extra.push(value);
|
|
@@ -117,8 +174,12 @@ function tryRealpath(p) {
|
|
|
117
174
|
}
|
|
118
175
|
}
|
|
119
176
|
export function resolveJailRoots(options) {
|
|
120
|
-
const workdir = path.resolve(options
|
|
121
|
-
const candidates = [
|
|
177
|
+
const workdir = options?.workdir?.trim() ? path.resolve(options.workdir) : undefined;
|
|
178
|
+
const candidates = [
|
|
179
|
+
...(workdir ? [workdir] : []),
|
|
180
|
+
...devOverlayRoots(),
|
|
181
|
+
...(options?.extraRoots ?? []),
|
|
182
|
+
];
|
|
122
183
|
const roots = [];
|
|
123
184
|
for (const raw of candidates) {
|
|
124
185
|
const resolved = path.resolve(raw);
|
|
@@ -128,27 +189,6 @@ export function resolveJailRoots(options) {
|
|
|
128
189
|
}
|
|
129
190
|
return roots;
|
|
130
191
|
}
|
|
131
|
-
function sandboxEnv(workdir, cwd, allowlist) {
|
|
132
|
-
const env = {
|
|
133
|
-
PATH: process.env.PATH ?? '/usr/bin:/bin:/usr/local/bin',
|
|
134
|
-
LANG: process.env.LANG ?? 'C.UTF-8',
|
|
135
|
-
LC_ALL: process.env.LC_ALL ?? process.env.LANG ?? 'C.UTF-8',
|
|
136
|
-
HOME: workdir,
|
|
137
|
-
TMPDIR: workdir,
|
|
138
|
-
TMP: workdir,
|
|
139
|
-
TEMP: workdir,
|
|
140
|
-
PWD: cwd,
|
|
141
|
-
};
|
|
142
|
-
const pinned = new Set(['HOME', 'TMPDIR', 'TMP', 'TEMP', 'PWD']);
|
|
143
|
-
for (const key of allowlist) {
|
|
144
|
-
if (pinned.has(key))
|
|
145
|
-
continue;
|
|
146
|
-
const value = process.env[key];
|
|
147
|
-
if (value !== undefined)
|
|
148
|
-
env[key] = value;
|
|
149
|
-
}
|
|
150
|
-
return env;
|
|
151
|
-
}
|
|
152
192
|
async function writeFileNoFollow(target, content) {
|
|
153
193
|
const flags = fsConstants.O_WRONLY |
|
|
154
194
|
fsConstants.O_CREAT |
|
|
@@ -176,6 +216,28 @@ function isInsideRoot(candidate, root) {
|
|
|
176
216
|
const rel = path.relative(root, candidate);
|
|
177
217
|
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
178
218
|
}
|
|
219
|
+
function realOrResolve(p) {
|
|
220
|
+
return tryRealpath(p) ?? path.resolve(p);
|
|
221
|
+
}
|
|
222
|
+
/** 路径落在写根外的 extraRoot 时,返回该 extraRoot。 */
|
|
223
|
+
export function extraRootContaining(abs, extraRoots, workdir) {
|
|
224
|
+
const target = realOrResolve(abs);
|
|
225
|
+
const work = realOrResolve(workdir);
|
|
226
|
+
if (isInsideRoot(target, work))
|
|
227
|
+
return undefined;
|
|
228
|
+
for (const raw of extraRoots) {
|
|
229
|
+
const root = realOrResolve(raw);
|
|
230
|
+
if (isInsideRoot(target, root))
|
|
231
|
+
return root;
|
|
232
|
+
}
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
function isPreinstallExtraRoot(agentDir, root) {
|
|
236
|
+
const resolved = path.resolve(root);
|
|
237
|
+
const tools = path.resolve(agentDir, 'preinstall', 'tools');
|
|
238
|
+
const skills = path.resolve(agentDir, 'preinstall', 'skills');
|
|
239
|
+
return isInsideRoot(resolved, tools) || isInsideRoot(resolved, skills);
|
|
240
|
+
}
|
|
179
241
|
function resolveViaExistingAncestor(abs) {
|
|
180
242
|
const suffix = [];
|
|
181
243
|
let cur = abs;
|
|
@@ -195,8 +257,15 @@ export function resolveJailedPath(requested, roots, opts) {
|
|
|
195
257
|
if (!trimmed || trimmed.includes('\0')) {
|
|
196
258
|
throw new Error('非法路径');
|
|
197
259
|
}
|
|
198
|
-
const
|
|
199
|
-
|
|
260
|
+
const relativeBase = opts?.relativeBase?.trim()
|
|
261
|
+
? path.resolve(opts.relativeBase)
|
|
262
|
+
: undefined;
|
|
263
|
+
if (!path.isAbsolute(trimmed) && !relativeBase) {
|
|
264
|
+
throw new Error(MISSING_USER_DIR);
|
|
265
|
+
}
|
|
266
|
+
const abs = path.isAbsolute(trimmed)
|
|
267
|
+
? path.normalize(trimmed)
|
|
268
|
+
: path.resolve(relativeBase, trimmed);
|
|
200
269
|
const existing = tryRealpath(abs);
|
|
201
270
|
if (opts?.mustExist && !existing) {
|
|
202
271
|
throw new Error(`文件不存在:${trimmed}`);
|
|
@@ -233,12 +302,28 @@ export class HarnessPrimitiveExecutor {
|
|
|
233
302
|
this.workdir = options?.workdir;
|
|
234
303
|
this.extraRoots = options?.extraRoots;
|
|
235
304
|
}
|
|
305
|
+
workdirFor(request) {
|
|
306
|
+
const fromRequest = request.jailWorkdir?.trim();
|
|
307
|
+
if (fromRequest)
|
|
308
|
+
return path.resolve(fromRequest);
|
|
309
|
+
return this.workdir?.trim() ? path.resolve(this.workdir) : undefined;
|
|
310
|
+
}
|
|
236
311
|
rootsFor(request) {
|
|
237
312
|
return resolveJailRoots({
|
|
238
|
-
workdir: this.
|
|
239
|
-
extraRoots:
|
|
313
|
+
workdir: this.workdirFor(request),
|
|
314
|
+
extraRoots: this.extraRootsFor(request),
|
|
240
315
|
});
|
|
241
316
|
}
|
|
317
|
+
extraRootsFor(request) {
|
|
318
|
+
return [...(this.extraRoots ?? []), ...(request.jailExtraRoots ?? [])];
|
|
319
|
+
}
|
|
320
|
+
requireWorkdir(request) {
|
|
321
|
+
const workdir = this.workdirFor(request);
|
|
322
|
+
if (!workdir) {
|
|
323
|
+
throw new Error(MISSING_USER_DIR);
|
|
324
|
+
}
|
|
325
|
+
return tryRealpath(workdir) ?? workdir;
|
|
326
|
+
}
|
|
242
327
|
async execute(request) {
|
|
243
328
|
try {
|
|
244
329
|
switch (request.toolName) {
|
|
@@ -258,7 +343,12 @@ export class HarnessPrimitiveExecutor {
|
|
|
258
343
|
}
|
|
259
344
|
async readFile(request) {
|
|
260
345
|
const filePath = String(request.arguments.path ?? request.arguments.file ?? '');
|
|
261
|
-
const
|
|
346
|
+
const workdir = this.workdirFor(request);
|
|
347
|
+
const relativeBase = workdir ? (tryRealpath(workdir) ?? path.resolve(workdir)) : undefined;
|
|
348
|
+
const target = resolveJailedPath(filePath, this.rootsFor(request), {
|
|
349
|
+
mustExist: true,
|
|
350
|
+
relativeBase,
|
|
351
|
+
});
|
|
262
352
|
const buf = await readFile(target);
|
|
263
353
|
const config = builtinConfig();
|
|
264
354
|
const lines = buf.toString('utf8').split('\n');
|
|
@@ -286,42 +376,59 @@ export class HarnessPrimitiveExecutor {
|
|
|
286
376
|
if (notes.length > 0) {
|
|
287
377
|
text = `${text}\n[${notes.join('; ')}]`;
|
|
288
378
|
}
|
|
379
|
+
if (isSkillEntryFile(target)) {
|
|
380
|
+
text = expandSkillScriptRefs(text, path.dirname(target));
|
|
381
|
+
}
|
|
289
382
|
return ok(request, text);
|
|
290
383
|
}
|
|
291
384
|
async writeFile(request) {
|
|
292
385
|
const filePath = String(request.arguments.path ?? '');
|
|
293
386
|
const content = String(request.arguments.content ?? '');
|
|
294
|
-
const workdir = this.
|
|
387
|
+
const workdir = this.requireWorkdir(request);
|
|
295
388
|
const writeRoots = [workdir];
|
|
296
|
-
const
|
|
389
|
+
const jailed = { relativeBase: workdir };
|
|
390
|
+
const target = resolveJailedPath(filePath, writeRoots, jailed);
|
|
391
|
+
if (PROTECTED_WRITE_NAMES.has(path.basename(target))) {
|
|
392
|
+
throw new Error(`禁止写入平台文件:${path.basename(target)}`);
|
|
393
|
+
}
|
|
297
394
|
await mkdir(path.dirname(target), { recursive: true });
|
|
298
|
-
const recast = resolveJailedPath(filePath, writeRoots);
|
|
395
|
+
const recast = resolveJailedPath(filePath, writeRoots, jailed);
|
|
396
|
+
if (PROTECTED_WRITE_NAMES.has(path.basename(recast))) {
|
|
397
|
+
throw new Error(`禁止写入平台文件:${path.basename(recast)}`);
|
|
398
|
+
}
|
|
299
399
|
await writeFileNoFollow(recast, content);
|
|
300
400
|
return ok(request, JSON.stringify({ ok: true, path: path.relative(workdir, recast) }));
|
|
301
401
|
}
|
|
302
402
|
async executeCommand(request) {
|
|
303
|
-
const command = String(request.arguments.command ?? '').trim();
|
|
403
|
+
const command = substituteSkillDir(String(request.arguments.command ?? '').trim(), request.skillDir);
|
|
304
404
|
if (!command)
|
|
305
405
|
return fail(request, 'command 不能为空');
|
|
306
|
-
const
|
|
307
|
-
|
|
406
|
+
const workdir = this.requireWorkdir(request);
|
|
407
|
+
await mkdir(workdir, { recursive: true });
|
|
308
408
|
const cwdArg = request.arguments.cwd != null ? String(request.arguments.cwd) : workdir;
|
|
309
|
-
const cwd = resolveJailedPath(cwdArg,
|
|
409
|
+
const cwd = resolveJailedPath(cwdArg, [workdir], {
|
|
410
|
+
mustExist: true,
|
|
411
|
+
relativeBase: workdir,
|
|
412
|
+
});
|
|
310
413
|
const config = builtinConfig();
|
|
311
414
|
const timeoutMs = clampBuiltinTimeout(request.arguments.timeoutMs, config);
|
|
312
415
|
const maxOutput = config.maxOutputBytes;
|
|
416
|
+
const shell = resolveShellSpawn(command);
|
|
417
|
+
const shimDir = ensureInterpreterShims(workdir);
|
|
418
|
+
const env = sandboxEnv(workdir, cwd, config.envAllowlist, shimDir, request.skillDir);
|
|
313
419
|
const output = await new Promise((resolve, reject) => {
|
|
314
|
-
const child = spawn(
|
|
420
|
+
const child = spawn(shell.file, shell.args, {
|
|
315
421
|
cwd,
|
|
316
|
-
env
|
|
422
|
+
env,
|
|
317
423
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
424
|
+
...spawnOptionsFor(shell),
|
|
318
425
|
});
|
|
319
426
|
let stdout = Buffer.alloc(0);
|
|
320
427
|
let stderr = Buffer.alloc(0);
|
|
321
428
|
let stdoutTruncated = false;
|
|
322
429
|
let stderrTruncated = false;
|
|
323
430
|
const timer = setTimeout(() => {
|
|
324
|
-
child
|
|
431
|
+
killProcessTree(child);
|
|
325
432
|
reject(new Error(`命令超时(${timeoutMs}ms)`));
|
|
326
433
|
}, timeoutMs);
|
|
327
434
|
child.stdout?.on('data', (chunk) => {
|
|
@@ -344,7 +451,7 @@ export class HarnessPrimitiveExecutor {
|
|
|
344
451
|
});
|
|
345
452
|
child.on('error', (err) => {
|
|
346
453
|
clearTimeout(timer);
|
|
347
|
-
reject(err);
|
|
454
|
+
reject(formatSpawnError(err, shell.file));
|
|
348
455
|
});
|
|
349
456
|
child.on('close', (code) => {
|
|
350
457
|
clearTimeout(timer);
|
|
@@ -4,40 +4,45 @@
|
|
|
4
4
|
* @see docs/contracts/tool-execution.md §2
|
|
5
5
|
* @see docs/contracts/cross-service-api.md §4.6
|
|
6
6
|
*/
|
|
7
|
+
import { ErrorCodes } from '@gt-fe/eap-contracts';
|
|
8
|
+
import { abortOrTimeoutMessage, isAbortError } from './timeout.js';
|
|
7
9
|
function mapGatewayHttpError(status, body, statusText) {
|
|
8
10
|
const code = body?.error?.code;
|
|
9
|
-
const
|
|
11
|
+
const bodyMessage = [body?.error?.message, body?.message].find((s) => typeof s === 'string' && s.trim()) ?? '';
|
|
12
|
+
const message = bodyMessage ||
|
|
13
|
+
(typeof statusText === 'string' && statusText.trim()) ||
|
|
14
|
+
'';
|
|
10
15
|
if (status === 403) {
|
|
11
16
|
return {
|
|
12
|
-
error:
|
|
17
|
+
error: bodyMessage || '用户无该 Tool 执行权限',
|
|
13
18
|
errorCode: code ?? 'NO_PERMISSION',
|
|
14
19
|
retryable: false,
|
|
15
20
|
};
|
|
16
21
|
}
|
|
17
22
|
if (status === 429) {
|
|
18
23
|
return {
|
|
19
|
-
error: message
|
|
24
|
+
error: message || `HTTP ${status}`,
|
|
20
25
|
errorCode: code ?? 'RATE_LIMITED',
|
|
21
26
|
retryable: body?.error?.retryable ?? true,
|
|
22
27
|
};
|
|
23
28
|
}
|
|
24
29
|
if (status === 503) {
|
|
25
30
|
return {
|
|
26
|
-
error: message
|
|
31
|
+
error: message || `HTTP ${status}`,
|
|
27
32
|
errorCode: code ?? 'CIRCUIT_OPEN',
|
|
28
33
|
retryable: body?.error?.retryable ?? true,
|
|
29
34
|
};
|
|
30
35
|
}
|
|
31
36
|
if (status === 404) {
|
|
32
37
|
return {
|
|
33
|
-
error: message
|
|
38
|
+
error: message || `HTTP ${status}`,
|
|
34
39
|
errorCode: code ?? 'TOOL_NOT_FOUND',
|
|
35
40
|
retryable: false,
|
|
36
41
|
};
|
|
37
42
|
}
|
|
38
43
|
if (status === 400) {
|
|
39
44
|
return {
|
|
40
|
-
error: message
|
|
45
|
+
error: message || `HTTP ${status}`,
|
|
41
46
|
errorCode: code ?? 'INVALID_ARGUMENTS',
|
|
42
47
|
retryable: false,
|
|
43
48
|
};
|
|
@@ -132,25 +137,15 @@ export class HttpToolExecutor {
|
|
|
132
137
|
}
|
|
133
138
|
catch (error) {
|
|
134
139
|
clearTimeout(timeoutId);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
toolCallId: request.toolCallId,
|
|
138
|
-
toolName: request.toolName,
|
|
139
|
-
content: '',
|
|
140
|
-
success: false,
|
|
141
|
-
error: `Request timeout after ${this.timeout}ms`,
|
|
142
|
-
errorCode: 'EXECUTION_FAILED',
|
|
143
|
-
runtime: 'server',
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
const message = error instanceof Error ? error.message : 'Unknown network error';
|
|
140
|
+
const timeoutSec = Math.max(1, Math.round(this.timeout / 1000));
|
|
141
|
+
const aborted = isAbortError(error);
|
|
147
142
|
return {
|
|
148
143
|
toolCallId: request.toolCallId,
|
|
149
144
|
toolName: request.toolName,
|
|
150
145
|
content: '',
|
|
151
146
|
success: false,
|
|
152
|
-
error:
|
|
153
|
-
errorCode: 'EXECUTION_FAILED',
|
|
147
|
+
error: abortOrTimeoutMessage(error, timeoutSec),
|
|
148
|
+
errorCode: aborted ? ErrorCodes.TOOL_TIMEOUT : 'EXECUTION_FAILED',
|
|
154
149
|
runtime: 'server',
|
|
155
150
|
};
|
|
156
151
|
}
|
package/dist/tool/index.d.ts
CHANGED
|
@@ -7,12 +7,13 @@ export { LocalToolExecutor, isLocalPreferredExecution, resolveCodeExecutionFromR
|
|
|
7
7
|
export { DevMcpExecutor, type DevMcpExecutorOptions, } from './dev-mcp-executor.js';
|
|
8
8
|
export { DevHttpExecutor, buildHttpToolUrl, resolveHttpExecutionFromRequest, type DevHttpExecution, } from './dev-http-executor.js';
|
|
9
9
|
export { DevToolExecutor, type DevToolExecutorOptions } from './dev-tool-executor.js';
|
|
10
|
-
export { MockToolExecutor, type MockToolExecutorOptions, } from './mock-tool-executor.js';
|
|
11
10
|
export { LocalMcpClient, type ILocalMcpClient, type LocalMcpInvokeContext, type LocalMcpInvokeResult, } from './local-mcp-client.js';
|
|
12
11
|
export { fileArtifactFromToolOutput, extractFileReference, resolveFileArtifact, type FileArtifact, } from './file-artifact.js';
|
|
13
12
|
export { ToolExecutionRouter, type DeploymentContext, type ToolExecutionRouterOptions, } from './tool-execution-router.js';
|
|
14
13
|
export { createToolExecutor, EmptyInstallStore, type CreateToolExecutorOptions, } from './create-tool-executor.js';
|
|
15
|
-
export { assembleLoadedTools, mergeHarnessPrimitives as mergeBuiltinTools, withHarnessPrimitives as withBuiltinTools, HarnessPrimitiveExecutor as BuiltinToolExecutor, type HarnessJailOptions as BuiltinJailOptions, } from './harness-primitives.js';
|
|
14
|
+
export { assembleLoadedTools, mergeHarnessPrimitives as mergeBuiltinTools, withHarnessPrimitives as withBuiltinTools, HarnessPrimitiveExecutor as BuiltinToolExecutor, resolveUserDir, resolveJailFromState, collectToolBundlePaths, type HarnessJailOptions as BuiltinJailOptions, } from './harness-primitives.js';
|
|
15
|
+
export { resolveShellSpawn, shellNameFor, commandDialectLines, findPythonInterpreter, findNodeInterpreter, ensureInterpreterShims, substituteSkillDir, } from './harness-command.js';
|
|
16
|
+
export { expandSkillScriptRefs, isSkillEntryFile, EAP_SKILL_DIR_ENV, } from './skill-script-refs.js';
|
|
16
17
|
export { ToolResultInjector, type ToolMessage, } from './tool-result-injector.js';
|
|
17
18
|
export { ToolEventEmitter } from './tool-event-emitter.js';
|
|
18
19
|
export { ToolConfirmationInterceptor } from './tool-confirmation.js';
|
package/dist/tool/index.js
CHANGED
|
@@ -6,12 +6,13 @@ export { LocalToolExecutor, isLocalPreferredExecution, resolveCodeExecutionFromR
|
|
|
6
6
|
export { DevMcpExecutor, } from './dev-mcp-executor.js';
|
|
7
7
|
export { DevHttpExecutor, buildHttpToolUrl, resolveHttpExecutionFromRequest, } from './dev-http-executor.js';
|
|
8
8
|
export { DevToolExecutor } from './dev-tool-executor.js';
|
|
9
|
-
export { MockToolExecutor, } from './mock-tool-executor.js';
|
|
10
9
|
export { LocalMcpClient, } from './local-mcp-client.js';
|
|
11
10
|
export { fileArtifactFromToolOutput, extractFileReference, resolveFileArtifact, } from './file-artifact.js';
|
|
12
11
|
export { ToolExecutionRouter, } from './tool-execution-router.js';
|
|
13
12
|
export { createToolExecutor, EmptyInstallStore, } from './create-tool-executor.js';
|
|
14
|
-
export { assembleLoadedTools, mergeHarnessPrimitives as mergeBuiltinTools, withHarnessPrimitives as withBuiltinTools, HarnessPrimitiveExecutor as BuiltinToolExecutor, } from './harness-primitives.js';
|
|
13
|
+
export { assembleLoadedTools, mergeHarnessPrimitives as mergeBuiltinTools, withHarnessPrimitives as withBuiltinTools, HarnessPrimitiveExecutor as BuiltinToolExecutor, resolveUserDir, resolveJailFromState, collectToolBundlePaths, } from './harness-primitives.js';
|
|
14
|
+
export { resolveShellSpawn, shellNameFor, commandDialectLines, findPythonInterpreter, findNodeInterpreter, ensureInterpreterShims, substituteSkillDir, } from './harness-command.js';
|
|
15
|
+
export { expandSkillScriptRefs, isSkillEntryFile, EAP_SKILL_DIR_ENV, } from './skill-script-refs.js';
|
|
15
16
|
export { ToolResultInjector, } from './tool-result-injector.js';
|
|
16
17
|
export { ToolEventEmitter } from './tool-event-emitter.js';
|
|
17
18
|
export { ToolConfirmationInterceptor } from './tool-confirmation.js';
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { spawn } from 'node:child_process';
|
|
8
8
|
import readline from 'node:readline';
|
|
9
|
+
import { isAbortError, resolveToolTimeoutSec, toToolTimeoutMs, toolTimeoutErrorMessage, } from './timeout.js';
|
|
9
10
|
import { fileArtifactFromToolOutput, fileBasesFromEnv } from './file-artifact.js';
|
|
10
11
|
export function formatMcpOutput(result) {
|
|
11
12
|
const r = result;
|
|
@@ -70,10 +71,13 @@ function withUserMeta(params, userId) {
|
|
|
70
71
|
function rpcStdio(session, method, params) {
|
|
71
72
|
const id = session.nextId++;
|
|
72
73
|
const timeoutMs = session.timeoutMs;
|
|
74
|
+
const timeoutSec = Math.max(1, Math.round(timeoutMs / 1000));
|
|
73
75
|
return new Promise((resolve, reject) => {
|
|
74
76
|
const timer = setTimeout(() => {
|
|
75
77
|
session.pending.delete(id);
|
|
76
|
-
|
|
78
|
+
const err = new Error(toolTimeoutErrorMessage(timeoutSec));
|
|
79
|
+
err.name = 'TimeoutError';
|
|
80
|
+
reject(err);
|
|
77
81
|
}, timeoutMs);
|
|
78
82
|
session.pending.set(id, {
|
|
79
83
|
resolve: (v) => { clearTimeout(timer); resolve(v); },
|
|
@@ -86,7 +90,7 @@ async function invokeStdio(execution, args, context) {
|
|
|
86
90
|
const { server } = execution;
|
|
87
91
|
const command = server.command ?? 'node';
|
|
88
92
|
const spawnArgs = server.args ?? [];
|
|
89
|
-
const timeoutMs = execution.timeout
|
|
93
|
+
const timeoutMs = toToolTimeoutMs(execution.timeout);
|
|
90
94
|
const userId = resolveUserId(execution, context);
|
|
91
95
|
const child = spawn(command, spawnArgs, {
|
|
92
96
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -138,12 +142,32 @@ async function invokeStdio(execution, args, context) {
|
|
|
138
142
|
rl.close();
|
|
139
143
|
}
|
|
140
144
|
}
|
|
145
|
+
async function postMcp(url, headers, payload, signal) {
|
|
146
|
+
const response = await fetch(url, {
|
|
147
|
+
method: 'POST',
|
|
148
|
+
headers,
|
|
149
|
+
body: JSON.stringify(payload),
|
|
150
|
+
signal,
|
|
151
|
+
});
|
|
152
|
+
const sessionId = response.headers.get('mcp-session-id') ?? undefined;
|
|
153
|
+
const raw = await response.text();
|
|
154
|
+
if (!response.ok) {
|
|
155
|
+
const snippet = raw.replace(/\s+/g, ' ').trim().slice(0, 240);
|
|
156
|
+
throw new Error(`MCP HTTP ${response.status}: ${response.statusText}${snippet ? ` ${snippet}` : ''}`);
|
|
157
|
+
}
|
|
158
|
+
const body = parseMcpHttpBody(raw);
|
|
159
|
+
if (body.error) {
|
|
160
|
+
throw new Error(body.error.message ?? 'MCP HTTP error');
|
|
161
|
+
}
|
|
162
|
+
return { body, sessionId };
|
|
163
|
+
}
|
|
141
164
|
async function invokeStreamableHttp(execution, args, context) {
|
|
142
165
|
const url = expandEnvRefs(execution.server.url);
|
|
143
166
|
if (!url) {
|
|
144
167
|
throw new Error('streamable_http MCP requires server.url');
|
|
145
168
|
}
|
|
146
|
-
const
|
|
169
|
+
const timeoutSec = resolveToolTimeoutSec(execution.timeout);
|
|
170
|
+
const timeoutMs = toToolTimeoutMs(timeoutSec);
|
|
147
171
|
const controller = new AbortController();
|
|
148
172
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
149
173
|
const userId = resolveUserId(execution, context);
|
|
@@ -161,28 +185,35 @@ async function invokeStreamableHttp(execution, args, context) {
|
|
|
161
185
|
if (userId) {
|
|
162
186
|
headers.userId = userId;
|
|
163
187
|
}
|
|
164
|
-
const body = {
|
|
165
|
-
jsonrpc: '2.0',
|
|
166
|
-
id: 1,
|
|
167
|
-
method: 'tools/call',
|
|
168
|
-
params: withUserMeta({ name: execution.toolName, arguments: args }, userId),
|
|
169
|
-
};
|
|
170
188
|
try {
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
if (
|
|
183
|
-
|
|
189
|
+
const init = await postMcp(url, headers, {
|
|
190
|
+
jsonrpc: '2.0',
|
|
191
|
+
id: 1,
|
|
192
|
+
method: 'initialize',
|
|
193
|
+
params: {
|
|
194
|
+
protocolVersion: '2024-11-05',
|
|
195
|
+
capabilities: {},
|
|
196
|
+
clientInfo: { name: 'eap-runtime-local', version: '0.1.0' },
|
|
197
|
+
},
|
|
198
|
+
}, controller.signal);
|
|
199
|
+
const callHeaders = { ...headers };
|
|
200
|
+
if (init.sessionId)
|
|
201
|
+
callHeaders['mcp-session-id'] = init.sessionId;
|
|
202
|
+
const call = await postMcp(url, callHeaders, {
|
|
203
|
+
jsonrpc: '2.0',
|
|
204
|
+
id: 2,
|
|
205
|
+
method: 'tools/call',
|
|
206
|
+
params: withUserMeta({ name: execution.toolName, arguments: args }, userId),
|
|
207
|
+
}, controller.signal);
|
|
208
|
+
return toInvokeResult(call.body.result, execution.server.env);
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
if (isAbortError(err)) {
|
|
212
|
+
const timeoutErr = new Error(toolTimeoutErrorMessage(timeoutSec));
|
|
213
|
+
timeoutErr.name = 'TimeoutError';
|
|
214
|
+
throw timeoutErr;
|
|
184
215
|
}
|
|
185
|
-
|
|
216
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
186
217
|
}
|
|
187
218
|
finally {
|
|
188
219
|
clearTimeout(timer);
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
import { spawn } from 'node:child_process';
|
|
8
8
|
import { existsSync } from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
|
-
import { resolveReadyInstallByToolName, resolveMcpExecutionFromInstall, resolveCodeHandlerFromInstall, } from '@gt-fe/eap-contracts';
|
|
10
|
+
import { ErrorCodes, resolveReadyInstallByToolName, resolveMcpExecutionFromInstall, resolveCodeHandlerFromInstall, } from '@gt-fe/eap-contracts';
|
|
11
|
+
import { isAbortError, toToolTimeoutMs, toolTimeoutErrorMessage } from './timeout.js';
|
|
11
12
|
import { fileArtifactFromToolOutput, fileBasesFromEnv } from './file-artifact.js';
|
|
12
13
|
import { LocalMcpClient } from './local-mcp-client.js';
|
|
13
14
|
/**
|
|
@@ -66,7 +67,9 @@ function runHandler(handlerPath, input, timeoutMs, execution) {
|
|
|
66
67
|
};
|
|
67
68
|
const timer = setTimeout(() => {
|
|
68
69
|
child.kill('SIGKILL');
|
|
69
|
-
|
|
70
|
+
const timeoutErr = new Error(toolTimeoutErrorMessage(Math.max(1, Math.round(timeoutMs / 1000))));
|
|
71
|
+
timeoutErr.name = 'TimeoutError';
|
|
72
|
+
finish(() => reject(timeoutErr));
|
|
70
73
|
}, timeoutMs);
|
|
71
74
|
child.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
72
75
|
child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
@@ -112,7 +115,7 @@ export function resolveCodeExecutionFromRequest(request) {
|
|
|
112
115
|
const handler = typeof e.handler === 'string' ? e.handler : '';
|
|
113
116
|
const command = typeof e.command === 'string' ? e.command : undefined;
|
|
114
117
|
const args = Array.isArray(e.args) ? e.args.map(String) : undefined;
|
|
115
|
-
const timeoutMs = typeof e.timeout === 'number' && e.timeout > 0 ? e.timeout : undefined;
|
|
118
|
+
const timeoutMs = typeof e.timeout === 'number' && e.timeout > 0 ? toToolTimeoutMs(e.timeout) : undefined;
|
|
116
119
|
if (runtime === 'executable') {
|
|
117
120
|
const handlerPath = handler
|
|
118
121
|
? (path.isAbsolute(handler) || !bundlePath
|
|
@@ -181,14 +184,14 @@ export class LocalToolExecutor {
|
|
|
181
184
|
};
|
|
182
185
|
}
|
|
183
186
|
catch (error) {
|
|
184
|
-
const
|
|
187
|
+
const aborted = isAbortError(error);
|
|
185
188
|
return {
|
|
186
189
|
toolCallId: request.toolCallId,
|
|
187
190
|
toolName: request.toolName,
|
|
188
191
|
content: '',
|
|
189
192
|
success: false,
|
|
190
|
-
error: message,
|
|
191
|
-
errorCode: 'EXECUTION_FAILED',
|
|
193
|
+
error: error instanceof Error ? error.message : String(error),
|
|
194
|
+
errorCode: aborted ? ErrorCodes.TOOL_TIMEOUT : 'EXECUTION_FAILED',
|
|
192
195
|
runtime: 'local',
|
|
193
196
|
};
|
|
194
197
|
}
|
|
@@ -203,6 +206,7 @@ export class LocalToolExecutor {
|
|
|
203
206
|
handlerPath,
|
|
204
207
|
runtime: execution?.runtime,
|
|
205
208
|
args: execution?.args,
|
|
209
|
+
timeoutMs: typeof execution?.timeout === 'number' ? toToolTimeoutMs(execution.timeout) : undefined,
|
|
206
210
|
});
|
|
207
211
|
}
|
|
208
212
|
async executeMcp(request, install) {
|
|
@@ -232,14 +236,14 @@ export class LocalToolExecutor {
|
|
|
232
236
|
};
|
|
233
237
|
}
|
|
234
238
|
catch (error) {
|
|
235
|
-
const
|
|
239
|
+
const aborted = isAbortError(error);
|
|
236
240
|
return {
|
|
237
241
|
toolCallId: request.toolCallId,
|
|
238
242
|
toolName: request.toolName,
|
|
239
243
|
content: '',
|
|
240
244
|
success: false,
|
|
241
|
-
error: message,
|
|
242
|
-
errorCode: 'EXECUTION_FAILED',
|
|
245
|
+
error: error instanceof Error ? error.message : String(error),
|
|
246
|
+
errorCode: aborted ? ErrorCodes.TOOL_TIMEOUT : 'EXECUTION_FAILED',
|
|
243
247
|
runtime: 'local',
|
|
244
248
|
};
|
|
245
249
|
}
|