@ljwei-stak/dsh-model-router 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dsh-plugin/client.js +82 -20
- package/.dsh-plugin/index.mjs +219 -74
- package/.dsh-plugin/shared/official-team-runtime.mjs +189 -49
- package/.dsh-plugin/shared/official-tool-executor.mjs +342 -69
- package/.dsh-plugin/shared/official-tools-remote.mjs +12 -0
- package/.dsh-plugin/shared/official-tools-runtime.mjs +22 -1
- package/.dsh-plugin/shared/run-ledger.mjs +51 -2
- package/.dsh-plugin/shared/task-executors.mjs +62 -18
- package/.dsh-plugin/shared/tool-health.mjs +24 -6
- package/CHANGELOG.md +18 -0
- package/README.md +19 -13
- package/README.zh.md +18 -12
- package/docs/WORKBENCH_USER_GUIDE.zh.md +32 -8
- package/docs/releases/0.20.0-validation.md +26 -0
- package/package.json +4 -3
|
@@ -18,15 +18,16 @@ import { access, readFile, realpath, stat } from 'node:fs/promises'
|
|
|
18
18
|
import { constants } from 'node:fs'
|
|
19
19
|
import { StringDecoder } from 'node:string_decoder'
|
|
20
20
|
import { basename, delimiter, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
21
|
+
import { homedir } from 'node:os'
|
|
21
22
|
import { getOfficialTool, installMethodFor, OFFICIAL_TOOLS } from './official-tool-registry.mjs'
|
|
22
23
|
import { currentInstallOptions, ensureNpmPrefixOnPath, installPreferences, preferredToolDirectories, usesConfiguredInstallPlan } from './official-tools-runtime.mjs'
|
|
23
24
|
import { resolveInstallPlan } from './tool-install-preferences.mjs'
|
|
24
25
|
import { buildMiniMaxInvocation, createMiniMaxStreamParser } from './vendor-minimax-adapter.mjs'
|
|
25
26
|
import { discoverZCodeBundle } from './zcode-bundle.mjs'
|
|
26
|
-
import { npmAttestor } from './npm-attestation.mjs'
|
|
27
|
+
import { CODE_FILE, npmAttestor } from './npm-attestation.mjs'
|
|
27
28
|
import { compareReleaseVersions, isReleaseVersion } from './latest-versions.mjs'
|
|
28
29
|
import { resolveMiMoGrokLaunch, createMiMoGrokParser, installedModuleRoots } from './vendor-mimo-grok-adapter.mjs'
|
|
29
|
-
import { emptyOutputError, outputIsEmpty, usageFromEvents, usageFromOutput } from './task-executors.mjs'
|
|
30
|
+
import { emptyOutputError, outputIsEmpty, redactDiagnostic, usageFromEvents, usageFromOutput } from './task-executors.mjs'
|
|
30
31
|
|
|
31
32
|
const IS_WINDOWS = process.platform === 'win32'
|
|
32
33
|
const MAX_TASK_BYTES = 64_000
|
|
@@ -38,12 +39,13 @@ const STOP_GRACE_MS = 5_000
|
|
|
38
39
|
const CLAUDE_MIN_RESTRICTED_VERSION = [2, 1, 259]
|
|
39
40
|
const CLAUDE_SIGNER = 'Anthropic, PBC'
|
|
40
41
|
const CODEX_SIGNER = 'OpenAI OpCo, LLC'
|
|
42
|
+
const CODEX_WRITE_UNSUPPORTED = 'Codex 当前仅支持只读任务:原 CODEX_HOME 需要保持可写以使用现有登录与配置;当前沙箱契约无法同时只授权独立 Git 工作区和该状态目录。可编辑团队步骤请改用已支持的其他官方工具。'
|
|
41
43
|
const WINDOWS_SYSTEM32 = join(process.env.SystemRoot ?? 'C:\\Windows', 'System32')
|
|
42
44
|
const ENVIRONMENT_KEYS = Object.freeze([
|
|
43
45
|
'PATH', 'Path', 'PATHEXT', 'SystemRoot', 'windir', 'ComSpec',
|
|
44
46
|
'HOME', 'USERPROFILE', 'HOMEDRIVE', 'HOMEPATH', 'APPDATA', 'LOCALAPPDATA',
|
|
45
47
|
'PROGRAMDATA', 'TEMP', 'TMP', 'TMPDIR', 'LANG', 'LC_ALL', 'TERM',
|
|
46
|
-
'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy',
|
|
48
|
+
'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'all_proxy', 'no_proxy',
|
|
47
49
|
'SSL_CERT_FILE', 'NODE_EXTRA_CA_CERTS',
|
|
48
50
|
])
|
|
49
51
|
|
|
@@ -52,7 +54,7 @@ const CAPABILITIES = Object.freeze({
|
|
|
52
54
|
? { supported: true, modes: Object.freeze(['read-only', 'workspace-write']) }
|
|
53
55
|
: { supported: false, reason: 'Claude 的受限原生入口目前仅在 Windows 完成适配。' }),
|
|
54
56
|
codex: Object.freeze(IS_WINDOWS
|
|
55
|
-
? { supported: true, modes: Object.freeze(['read-only'
|
|
57
|
+
? { supported: true, modes: Object.freeze(['read-only']) }
|
|
56
58
|
: { supported: false, reason: 'Codex 的签名原生入口目前仅在 Windows 完成适配。' }),
|
|
57
59
|
'kimi-code': Object.freeze({
|
|
58
60
|
supported: true, modes: Object.freeze(['workspace-write']),
|
|
@@ -82,8 +84,10 @@ const CAPABILITIES = Object.freeze({
|
|
|
82
84
|
})
|
|
83
85
|
|
|
84
86
|
/** A UI/Host can show this without implying that installation means execution readiness. */
|
|
85
|
-
export function officialToolExecutionCapabilities() {
|
|
86
|
-
return OFFICIAL_TOOLS.map(tool => ({ id: tool.id, ...CAPABILITIES[tool.id]
|
|
87
|
+
export function officialToolExecutionCapabilities({ executionEnvironment = 'harness' } = {}) {
|
|
88
|
+
return OFFICIAL_TOOLS.map(tool => ({ id: tool.id, ...CAPABILITIES[tool.id],
|
|
89
|
+
...(executionEnvironment === 'cli-native' && IS_WINDOWS && tool.id === 'codex'
|
|
90
|
+
? { modes: ['read-only', 'workspace-write'] } : {}) }))
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
function inside(parent, child) {
|
|
@@ -143,12 +147,14 @@ function checkedTimeout(timeoutMs) {
|
|
|
143
147
|
}
|
|
144
148
|
|
|
145
149
|
/** API-key variables removed when a run must use the CLI's own subscription login. */
|
|
146
|
-
const API_KEY_VARIABLES = Object.freeze(['ANTHROPIC_API_KEY', 'CODEX_API_KEY', 'OPENAI_API_KEY', 'KIMI_API_KEY',
|
|
150
|
+
const API_KEY_VARIABLES = Object.freeze(['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'CODEX_API_KEY', 'OPENAI_API_KEY', 'KIMI_API_KEY',
|
|
147
151
|
'MOONSHOT_API_KEY', 'MINIMAX_API_KEY', 'MIMO_API_KEY', 'XAI_API_KEY', 'ZAI_API_KEY', 'GEMINI_API_KEY', 'GOOGLE_API_KEY'])
|
|
148
152
|
|
|
149
|
-
function executionEnvironment(toolId, { sessionOnly = false } = {}) {
|
|
153
|
+
export function executionEnvironment(toolId, { sessionOnly = false, env: source = process.env } = {}) {
|
|
150
154
|
const vendorKeys = {
|
|
151
|
-
'claude-code': ['ANTHROPIC_API_KEY', '
|
|
155
|
+
'claude-code': ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL', 'ANTHROPIC_MODEL',
|
|
156
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL', 'ANTHROPIC_DEFAULT_SONNET_MODEL', 'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
157
|
+
'ANTHROPIC_SMALL_FAST_MODEL', 'CLAUDE_CODE_OAUTH_TOKEN', 'CLAUDE_CONFIG_DIR'],
|
|
152
158
|
codex: ['CODEX_API_KEY', 'OPENAI_API_KEY', 'CODEX_HOME'],
|
|
153
159
|
'kimi-code': ['KIMI_CODE_HOME', 'KIMI_API_KEY', 'MOONSHOT_API_KEY'],
|
|
154
160
|
'minimax-code': ['MINIMAX_API_KEY', 'MINIMAX_BASE_URL'],
|
|
@@ -160,7 +166,7 @@ function executionEnvironment(toolId, { sessionOnly = false } = {}) {
|
|
|
160
166
|
.filter(key => !sessionOnly || !API_KEY_VARIABLES.includes(key))
|
|
161
167
|
const env = {}
|
|
162
168
|
for (const key of keys) {
|
|
163
|
-
if (
|
|
169
|
+
if (source[key] !== undefined) env[key] = source[key]
|
|
164
170
|
}
|
|
165
171
|
return env
|
|
166
172
|
}
|
|
@@ -307,6 +313,23 @@ function newestFirst(entries) {
|
|
|
307
313
|
return [...entries].sort((left, right) => (compareReleaseVersions(right.version, left.version) ?? 0))
|
|
308
314
|
}
|
|
309
315
|
|
|
316
|
+
/** Native launches cannot trust unsigned npm code or a locally changed bin map. */
|
|
317
|
+
async function attestNativeNpmEntry(found, tool, selectionOptions) {
|
|
318
|
+
if (selectionOptions.executionEnvironment !== 'cli-native') return found
|
|
319
|
+
const attestor = selectionOptions.attestor ?? npmAttestor
|
|
320
|
+
try {
|
|
321
|
+
const include = new RegExp(`${CODE_FILE.source}|(?:^|/)package\\.json$`, 'i')
|
|
322
|
+
const checked = await attestor.matchesPackage(found.packageRoot, tool.package, found.version, include)
|
|
323
|
+
if (!checked.ok) return { unsupported: `${tool.label} 安装文件未通过官方 npm 摘要核验:${redactDiagnostic(checked.reason)};拒绝启动所选副本,不改用其他安装位置。` }
|
|
324
|
+
const member = relative(found.packageRoot, found.entry).split(sep).join('/')
|
|
325
|
+
const entry = await attestor.matches(found.entry, tool.package, found.version, member)
|
|
326
|
+
if (!entry.ok) return { unsupported: `${tool.label} 入口未通过官方 npm 摘要核验:${redactDiagnostic(entry.reason)};拒绝启动所选副本,不改用其他安装位置。` }
|
|
327
|
+
return { ...found, source: 'official-npm-registry-attested' }
|
|
328
|
+
} catch (error) {
|
|
329
|
+
return { unsupported: `${tool.label} 官方 npm 摘要核验不可用:${redactDiagnostic(error?.message ?? error)};拒绝启动所选副本,不改用其他安装位置。` }
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
310
333
|
async function findSignedNativeExecutable(toolId, workspace, signer, selection = officialToolEntrySelection(toolId)) {
|
|
311
334
|
if (!selection.preferred) await ensureNpmPrefixOnPath()
|
|
312
335
|
const command = toolId === 'claude-code' ? 'claude' : toolId
|
|
@@ -537,8 +560,10 @@ async function launchSpec(toolId, workspace, mode, modelId, task = 'Check readin
|
|
|
537
560
|
if (selection.plan.kind === 'script') return { unsupported: '所选 Kimi 原生脚本安装没有可核验的 npm 执行入口;不会改用其他安装目录中的 Kimi。' }
|
|
538
561
|
const tool = getOfficialTool(toolId)
|
|
539
562
|
const entries = await findGlobalPackageEntries(tool.package, 'kimi', 'dist/main.mjs', workspace, { selection })
|
|
540
|
-
|
|
563
|
+
let found = newestFirst(entries)[0]
|
|
541
564
|
if (!found) return { unsupported: `未找到官方 npm 包 ${tool.package} 的 kimi 入口(dist/main.mjs)。` }
|
|
565
|
+
found = await attestNativeNpmEntry(found, tool, selectionOptions)
|
|
566
|
+
if (found.unsupported) return found
|
|
542
567
|
const nodeExecutable = await findNodeExecutable(workspace)
|
|
543
568
|
if (!nodeExecutable) return { unsupported: '未找到工作区外的 Node.js 可执行文件。' }
|
|
544
569
|
if (Buffer.byteLength(task, 'utf8') > 16_000) {
|
|
@@ -558,8 +583,11 @@ async function launchSpec(toolId, workspace, mode, modelId, task = 'Check readin
|
|
|
558
583
|
? await findManagedMiniMaxEntry(workspace, selection.directories)
|
|
559
584
|
: !selection.configured ? await findManagedMiniMaxEntry(workspace) : null
|
|
560
585
|
const entries = selection.plan.kind === 'script' ? [] : await findGlobalPackageEntries(tool.package, 'mcode', 'cli.js', workspace, { selection })
|
|
561
|
-
|
|
586
|
+
let found = managed ?? newestFirst(entries)[0]
|
|
562
587
|
if (!found) return { unsupported: `未找到官方 npm 包 ${tool.package} 的 mcode 入口(cli.js)。` }
|
|
588
|
+
// Managed Windows releases were already attested while resolving them.
|
|
589
|
+
if (!managed) found = await attestNativeNpmEntry(found, tool, selectionOptions)
|
|
590
|
+
if (found.unsupported) return found
|
|
563
591
|
const nodeExecutable = await findNodeExecutable(workspace, [22, 19, 0], version => {
|
|
564
592
|
const major = Number(String(version).split('.')[0])
|
|
565
593
|
return major === 22 || (major >= 24 && major < 27)
|
|
@@ -657,36 +685,69 @@ export function runnerEnvironment(argv0, { execPath = process.execPath, electron
|
|
|
657
685
|
}
|
|
658
686
|
|
|
659
687
|
/**
|
|
660
|
-
*
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
*
|
|
664
|
-
*
|
|
665
|
-
* is denied (os error 5)". Returning "unsupported" lets a read-only step use
|
|
666
|
-
* the direct `codex exec --sandbox read-only` launch the user approves as
|
|
667
|
-
* "unsandboxed CLI", instead of failing every time.
|
|
688
|
+
* Codex's native read-only mode still needs its own state writable. Keep its
|
|
689
|
+
* existing home (auth, config, skills, MCP and token refresh) under the outer
|
|
690
|
+
* ACL boundary, while giving the project no write SID. No credential cloning
|
|
691
|
+
* or home relocation is needed. The current one-root SDK cannot also grant
|
|
692
|
+
* an independent Git worktree, so editable Codex runs fail closed.
|
|
668
693
|
*/
|
|
669
|
-
export function harnessSandboxBlocker(toolId, platform = process.platform) {
|
|
670
|
-
if (toolId === 'codex' && platform === 'win32')
|
|
671
|
-
return 'Codex 启动时必须写入 CODEX_HOME(~/.codex),Harness 的 Windows 进程沙箱会拒绝这些写入(os error 5);改用 Codex 自带的只读沙箱直接启动。'
|
|
672
|
-
}
|
|
694
|
+
export function harnessSandboxBlocker(toolId, platform = process.platform, mode = 'read-only') {
|
|
695
|
+
if (toolId === 'codex' && platform === 'win32' && mode !== 'read-only') return CODEX_WRITE_UNSUPPORTED
|
|
673
696
|
return null
|
|
674
697
|
}
|
|
675
698
|
|
|
699
|
+
/** The only outer writable root is the user's exact, existing Codex home. */
|
|
700
|
+
export async function codexReadOnlyPolicy(workspace, home = process.env.CODEX_HOME ?? join(homedir(), '.codex')) {
|
|
701
|
+
const project = await checkedWorkspace(workspace)
|
|
702
|
+
const stateHome = await checkedWorkspace(home)
|
|
703
|
+
if (project === stateHome || inside(project, stateHome) || inside(stateHome, project)) {
|
|
704
|
+
throw new TypeError('Codex 的 CODEX_HOME 与项目工作区必须互不包含;拒绝向只读项目授予写权限。')
|
|
705
|
+
}
|
|
706
|
+
return { mode: 'workspace-write', workspaceRoot: stateHome }
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** Require the reviewed Windows ACL runner to preserve the exact argv/policy. */
|
|
710
|
+
export function codexStateSandboxContract(confined, requestedArgv, policy) {
|
|
711
|
+
const argv = confined?.argv
|
|
712
|
+
if (!Array.isArray(argv) || !argv.every(value => typeof value === 'string')) return false
|
|
713
|
+
const boundary = argv.indexOf('--')
|
|
714
|
+
if (boundary < 2 || argv.length - boundary - 1 !== requestedArgv.length
|
|
715
|
+
|| requestedArgv.some((value, index) => argv[boundary + 1 + index] !== value)) return false
|
|
716
|
+
const runnerIndex = argv.findIndex(value => /(?:^|[\\/])@deepseek-ai[\\/]dsh-sandbox-windows-acl[\\/]lib[\\/]runner\.js$/i.test(value))
|
|
717
|
+
if (runnerIndex < 1 || runnerIndex >= boundary) return false
|
|
718
|
+
const flags = new Map()
|
|
719
|
+
for (let index = runnerIndex + 1; index < boundary; index += 2) {
|
|
720
|
+
const key = argv[index]
|
|
721
|
+
if (!['--workspace', '--temp', '--mode', '--write-sid', '--temp-write-sid'].includes(key)
|
|
722
|
+
|| flags.has(key) || index + 1 >= boundary) return false
|
|
723
|
+
flags.set(key, argv[index + 1])
|
|
724
|
+
}
|
|
725
|
+
return policy.mode === 'workspace-write' && flags.get('--mode') === policy.mode
|
|
726
|
+
&& flags.get('--workspace') === policy.workspaceRoot && isAbsolute(flags.get('--temp') ?? '')
|
|
727
|
+
&& flags.has('--write-sid') === flags.has('--temp-write-sid')
|
|
728
|
+
}
|
|
729
|
+
|
|
676
730
|
/** Check the actual trusted launch entry without starting an account call. */
|
|
677
731
|
export async function officialToolReadiness(toolId, workspace = process.cwd(), selectionOptions = {}) {
|
|
678
732
|
const tool = getOfficialTool(toolId)
|
|
679
|
-
if (!tool) return { id: String(toolId), supported: false, ready: false, reason: '工具不在固定官方注册表中。' }
|
|
680
|
-
const
|
|
681
|
-
if (!
|
|
733
|
+
if (!tool) return { id: String(toolId), supported: false, ready: false, readyModes: [], reason: '工具不在固定官方注册表中。' }
|
|
734
|
+
const environment = selectionOptions.executionEnvironment ?? 'harness'
|
|
735
|
+
if (!['harness', 'cli-native'].includes(environment)) throw new TypeError('unknown CLI execution environment')
|
|
736
|
+
const capability = officialToolExecutionCapabilities({ executionEnvironment: environment }).find(item => item.id === tool.id)
|
|
737
|
+
if (!capability?.supported) return { id: tool.id, supported: false, ready: false, readyModes: [], reason: capability?.reason ?? '当前平台不支持托管执行。' }
|
|
682
738
|
try {
|
|
683
739
|
const directory = await checkedWorkspace(workspace)
|
|
740
|
+
if (tool.id === 'codex' && environment === 'harness') await codexReadOnlyPolicy(directory)
|
|
684
741
|
const spec = await launchSpec(tool.id, directory, capability.modes[0], null, undefined, selectionOptions)
|
|
685
742
|
return spec.unsupported
|
|
686
|
-
? { id: tool.id, supported: true, ready: false, reason: spec.unsupported }
|
|
687
|
-
: { id: tool.id, supported: true, ready: true, source: spec.source, modes: capability.modes
|
|
743
|
+
? { id: tool.id, supported: true, ready: false, readyModes: [], reason: spec.unsupported }
|
|
744
|
+
: { id: tool.id, supported: true, ready: true, source: spec.source, modes: capability.modes,
|
|
745
|
+
readyModes: capability.modes,
|
|
746
|
+
...(tool.id === 'codex' && environment === 'harness' ? { runtimeState: 'existing-codex-home',
|
|
747
|
+
executionRequirements: ['harness-windows-acl', 'codex-home-state-write'],
|
|
748
|
+
modeReasons: { 'workspace-write': CODEX_WRITE_UNSUPPORTED } } : {}) }
|
|
688
749
|
} catch (error) {
|
|
689
|
-
return { id: tool.id, supported: true, ready: false, reason: String(error?.message ?? error).slice(0, 300) }
|
|
750
|
+
return { id: tool.id, supported: true, ready: false, readyModes: [], reason: String(error?.message ?? error).slice(0, 300) }
|
|
690
751
|
}
|
|
691
752
|
}
|
|
692
753
|
|
|
@@ -717,9 +778,193 @@ function stopProcessTree(child) {
|
|
|
717
778
|
}
|
|
718
779
|
}
|
|
719
780
|
|
|
781
|
+
/** Parse the complete Claude terminal JSON even when its process exits nonzero. */
|
|
782
|
+
export function parseClaudeProcessOutput(stdout, exitCode, { secrets = [] } = {}) {
|
|
783
|
+
let result
|
|
784
|
+
try { result = JSON.parse(stdout) } catch {
|
|
785
|
+
return { status: 'failed', exitCode, error: 'Claude 未返回有效的结果 JSON。' }
|
|
786
|
+
}
|
|
787
|
+
const validResult = result && typeof result === 'object' && !Array.isArray(result)
|
|
788
|
+
&& result.type === 'result'
|
|
789
|
+
&& (result.is_error === undefined || typeof result.is_error === 'boolean')
|
|
790
|
+
const succeeded = validResult && result.subtype === 'success' && result.is_error !== true
|
|
791
|
+
&& typeof result.result === 'string' && result.result.trim().length > 0
|
|
792
|
+
const failed = validResult && (result.is_error === true
|
|
793
|
+
|| (typeof result.subtype === 'string' && /^error(?:_|$)/.test(result.subtype)))
|
|
794
|
+
const evidence = {}
|
|
795
|
+
// A parsed object or a partial usage map does not establish zero usage. Only
|
|
796
|
+
// a recognizable terminal result with both required counters can report it.
|
|
797
|
+
if (succeeded || failed) {
|
|
798
|
+
const usage = result.usage
|
|
799
|
+
const validCount = value => Number.isSafeInteger(value) && value >= 0
|
|
800
|
+
if (usage && typeof usage === 'object' && !Array.isArray(usage)
|
|
801
|
+
&& validCount(usage.input_tokens) && validCount(usage.output_tokens)
|
|
802
|
+
&& ['cache_read_input_tokens', 'cache_creation_input_tokens']
|
|
803
|
+
.every(key => usage[key] === undefined || validCount(usage[key]))) {
|
|
804
|
+
const reported = usageFromOutput('claude-json', stdout)
|
|
805
|
+
if (reported?.usage) evidence.usage = reported.usage
|
|
806
|
+
if (reported?.reportedModel) evidence.reportedModel = reported.reportedModel
|
|
807
|
+
}
|
|
808
|
+
if (Number.isFinite(result.total_cost_usd) && result.total_cost_usd >= 0) {
|
|
809
|
+
evidence.reportedCostUsd = result.total_cost_usd
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
if (exitCode === 0 && succeeded) {
|
|
813
|
+
return { status: 'succeeded', exitCode, terminalEvent: 'completed', finalText: result.result, ...evidence }
|
|
814
|
+
}
|
|
815
|
+
const message = value => typeof value === 'string' ? value
|
|
816
|
+
: typeof value?.message === 'string' ? value.message
|
|
817
|
+
: typeof value?.error === 'string' ? value.error
|
|
818
|
+
: typeof value?.error?.message === 'string' ? value.error.message : ''
|
|
819
|
+
const details = validResult ? [
|
|
820
|
+
...(Array.isArray(result.errors) ? result.errors.slice(0, 32).map(message) : []),
|
|
821
|
+
message(result.error),
|
|
822
|
+
typeof result.result === 'string' ? result.result : '',
|
|
823
|
+
].filter(value => value.trim()) : []
|
|
824
|
+
let detail = [...new Set(details)].join(' | ')
|
|
825
|
+
for (const secret of Array.isArray(secrets) ? secrets : []) {
|
|
826
|
+
if (typeof secret === 'string' && secret) detail = detail.split(secret).join('[redacted]')
|
|
827
|
+
}
|
|
828
|
+
detail = redactDiagnostic(detail, '', 2_000)
|
|
829
|
+
const error = exitCode !== 0 ? '官方 CLI 退出码非零。' : 'Claude 未返回成功终态。'
|
|
830
|
+
return { status: 'failed', exitCode, ...(failed ? { terminalEvent: 'failed' } : {}),
|
|
831
|
+
error: detail ? `${error}${detail}` : error, ...(detail ? { detail } : {}), ...evidence }
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const tokenCount = value => Number.isSafeInteger(value) && value >= 0
|
|
835
|
+
const usageObject = value => value && typeof value === 'object' && !Array.isArray(value)
|
|
836
|
+
const validCounts = (value, required, optional = []) => usageObject(value)
|
|
837
|
+
&& required.every(key => tokenCount(value[key]))
|
|
838
|
+
&& optional.every(key => value[key] === undefined || tokenCount(value[key]))
|
|
839
|
+
|
|
840
|
+
/** Accept accounting only from a complete terminal event, never a partial map. */
|
|
841
|
+
export function officialToolTerminalEvidence(format, value) {
|
|
842
|
+
if (!usageObject(value)) return {}
|
|
843
|
+
if (format === 'codex-jsonl') {
|
|
844
|
+
if (value.type !== 'turn.completed' || !validCounts(value.usage,
|
|
845
|
+
['input_tokens', 'output_tokens'], ['cached_input_tokens'])
|
|
846
|
+
|| (value.usage.cached_input_tokens ?? 0) > value.usage.input_tokens) return {}
|
|
847
|
+
return usageFromOutput('codex-jsonl', JSON.stringify(value)) ?? {}
|
|
848
|
+
}
|
|
849
|
+
if (format === 'grok-jsonl') {
|
|
850
|
+
if (value.type !== 'end' || typeof value.sessionId !== 'string' || !value.sessionId
|
|
851
|
+
|| typeof value.stopReason !== 'string' || !value.stopReason
|
|
852
|
+
|| (value.cost_is_partial !== undefined && typeof value.cost_is_partial !== 'boolean')) return {}
|
|
853
|
+
const evidence = {}
|
|
854
|
+
if (validCounts(value.usage, ['input_tokens', 'output_tokens'],
|
|
855
|
+
['cache_read_input_tokens', 'cache_creation_input_tokens'])) {
|
|
856
|
+
evidence.usage = usageFromEvents('grok-end', value)?.usage
|
|
857
|
+
}
|
|
858
|
+
if (value.cost_is_partial !== true && Number.isFinite(value.total_cost_usd) && value.total_cost_usd >= 0) {
|
|
859
|
+
evidence.reportedCostUsd = value.total_cost_usd
|
|
860
|
+
}
|
|
861
|
+
return evidence
|
|
862
|
+
}
|
|
863
|
+
if (format === 'mimo-jsonl') {
|
|
864
|
+
const part = value.part
|
|
865
|
+
if (value.type !== 'step_finish' || typeof value.sessionID !== 'string' || !value.sessionID
|
|
866
|
+
|| part?.type !== 'step-finish' || typeof part.reason !== 'string' || !part.reason
|
|
867
|
+
|| !validCounts(part.tokens, ['input', 'output'], ['reasoning'])
|
|
868
|
+
|| (part.tokens.cache !== undefined && !validCounts(part.tokens.cache, [], ['read', 'write']))) return {}
|
|
869
|
+
return usageFromEvents('mimo-steps', [part]) ?? {}
|
|
870
|
+
}
|
|
871
|
+
if (format === 'minimax-stream-json') {
|
|
872
|
+
// `value` is produced only by the schema/sequence/session-validating parser.
|
|
873
|
+
if (!['turn.completed', 'turn.failed'].includes(value.terminalEvent)
|
|
874
|
+
|| !['succeeded', 'failed', 'timeout', 'cancelled', 'limit_exceeded'].includes(value.vendorStatus)
|
|
875
|
+
|| ['runId', 'sessionId', 'turnId'].some(key => typeof value[key] !== 'string' || !value[key])
|
|
876
|
+
|| value.usageSource === 'unavailable'
|
|
877
|
+
|| !validCounts(value.usage, ['inputTokens', 'outputTokens'],
|
|
878
|
+
['cacheReadTokens', 'cacheWriteTokens', 'reasoningTokens'])) return {}
|
|
879
|
+
return usageFromEvents('minimax-result', value) ?? {}
|
|
880
|
+
}
|
|
881
|
+
return {}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/** Keep verified completed-turn accounting independently of process success. */
|
|
885
|
+
export function createOfficialToolEvidenceCollector(format) {
|
|
886
|
+
let evidence = {}
|
|
887
|
+
let pending = false
|
|
888
|
+
let incomplete = false
|
|
889
|
+
let terminalSeen = false
|
|
890
|
+
let sessionId = null
|
|
891
|
+
let rejected = false
|
|
892
|
+
const add = next => {
|
|
893
|
+
if (next.usage) {
|
|
894
|
+
const previous = evidence.usage
|
|
895
|
+
evidence.usage = Object.fromEntries(Object.entries(next.usage)
|
|
896
|
+
.map(([key, value]) => [key, (previous?.[key] ?? 0) + value]))
|
|
897
|
+
}
|
|
898
|
+
if (!terminalSeen) evidence.reportedCostUsd = next.reportedCostUsd
|
|
899
|
+
else if (Number.isFinite(evidence.reportedCostUsd) && Number.isFinite(next.reportedCostUsd)) {
|
|
900
|
+
evidence.reportedCostUsd = Number((evidence.reportedCostUsd + next.reportedCostUsd).toFixed(8))
|
|
901
|
+
} else delete evidence.reportedCostUsd
|
|
902
|
+
}
|
|
903
|
+
return {
|
|
904
|
+
push(event) {
|
|
905
|
+
if (rejected || !usageObject(event)) return
|
|
906
|
+
if (format === 'codex-jsonl') {
|
|
907
|
+
if (event.type === 'turn.started') pending = true
|
|
908
|
+
else if (event.type === 'turn.failed') { pending = true; incomplete = true }
|
|
909
|
+
else if (event.type === 'turn.completed') {
|
|
910
|
+
const next = officialToolTerminalEvidence(format, event)
|
|
911
|
+
if (!next.usage) { incomplete = true; return }
|
|
912
|
+
if (terminalSeen && !pending) { incomplete = true; return }
|
|
913
|
+
add(next)
|
|
914
|
+
terminalSeen = true
|
|
915
|
+
pending = false
|
|
916
|
+
}
|
|
917
|
+
} else if (format === 'grok-jsonl') {
|
|
918
|
+
if (event.type !== 'end') return
|
|
919
|
+
if (terminalSeen) { rejected = true; return }
|
|
920
|
+
evidence = officialToolTerminalEvidence(format, event)
|
|
921
|
+
terminalSeen = true
|
|
922
|
+
} else if (format === 'mimo-jsonl') {
|
|
923
|
+
if (typeof event.sessionID !== 'string' || !event.sessionID
|
|
924
|
+
|| (sessionId && sessionId !== event.sessionID)) { rejected = true; return }
|
|
925
|
+
sessionId = event.sessionID
|
|
926
|
+
if (event.type === 'step_start') pending = true
|
|
927
|
+
else if (event.type === 'error') { pending = true; incomplete = true }
|
|
928
|
+
else if (event.type === 'step_finish') {
|
|
929
|
+
const next = officialToolTerminalEvidence(format, event)
|
|
930
|
+
if (!next.usage) { incomplete = true; return }
|
|
931
|
+
if (terminalSeen && !pending) { incomplete = true; return }
|
|
932
|
+
add(next)
|
|
933
|
+
terminalSeen = true
|
|
934
|
+
pending = event.part.reason !== 'stop'
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
},
|
|
938
|
+
snapshot() {
|
|
939
|
+
if (rejected) return {}
|
|
940
|
+
const result = { ...evidence }
|
|
941
|
+
if (pending || incomplete) {
|
|
942
|
+
delete result.reportedCostUsd
|
|
943
|
+
if (result.usage) result.usageIncomplete = true
|
|
944
|
+
}
|
|
945
|
+
return result
|
|
946
|
+
},
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/** Accounting cannot change a failed, cancelled or timed-out process outcome. */
|
|
951
|
+
export function finalizeOfficialProcessResult(result, evidence = {}) {
|
|
952
|
+
const { usage: discardedUsage, reportedCostUsd: discardedCost, usageIncomplete: discardedIncomplete, ...outcome } = result
|
|
953
|
+
const usage = evidence?.usage
|
|
954
|
+
if (validCounts(usage, ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens'])) {
|
|
955
|
+
outcome.usage = usage
|
|
956
|
+
if (evidence.usageIncomplete === true) outcome.usageIncomplete = true
|
|
957
|
+
}
|
|
958
|
+
if (Number.isFinite(evidence?.reportedCostUsd) && evidence.reportedCostUsd >= 0) {
|
|
959
|
+
outcome.reportedCostUsd = evidence.reportedCostUsd
|
|
960
|
+
}
|
|
961
|
+
return outcome
|
|
962
|
+
}
|
|
963
|
+
|
|
720
964
|
function captureProcess(spec, task, workspace, signal, timeoutMs, toolId, sessionOnly = false, extraEnv = {}) {
|
|
721
965
|
return new Promise(resolveResult => {
|
|
722
966
|
let child
|
|
967
|
+
const childEnvironment = { ...executionEnvironment(toolId, { sessionOnly }), ...extraEnv }
|
|
723
968
|
try {
|
|
724
969
|
child = spawn(spec.file, spec.args, {
|
|
725
970
|
cwd: workspace,
|
|
@@ -727,7 +972,7 @@ function captureProcess(spec, task, workspace, signal, timeoutMs, toolId, sessio
|
|
|
727
972
|
windowsHide: true,
|
|
728
973
|
detached: !IS_WINDOWS,
|
|
729
974
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
730
|
-
env:
|
|
975
|
+
env: childEnvironment,
|
|
731
976
|
})
|
|
732
977
|
} catch (error) {
|
|
733
978
|
resolveResult({ status: 'failed', error: String(error?.message ?? error), exitCode: null })
|
|
@@ -741,9 +986,8 @@ function captureProcess(spec, task, workspace, signal, timeoutMs, toolId, sessio
|
|
|
741
986
|
let stderrTail = ''
|
|
742
987
|
let outputBytes = 0
|
|
743
988
|
let jsonlBuffer = ''
|
|
744
|
-
let codexUsageLine = ''
|
|
745
989
|
let codexEvents = 0
|
|
746
|
-
|
|
990
|
+
const accounting = createOfficialToolEvidenceCollector(spec.format)
|
|
747
991
|
let terminal = null
|
|
748
992
|
let failedEvent = false
|
|
749
993
|
let finalText = ''
|
|
@@ -761,9 +1005,11 @@ function captureProcess(spec, task, workspace, signal, timeoutMs, toolId, sessio
|
|
|
761
1005
|
|
|
762
1006
|
const readJsonlLine = line => {
|
|
763
1007
|
if (!line.trim()) return
|
|
764
|
-
if (nativeParser)
|
|
1008
|
+
if (nativeParser) nativeParser.push(line)
|
|
765
1009
|
try {
|
|
766
1010
|
const event = JSON.parse(line)
|
|
1011
|
+
accounting.push(event)
|
|
1012
|
+
if (nativeParser) return
|
|
767
1013
|
if (spec.format === 'zcode-stream-json') {
|
|
768
1014
|
if (!event || typeof event !== 'object' || Array.isArray(event)
|
|
769
1015
|
|| typeof event.type !== 'string' || typeof event.sessionId !== 'string'
|
|
@@ -807,7 +1053,6 @@ function captureProcess(spec, task, workspace, signal, timeoutMs, toolId, sessio
|
|
|
807
1053
|
return
|
|
808
1054
|
}
|
|
809
1055
|
codexEvents += 1
|
|
810
|
-
if (event.type === 'turn.completed' && event.usage) codexUsageLine = line
|
|
811
1056
|
if (event.type === 'turn.completed' && !failedEvent) terminal = 'completed'
|
|
812
1057
|
// Top-level `error` events include transient reconnect notices; only turn.failed is terminal.
|
|
813
1058
|
if (event.type === 'turn.failed') {
|
|
@@ -840,7 +1085,20 @@ function captureProcess(spec, task, workspace, signal, timeoutMs, toolId, sessio
|
|
|
840
1085
|
clearTimeout(graceTimer)
|
|
841
1086
|
clearTimeout(escalationTimer)
|
|
842
1087
|
signal?.removeEventListener('abort', onAbort)
|
|
843
|
-
|
|
1088
|
+
if (jsonlBuffer.trim()) {
|
|
1089
|
+
readJsonlLine(jsonlBuffer)
|
|
1090
|
+
jsonlBuffer = ''
|
|
1091
|
+
}
|
|
1092
|
+
let evidence = accounting.snapshot()
|
|
1093
|
+
if (spec.format === 'claude-json') {
|
|
1094
|
+
evidence = parseClaudeProcessOutput(stdout, result.exitCode, {
|
|
1095
|
+
secrets: [...API_KEY_VARIABLES, 'CLAUDE_CODE_OAUTH_TOKEN'].map(key => childEnvironment[key]),
|
|
1096
|
+
})
|
|
1097
|
+
} else if (miniMaxParser) {
|
|
1098
|
+
evidence = officialToolTerminalEvidence(spec.format, miniMaxParser.finish(result.exitCode))
|
|
1099
|
+
}
|
|
1100
|
+
resolveResult({ stdoutTail, stderrTail, terminalEvent: terminal, finalText,
|
|
1101
|
+
...finalizeOfficialProcessResult(result, evidence) })
|
|
844
1102
|
}
|
|
845
1103
|
function requestStop(reason) {
|
|
846
1104
|
if (finished || stopReason) return
|
|
@@ -878,7 +1136,7 @@ function captureProcess(spec, task, workspace, signal, timeoutMs, toolId, sessio
|
|
|
878
1136
|
else readJsonl(finalStdout)
|
|
879
1137
|
}
|
|
880
1138
|
stderrTail = tail(stderrTail, stderrDecoder.end())
|
|
881
|
-
if (jsonlBuffer.trim()) readJsonlLine(jsonlBuffer)
|
|
1139
|
+
if (jsonlBuffer.trim()) { readJsonlLine(jsonlBuffer); jsonlBuffer = '' }
|
|
882
1140
|
if (stopReason) {
|
|
883
1141
|
settle({ status: stopReason, exitCode: code, exitSignal })
|
|
884
1142
|
return
|
|
@@ -890,34 +1148,24 @@ function captureProcess(spec, task, workspace, signal, timeoutMs, toolId, sessio
|
|
|
890
1148
|
}
|
|
891
1149
|
if (miniMaxParser) {
|
|
892
1150
|
const outcome = miniMaxParser.finish(code)
|
|
893
|
-
|
|
894
|
-
settle({ ...outcome, usage: reported?.usage ?? null })
|
|
1151
|
+
settle(outcome)
|
|
895
1152
|
return
|
|
896
1153
|
}
|
|
897
1154
|
if (nativeParser) {
|
|
898
1155
|
settle({ exitCode: code, ...nativeParser.finish(code) })
|
|
899
1156
|
return
|
|
900
1157
|
}
|
|
1158
|
+
if (spec.format === 'claude-json') {
|
|
1159
|
+
settle({ ...parseClaudeProcessOutput(stdout, code, {
|
|
1160
|
+
secrets: [...API_KEY_VARIABLES, 'CLAUDE_CODE_OAUTH_TOKEN'].map(key => childEnvironment[key]),
|
|
1161
|
+
}), exitSignal })
|
|
1162
|
+
return
|
|
1163
|
+
}
|
|
901
1164
|
if (code !== 0) {
|
|
902
1165
|
settle({ status: 'failed', exitCode: code, exitSignal, error: '官方 CLI 退出码非零。' })
|
|
903
1166
|
return
|
|
904
1167
|
}
|
|
905
|
-
if (spec.format === '
|
|
906
|
-
try {
|
|
907
|
-
const result = JSON.parse(stdout)
|
|
908
|
-
if (result.type !== 'result' || result.is_error === true || result.subtype !== 'success'
|
|
909
|
-
|| typeof result.result !== 'string') {
|
|
910
|
-
settle({ status: 'failed', exitCode: code, error: 'Claude 未返回成功终态。' })
|
|
911
|
-
return
|
|
912
|
-
}
|
|
913
|
-
terminal = 'completed'
|
|
914
|
-
finalText = result.result
|
|
915
|
-
reportedUsage = usageFromOutput('claude-json', stdout)
|
|
916
|
-
} catch {
|
|
917
|
-
settle({ status: 'failed', exitCode: code, error: 'Claude 未返回有效的结果 JSON。' })
|
|
918
|
-
return
|
|
919
|
-
}
|
|
920
|
-
} else if (spec.format === 'zcode-stream-json') {
|
|
1168
|
+
if (spec.format === 'zcode-stream-json') {
|
|
921
1169
|
const turnId = zcodeResult?.turnId
|
|
922
1170
|
if (protocolError || failedEvent || !zcodeResult || typeof turnId !== 'string'
|
|
923
1171
|
|| !turnId || !zcodeStarted.has(turnId) || !zcodeCompleted.has(turnId)
|
|
@@ -939,10 +1187,7 @@ function captureProcess(spec, task, workspace, signal, timeoutMs, toolId, sessio
|
|
|
939
1187
|
settle({ status: 'failed', exitCode: code, error: protocolError ?? codexIncompleteError(codexEvents, stderrTail) })
|
|
940
1188
|
return
|
|
941
1189
|
}
|
|
942
|
-
|
|
943
|
-
settle({ status: 'succeeded', exitCode: code,
|
|
944
|
-
...(reportedUsage?.usage ? { usage: reportedUsage.usage } : {}),
|
|
945
|
-
...(reportedUsage?.reportedCostUsd ? { reportedCostUsd: reportedUsage.reportedCostUsd } : {}) })
|
|
1190
|
+
settle({ status: 'succeeded', exitCode: code })
|
|
946
1191
|
})
|
|
947
1192
|
|
|
948
1193
|
timeoutTimer = setTimeout(() => requestStop('timed-out'), timeoutMs)
|
|
@@ -960,51 +1205,79 @@ function captureProcess(spec, task, workspace, signal, timeoutMs, toolId, sessio
|
|
|
960
1205
|
*/
|
|
961
1206
|
export async function runOfficialTool({
|
|
962
1207
|
toolId, task, workspace, mode = 'read-only', isolatedRoot, modelId, signal, sandbox, timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
963
|
-
sessionOnly = false,
|
|
1208
|
+
sessionOnly = false, executionEnvironment: environment = 'harness',
|
|
964
1209
|
}) {
|
|
965
1210
|
const tool = getOfficialTool(toolId)
|
|
966
1211
|
if (!tool) return { toolId, status: 'unsupported', reason: '未知的官方工具注册表 ID。' }
|
|
967
|
-
|
|
1212
|
+
if (!['harness', 'cli-native'].includes(environment)) throw new TypeError('unknown CLI execution environment')
|
|
1213
|
+
const capability = officialToolExecutionCapabilities({ executionEnvironment: environment }).find(item => item.id === tool.id)
|
|
968
1214
|
if (!capability?.supported) return { toolId: tool.id, status: 'unsupported', reason: capability?.reason ?? '尚无执行适配器。' }
|
|
969
1215
|
const prompt = checkedTask(task)
|
|
970
1216
|
const requestedModel = checkedModel(modelId)
|
|
971
1217
|
const cwd = await checkedWorkspace(workspace)
|
|
972
1218
|
const executionMode = checkedMode(mode)
|
|
1219
|
+
if (environment === 'cli-native' && executionMode !== 'workspace-write') {
|
|
1220
|
+
return { toolId: tool.id, status: 'unsupported', reason: '厂商 CLI 正常环境仅用于明确确认的隔离工作树编辑任务。' }
|
|
1221
|
+
}
|
|
973
1222
|
if (!capability.modes.includes(executionMode)) {
|
|
974
|
-
return { toolId: tool.id, status: 'unsupported', reason:
|
|
1223
|
+
return { toolId: tool.id, status: 'unsupported', reason: tool.id === 'codex' ? CODEX_WRITE_UNSUPPORTED
|
|
1224
|
+
: `${tool.label} 的无界面模式会自动执行工具;当前仅允许经审批的独立 Git 工作区可编辑执行。` }
|
|
975
1225
|
}
|
|
976
1226
|
if (executionMode === 'workspace-write') await checkedWriteWorkspace(cwd, isolatedRoot)
|
|
977
1227
|
const timeout = checkedTimeout(timeoutMs)
|
|
978
1228
|
if (signal?.aborted) return { toolId: tool.id, status: 'cancelled', workspace: cwd }
|
|
979
|
-
|
|
1229
|
+
if (environment === 'cli-native') {
|
|
1230
|
+
// Explicit Host-authorized environment, never an automatic retry. Keep
|
|
1231
|
+
// entry attestation, bounded argv, process-tree cancellation and Git gates.
|
|
1232
|
+
const spec = await launchSpec(tool.id, cwd, executionMode, requestedModel, prompt, { executionEnvironment: environment })
|
|
1233
|
+
if (spec.unsupported) return { toolId: tool.id, status: 'unsupported', reason: spec.unsupported }
|
|
1234
|
+
const startedAt = Date.now()
|
|
1235
|
+
const outcome = await captureProcess(spec, prompt, cwd, signal, timeout, tool.id, sessionOnly === true)
|
|
1236
|
+
return { toolId: tool.id, mode: executionMode, executionEnvironment: environment,
|
|
1237
|
+
requestedModel: spec.requestedModel === undefined ? requestedModel : spec.requestedModel,
|
|
1238
|
+
...(spec.modelNotice ? { modelNotice: spec.modelNotice } : {}), workspace: cwd,
|
|
1239
|
+
source: spec.source, sandboxEnforcement: 'none', startedAt, finishedAt: Date.now(), ...outcome }
|
|
1240
|
+
}
|
|
1241
|
+
const blocker = harnessSandboxBlocker(tool.id, process.platform, executionMode)
|
|
980
1242
|
if (blocker) return { toolId: tool.id, status: 'unsupported', reason: blocker }
|
|
981
|
-
const spec = await launchSpec(tool.id, cwd, executionMode, requestedModel, prompt)
|
|
982
|
-
if (spec.unsupported) return { toolId: tool.id, status: 'unsupported', reason: spec.unsupported }
|
|
983
1243
|
if (typeof sandbox?.confine !== 'function') {
|
|
984
1244
|
return { toolId: tool.id, status: 'unsupported', reason: '官方 Harness 进程沙箱不可用,拒绝直接启动 CLI。' }
|
|
985
1245
|
}
|
|
1246
|
+
let policy
|
|
1247
|
+
try {
|
|
1248
|
+
policy = tool.id === 'codex' ? await codexReadOnlyPolicy(cwd)
|
|
1249
|
+
: { mode: executionMode, workspaceRoot: cwd }
|
|
1250
|
+
} catch (error) {
|
|
1251
|
+
return { toolId: tool.id, status: 'unsupported', reason: String(error?.message ?? error).slice(0, 300) }
|
|
1252
|
+
}
|
|
1253
|
+
const spec = await launchSpec(tool.id, cwd, executionMode, requestedModel, prompt)
|
|
1254
|
+
if (spec.unsupported) return { toolId: tool.id, status: 'unsupported', reason: spec.unsupported }
|
|
986
1255
|
let confined
|
|
987
1256
|
try {
|
|
988
|
-
confined = await sandbox.confine([spec.file, ...spec.args],
|
|
989
|
-
mode: executionMode, workspaceRoot: cwd,
|
|
990
|
-
}, signal)
|
|
1257
|
+
confined = await sandbox.confine([spec.file, ...spec.args], policy, signal)
|
|
991
1258
|
} catch (error) {
|
|
992
1259
|
return { toolId: tool.id, status: 'unsupported', reason: `官方进程沙箱拒绝启动:${String(error?.message ?? error).slice(0, 300)}` }
|
|
993
1260
|
}
|
|
994
1261
|
if (!Array.isArray(confined?.argv) || confined.argv.length < 2 || !['full', 'partial'].includes(confined.enforcement)) {
|
|
995
1262
|
return { toolId: tool.id, status: 'unsupported', reason: '官方进程沙箱未返回有效的受限启动参数。' }
|
|
996
1263
|
}
|
|
1264
|
+
if (tool.id === 'codex' && !codexStateSandboxContract(confined, [spec.file, ...spec.args], policy)) {
|
|
1265
|
+
return { toolId: tool.id, status: 'unsupported', reason: 'Codex 的只读任务需要已核验的 Harness Windows ACL 状态目录沙箱;当前 runner 契约不匹配,拒绝直接或不受限启动。' }
|
|
1266
|
+
}
|
|
997
1267
|
const startedAt = Date.now()
|
|
998
1268
|
const outcome = await captureProcess({ ...spec, file: confined.argv[0], args: confined.argv.slice(1) },
|
|
999
|
-
prompt, cwd, signal, timeout, tool.id, sessionOnly === true,
|
|
1269
|
+
prompt, cwd, signal, timeout, tool.id, sessionOnly === true,
|
|
1270
|
+
{ ...runnerEnvironment(confined.argv[0]), ...(tool.id === 'codex' ? { CODEX_HOME: policy.workspaceRoot } : {}) })
|
|
1000
1271
|
return {
|
|
1001
1272
|
toolId: tool.id,
|
|
1002
1273
|
mode: executionMode,
|
|
1274
|
+
executionEnvironment: environment,
|
|
1003
1275
|
requestedModel: spec.requestedModel === undefined ? requestedModel : spec.requestedModel,
|
|
1004
1276
|
...(spec.modelNotice ? { modelNotice: spec.modelNotice } : {}),
|
|
1005
1277
|
workspace: cwd,
|
|
1006
1278
|
source: spec.source,
|
|
1007
1279
|
sandboxEnforcement: confined.enforcement,
|
|
1280
|
+
...(tool.id === 'codex' ? { runtimeState: 'existing-codex-home' } : {}),
|
|
1008
1281
|
startedAt,
|
|
1009
1282
|
finishedAt: Date.now(),
|
|
1010
1283
|
...outcome,
|