@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.
Files changed (56) hide show
  1. package/README.md +3 -3
  2. package/dist/api/handlers/invoke.handler.js +4 -0
  3. package/dist/api/handlers/stream.handler.js +4 -0
  4. package/dist/contracts/trace-collector.d.ts +1 -1
  5. package/dist/governance/governance-client.js +40 -16
  6. package/dist/governance/index.d.ts +1 -1
  7. package/dist/governance/index.js +1 -1
  8. package/dist/governance/permission-guard.d.ts +1 -0
  9. package/dist/governance/permission-guard.js +13 -4
  10. package/dist/graph/adapters/langgraph.adapter.js +11 -6
  11. package/dist/graph/contracts/executable-graph.d.ts +2 -0
  12. package/dist/graph/hitl/hitl-handler.d.ts +3 -0
  13. package/dist/graph/hitl/hitl-handler.js +4 -1
  14. package/dist/graph/nodes/tool-node.js +27 -2
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.js +1 -1
  17. package/dist/providers/echo-llm-provider.js +2 -2
  18. package/dist/registry/deployment-snapshot.d.ts +14 -2
  19. package/dist/registry/deployment-snapshot.js +56 -30
  20. package/dist/registry/graph-loader.js +4 -4
  21. package/dist/registry/index.d.ts +1 -1
  22. package/dist/registry/index.js +1 -1
  23. package/dist/runtime/agent-runtime.js +18 -4
  24. package/dist/runtime/dev-runtime.d.ts +1 -1
  25. package/dist/runtime/dev-runtime.js +6 -10
  26. package/dist/tool/dev-http-executor.js +9 -6
  27. package/dist/tool/dev-mcp-executor.js +5 -3
  28. package/dist/tool/dev-tool-executor.d.ts +1 -1
  29. package/dist/tool/dev-tool-executor.js +2 -2
  30. package/dist/tool/harness-command.d.ts +32 -0
  31. package/dist/tool/harness-command.js +276 -0
  32. package/dist/tool/harness-primitives.d.ts +25 -3
  33. package/dist/tool/harness-primitives.js +156 -49
  34. package/dist/tool/http-tool-executor.js +15 -20
  35. package/dist/tool/index.d.ts +3 -2
  36. package/dist/tool/index.js +3 -2
  37. package/dist/tool/local-mcp-client.js +54 -23
  38. package/dist/tool/local-tool-executor.js +13 -9
  39. package/dist/tool/mcp-tool-executor.js +14 -6
  40. package/dist/tool/skill-script-refs.d.ts +20 -0
  41. package/dist/tool/skill-script-refs.js +62 -0
  42. package/dist/tool/system-prompt-builder.d.ts +2 -2
  43. package/dist/tool/system-prompt-builder.js +7 -4
  44. package/dist/tool/timeout.d.ts +14 -0
  45. package/dist/tool/timeout.js +36 -0
  46. package/dist/tool/tool-confirmation.js +2 -1
  47. package/dist/tool/tool-executor.interface.d.ts +13 -3
  48. package/dist/tool/tool-orchestrator.d.ts +1 -1
  49. package/dist/tool/tool-orchestrator.js +6 -3
  50. package/package.json +3 -3
  51. package/dist/common/concurrency.d.ts +0 -79
  52. package/dist/common/concurrency.js +0 -121
  53. package/dist/common/queue-consumer.d.ts +0 -125
  54. package/dist/common/queue-consumer.js +0 -159
  55. package/dist/tool/mock-tool-executor.d.ts +0 -28
  56. package/dist/tool/mock-tool-executor.js +0 -65
@@ -7,7 +7,7 @@
7
7
  * @see 04-runtime-core/design.md §1.5 / §1.7
8
8
  */
9
9
  import { randomUUID } from 'node:crypto';
10
- import { HitlAlreadyTimedOutError, HitlNotWaitingError, buildHitlWaitedPayload, isHitlDeadlinePassed, readPersistedHitlWaiting, unwrapHitlPaused, } from '../graph/hitl/hitl-handler.js';
10
+ import { HITL_TIMEOUT_ERROR_CODE, HITL_TIMEOUT_MESSAGE, HitlAlreadyTimedOutError, HitlNotWaitingError, buildHitlWaitedPayload, isHitlDeadlinePassed, readPersistedHitlWaiting, unwrapHitlPaused, } from '../graph/hitl/hitl-handler.js';
11
11
  import { toLoadedSkillMap } from '../tool/system-prompt-builder.js';
12
12
  import { assembleLoadedTools } from '../tool/harness-primitives.js';
13
13
  import { createFileLlmTurnLogFromEnv, } from '../debug/llm-turn-log.js';
@@ -268,6 +268,9 @@ export class AgentRuntime {
268
268
  const promptRoots = this.deps.graphLoader
269
269
  ? await this.deps.graphLoader.resolvePromptRoots(agentCode, params.agentVersion)
270
270
  : [];
271
+ const agentDir = this.deps.graphLoader
272
+ ? (await this.deps.graphLoader.resolveAgentDir(agentCode, params.agentVersion)) ?? undefined
273
+ : undefined;
271
274
  const history = messageRecordsToGraphMessages(sessionContext.messages);
272
275
  const messages = await buildTurnGraphMessages({
273
276
  history,
@@ -297,6 +300,7 @@ export class AgentRuntime {
297
300
  loadedSkills,
298
301
  skillDefinitions,
299
302
  promptRoots,
303
+ agentDir,
300
304
  skillInput: resolveSkillInputFromInvokeInput(params.input),
301
305
  traceId: resolveTraceId(params),
302
306
  traceCollector: this.deps.traceCollector,
@@ -442,7 +446,7 @@ export class AgentRuntime {
442
446
  }
443
447
  catch (err) {
444
448
  if (err instanceof HitlAlreadyTimedOutError) {
445
- throw new AppError(ErrorCodes.TASK_HITL_TIMEOUT, `Task ${taskId} HITL wait exceeded deadline`, { exposeMessage: true });
449
+ throw new AppError(ErrorCodes.TASK_HITL_TIMEOUT, HITL_TIMEOUT_MESSAGE, { exposeMessage: true });
446
450
  }
447
451
  if (err instanceof HitlNotWaitingError) {
448
452
  throw new AppError(ErrorCodes.TASK_HITL_INVALID_STATE, `Task ${taskId} is not waiting for HITL`, { exposeMessage: true });
@@ -482,6 +486,10 @@ export class AgentRuntime {
482
486
  taskId,
483
487
  correlationId: resumedState.correlationId ?? correlationId,
484
488
  agentVersion: agentVersion ?? resumedState.agentVersion,
489
+ agentDir: (typeof resumedState.agentDir === 'string' && resumedState.agentDir) ||
490
+ (this.deps.graphLoader
491
+ ? (await this.deps.graphLoader.resolveAgentDir(agentCode, agentVersion)) ?? undefined
492
+ : undefined),
485
493
  activatedSkill: resumedState.activatedSkill,
486
494
  skillRoute: resumedState.skillRoute ??
487
495
  (typeof resumedState.activatedSkill === 'string' && resumedState.activatedSkill
@@ -640,10 +648,15 @@ export class AgentRuntime {
640
648
  const loadedToolsBase = await this.resolveLoadedTools(agentCode, agentVersion);
641
649
  const skillBundle = await this.resolveLoadedSkills(agentCode, userId, agentVersion);
642
650
  const loadedTools = assembleLoadedTools(loadedToolsBase);
651
+ const agentDir = (typeof forkedCheckpoint.state.agentDir === 'string' && forkedCheckpoint.state.agentDir) ||
652
+ (this.deps.graphLoader
653
+ ? (await this.deps.graphLoader.resolveAgentDir(agentCode, agentVersion)) ?? undefined
654
+ : undefined);
643
655
  try {
644
656
  const result = await this.deps.graphEngine.invoke(graph, {
645
657
  ...forkedCheckpoint.state,
646
658
  agentVersion,
659
+ agentDir,
647
660
  userId,
648
661
  delegationToken: overrides.executionContext?.delegationToken,
649
662
  executionContext: overrides.executionContext,
@@ -740,7 +753,7 @@ export class AgentRuntime {
740
753
  toolSpanId: persisted.toolSpanId,
741
754
  skillSpanId: persisted.skillSpanId,
742
755
  });
743
- throw new AppError(ErrorCodes.TASK_HITL_TIMEOUT, `Task ${taskId} HITL wait exceeded deadline`, { exposeMessage: true });
756
+ throw new AppError(ErrorCodes.TASK_HITL_TIMEOUT, HITL_TIMEOUT_MESSAGE, { exposeMessage: true });
744
757
  }
745
758
  return {
746
759
  task,
@@ -780,7 +793,7 @@ export class AgentRuntime {
780
793
  return;
781
794
  }
782
795
  const collector = this.deps.traceCollector;
783
- const error = event.behavior === 'cancel' ? 'HITL cancelled by timeout' : 'HITL wait timed out';
796
+ const error = HITL_TIMEOUT_MESSAGE;
784
797
  if (collector) {
785
798
  if (event.toolSpanId) {
786
799
  try {
@@ -807,6 +820,7 @@ export class AgentRuntime {
807
820
  toolCallId: event.toolCallId,
808
821
  toolName: event.toolName,
809
822
  error,
823
+ errorCode: HITL_TIMEOUT_ERROR_CODE,
810
824
  });
811
825
  }
812
826
  catch {
@@ -18,7 +18,7 @@
18
18
  *
19
19
  * 装配策略(与 server.ts 一致):
20
20
  * - LLM Provider: Routing > HttpLLMProvider(OpenAI) > Echo 兜底
21
- * - Tool Executor: mock(显式)/ DevToolExecutor(dev+mcp,缺配置失败)/ 通用路由
21
+ * - Tool Executor: DevToolExecutor(dev+mcp,缺配置失败)/ 通用路由
22
22
  * - Governance: AllowAll(本地闭环默认) / HttpGovernance(显式 GOVERNANCE_DISABLED=0)
23
23
  * - 存储: EAP_RUNTIME_STORAGE_PROFILE(默认 dev = File Session/Message/Checkpoint)
24
24
  * - Registry: 本地闭环不装配,由 GraphLoader.deploymentSnapshot 兜底
@@ -18,7 +18,7 @@
18
18
  *
19
19
  * 装配策略(与 server.ts 一致):
20
20
  * - LLM Provider: Routing > HttpLLMProvider(OpenAI) > Echo 兜底
21
- * - Tool Executor: mock(显式)/ DevToolExecutor(dev+mcp,缺配置失败)/ 通用路由
21
+ * - Tool Executor: DevToolExecutor(dev+mcp,缺配置失败)/ 通用路由
22
22
  * - Governance: AllowAll(本地闭环默认) / HttpGovernance(显式 GOVERNANCE_DISABLED=0)
23
23
  * - 存储: EAP_RUNTIME_STORAGE_PROFILE(默认 dev = File Session/Message/Checkpoint)
24
24
  * - Registry: 本地闭环不装配,由 GraphLoader.deploymentSnapshot 兜底
@@ -28,7 +28,6 @@ import { HttpLLMProvider } from '../providers/http-llm-provider.js';
28
28
  import { createLocalLlmProvider, requireLocalLlmConnections } from '../providers/local-llm-provider.js';
29
29
  import { RoutingLLMProvider } from '../providers/routing-llm-provider.js';
30
30
  import { createToolExecutor } from '../tool/create-tool-executor.js';
31
- import { MockToolExecutor } from '../tool/mock-tool-executor.js';
32
31
  import { DevToolExecutor } from '../tool/dev-tool-executor.js';
33
32
  import { withHarnessPrimitives } from '../tool/harness-primitives.js';
34
33
  import { AllowAllGovernanceClient, HttpGovernanceClient, } from '../governance/index.js';
@@ -75,14 +74,11 @@ export function createDevRuntime() {
75
74
  llmProvider = createDefaultLLMProvider(turnLog);
76
75
  llmProviderKind = 'echo';
77
76
  }
78
- // ── Tool Executor(显式 mock → dev+mcp 直连,缺配置失败 → 通用路由)──
79
- const needsHarnessWrap = toolExecutionMode === 'mock' ||
80
- (toolExecutionMode === 'mcp' && deploymentContext === 'dev');
81
- const innerExecutor = toolExecutionMode === 'mock'
82
- ? new MockToolExecutor({ toolsDir: process.env.EAP_LOCAL_TOOLS_DIR })
83
- : toolExecutionMode === 'mcp' && deploymentContext === 'dev'
84
- ? new DevToolExecutor()
85
- : createToolExecutor({ foundationUrl, deploymentContext });
77
+ // ── Tool Executor(dev+mcp 直连,缺配置失败 → 通用路由)──
78
+ const needsHarnessWrap = toolExecutionMode === 'mcp' && deploymentContext === 'dev';
79
+ const innerExecutor = toolExecutionMode === 'mcp' && deploymentContext === 'dev'
80
+ ? new DevToolExecutor()
81
+ : createToolExecutor({ foundationUrl, deploymentContext });
86
82
  const toolExecutor = needsHarnessWrap
87
83
  ? withHarnessPrimitives(innerExecutor)
88
84
  : innerExecutor;
@@ -2,7 +2,8 @@
2
2
  * Dev 下按 type:http 清单直连出站(gateway.baseUrl + pathTemplate)。
3
3
  * URL 拼接与 host 白名单与 Tool Gateway 共用 @gt-fe/eap-contracts http-outbound。
4
4
  */
5
- import { assertHttpHostAllowlisted, buildHttpToolUrl as buildSharedHttpToolUrl } from '@gt-fe/eap-contracts';
5
+ import { assertHttpHostAllowlisted, buildHttpToolUrl as buildSharedHttpToolUrl, ErrorCodes } from '@gt-fe/eap-contracts';
6
+ import { abortOrTimeoutMessage, DEFAULT_HTTP_TOOL_TIMEOUT_SEC, isAbortError, resolveToolTimeoutSec, toToolTimeoutMs, } from './timeout.js';
6
7
  export function resolveHttpExecutionFromRequest(request) {
7
8
  const raw = request.execution;
8
9
  if (!raw || typeof raw !== 'object')
@@ -22,7 +23,7 @@ export function resolveHttpExecutionFromRequest(request) {
22
23
  allowedHosts: Array.isArray(e.allowedHosts)
23
24
  ? e.allowedHosts.filter((item) => typeof item === 'string')
24
25
  : [],
25
- timeout: typeof e.timeout === 'number' ? e.timeout : 10000,
26
+ timeout: resolveToolTimeoutSec(e.timeout, DEFAULT_HTTP_TOOL_TIMEOUT_SEC),
26
27
  };
27
28
  }
28
29
  export function buildHttpToolUrl(spec, args) {
@@ -48,8 +49,9 @@ export class DevHttpExecutor {
48
49
  const args = request.arguments ?? {};
49
50
  const url = buildHttpToolUrl(spec, args);
50
51
  const method = (spec.method ?? 'GET').toUpperCase();
52
+ const timeoutSec = resolveToolTimeoutSec(spec.timeout, DEFAULT_HTTP_TOOL_TIMEOUT_SEC);
51
53
  const controller = new AbortController();
52
- const timer = setTimeout(() => controller.abort(), spec.timeout ?? 10000);
54
+ const timer = setTimeout(() => controller.abort(), toToolTimeoutMs(timeoutSec, DEFAULT_HTTP_TOOL_TIMEOUT_SEC));
53
55
  const headers = {
54
56
  Accept: 'application/json, text/plain;q=0.9',
55
57
  };
@@ -81,14 +83,15 @@ export class DevHttpExecutor {
81
83
  };
82
84
  }
83
85
  catch (err) {
84
- const message = err instanceof Error ? err.message : String(err);
86
+ const timeoutSec = resolveToolTimeoutSec(spec?.timeout, DEFAULT_HTTP_TOOL_TIMEOUT_SEC);
87
+ const aborted = isAbortError(err);
85
88
  return {
86
89
  toolCallId: request.toolCallId,
87
90
  toolName: request.toolName,
88
91
  content: '',
89
92
  success: false,
90
- error: message,
91
- errorCode: 'EXECUTION_FAILED',
93
+ error: abortOrTimeoutMessage(err, timeoutSec),
94
+ errorCode: aborted ? ErrorCodes.TOOL_TIMEOUT : 'EXECUTION_FAILED',
92
95
  runtime: 'local',
93
96
  };
94
97
  }
@@ -17,6 +17,8 @@
17
17
  * @see docs/workflows/local-agent-dev-loop.md
18
18
  * @see runtime/src/tool/local-mcp-client.ts
19
19
  */
20
+ import { ErrorCodes } from '@gt-fe/eap-contracts';
21
+ import { isAbortError } from './timeout.js';
20
22
  import { LocalMcpClient } from './local-mcp-client.js';
21
23
  export class DevMcpExecutor {
22
24
  client;
@@ -50,14 +52,14 @@ export class DevMcpExecutor {
50
52
  };
51
53
  }
52
54
  catch (err) {
53
- const message = err instanceof Error ? err.message : String(err);
55
+ const aborted = isAbortError(err);
54
56
  return {
55
57
  toolCallId: request.toolCallId,
56
58
  toolName: request.toolName,
57
59
  content: '',
58
60
  success: false,
59
- error: message,
60
- errorCode: 'EXECUTION_FAILED',
61
+ error: err instanceof Error ? err.message : String(err),
62
+ errorCode: aborted ? ErrorCodes.TOOL_TIMEOUT : 'EXECUTION_FAILED',
61
63
  runtime: 'local',
62
64
  };
63
65
  }
@@ -7,7 +7,7 @@
7
7
  * - execution.protocol=mcp → DevMcpExecutor
8
8
  * - execution.protocol=http → DevHttpExecutor
9
9
  * - 能解析出 code handler → LocalToolExecutor
10
- * - 其余 → NOT_CONFIGURED(mock 只走显式 `--tool-mode mock`)
10
+ * - 其余 → NOT_CONFIGURED
11
11
  */
12
12
  import { DevMcpExecutor } from './dev-mcp-executor.js';
13
13
  import type { IToolExecutor, ToolCallRequest, ToolCallResult } from './tool-executor.interface.js';
@@ -7,7 +7,7 @@
7
7
  * - execution.protocol=mcp → DevMcpExecutor
8
8
  * - execution.protocol=http → DevHttpExecutor
9
9
  * - 能解析出 code handler → LocalToolExecutor
10
- * - 其余 → NOT_CONFIGURED(mock 只走显式 `--tool-mode mock`)
10
+ * - 其余 → NOT_CONFIGURED
11
11
  */
12
12
  import { EmptyInstallStore } from './create-tool-executor.js';
13
13
  import { DevMcpExecutor } from './dev-mcp-executor.js';
@@ -42,7 +42,7 @@ export class DevToolExecutor {
42
42
  content: '',
43
43
  success: false,
44
44
  error: `Dev tool "${request.toolName}" is not executable: no MCP server, ` +
45
- 'HTTP gateway+pathTemplate, or code handler. Use --tool-mode mock for MockToolExecutor.',
45
+ 'HTTP gateway+pathTemplate, or code handler.',
46
46
  errorCode: 'NOT_CONFIGURED',
47
47
  runtime: 'local',
48
48
  };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * execute_command 的跨 OS 底板:本机壳、沙箱环境、解释器 shim、超时杀树。
3
+ * 不翻译 ls/dir;命令字符串按当前壳解释。
4
+ */
5
+ import { type ChildProcess, type SpawnOptions } from 'node:child_process';
6
+ export interface ShellSpawn {
7
+ file: string;
8
+ args: string[];
9
+ shellName: string;
10
+ windowsVerbatimArguments?: boolean;
11
+ windowsHide?: boolean;
12
+ }
13
+ export declare function shellNameFor(platform?: string): string;
14
+ /** system / 工具描述用的本机命令对照(只渲染当前 OS)。 */
15
+ export declare function commandDialectLines(platform?: string): string[];
16
+ export declare function resolveShellSpawn(command: string, platform?: string): ShellSpawn;
17
+ export declare function spawnOptionsFor(shell: ShellSpawn): Pick<SpawnOptions, 'windowsHide' | 'windowsVerbatimArguments'>;
18
+ export declare function findOnPath(names: string[], extraDirs?: string[]): string | undefined;
19
+ export declare function findPythonInterpreter(): string | undefined;
20
+ export declare function findNodeInterpreter(): string | undefined;
21
+ /** 在写根建立 python/node/sh shim。本机一个都没有则返回 undefined。 */
22
+ export declare function ensureInterpreterShims(workdir: string): string | undefined;
23
+ /** @deprecated 使用 ensureInterpreterShims */
24
+ export declare function ensurePythonShims(workdir: string): string | undefined;
25
+ /**
26
+ * 模型可能从 SKILL.md 抄 `$EAP_SKILL_DIR`(POSIX)或 `%EAP_SKILL_DIR%`(cmd)。
27
+ * spawn 前替换成绝对路径,不依赖壳去展开。
28
+ */
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;
31
+ export declare function formatSpawnError(err: unknown, shellFile: string): Error;
32
+ export declare function killProcessTree(child: ChildProcess): void;
@@ -0,0 +1,276 @@
1
+ /**
2
+ * execute_command 的跨 OS 底板:本机壳、沙箱环境、解释器 shim、超时杀树。
3
+ * 不翻译 ls/dir;命令字符串按当前壳解释。
4
+ */
5
+ import { spawnSync } from 'node:child_process';
6
+ import { chmodSync, mkdirSync, statSync, writeFileSync, } from 'node:fs';
7
+ import path from 'node:path';
8
+ const SHIM_DIR_NAME = '.eap-shim';
9
+ export function shellNameFor(platform = process.platform) {
10
+ return platform === 'win32' ? 'cmd.exe' : '/bin/sh';
11
+ }
12
+ /** system / 工具描述用的本机命令对照(只渲染当前 OS)。 */
13
+ export function commandDialectLines(platform = process.platform) {
14
+ const shell = shellNameFor(platform);
15
+ const lines = [
16
+ `当前 shell:${shell}。\`execute_command\` 的 command 必须是该 shell 的语法。`,
17
+ ];
18
+ if (platform === 'win32') {
19
+ lines.push('基础命令:当前目录 `cd`;列目录 `dir`;环境变量 `%VAR%`;串命令用 `&&`。');
20
+ }
21
+ else {
22
+ lines.push('基础命令:当前目录 `pwd`;列目录 `ls`;环境变量 `$VAR`;串命令用 `&&`。');
23
+ }
24
+ lines.push('解释器:`python`/`python3`、`node`/`nodejs`、`sh`/`bash`(Windows 上 `sh` 需本机有 Git Bash 等)。', '可执行文件:用绝对路径或相对于 cwd(Windows `.exe`/`.bat`/`.cmd`;Unix 二进制需可执行或用 `sh`)。', '`execute_command` 的 cwd 是用户目录。技能包只读。包内任意文件用 `EAP_SKILL_DIR` 加相对路径(不限 `scripts/`)。读到的 SKILL.md 里像脚本的相对路径已展开为绝对路径。不要把 cwd 设成技能目录。相对写出在用户目录。');
25
+ return lines;
26
+ }
27
+ export function resolveShellSpawn(command, platform = process.platform) {
28
+ if (platform === 'win32') {
29
+ const file = process.env.ComSpec?.trim() || 'cmd.exe';
30
+ return {
31
+ file,
32
+ args: ['/d', '/s', '/c', `"${command}"`],
33
+ shellName: 'cmd.exe',
34
+ windowsVerbatimArguments: true,
35
+ windowsHide: true,
36
+ };
37
+ }
38
+ return {
39
+ file: '/bin/sh',
40
+ args: ['-c', command],
41
+ shellName: '/bin/sh',
42
+ };
43
+ }
44
+ export function spawnOptionsFor(shell) {
45
+ return {
46
+ windowsHide: shell.windowsHide,
47
+ windowsVerbatimArguments: shell.windowsVerbatimArguments,
48
+ };
49
+ }
50
+ function isExecutableFile(candidate) {
51
+ try {
52
+ return statSync(candidate).isFile();
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ }
58
+ function pathSearchDirs(extraDirs = []) {
59
+ const fromEnv = (process.env.PATH ?? '')
60
+ .split(path.delimiter)
61
+ .map((dir) => dir.replace(/^"|"$/g, '').trim())
62
+ .filter((dir) => dir && path.basename(dir) !== SHIM_DIR_NAME);
63
+ const out = [];
64
+ for (const dir of [...extraDirs, ...fromEnv]) {
65
+ if (/WindowsApps/i.test(dir))
66
+ continue;
67
+ if (!out.includes(dir))
68
+ out.push(dir);
69
+ }
70
+ return out;
71
+ }
72
+ export function findOnPath(names, extraDirs = []) {
73
+ const pathext = process.platform === 'win32'
74
+ ? (process.env.PATHEXT ?? '.EXE;.CMD;.BAT').split(';').filter(Boolean)
75
+ : [];
76
+ for (const dir of pathSearchDirs(extraDirs)) {
77
+ for (const name of names) {
78
+ const candidates = [path.join(dir, name)];
79
+ if (process.platform === 'win32' && !path.extname(name)) {
80
+ for (const ext of pathext) {
81
+ candidates.push(path.join(dir, name + ext));
82
+ }
83
+ }
84
+ for (const candidate of candidates) {
85
+ if (isExecutableFile(candidate))
86
+ return candidate;
87
+ }
88
+ }
89
+ }
90
+ return undefined;
91
+ }
92
+ function gitBashDirs() {
93
+ if (process.platform !== 'win32')
94
+ return [];
95
+ const dirs = [];
96
+ for (const key of ['ProgramFiles', 'ProgramFiles(x86)', 'LOCALAPPDATA']) {
97
+ const root = process.env[key];
98
+ if (root)
99
+ dirs.push(path.join(root, 'Git', 'bin'));
100
+ }
101
+ return dirs;
102
+ }
103
+ const INTERPRETER_FAMILIES = [
104
+ {
105
+ shimNames: ['python', 'python3'],
106
+ searchNames: process.platform === 'win32'
107
+ ? ['python.exe', 'python3.exe', 'python', 'python3', 'py.exe']
108
+ : ['python3', 'python'],
109
+ },
110
+ {
111
+ shimNames: ['node', 'nodejs'],
112
+ searchNames: process.platform === 'win32' ? ['node.exe', 'nodejs.exe', 'node', 'nodejs'] : ['node', 'nodejs'],
113
+ },
114
+ {
115
+ shimNames: ['sh', 'bash'],
116
+ searchNames: process.platform === 'win32' ? ['bash.exe', 'sh.exe', 'bash', 'sh'] : ['bash', 'sh'],
117
+ extraSearchDirs: gitBashDirs,
118
+ },
119
+ ];
120
+ export function findPythonInterpreter() {
121
+ return findOnPath(INTERPRETER_FAMILIES[0].searchNames);
122
+ }
123
+ export function findNodeInterpreter() {
124
+ return findOnPath(INTERPRETER_FAMILIES[1].searchNames);
125
+ }
126
+ function shimBody(interpreter, extraArgs) {
127
+ if (process.platform === 'win32') {
128
+ return `@echo off\r\n"${interpreter}"${extraArgs} %*\r\n`;
129
+ }
130
+ return extraArgs
131
+ ? `#!/bin/sh\nexec "${interpreter}"${extraArgs} "$@"\n`
132
+ : `#!/bin/sh\nexec "${interpreter}" "$@"\n`;
133
+ }
134
+ function extraArgsFor(interpreter) {
135
+ const base = path.basename(interpreter).toLowerCase();
136
+ return base === 'py.exe' || base === 'py' ? ' -3' : '';
137
+ }
138
+ function writeShim(shimDir, name, interpreter, extraArgs) {
139
+ const body = shimBody(interpreter, extraArgs);
140
+ if (process.platform === 'win32') {
141
+ writeFileSync(path.join(shimDir, `${name}.cmd`), body);
142
+ return;
143
+ }
144
+ const target = path.join(shimDir, name);
145
+ writeFileSync(target, body, { mode: 0o755 });
146
+ chmodSync(target, 0o755);
147
+ }
148
+ /** 在写根建立 python/node/sh shim。本机一个都没有则返回 undefined。 */
149
+ export function ensureInterpreterShims(workdir) {
150
+ const shimDir = path.join(workdir, SHIM_DIR_NAME);
151
+ mkdirSync(shimDir, { recursive: true });
152
+ let wrote = false;
153
+ for (const family of INTERPRETER_FAMILIES) {
154
+ const found = findOnPath(family.searchNames, family.extraSearchDirs?.() ?? []);
155
+ if (!found)
156
+ continue;
157
+ wrote = true;
158
+ const extra = extraArgsFor(found);
159
+ for (const name of family.shimNames) {
160
+ writeShim(shimDir, name, found, extra);
161
+ }
162
+ }
163
+ return wrote ? shimDir : undefined;
164
+ }
165
+ /** @deprecated 使用 ensureInterpreterShims */
166
+ export function ensurePythonShims(workdir) {
167
+ return ensureInterpreterShims(workdir);
168
+ }
169
+ /**
170
+ * 模型可能从 SKILL.md 抄 `$EAP_SKILL_DIR`(POSIX)或 `%EAP_SKILL_DIR%`(cmd)。
171
+ * spawn 前替换成绝对路径,不依赖壳去展开。
172
+ */
173
+ export function substituteSkillDir(command, skillDir) {
174
+ const dir = skillDir?.trim();
175
+ if (!dir)
176
+ return command;
177
+ return command
178
+ .replace(/\$\{EAP_SKILL_DIR\}/g, dir)
179
+ .replace(/\$EAP_SKILL_DIR\b/g, dir)
180
+ .replace(/%EAP_SKILL_DIR%/gi, dir);
181
+ }
182
+ export function sandboxEnv(workdir, cwd, allowlist, shimDir, skillDir) {
183
+ const win = process.platform === 'win32';
184
+ const hostPath = process.env.PATH
185
+ ?? (win
186
+ ? (process.env.SystemRoot ? path.join(process.env.SystemRoot, 'System32') : 'C:\\Windows\\System32')
187
+ : '/usr/bin:/bin:/usr/local/bin');
188
+ const pathValue = shimDir ? `${shimDir}${path.delimiter}${hostPath}` : hostPath;
189
+ const env = {
190
+ PATH: pathValue,
191
+ LANG: process.env.LANG ?? 'C.UTF-8',
192
+ LC_ALL: process.env.LC_ALL ?? process.env.LANG ?? 'C.UTF-8',
193
+ HOME: workdir,
194
+ USERPROFILE: workdir,
195
+ TMPDIR: workdir,
196
+ TMP: workdir,
197
+ TEMP: workdir,
198
+ PWD: cwd,
199
+ PYTHONUTF8: '1',
200
+ PYTHONIOENCODING: 'utf-8',
201
+ };
202
+ if (skillDir?.trim())
203
+ env.EAP_SKILL_DIR = skillDir.trim();
204
+ if (win) {
205
+ env.ComSpec = process.env.ComSpec ?? 'cmd.exe';
206
+ if (process.env.SystemRoot)
207
+ env.SystemRoot = process.env.SystemRoot;
208
+ if (process.env.SYSTEMDRIVE)
209
+ env.SYSTEMDRIVE = process.env.SYSTEMDRIVE;
210
+ if (process.env.WINDIR)
211
+ env.WINDIR = process.env.WINDIR;
212
+ env.PATHEXT = process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD;.VBS;.JS;.WS;.MSC';
213
+ }
214
+ const pinned = new Set([
215
+ 'PATH',
216
+ 'HOME',
217
+ 'USERPROFILE',
218
+ 'TMPDIR',
219
+ 'TMP',
220
+ 'TEMP',
221
+ 'PWD',
222
+ 'PYTHONUTF8',
223
+ 'PYTHONIOENCODING',
224
+ 'EAP_SKILL_DIR',
225
+ 'ComSpec',
226
+ 'SystemRoot',
227
+ 'SYSTEMDRIVE',
228
+ 'WINDIR',
229
+ 'PATHEXT',
230
+ ]);
231
+ for (const key of allowlist) {
232
+ if (pinned.has(key))
233
+ continue;
234
+ const value = process.env[key];
235
+ if (value !== undefined)
236
+ env[key] = value;
237
+ }
238
+ return env;
239
+ }
240
+ export function formatSpawnError(err, shellFile) {
241
+ const code = err.code;
242
+ if (code === 'ENOENT') {
243
+ return new Error(`本机没有命令壳(${shellFile}),无法执行 execute_command`);
244
+ }
245
+ if (code === 'EACCES') {
246
+ return new Error(`没有权限启动命令壳(${shellFile})`);
247
+ }
248
+ return err instanceof Error ? err : new Error(String(err));
249
+ }
250
+ export function killProcessTree(child) {
251
+ const pid = child.pid;
252
+ if (pid == null) {
253
+ try {
254
+ child.kill('SIGKILL');
255
+ }
256
+ catch {
257
+ /* already gone */
258
+ }
259
+ return;
260
+ }
261
+ if (process.platform === 'win32') {
262
+ spawnSync('taskkill', ['/pid', String(pid), '/T', '/F'], {
263
+ stdio: 'ignore',
264
+ windowsHide: true,
265
+ timeout: 5_000,
266
+ });
267
+ return;
268
+ }
269
+ spawnSync('pkill', ['-KILL', '-P', String(pid)], { stdio: 'ignore', timeout: 5_000 });
270
+ try {
271
+ child.kill('SIGKILL');
272
+ }
273
+ catch {
274
+ /* already gone */
275
+ }
276
+ }
@@ -2,9 +2,10 @@
2
2
  * 平台默认 Harness 原语:read_file / write_file / execute_command。
3
3
  *
4
4
  * 不写进 agent.manifest.toolsets。同名业务工具优先(问数 file-io 等)。
5
- * 读:工作目录 + 技能包 + EAP_LOCAL_SKILLS_DIR + FILE_IO_ROOT。
6
- * 写:仅工作目录,且不跟随符号链接。
7
- * 命令:cwd 落在 jail 内;环境变量白名单,不继承 process.env。
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 type { LoadedToolMap } from './loaded-tool-definition.js';
10
11
  import type { IToolExecutor, ToolCallRequest, ToolCallResult } from './tool-executor.interface.js';
@@ -12,18 +13,39 @@ export declare function mergeHarnessPrimitives(loadedTools: LoadedToolMap | unde
12
13
  /** 装配点:业务 toolsets + 平台 Harness 原语。 */
13
14
  export declare function assembleLoadedTools(loadedTools: LoadedToolMap | undefined): LoadedToolMap | undefined;
14
15
  export interface HarnessJailOptions {
16
+ /** 用户写根;缺省则拒绝 write_file / execute_command */
15
17
  workdir?: string;
16
18
  extraRoots?: string[];
17
19
  }
20
+ /** `{agentDir}/users/<userId>`(写根;含用户 skills) */
21
+ export declare function resolveUserDir(agentDir: string, userId: string): string;
22
+ export declare function collectToolBundlePaths(loadedTools?: LoadedToolMap): string[];
23
+ export declare function resolveJailFromState(state: {
24
+ agentDir?: unknown;
25
+ userId?: unknown;
26
+ executionContext?: {
27
+ userId?: string | null;
28
+ };
29
+ loadedTools?: LoadedToolMap;
30
+ skillDefinitions?: Record<string, {
31
+ bundlePath?: string;
32
+ }>;
33
+ }): HarnessJailOptions;
18
34
  export declare function resolveJailRoots(options?: HarnessJailOptions): string[];
35
+ /** 路径落在写根外的 extraRoot 时,返回该 extraRoot。 */
36
+ export declare function extraRootContaining(abs: string, extraRoots: string[], workdir: string): string | undefined;
19
37
  export declare function resolveJailedPath(requested: string, roots: string[], opts?: {
20
38
  mustExist?: boolean;
39
+ relativeBase?: string;
21
40
  }): string;
22
41
  export declare class HarnessPrimitiveExecutor implements IToolExecutor {
23
42
  private readonly workdir?;
24
43
  private readonly extraRoots?;
25
44
  constructor(options?: HarnessJailOptions);
45
+ private workdirFor;
26
46
  private rootsFor;
47
+ private extraRootsFor;
48
+ private requireWorkdir;
27
49
  execute(request: ToolCallRequest): Promise<ToolCallResult>;
28
50
  private readFile;
29
51
  private writeFile;