@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
|
@@ -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,
|
|
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,
|
|
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 =
|
|
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:
|
|
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:
|
|
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
|
|
79
|
-
const needsHarnessWrap = toolExecutionMode === '
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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:
|
|
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(),
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
*
|
|
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 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;
|