@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.
@@ -23,6 +23,7 @@ import { routeBoundaries } from './shared/security-boundaries.mjs'
23
23
  import { createPlanFromRoutes } from './shared/harness-plan.mjs'
24
24
  import { applyModelProfiles, parseModelProfilesJson } from './shared/model-profiles.mjs'
25
25
  import { registerOfficialToolsRemote } from './official-tools-remote-service.mjs'
26
+ import { parseRunRequest } from './shared/official-tools-remote.mjs'
26
27
  import { createTerminalManager } from './shared/cli-terminal.mjs'
27
28
  import {
28
29
  parseTerminalRead,
@@ -42,7 +43,7 @@ import {
42
43
  } from './shared/official-tool-registry.mjs'
43
44
  import { parseInstallPreferences, resolveInstallPlan } from './shared/tool-install-preferences.mjs'
44
45
  import { officialToolExecutionCapabilities, officialToolReadiness, runOfficialTool } from './shared/official-tool-executor.mjs'
45
- import { runOfficialTask, runOfficialTeam, sessionWorkspace } from './shared/official-team-runtime.mjs'
46
+ import { preflightOfficialWorkspace, runOfficialTask, runOfficialTeam, sessionWorkspace } from './shared/official-team-runtime.mjs'
46
47
  import { downstreamPackageIds, executeAssignmentPlan, rerunAssignmentPackage, taskTextProblem } from './shared/task-executors.mjs'
47
48
  import {
48
49
  probeAllTools,
@@ -387,9 +388,21 @@ export function billingForResult(result) {
387
388
  })
388
389
  }
389
390
 
390
- function routePricing(routes, item) {
391
- const route = routes.find(route => route.provider === item?.provider && route.model === item?.model)
392
- return route?.pricing ? { ...route.pricing, source: route.pricingSource ?? 'route', version: route.pricingVersion ?? null, asOf: route.snapshotAsOf ?? null } : null
391
+ export function routePricing(routes, item) {
392
+ const matches = routes.filter(route => {
393
+ if (route.provider !== item?.provider) return false
394
+ if (item?.channel !== 'official-cli') return route.model === item?.model
395
+ // A routing recommendation cannot establish which model the CLI billed.
396
+ return typeof item.actualModel === 'string' && [route.model, route.cliModel, `${route.provider}/${route.model}`].includes(item.actualModel)
397
+ })
398
+ // Different catalog routes can share a CLI alias. Conflicting prices are
399
+ // not evidence of what the CLI actually billed.
400
+ const prices = new Set(matches.map(route => JSON.stringify(route.pricing ?? null)))
401
+ if (prices.size > 1) return null
402
+ const route = matches[0]
403
+ return route?.pricing ? { ...route.pricing, provider: route.provider, model: route.model,
404
+ ...(route.cliModel ? { cliModel: route.cliModel } : {}),
405
+ source: route.pricingSource ?? 'route', version: route.pricingVersion ?? null, asOf: route.snapshotAsOf ?? null } : null
393
406
  }
394
407
 
395
408
  function feedbackOptions(config) {
@@ -463,7 +476,7 @@ export async function createRoutePlan(ctx, task, config = {}, options = {}) {
463
476
  const readiness = options.skipToolProbe === true ? []
464
477
  : await Promise.all(installed.map(id => officialToolReadiness(id, options.workspace ?? process.cwd())))
465
478
  const executable = new Set(Array.isArray(options.runnableToolIds)
466
- ? options.runnableToolIds : readiness.filter(item => item.ready).map(item => item.id))
479
+ ? options.runnableToolIds : readiness.filter(item => item.ready && (item.readyModes ?? item.modes ?? []).includes('read-only')).map(item => item.id))
467
480
  const loggedOut = options.skipToolProbe === true
468
481
  ? (Array.isArray(options.loggedOutToolIds) ? options.loggedOutToolIds : [])
469
482
  : loggedOutIds(await currentHealth())
@@ -699,7 +712,7 @@ export async function planAssignment(ctx, task, config = {}, options = {}) {
699
712
  : await Promise.all(installed.map(id => officialToolReadiness(id, options.workspace ?? process.cwd())))
700
713
  const runnableToolIds = Array.isArray(options.runnableToolIds)
701
714
  ? options.runnableToolIds
702
- : readiness.filter(item => item.ready).map(item => item.id)
715
+ : readiness.filter(item => item.ready && (item.readyModes ?? item.modes ?? []).includes('read-only')).map(item => item.id)
703
716
  const loggedOut = options.skipToolProbe === true
704
717
  ? (Array.isArray(options.loggedOutToolIds) ? options.loggedOutToolIds : [])
705
718
  : loggedOutIds(await currentHealth())
@@ -823,7 +836,7 @@ export function rerunSupport(run) {
823
836
  return { ok: true, kind: run.kind === 'team' ? 'team' : 'assign', writes: false }
824
837
  }
825
838
 
826
- export async function rerunRecordedStep(ctx, config = {}, { runId, packageId, provider, model, confirmOverBudget = false, confirmWrite = false, subscriptionChoice = null, workspace, root, signal, ...options } = {}) {
839
+ export async function rerunRecordedStep(ctx, config = {}, { runId, packageId, provider, model, confirmOverBudget = false, confirmWrite = false, confirmUnsandboxed = false, subscriptionChoice = null, workspace, root, signal, ...options } = {}) {
827
840
  const { run, saved } = await storedRun(runId)
828
841
  const target = run.packages.find(item => item.id === text(packageId))
829
842
  if (!target) throw new Error(`运行记录中没有工作包 ${text(packageId)}`)
@@ -854,9 +867,12 @@ export async function rerunRecordedStep(ctx, config = {}, { runId, packageId, pr
854
867
  if (support.kind === 'team-write' && target.ok) {
855
868
  throw new Error('可编辑团队运行只能续跑失败或未完成的步骤:已成功步骤的改动已在原独立工作区中,重跑会重复套用。如需重做请重新调用 model_router_team_execute。')
856
869
  }
857
- if (support.writes && confirmWrite !== true) {
858
- return { paused: true, needsConfirmation: ['workspace-write'], run,
859
- budget: null, message: '续跑可编辑团队步骤会在新的独立 Git 工作树中套用之前的改动并修改文件,需要先确认。' }
870
+ const missingConfirmations = []
871
+ if (support.writes && confirmWrite !== true) missingConfirmations.push('workspace-write')
872
+ if (run.kind === 'team' && run.executionEnvironment === 'cli-native' && confirmUnsandboxed !== true) missingConfirmations.push('unsandboxed')
873
+ if (missingConfirmations.length) {
874
+ return { paused: true, needsConfirmation: missingConfirmations, run,
875
+ budget: null, message: '请确认续跑所需的文件修改与执行环境权限后继续;厂商 CLI 正常环境以当前用户权限运行,独立 Git 工作树不是操作系统读写边界。' }
860
876
  }
861
877
  const sameRoute = !override || (override.provider === target.recommendedProvider && override.model === target.recommendedModel)
862
878
  const budget = budgetFor(config, saved, sameRoute ? target.estimatedCost : null)
@@ -930,16 +946,21 @@ function teamPlanFromRun(run, ids, override, targetId) {
930
946
  */
931
947
  async function rerunTeamStep(ctx, { run, target, override, routes, budget, cwd, root, signal, options, config = {} }) {
932
948
  const mode = run.executionMode === 'workspace-write' ? 'workspace-write' : 'read-only'
949
+ const executionEnvironment = teamExecutionEnvironment(run.executionEnvironment, mode)
933
950
  const downstream = downstreamPackageIds(run.packages, target.id)
934
951
  .filter(id => !run.packages.find(item => item.id === id)?.ok)
935
952
  const ids = [target.id, ...downstream]
936
953
  const plan = teamPlanFromRun(run, ids, override, target.id)
937
954
  const cliModels = Object.fromEntries(Object.entries(run.cliModels ?? {}).filter(([key]) => ids.includes(key) && !(override && key === target.id)))
955
+ if (override) {
956
+ const reassignedPlan = { team: { workPackages: plan.team.workPackages.filter(item => item.id === target.id) } }
957
+ Object.assign(cliModels, resolveTeamCliModelBindings(reassignedPlan, routes))
958
+ }
938
959
  const previous = run.packages.filter(item => item.ok && !ids.includes(item.id))
939
960
  .map(item => ({ id: item.id, name: item.name, finalText: item.answer }))
940
961
  const runTeam = options.runTeam ?? runOfficialTeam
941
962
  const installed = Array.isArray(options.installedToolIds) ? options.installedToolIds : await installedToolIds()
942
- const execution = await runTeam({ plan, task: run.task, workspace: cwd, allowedRoot: root ?? cwd, mode,
963
+ const execution = await runTeam({ plan, task: run.task, workspace: cwd, allowedRoot: root ?? cwd, mode, executionEnvironment,
943
964
  installedIds: installed, cliModels, signal, sandbox: ctx.sandbox, previous, onlyIds: ids,
944
965
  ...(mode === 'workspace-write' ? { seedFrom: { workspace: run.isolatedWorkspace, baseCommit: run.baseCommit } } : {}),
945
966
  ...(options.runtime ? { runtime: options.runtime } : {}) })
@@ -961,6 +982,7 @@ async function rerunTeamStep(ctx, { run, target, override, routes, budget, cwd,
961
982
  if (stored) stored.billingSwitch = note
962
983
  }
963
984
  current.status = execution?.status ?? current.status
985
+ current.executionEnvironment = executionEnvironment
964
986
  if (mode === 'workspace-write' && execution?.workspace && execution.status !== 'blocked') {
965
987
  current.isolatedWorkspace = String(execution.workspace)
966
988
  if (typeof execution.baseCommit === 'string') current.baseCommit = execution.baseCommit
@@ -969,6 +991,10 @@ async function rerunTeamStep(ctx, { run, target, override, routes, budget, cwd,
969
991
  if (override) {
970
992
  const stored = current.packages.find(item => item.id === target.id)
971
993
  if (stored) { stored.recommendedProvider = override.provider; stored.recommendedModel = override.model }
994
+ const bindings = { ...(current.cliModels ?? {}) }
995
+ delete bindings[target.id]
996
+ if (cliModels[target.id]) bindings[target.id] = cliModels[target.id]
997
+ current.cliModels = bindings
972
998
  }
973
999
  })
974
1000
  return { run: updated, execution: { ...converted, ranIds: ids, raw: execution }, budget }
@@ -1011,13 +1037,14 @@ async function annotateCliQuota(run, config, failures) {
1011
1037
  }
1012
1038
  }
1013
1039
 
1014
- export async function recordTeamRun({ task, plan, execution, mode, workspace, routes, cliModels, budget, startedAt, preset, config = {} }) {
1040
+ export async function recordTeamRun({ task, plan, execution, mode, executionEnvironment = 'harness', workspace, routes, cliModels, budget, startedAt, preset, config = {} }) {
1015
1041
  const run = buildTeamRunRecord({
1016
1042
  id: randomUUID(), createdAt: startedAt, finishedAt: Date.now(), task, plan, execution, mode, workspace,
1017
1043
  preset: plan?.preset ?? preset ?? DEFAULT_ROUTING_PRESET,
1018
1044
  pricingFor: item => routePricing(routes, item), billingFor: billingForResult,
1019
1045
  budget: budget ? { estimateUsd: budget.estimateUsd, exceeded: budget.exceeded, downgraded: false, confirmed: budget.exceeded ? true : undefined } : null,
1020
1046
  })
1047
+ run.executionEnvironment = teamExecutionEnvironment(executionEnvironment, mode ?? 'read-only')
1021
1048
  const bindings = Object.fromEntries(Object.entries(cliModels ?? {}).filter(([, value]) => typeof value === 'string' && value))
1022
1049
  if (Object.keys(bindings).length) run.cliModels = bindings
1023
1050
  await annotateCliQuota(run, config, (Array.isArray(execution?.results) ? execution.results : [])
@@ -1049,9 +1076,7 @@ export async function planToolRun(ctx, config, args = {}, { signal } = {}) {
1049
1076
  routes = await routesWithLearning(ctx, config, signal, refreshed.runs, refreshed.dynamicData, refreshed.routingLearning)
1050
1077
  route = routes.find(item => item.provider === text(args.provider) && item.model === text(args.model)) ?? null
1051
1078
  if (!route) throw new Error('所选 provider/model 不在官方模型目录中')
1052
- modelId = route.cliModel && toolId !== 'zcode'
1053
- ? route.cliModel
1054
- : toolId === 'claude-code' || toolId === 'codex' ? route.model : null
1079
+ modelId = route.cliModel && toolId !== 'zcode' ? route.cliModel : null
1055
1080
  }
1056
1081
  if (text(args.cliModel)) {
1057
1082
  if (!route) throw new Error('cliModel 需要同时提供已配置的 provider 和 model 路线')
@@ -1061,7 +1086,7 @@ export async function planToolRun(ctx, config, args = {}, { signal } = {}) {
1061
1086
  const estimate = route ? createPlanFromRoutes(taskText, routes, { mode: 'direct', directProvider: route.provider, directModel: route.model }).estimatedCost : null
1062
1087
  const budget = budgetFor(config, await savedState(), Number.isFinite(estimate) ? estimate : null)
1063
1088
  return { toolId, tool, taskText, mode, modelId, route, routes, estimate: Number.isFinite(estimate) ? estimate : null, budget,
1064
- provider: route?.provider ?? '', model: route?.model ?? '', cliModel: text(args.cliModel) || null }
1089
+ provider: route?.provider ?? '', model: route?.model ?? '', cliModel: modelId }
1065
1090
  }
1066
1091
 
1067
1092
  /** Run a planned tool call and record it; `rerunOf` links a rerun to the original run. */
@@ -1200,6 +1225,103 @@ export function resolveTeamCliModelBindings(plan, routes, requested = {}) {
1200
1225
  .filter(([key]) => !Object.hasOwn(bindings, key))) }
1201
1226
  }
1202
1227
 
1228
+ /** Only plan team packages on an installed CLI that is ready in this mode. */
1229
+ export function executableTeamRoutes(routes, installed, readiness, mode, capabilities = officialToolExecutionCapabilities()) {
1230
+ const available = new Set(installed)
1231
+ const ready = new Map(readiness.map(item => [item.id, item]))
1232
+ const supported = new Map(capabilities.map(item => [item.id, item]))
1233
+ return routes.filter(route => {
1234
+ const id = toolForProvider(route.provider)?.id
1235
+ const state = ready.get(id)
1236
+ const capability = supported.get(id)
1237
+ const readyModes = state?.readyModes ?? state?.modes ?? capability?.modes ?? []
1238
+ return available.has(id) && state?.ready === true && capability?.modes?.includes(mode)
1239
+ && readyModes.includes(mode)
1240
+ })
1241
+ }
1242
+
1243
+ function teamExecutionEnvironment(value = 'harness', mode = 'read-only') {
1244
+ if (!['harness', 'cli-native'].includes(value)) throw new Error('团队执行环境必须是 harness 或 cli-native')
1245
+ if (value === 'cli-native' && mode !== 'workspace-write') throw new Error('厂商 CLI 正常环境仅支持官方团队的 workspace-write 模式')
1246
+ return value
1247
+ }
1248
+
1249
+ /** Plan a CLI-only team without starting a provider request or changing files. */
1250
+ export async function planOfficialTeam(ctx, config = {}, args = {}, options = {}) {
1251
+ const taskText = text(args.task)
1252
+ const problem = taskTextProblem(taskText)
1253
+ if (problem) throw new Error(problem)
1254
+ const mode = args.mode ?? args.executionMode ?? 'read-only'
1255
+ if (!['read-only', 'workspace-write'].includes(mode)) throw new Error('团队执行模式必须是 read-only 或 workspace-write')
1256
+ const executionEnvironment = teamExecutionEnvironment(args.executionEnvironment, mode)
1257
+ const workspace = options.workspace ?? process.cwd()
1258
+ throwIfAborted(options.signal)
1259
+ const saved = await routingSavedState(config)
1260
+ const [routes, installed] = await Promise.all([
1261
+ routesWithLearning(ctx, config, options.signal, saved.runs, saved.dynamicData, saved.routingLearning),
1262
+ Array.isArray(options.installedToolIds) ? options.installedToolIds : installedToolIds(),
1263
+ ])
1264
+ const readiness = Array.isArray(options.readiness) ? options.readiness
1265
+ : await Promise.all(installed.map(id => (options.readinessFor ?? officialToolReadiness)(id, workspace, { executionEnvironment })))
1266
+ const capabilities = options.executionCapabilities ?? (options.capabilitiesFor ?? officialToolExecutionCapabilities)({ executionEnvironment })
1267
+ const executableRoutes = executableTeamRoutes(routes, installed, readiness, mode, capabilities)
1268
+ const base = { taskText, workspace, mode, executionEnvironment, plan: null, routes: executableRoutes, installed, readiness,
1269
+ executionCapabilities: capabilities, executionReadiness: readiness, cliModels: {}, budget: null }
1270
+ if (executionEnvironment === 'harness' && typeof ctx.sandbox?.confine !== 'function') {
1271
+ return { ...base, status: 'blocked', reason: '官方 Harness 进程沙箱不可用,不能执行官方 CLI 团队。' }
1272
+ }
1273
+ if (executableRoutes.length === 0) return { ...base, status: 'blocked',
1274
+ reason: `官方模型目录中没有同时满足已配置路线、已安装 CLI、托管执行适配器与 ${mode} 模式的供应商。请检查该模式下的 CLI 执行就绪状态。` }
1275
+ const supported = new Set(executableRoutes.map(route => toolForProvider(route.provider).id))
1276
+ const configuredBudget = valueOf(config, 'budgetUsd', DEFAULT_ROUTER_SETTINGS.budgetUsd)
1277
+ const budgetUsd = Math.max(0, finiteNumber(args.budgetUsd, finiteNumber(configuredBudget, 0)))
1278
+ const plan = createPlanFromRoutes(taskText, executableRoutes, {
1279
+ ...planDataOptions(saved, config), mode: 'team', budgetUsd, installedToolIds: installed,
1280
+ runnableToolIds: installed.filter(id => supported.has(id)), preset: routingPresetOf(config, args.preset),
1281
+ })
1282
+ const bindingsText = text(args.cliModelsJson)
1283
+ if (bindingsText.length > 4_000) throw new Error('cliModelsJson 超过 4000 字符上限')
1284
+ let requestedModels = {}
1285
+ if (bindingsText) {
1286
+ try { requestedModels = JSON.parse(bindingsText) }
1287
+ catch { throw new Error('cliModelsJson 不是有效的 JSON 对象') }
1288
+ }
1289
+ const cliModels = resolveTeamCliModelBindings(plan, executableRoutes, requestedModels)
1290
+ const budget = budgetFor(config, await savedState(), plan.estimatedCost)
1291
+ return { ...base, status: 'planned', plan, cliModels, budget }
1292
+ }
1293
+
1294
+ /** Execute and record a planned team through official CLIs only. */
1295
+ export async function executeOfficialTeamPlan(ctx, config = {}, planned, options = {}) {
1296
+ if (planned?.status === 'blocked') return { ...planned }
1297
+ if (!planned?.plan || !['read-only', 'workspace-write'].includes(planned.mode)) throw new Error('缺少有效的官方 CLI 团队计划')
1298
+ const executionEnvironment = teamExecutionEnvironment(planned.executionEnvironment, planned.mode)
1299
+ const workspace = options.workspace ?? planned.workspace
1300
+ const budget = budgetFor(config, await savedState(), planned.plan.estimatedCost)
1301
+ const common = { plan: planned.plan, workspace, mode: planned.mode, executionEnvironment, budget }
1302
+ if (budget.exceeded && options.confirmOverBudget !== true) return { ...common, status: 'paused-budget',
1303
+ notice: `${budget.message} 已暂停团队执行。请向用户确认后再执行。` }
1304
+ if (executionEnvironment === 'cli-native' && options.confirmUnsandboxed !== true) return { ...common,
1305
+ status: 'needs-confirmation', missing: ['unsandboxed'],
1306
+ reasons: confirmationReasons(['unsandboxed'], { executionEnvironment }),
1307
+ notice: '厂商 CLI 正常环境必须明确确认后才会启动。' }
1308
+ if (executionEnvironment === 'harness' && typeof ctx.sandbox?.confine !== 'function') return { ...common, status: 'blocked',
1309
+ reason: '官方 Harness 进程沙箱不可用,不能执行官方 CLI 团队。' }
1310
+ throwIfAborted(options.signal)
1311
+ const startedAt = Date.now()
1312
+ const execution = await (options.runTeam ?? runOfficialTeam)({ plan: planned.plan, task: planned.taskText, workspace,
1313
+ allowedRoot: options.root ?? workspace, mode: planned.mode, executionEnvironment, installedIds: planned.installed,
1314
+ cliModels: planned.cliModels, signal: options.signal, sandbox: ctx.sandbox })
1315
+ const run = await recordTeamRun({ task: planned.taskText, plan: planned.plan, execution, mode: planned.mode, executionEnvironment, workspace,
1316
+ routes: planned.routes, cliModels: planned.cliModels, budget, startedAt, config })
1317
+ return { ...common, status: execution.status, execution, run, runId: run?.id ?? null,
1318
+ rerunNotice: planned.mode === 'read-only'
1319
+ ? '失败的工作包可用 model_router_rerun_step 单步重跑(只重跑该步及其未完成的下游)。'
1320
+ : '若在某个步骤失败而停止,可用 model_router_rerun_step 在新的独立工作树中套用之前的改动后续跑(需审批);已整合或待人工整合的运行不能单步重跑。',
1321
+ modelNotice: '已配置 cliModel 的路线按工作包传给官方 CLI;所有厂商无映射时使用 CLI 默认模型。请求模型、Harness 推荐模型与 CLI 回报的实际模型分别记录;未核验实际模型时不按推荐路线价格计算实际费用。',
1322
+ billingNotice: 'budgetUsd 仅影响估算与路由,无法限制官方 CLI 账号实际费用。' }
1323
+ }
1324
+
1203
1325
  /** What was assigned and why, per package: route, difficulty, estimated cost, channel. */
1204
1326
  export function decisionSummary(plan) {
1205
1327
  const packages = plan?.routingBypassed
@@ -1292,12 +1414,28 @@ export function approvalReasonCodes(name, args = {}, config = {}, { rerunRun = n
1292
1414
  if (name === 'model_router_rerun_step' && args.subscriptionChoice === 'api') codes.push('subscription-api')
1293
1415
  if (args.confirmOverBudget === true) codes.push('over-budget')
1294
1416
  const directRun = name === 'model_router_execute' || (name === 'model_router_rerun_step' && (!rerunRun || rerunSupport(rerunRun).kind === 'assign'))
1417
+ if ((name === 'model_router_team_execute' && args.executionEnvironment === 'cli-native')
1418
+ || (name === 'model_router_rerun_step' && rerunRun?.executionEnvironment === 'cli-native')) codes.push('unsandboxed')
1295
1419
  if (directRun && valueOf(config, 'confirmUnsandboxedCli', true) !== false && mayLaunchDirectCli(args)) codes.push('unsandboxed')
1296
1420
  return codes
1297
1421
  }
1298
1422
 
1423
+ function executionApprovalText(code, sandboxAvailable, executionEnvironment) {
1424
+ if (executionEnvironment === 'cli-native' && ['workspace-write', 'unsandboxed'].includes(code)) {
1425
+ return { reason: 'Run verified official CLIs with the current user permissions, outside the Harness process sandbox',
1426
+ zh: code === 'workspace-write'
1427
+ ? '修改文件:CLI 在独立 Git 工作树执行,成功后核对并整合补丁。工作树用于改动回收,不构成操作系统读写边界。'
1428
+ : '厂商 CLI 正常环境:使用原登录、配置、技能及 MCP,以当前用户权限运行,不经过 Harness 进程沙箱。工具可能访问工作树外当前用户可访问的资源;不会自动切换执行环境。',
1429
+ en: code === 'workspace-write'
1430
+ ? 'Edit in an isolated Git worktree and integrate the verified patch. The worktree collects changes; it is not an OS file-access boundary.'
1431
+ : 'Run the verified vendor CLI with its original login, configuration, skills and MCP under the current user permissions, outside the Harness process sandbox. Tools can access resources outside the worktree. This environment is never selected automatically.' }
1432
+ }
1433
+ return APPROVAL_TEXT[code](sandboxAvailable)
1434
+ }
1435
+
1299
1436
  function approvalReasons(name, args, config, { sandboxAvailable = false, rerunRun = null } = {}) {
1300
- return approvalReasonCodes(name, args, config, { rerunRun }).map(code => ({ code, ...APPROVAL_TEXT[code](sandboxAvailable) }))
1437
+ return approvalReasonCodes(name, args, config, { rerunRun }).map(code => ({ code,
1438
+ ...executionApprovalText(code, sandboxAvailable, rerunRun?.executionEnvironment ?? args.executionEnvironment) }))
1301
1439
  }
1302
1440
 
1303
1441
  /** One approval prompt listing every reason, so the user is asked once. */
@@ -1318,8 +1456,8 @@ export function combinedAsk(reasons) {
1318
1456
  }
1319
1457
 
1320
1458
  /** Reasons in the workbench's shape: code plus the Chinese and English text. */
1321
- export function confirmationReasons(codes, { sandboxAvailable = false } = {}) {
1322
- return codes.map(code => ({ code, zh: APPROVAL_TEXT[code](sandboxAvailable).zh, en: APPROVAL_TEXT[code](sandboxAvailable).en }))
1459
+ export function confirmationReasons(codes, { sandboxAvailable = false, executionEnvironment = 'harness' } = {}) {
1460
+ return codes.map(code => ({ code, ...executionApprovalText(code, sandboxAvailable, executionEnvironment) }))
1323
1461
  }
1324
1462
 
1325
1463
  /**
@@ -1337,10 +1475,39 @@ export async function workbenchWorkspace(requested, runs = []) {
1337
1475
  return chosen
1338
1476
  }
1339
1477
 
1340
- /** Plan preview for the workbench: decision, estimate, budget and what needs confirming. */
1341
- export async function previewWorkbenchRun(ctx, config, request = {}, options = {}) {
1478
+ /** Resolve one fresh preview and retain its exact CLI plan for the confirmed run. */
1479
+ async function prepareWorkbenchPreview(ctx, config, supplied = {}, options = {}) {
1480
+ const request = parseRunRequest(supplied)
1342
1481
  const saved = await savedState()
1343
1482
  const workspace = await workbenchWorkspace(request.workspace, saved.runs)
1483
+ if (request.executor === 'official-team') {
1484
+ const mode = request.executionMode
1485
+ let planned
1486
+ try {
1487
+ await (options.preflightWorkspace ?? preflightOfficialWorkspace)(workspace, mode, options.signal)
1488
+ planned = await planOfficialTeam(ctx, config, { ...request, mode }, {
1489
+ workspace, signal: options.signal, ...options.planOptions,
1490
+ })
1491
+ } catch (error) {
1492
+ return { request, preview: { executor: 'official-team', executionMode: mode, executionEnvironment: request.executionEnvironment ?? 'harness', workspace,
1493
+ status: 'blocked', reason: errorText(error), decision: { packages: [] }, selected: null,
1494
+ estimatedCost: null, budget: null, reasons: [], readOnly: mode === 'read-only' } }
1495
+ }
1496
+ const decision = decisionSummary(planned.plan)
1497
+ decision.packages = decision.packages.map(item => ({ ...item, channel: 'official-cli',
1498
+ channelDetail: `官方 CLI;${mode === 'workspace-write' ? '在隔离 Git 工作树中使用自己的工具' : '只读检查工程,工具集按各 CLI 只读适配限制;Claude 禁用 shell、技能及既有 MCP,并忽略用户设置'};未配置映射时使用 CLI 默认模型;失败停止,不自动转 API。` }))
1499
+ const codes = planned.status === 'blocked' ? [] : approvalReasonCodes('model_router_team_execute', {
1500
+ mode, executionEnvironment: planned.executionEnvironment, confirmOverBudget: Boolean(planned.budget?.exceeded),
1501
+ }, config)
1502
+ return { request, planned, preview: {
1503
+ executor: 'official-team', executionMode: mode, executionEnvironment: planned.executionEnvironment, workspace, status: planned.status,
1504
+ ...(planned.reason ? { reason: planned.reason } : {}),
1505
+ decision, selected: planned.plan?.selected ?? null, routingBypassed: false,
1506
+ estimatedCost: planned.plan?.estimatedCost ?? null, budget: planned.budget,
1507
+ reasons: confirmationReasons(codes, { sandboxAvailable: typeof ctx.sandbox?.confine === 'function', executionEnvironment: planned.executionEnvironment }),
1508
+ readOnly: mode === 'read-only',
1509
+ } }
1510
+ }
1344
1511
  const configuredBudget = valueOf(config, 'budgetUsd', DEFAULT_ROUTER_SETTINGS.budgetUsd)
1345
1512
  const planned = await planAssignment(ctx, request.task, config, {
1346
1513
  provider: request.provider, model: request.model, planMode: request.planMode, preset: request.preset,
@@ -1348,7 +1515,7 @@ export async function previewWorkbenchRun(ctx, config, request = {}, options = {
1348
1515
  })
1349
1516
  await hydrateHealth()
1350
1517
  const codes = approvalReasonCodes('model_router_execute', { provider: request.provider, confirmOverBudget: Boolean(planned.budget.exceeded) }, config)
1351
- return {
1518
+ return { request, preview: {
1352
1519
  workspace,
1353
1520
  decision: decisionSummary(planned.plan),
1354
1521
  selected: planned.plan.selected ?? null,
@@ -1357,15 +1524,32 @@ export async function previewWorkbenchRun(ctx, config, request = {}, options = {
1357
1524
  budget: planned.budget,
1358
1525
  reasons: confirmationReasons(codes, { sandboxAvailable: typeof ctx.sandbox?.confine === 'function' }),
1359
1526
  readOnly: true,
1360
- }
1527
+ } }
1528
+ }
1529
+
1530
+ /** Plan preview: no model calls, workspace changes or worktree creation. */
1531
+ export async function previewWorkbenchRun(ctx, config, request = {}, options = {}) {
1532
+ return (await prepareWorkbenchPreview(ctx, config, request, options)).preview
1361
1533
  }
1362
1534
 
1363
1535
  /** Execute from the workbench; refuses until every current reason is in confirmedReasons. */
1364
- export async function startWorkbenchRun(ctx, config, request = {}, options = {}) {
1365
- const preview = await previewWorkbenchRun(ctx, config, request, options)
1536
+ export async function startWorkbenchRun(ctx, config, supplied = {}, options = {}) {
1537
+ const prepared = await prepareWorkbenchPreview(ctx, config, supplied, options)
1538
+ const { preview, request } = prepared
1539
+ if (preview.status === 'blocked') return preview
1366
1540
  const confirmed = new Set(Array.isArray(request.confirmedReasons) ? request.confirmedReasons : [])
1367
1541
  const missing = preview.reasons.filter(item => !confirmed.has(item.code))
1368
- if (missing.length) return { status: 'needs-confirmation', ...preview, reasons: preview.reasons, missing: missing.map(item => item.code) }
1542
+ if (missing.length) return { ...preview, status: 'needs-confirmation', reasons: preview.reasons, missing: missing.map(item => item.code) }
1543
+ if (request.executor === 'official-team') {
1544
+ const result = await (options.executeOfficialTeam ?? executeOfficialTeamPlan)(ctx, config, prepared.planned, {
1545
+ workspace: preview.workspace, root: preview.workspace, signal: options.signal,
1546
+ confirmOverBudget: confirmed.has('over-budget'), confirmUnsandboxed: confirmed.has('unsandboxed'), ...(options.teamOptions ?? {}),
1547
+ })
1548
+ return { status: result.status, runId: result.runId ?? null, workspace: preview.workspace,
1549
+ executor: 'official-team', executionMode: request.executionMode, executionEnvironment: preview.executionEnvironment, decision: preview.decision,
1550
+ budget: result.budget ?? preview.budget, ...(result.reason ? { reason: result.reason } : {}),
1551
+ ...(result.notice ? { notice: result.notice } : {}) }
1552
+ }
1369
1553
  const configuredBudget = valueOf(config, 'budgetUsd', DEFAULT_ROUTER_SETTINGS.budgetUsd)
1370
1554
  const result = await (options.execute ?? executeConfiguredAssignment)(ctx, request.task, config, {
1371
1555
  provider: request.provider, model: request.model, planMode: request.planMode, preset: request.preset,
@@ -1766,7 +1950,7 @@ function registerOfficialToolModels(ctx, config, assertInstallPreferences) {
1766
1950
  }))
1767
1951
  ctx.tools.register(defineTool({
1768
1952
  name: 'model_router_tool_run',
1769
- description: 'Run one supported official model CLI in the current Harness session workspace. Claude, Codex, MiMo and Grok support read-only; Kimi, MiniMax and ZCode require workspace-write. Write mode needs approval and a clean Git repository. Optional provider/model must match a configured Harness route and selected vendor; cliModel can specify that vendor CLI’s own configured model name. Without cliModel, Kimi, MiniMax, MiMo, Grok and ZCode use the CLI default.',
1953
+ description: 'Run one supported official model CLI in the current Harness session workspace. Claude, Codex, MiMo and Grok support read-only; Kimi, MiniMax and ZCode require workspace-write. Write mode needs approval and a clean Git repository. Optional provider/model must match a configured Harness route and selected vendor; cliModel can specify that vendor CLI’s own configured model name. Every CLI uses its own default unless an explicit cliModel or saved route mapping is configured.',
1770
1954
  parameters: {
1771
1955
  tool: { type: 'string', required: true, description: 'Fixed registry tool id, e.g. claude-code or codex.' },
1772
1956
  task: { type: 'string', required: true, description: 'Concrete task for the official CLI model.' },
@@ -1867,7 +2051,7 @@ function registerOfficialToolModels(ctx, config, assertInstallPreferences) {
1867
2051
  // The pre-execute hook already asked the user about file edits for editable reruns.
1868
2052
  const result = await rerunRecordedStep(ctx, config, {
1869
2053
  runId: args.runId, packageId: args.packageId, provider: args.provider, model: args.model,
1870
- confirmOverBudget: args.confirmOverBudget === true, confirmWrite: true, workspace: cwd, root, signal: exec.signal,
2054
+ confirmOverBudget: args.confirmOverBudget === true, confirmWrite: true, confirmUnsandboxed: stored?.executionEnvironment === 'cli-native', workspace: cwd, root, signal: exec.signal,
1871
2055
  ...(text(args.subscriptionChoice) ? { subscriptionChoice: text(args.subscriptionChoice) } : {}),
1872
2056
  })
1873
2057
  return jsonValue({
@@ -1894,10 +2078,11 @@ function registerOfficialToolModels(ctx, config, assertInstallPreferences) {
1894
2078
  }))
1895
2079
  ctx.tools.register(defineTool({
1896
2080
  name: 'model_router_team_execute',
1897
- description: 'Plan a complex task into dependent work packages, route among configured providers with ready official CLIs, and run each package sequentially. Claude/Codex request the planned model ID; other CLIs use their configured default unless cliModelsJson supplies exact CLI names. Editable runs use one isolated Git worktree and integrate source changes after CLI success. Confirm the actual model from vendor records.',
2081
+ description: 'Plan a complex task into dependent work packages, route among configured providers with ready official CLIs, and run each package sequentially. Every CLI uses its own default unless a saved cliModel mapping or cliModelsJson supplies exact CLI names. Editable runs use one isolated Git worktree and integrate source changes after CLI success. Confirm the actual model from vendor records.',
1898
2082
  parameters: {
1899
2083
  task: { type: 'string', required: true, description: 'Full task to plan, distribute and execute.' },
1900
2084
  mode: { type: 'string', enum: ['read-only', 'workspace-write'], description: 'Default read-only; workspace-write needs a clean Git repository and approval.' },
2085
+ executionEnvironment: { type: 'string', enum: ['harness', 'cli-native'], description: 'Default harness. cli-native requires workspace-write and separate approval: normal vendor tools run with current user permissions outside Harness sandbox; Git isolation only collects changes.' },
1901
2086
  budgetUsd: { type: 'number', description: 'Estimated planning ceiling only, not a vendor billing limit.' },
1902
2087
  cliModelsJson: { type: 'string', description: 'Optional JSON object mapping official tool IDs or work package IDs to exact model names configured in those CLIs. MiniMax/MiMo require provider/model; ZCode cannot switch per call.' },
1903
2088
  confirmOverBudget: { type: 'boolean', description: 'Set only after the user agreed to exceed the daily/monthly budget. Triggers an approval prompt.' },
@@ -1907,51 +2092,11 @@ function registerOfficialToolModels(ctx, config, assertInstallPreferences) {
1907
2092
  const { cwd, root, sandboxMode } = await sessionWorkspace(ctx, exec)
1908
2093
  const mode = args.mode === 'workspace-write' ? 'workspace-write' : 'read-only'
1909
2094
  if (mode === 'workspace-write' && sandboxMode === 'read-only') throw new Error('当前 Harness 会话为只读模式,不能请求可编辑团队执行')
1910
- const saved = await routingSavedState(config)
1911
- const [routes, installed] = await Promise.all([routesWithLearning(ctx, config, exec.signal, saved.runs, saved.dynamicData, saved.routingLearning), installedToolIds()])
1912
- const readiness = await Promise.all(installed.map(id => officialToolReadiness(id, cwd)))
1913
- const supported = new Set(readiness.filter(item => item.ready).map(item => item.id))
1914
- const capabilities = new Map(officialToolExecutionCapabilities().map(item => [item.id, item]))
1915
- const executableRoutes = routes.filter(route => {
1916
- const tool = toolForProvider(route.provider)
1917
- return tool && installed.includes(tool.id) && supported.has(tool.id)
1918
- && capabilities.get(tool.id)?.modes?.includes(mode)
1919
- })
1920
- if (executableRoutes.length === 0) return jsonValue({ status: 'blocked',
1921
- reason: `官方模型目录中没有同时满足已配置路线、已安装 CLI、托管执行适配器与 ${mode} 模式的供应商;Kimi、MiniMax、ZCode 仅支持经审批的 workspace-write。`,
1922
- installed, executionCapabilities: officialToolExecutionCapabilities(), executionReadiness: readiness })
1923
- const configuredBudget = valueOf(config, 'budgetUsd', DEFAULT_ROUTER_SETTINGS.budgetUsd)
1924
- const budgetUsd = Math.max(0, finiteNumber(args.budgetUsd, finiteNumber(configuredBudget, 0)))
1925
- const plan = createPlanFromRoutes(args.task, executableRoutes, {
1926
- ...planDataOptions(saved, config),
1927
- mode: 'team', budgetUsd, installedToolIds: installed,
1928
- runnableToolIds: installed.filter(id => supported.has(id)),
1929
- preset: routingPresetOf(config),
1930
- })
1931
- const budget = budgetFor(config, await savedState(), plan.estimatedCost)
1932
- if (budget.exceeded && args.confirmOverBudget !== true) {
1933
- return jsonValue({ status: 'paused-budget', budget, plan,
1934
- notice: `${budget.message} 已暂停团队执行。请向用户确认后以 confirmOverBudget: true 重新调用。` })
1935
- }
1936
- const bindingsText = text(args.cliModelsJson)
1937
- if (bindingsText.length > 4_000) throw new Error('cliModelsJson 超过 4000 字符上限')
1938
- let cliModels = null
1939
- if (bindingsText) {
1940
- try { cliModels = JSON.parse(bindingsText) }
1941
- catch { throw new Error('cliModelsJson 不是有效的 JSON 对象') }
1942
- }
1943
- cliModels = resolveTeamCliModelBindings(plan, executableRoutes, cliModels ?? {})
1944
- const startedAt = Date.now()
1945
- const execution = await runOfficialTeam({ plan, task: args.task, workspace: cwd,
1946
- allowedRoot: root, mode, installedIds: installed, cliModels, signal: exec.signal, sandbox: ctx.sandbox })
1947
- const recorded = await recordTeamRun({ task: args.task, plan, execution, mode, workspace: cwd,
1948
- routes: executableRoutes, cliModels, budget, startedAt, config })
1949
- return jsonValue({ plan, execution, runId: recorded?.id ?? null,
1950
- rerunNotice: mode === 'read-only'
1951
- ? '失败的工作包可用 model_router_rerun_step 单步重跑(只重跑该步及其未完成的下游)。'
1952
- : '若在某个步骤失败而停止,可用 model_router_rerun_step 在新的独立工作树中套用之前的改动后续跑(需审批);已整合或待人工整合的运行不能单步重跑。',
1953
- modelNotice: '已配置 cliModel 的路线按工作包传给官方 CLI;Claude/Codex 在未配置映射时请求 Harness 模型 ID。其他厂商无映射时使用 CLI 默认模型。多数 CLI 尚不返回可核验的实际模型 ID,须以厂商运行记录核对。',
1954
- billingNotice: 'budgetUsd 仅影响估算与路由,无法限制官方 CLI 账号实际费用。' })
2095
+ const planned = await planOfficialTeam(ctx, config, { ...args, mode }, { workspace: cwd, signal: exec.signal })
2096
+ return jsonValue(await executeOfficialTeamPlan(ctx, config, planned, {
2097
+ workspace: cwd, root, signal: exec.signal, confirmOverBudget: args.confirmOverBudget === true,
2098
+ confirmUnsandboxed: args.executionEnvironment === 'cli-native',
2099
+ }))
1955
2100
  },
1956
2101
  }))
1957
2102
  }