@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.
@@ -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', 'workspace-write']) }
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', 'CLAUDE_CODE_OAUTH_TOKEN', 'CLAUDE_CONFIG_DIR'],
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 (process.env[key] !== undefined) env[key] = process.env[key]
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
- const found = newestFirst(entries)[0]
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
- const found = managed ?? newestFirst(entries)[0]
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
- * CLIs that cannot start inside the Harness Windows ACL process sandbox.
661
- * Codex 0.157 must write CODEX_HOME (~/.codex: app-server state, sessions,
662
- * temp aliases) before its first turn; the ACL runner denies every write in
663
- * read-only mode and every write outside the workspace in workspace-write, so
664
- * Codex exits with "failed to initialize in-process app-server client: Access
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 capability = CAPABILITIES[tool.id]
681
- if (!capability?.supported) return { id: tool.id, supported: false, ready: false, reason: capability?.reason ?? '当前平台不支持托管执行。' }
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: { ...executionEnvironment(toolId, { sessionOnly }), ...extraEnv },
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
- let reportedUsage = null
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) { nativeParser.push(line); return }
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
- resolveResult({ stdoutTail, stderrTail, terminalEvent: terminal, finalText, ...result })
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
- const reported = usageFromEvents('minimax-result', outcome)
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 === 'claude-json') {
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
- if (!reportedUsage && codexUsageLine) reportedUsage = usageFromOutput('codex-jsonl', codexUsageLine)
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
- const capability = CAPABILITIES[tool.id]
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: `${tool.label} 的无界面模式会自动执行工具;当前仅允许经审批的独立 Git 工作区可编辑执行。` }
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
- const blocker = harnessSandboxBlocker(tool.id)
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, runnerEnvironment(confined.argv[0]))
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,