codebee 0.1.97 → 0.1.99

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/CHANGELOG.md CHANGED
@@ -4,9 +4,19 @@ CodeBee 的用户可感知变更记录。发布新版时:最上面加一节,
4
4
  `<!-- relnotes:start -->…<!-- relnotes:end -->` 段(那段会被 `npm view` 的
5
5
  README 元数据带回,供老版本在「发现新版本」时展示新版更新内容)。
6
6
 
7
- ## 未发布
7
+ ## v0.1.99(2026-10-09)
8
8
 
9
- - **蜂巢「3D 场景」重做为真 3D 蜜蜂办公室(WebGL)**:此前的 3D 只是把平面卡片整体斜切(CSS 透视),现换成零依赖 WebGL2 引擎渲染的蜜蜂办公室——每个流水线阶段是一排工位,每个步骤是一只坐在桌前对着显示器的低多边形蜜蜂:运行中的屏幕亮蓝脉动+蜜蜂打字颤翅、完成绿屏、失败红屏+趴桌、排队黑屏打盹;蜂王戴皇冠坐讲台、始终面向当前在干活的工位,巡逻蜂平时绕场巡查、有格子跑起来就飞去悬停督工。左键拖动旋转/滚轮缩放/右键升降/双击空白复位,空闲 8 秒缓慢自转(尊重系统减弱动效设置);悬停工位出气泡卡(角色/模型/实时输出/思考行),点按直接开日志抽屉;换肤与明暗主题自动跟随;WebGL 不可用或上下文丢失自动回落 2D 列表并记忆视图偏好。
9
+ - **直连对话「本轮失败 NameError」修复**:内置智能体(CodeBee 直连)每轮起跑数秒即失败,根因是沙箱加固提交把 `sandbox=task.get("sandbox")` 误引入无 task 上下文的内置步骤函数——实参求值即 NameError,Mac/Windows 全平台直连对话不可用。现改为显式传参,任务沙箱策略照常透传生效(回归测试锁定形参与全链)
10
+ - **命令执行沙箱闸策略感知**:沙箱加固曾把内置智能体的 run_command 在 Windows/macOS 一律拒跑(「未接入文件系统隔离后端」)——无后端平台上命令工具全死。现与外部 CLI 闸同口径:默认宽策略照常全信任直跑(2026-09-22 拍板档位),进程内无法兑现的收窄仍拒(禁网需 OS 后端;环境变量白名单在子进程侧实际滤不掉,宁拒不假装);Linux bubblewrap 全策略强制不变
11
+ - **大屏 KPI 长数值不再裁字**:万位级数值(如「2246.7万」)在 KPI 卡被挤到第二行、遭 overflow 裁切只剩半截;现 KPI 数字禁止换行,并按内容长度三档自动降字号,长数值单行完整显示
12
+ - **经验库设置页文案对齐**:注入范围说明「小说类任务」改「同类任务」,与知识注入按任务类型全量覆盖的实现口径一致
13
+
14
+ ## v0.1.98(2026-10-09)
15
+
16
+ - **连载起草误拒修复(沙箱闸策略感知)**:0.1.97 起任务创建强制落默认执行策略,沙箱闸把非 codex 外部 CLI(claude-code/opencode/kimi-code 等)一律拒跑——89—96 批连载起草整批被拒死、连载停摆。现改为策略感知:默认策略(工作目录白名单+允许网络+无禁用)与外部 CLI 天然权限一致,放行;仅当任务带收窄策略(禁网/禁工具/环境变量白名单/目录白名单不含工作目录)时仍拒绝(fail-closed)。codex 分支语义原样保留
17
+ - **蜂巢「3D 场景」重做为真 3D 蜜蜂办公室(WebGL)**:此前的 3D 只是把平面卡片整体斜切(CSS 透视),现换成零依赖 WebGL2 引擎渲染的蜜蜂办公室——每个流水线阶段是一排工位,每个步骤是一只坐在桌前对着显示器的低多边形蜜蜂:运行中的屏幕亮蓝脉动+蜜蜂打字颤翅、完成绿屏、失败红屏+趴桌、排队黑屏打盹;蜂王戴皇冠坐讲台、始终面向当前在干活的工位,巡逻蜂平时绕场巡查、有格子跑起来就飞去悬停督工。左键拖动旋转/滚轮缩放/右键升降/双击空白复位,空闲 8 秒缓慢自转(尊重系统减弱动效设置);悬停工位出气泡卡(角色/模型/实时输出/思考行),点按直接开日志抽屉;换肤与明暗主题自动跟随;WebGL 不可用或上下文丢失自动回落 2D 列表并记忆视图偏好
18
+ - **uv 缺失自动补装**:管理页 uv 系安装命令遇到 uv 缺失,从「人话指引」升级为自动补装(Windows 先 winget 后 pip、Mac 先 brew 后官方脚本,全部免 sudo 非交互),补装成功直接接管安装;uv 全家桶命令挂国内镜像兜底
19
+ - **uv 系安装链 Mac 三防**:PATH 快照落点兜底(~/.local/bin 等)+ uv 缺失人话短路 + winget 头识别放宽
10
20
 
11
21
  ## v0.1.97(2026-10-09)
12
22
 
package/README.md CHANGED
@@ -25,10 +25,12 @@ Trae Agent、OpenClaw……),提供
25
25
  ---
26
26
 
27
27
  <!-- relnotes:start -->
28
- ### 最新版更新内容(v0.1.97)
28
+ ### 最新版更新内容(v0.1.99)
29
29
 
30
- - 知识注入扩展到全部任务类型:引擎经验库/知识库全量覆盖,不再只有连载链受益
31
- - 命中计数不重复累加,修复「从未被读到反而升权」的反馈回路缺陷
30
+ - 修复直连对话每轮起跑即「本轮失败 NameError」:Mac/Windows 全平台直连对话恢复可用
31
+ - 内置智能体命令执行沙箱闸策略感知:默认策略照常直跑,仅收窄策略仍拒绝
32
+ - 大屏 KPI 长数值不再裁字:禁换行 + 按内容长度三档降字号
33
+ - 经验库设置页注入范围文案与全类型实现口径对齐
32
34
  <!-- relnotes:end -->
33
35
 
34
36
 
@@ -98,15 +98,15 @@ def resolve(provider_id="", model="", difficulty="default"):
98
98
  if selected not in enabled_names:
99
99
  return None
100
100
  if not selected:
101
- selected = (p.get("model_hard") if difficulty == "hard" else
102
- p.get("model_easy") if difficulty == "easy" else
103
- p.get("model") or "").strip()
104
- if not selected:
101
+ # 默认/难度模型键位与 models[] 启停开关不联动:键位指向已停用
102
+ # 模型时对账启用名单回落,不冒充可用(停用的模型不准用)
105
103
  try:
106
- names = modelhub._enabled_models(p)
104
+ selected = modelhub._usable_default_model(
105
+ p, (p.get("model_hard") if difficulty == "hard" else
106
+ p.get("model_easy") if difficulty == "easy" else
107
+ p.get("model")) or "")
107
108
  except Exception:
108
- names = []
109
- selected = names[0]["name"] if names else ""
109
+ selected = (p.get("model") or "").strip()
110
110
  if selected:
111
111
  return {"prov": p, "model": selected,
112
112
  "provider_id": p.get("id") or "",
@@ -125,15 +125,13 @@ def resolve(provider_id="", model="", difficulty="default"):
125
125
  for p in modelhub.providers():
126
126
  if not p.get("enabled", True) or not p.get("api_key"):
127
127
  continue
128
- m = ((p.get("model_hard") if difficulty == "hard" else
129
- p.get("model_easy") if difficulty == "easy" else
130
- p.get("model")) or "").strip()
131
- if not m:
132
- try:
133
- names = modelhub._enabled_models(p)
134
- except Exception:
135
- names = []
136
- m = names[0]["name"] if names else ""
128
+ try:
129
+ m = modelhub._usable_default_model(
130
+ p, (p.get("model_hard") if difficulty == "hard" else
131
+ p.get("model_easy") if difficulty == "easy" else
132
+ p.get("model")) or "")
133
+ except Exception:
134
+ m = (p.get("model") or "").strip()
137
135
  if m:
138
136
  return {"prov": p, "model": m, "provider_id": p.get("id") or "",
139
137
  "provider_name": p.get("name") or p.get("id") or ""}
@@ -422,7 +420,11 @@ def _tool_run_command(workdir, args, cancel_event=None, deadline=None, sandbox=N
422
420
  → cmd /c 或 /bin/sh -c),仓库唯一的 shell 落点,不另起炉灶。全信任档
423
421
  (用户拍板 2026-09-22):无白名单无确认闸,与 codex/claude 全权沙箱同档。
424
422
  提权靠模型自己包 Start-Process -Verb RunAs——UAC 必须人点,绕不过也不该绕。
425
- 输出按字符头尾截断(解码已在 run_process 完成,无残缺多字节问题)。"""
423
+ 输出按字符头尾截断(解码已在 run_process 完成,无残缺多字节问题)。
424
+ 沙箱闸策略感知(与外部 CLI 闸 b267107 同口径):Linux 有 bwrap 全策略
425
+ 强制;Win/Mac 无 OS 后端,进程内无法兑现的收窄仍拒(禁网、env 白名单
426
+ ——run_process 的 env 是注入语义滤不掉),默认宽策略保持全信任档直跑,
427
+ 目录闸由 path_allowed 兑现。"""
426
428
  cmdline = str(args.get("command") or "").strip()
427
429
  if not cmdline:
428
430
  return "(命令为空,未执行)"
@@ -432,15 +434,20 @@ def _tool_run_command(workdir, args, cancel_event=None, deadline=None, sandbox=N
432
434
  cwd = os.path.abspath(workdir or ".")
433
435
  if not policy.path_allowed(cwd, sandbox):
434
436
  return "(沙箱拒绝:工作目录不在允许范围内,命令未执行)"
435
- # This process runner has no OS-level network/filesystem namespace. Do not
436
- # imply network isolation when policy requests it; fail closed instead.
437
- if sandbox.get("network") is False:
438
- if os.name != "posix" or not shutil.which("bwrap"):
439
- return "(沙箱拒绝:当前系统没有可用的网络隔离后端,命令未执行)"
440
- # Enforce the declared filesystem boundary at the process boundary too.
441
- # bubblewrap is the supported Linux backend. Windows/macOS without an
442
- # equivalent backend fail closed rather than pretending cwd is a sandbox.
443
- if os.name == "posix" and shutil.which("bwrap"):
437
+ # Enforce the declared boundary at the process boundary where an OS
438
+ # backend exists (bubblewrap on Linux). Windows/macOS have no equivalent:
439
+ # stay policy-aware like the external-CLI gate (b267107) — the narrowings
440
+ # this in-process runner cannot honor fail closed, while a default wide
441
+ # policy keeps the user-approved full-trust tier (2026-09-22) instead of
442
+ # refusing every command.
443
+ has_bwrap = os.name == "posix" and bool(shutil.which("bwrap"))
444
+ if sandbox.get("network") is False and not has_bwrap:
445
+ return "(沙箱拒绝:当前系统没有可用的网络隔离后端,命令未执行)"
446
+ if sandbox.get("env_allowlist"):
447
+ # run_process 的 env 是注入语义(合并进完整父环境,供凭据注入),
448
+ # 不是白名单替换——环境收窄在子进程侧无法兑现,宁拒不假装
449
+ return "(沙箱拒绝:该任务带环境变量白名单,命令执行无法兑现该策略,未执行)"
450
+ if has_bwrap:
444
451
  argv = ["bwrap", "--die-with-parent", "--new-session", "--tmpfs", "/"]
445
452
  # Mount only the runtime needed by ordinary commands. Do not expose the
446
453
  # entire host root read-only: read access can still leak user secrets.
@@ -461,18 +468,10 @@ def _tool_run_command(workdir, args, cancel_event=None, deadline=None, sandbox=N
461
468
  argv.extend(["--dir", root_path])
462
469
  argv.extend(["--bind", root_path, root_path])
463
470
  argv.extend(["--chdir", cwd, "--", "/bin/sh", "-lc", cmdline])
464
- elif os.name == "posix" and sys.platform == "darwin":
465
- return "(沙箱拒绝:当前 macOS 执行器尚未接入文件系统隔离后端,命令未执行)"
466
- elif os.name != "posix":
467
- return "(沙箱拒绝:当前 Windows 执行器尚未接入文件系统隔离后端,命令未执行)"
468
471
  else:
469
- return "(沙箱拒绝:未安装 bubblewrap 隔离后端,命令未执行)"
470
- env_policy = dict(sandbox)
471
- env_policy["env_allowlist"] = list(dict.fromkeys(
472
- list(sandbox.get("env_allowlist") or [])
473
- + ["PATH", "SYSTEMROOT", "WINDIR", "TEMP", "TMP", "COMSPEC", "HOME", "USERPROFILE"]))
474
- r = runner.run_process(argv=argv, cwd=cwd,
475
- env=policy.filter_env(os.environ, env_policy),
472
+ argv = None # 全信任档:runner 按平台自包 cmd /c 或 /bin/sh -c
473
+ r = runner.run_process(argv=argv, shell_cmd=cmdline if argv is None else None,
474
+ cwd=cwd, env=None,
476
475
  timeout=min(timeout, CMD_MAX_TIMEOUT),
477
476
  deadline=deadline, cancel_event=cancel_event)
478
477
  out = runner.clean_cli_text(
package/app/core/jobs.py CHANGED
@@ -1013,9 +1013,15 @@ def _do_mgmt(job, ev):
1013
1013
  # 修复智能体面对文件锁只会给出 taskkill 全杀 node 之类白名单必拒的危险
1014
1014
  # 命令,白白烧一轮 300s 诊断(2026-09-18 dsh 同版本重装 EBUSY 案)
1015
1015
  lock_hit = (not ok and op in ("install", "upgrade") and _file_lock_error(res))
1016
+ # uv 缺失同跳 AI 修复:修复链就算装上 uv,复检目标 CLI 仍未装,结论还是
1017
+ # 失败,白烧一轮;人话指路「先装 uv 再点安装」一步到位(2026-10-09 Mac
1018
+ # aider/trae 双装失败实案)
1019
+ uv_missing = bool(res.get("uv_missing"))
1016
1020
  if lock_hit:
1017
1021
  summary = ("失败:安装文件被占用(可能有同名程序在运行,或杀毒软件正在扫描),"
1018
1022
  "请关闭占用该文件的程序后重试。完整输出见日志。")
1023
+ elif uv_missing:
1024
+ summary = "失败:" + str(res.get("error") or "")[:300]
1019
1025
  else:
1020
1026
  # note 优先:良性收口(如 winget「已装无可升级」复检在装)给用户
1021
1027
  # 看原因,别让「完成」两个字孤零零不带解释
@@ -1026,7 +1032,7 @@ def _do_mgmt(job, ev):
1026
1032
  summary=summary,
1027
1033
  exit_code=res.get("exit_code"))
1028
1034
  # AI 修复只针对安装类失败;卸载失败多为权限/程序占用,留给用户看日志处理
1029
- if not ok and op in ("install", "upgrade") and not lock_hit:
1035
+ if not ok and op in ("install", "upgrade") and not lock_hit and not uv_missing:
1030
1036
  ok = _ai_repair(run_id, entry, ev, entry.get(op), log_abs)
1031
1037
  elif op == "smoke":
1032
1038
  from . import runner as _r
@@ -1129,6 +1135,7 @@ def _ai_repair(run_id, entry, ev, failed_cmd, orig_log):
1129
1135
  "npm: %s" % (shutil.which("npm") or "缺失"),
1130
1136
  "pnpm: %s" % (shutil.which("pnpm") or "缺失"),
1131
1137
  "python: %s" % (shutil.which("python3" if not is_win else "python") or "缺失"),
1138
+ "uv: %s" % (shutil.which("uv") or "缺失"),
1132
1139
  ]
1133
1140
  if not is_win:
1134
1141
  env_lines.append("brew: %s" % (shutil.which("brew") or "缺失"))
@@ -91,19 +91,21 @@ def _safe_config_path(raw):
91
91
  # ---------------------------------------------------------------- 检测
92
92
 
93
93
  def _which_cli(cli):
94
- """PATH 找 CLI;Windows 上再兜底 winget 便携包两处标准落点。
95
-
96
- winget 装的 CLI(如 Claude Code)把 exe 放进
97
- %LOCALAPPDATA%\\Microsoft\\WinGet\\Packages\\<PkgId>_*\\,并把该目录写进
98
- 注册表用户 PATH——注册表更新后已在跑的进程(本服务)不继承,shutil.which
99
- 落空 → 卡片「未安装」假象,点安装又被 winget「已装无可升级」非零退出拒掉
100
- (2026-09-30 实案)。Links 是便携包符号链接标准目录,Packages 按
101
- <cli>.exe 通配兜另一形态;两条都只兜 which 落空的场景,零额外开销。"""
94
+ """PATH 找 CLI;落空再按平台兜底磁盘已知安装位。
95
+
96
+ Windows 兜 winget 便携包两处标准落点:winget 装的 CLI(如 Claude Code)
97
+ 把 exe 放进 %LOCALAPPDATA%\\Microsoft\\WinGet\\Packages\\<PkgId>_*\\,并把
98
+ 该目录写进注册表用户 PATH——注册表更新后已在跑的进程(本服务)不继承,
99
+ shutil.which 落空 → 卡片「未安装」假象,点安装又被 winget「已装无可升级」
100
+ 非零退出拒掉(2026-09-30 实案)。Links 是便携包符号链接标准目录,Packages
101
+ 按 <cli>.exe 通配兜另一形态。POSIX 兜 runner 同款三落点(~/.local/bin 与
102
+ brew bin,Mac 服务吃不到 shell rc 里的 PATH 时同样假「未安装」)。
103
+ 两条都只兜 which 落空的场景,零额外开销。"""
102
104
  path = shutil.which(cli)
103
105
  if path:
104
106
  return path
105
107
  if os.name != "nt":
106
- return None
108
+ return runner._fallback_probe_cli(cli)
107
109
  local = os.environ.get("LOCALAPPDATA")
108
110
  if not local:
109
111
  return None
@@ -1974,7 +1976,10 @@ def _winget_already_ok(cmd, op, res):
1974
1976
  还要复检 installed 才按成功收口。"""
1975
1977
  if res.get("ok") or op not in ("install", "upgrade"):
1976
1978
  return False
1977
- if not str(cmd or "").strip().lower().startswith("winget"):
1979
+ # 首 token 可能已被 _resolve_mgmt_tool 换成绝对路径(PATH 快照落空兜底),
1980
+ # 所以按 basename 认 winget,别用 startswith
1981
+ head = str(cmd or "").strip().split()[0] if str(cmd or "").strip() else ""
1982
+ if os.path.basename(head).lower().rstrip(".exe") != "winget":
1978
1983
  return False
1979
1984
  code = res.get("exit_code")
1980
1985
  try:
@@ -1986,6 +1991,85 @@ def _winget_already_ok(cmd, op, res):
1986
1991
  return any(m in out for m in _WINGET_NOOP_MARKERS)
1987
1992
 
1988
1993
 
1994
+ def _uv_install_hint():
1995
+ if sys.platform == "darwin":
1996
+ return "brew install uv(或官网 curl -LsSf https://astral.sh/uv/install.sh | sh)"
1997
+ return "winget install astral-sh.uv,或 pip install uv"
1998
+
1999
+
2000
+ def _uv_mirror_env():
2001
+ """uv 系命令的镜像兜底 env(用户没自己配才补默认,绝不覆盖)。
2002
+
2003
+ uv tool install --python 3.12 会现场下载 CPython 独立构建(GitHub 直连,
2004
+ 国内常断流),包体走 PyPI——npmmirror/清华这对镜像是 2026-09 本机 uv
2005
+ 实装验证过的组合。"""
2006
+ env = {}
2007
+ if not os.environ.get("UV_PYTHON_INSTALL_MIRROR"):
2008
+ env["UV_PYTHON_INSTALL_MIRROR"] = (
2009
+ "https://registry.npmmirror.com/-/binary/python-build-standalone")
2010
+ if not os.environ.get("UV_DEFAULT_INDEX"):
2011
+ env["UV_DEFAULT_INDEX"] = "https://pypi.tuna.tsinghua.edu.cn/simple"
2012
+ return env
2013
+
2014
+
2015
+ def _cmd_head_is_uv(cmd):
2016
+ stripped = str(cmd or "").strip()
2017
+ if not stripped:
2018
+ return False
2019
+ return os.path.basename(stripped.split()[0]).lower() in ("uv", "uv.exe")
2020
+
2021
+
2022
+ def _bootstrap_uv(cancel_event=None, log_path=None):
2023
+ """uv 不在本机时自动补装一个——安装按钮「点了就该装上」的契约兜底。
2024
+
2025
+ 按平台选渠道,全部免 sudo 非交互:POSIX 先 brew(在才用)后官方 curl
2026
+ 脚本(落 ~/.local/bin,探测表可寻址);Windows 先 winget 后 pip。装完
2027
+ 不指望 PATH 快照自愈——调用方重跑 _resolve_mgmt_tool 用绝对路径接管。
2028
+ 返回 {ok, detail};失败 detail 列出试过的渠道与结局,供人话收口。"""
2029
+ candidates = []
2030
+ if sys.platform == "win32":
2031
+ if shutil.which("winget"):
2032
+ candidates.append(("winget", "winget install -e --id astral-sh.uv --silent"))
2033
+ if shutil.which("py"):
2034
+ candidates.append(("pip(py)", "py -3 -m pip install uv"))
2035
+ elif shutil.which("python"):
2036
+ candidates.append(("pip", "python -m pip install uv"))
2037
+ else:
2038
+ if shutil.which("brew"):
2039
+ candidates.append(("brew", "brew install uv"))
2040
+ if shutil.which("curl"):
2041
+ candidates.append(("curl", "curl -LsSf https://astral.sh/uv/install.sh | sh"))
2042
+ attempts = []
2043
+ for label, cmd in candidates:
2044
+ res = runner.run_process(shell_cmd=cmd, cwd=str(paths.ROOT),
2045
+ timeout=900, cancel_event=cancel_event,
2046
+ log_path=log_path)
2047
+ if res["ok"]:
2048
+ return {"ok": True, "detail": "已自动补装 uv(%s)" % label}
2049
+ attempts.append("%s 退出码 %s" % (label, res.get("exit_code")))
2050
+ return {"ok": False,
2051
+ "detail": "、".join(attempts) or "本机没有 brew/curl/winget/pip 任一渠道"}
2052
+
2053
+
2054
+ def _resolve_mgmt_tool(cmd, op):
2055
+ """install/upgrade 命令首 token 的工具本体解析。
2056
+
2057
+ 服务 PATH 是启动快照,本机后装的 uv/npm 不在快照里时 shell 报
2058
+ command not found 退出码 127,零信息量。磁盘已知落点能探到就把首 token
2059
+ 原地换成绝对路径(装完即用,不必重启服务);探不到且是 uv 系安装命令
2060
+ 则返回 uv_missing=True——本机连 uv 都没有,包无从装起,人话报错并跳过
2061
+ AI 修复(修复链装上 uv 后复检目标 CLI 仍未装,白烧一轮诊断)。
2062
+ 返回 (替换后的命令, uv_missing)。"""
2063
+ stripped = str(cmd or "").strip()
2064
+ first = stripped.split()[0] if stripped else ""
2065
+ if not first or os.path.isabs(first) or shutil.which(first):
2066
+ return cmd, False
2067
+ found = runner._fallback_probe_cli(first)
2068
+ if found:
2069
+ return found + stripped[len(first):], False
2070
+ return cmd, (first == "uv" and op in ("install", "upgrade"))
2071
+
2072
+
1989
2073
  def run_mgmt_command(entry, op, cancel_event=None, log_path=None):
1990
2074
  """执行 install/upgrade/uninstall 命令(由任务执行器异步运行,日志实时落盘)。"""
1991
2075
  if op == "uninstall":
@@ -2000,8 +2084,22 @@ def run_mgmt_command(entry, op, cancel_event=None, log_path=None):
2000
2084
  return {"ok": False,
2001
2085
  "error": "未配置 %s 命令:请在 data/catalog.json 的 \"%s\" 里补充,或用官方渠道安装"
2002
2086
  % (op, entry["id"])}
2087
+ cmd, uv_missing = _resolve_mgmt_tool(cmd, op)
2088
+ if uv_missing:
2089
+ # 「提供安装就该能装上」:uv 不在本机先自动补装,装 uv 不该是用户
2090
+ # 操心的事;真补不上才退回人话指引(2026-10-09 Mac 双装失败实案)
2091
+ boot = _bootstrap_uv(cancel_event=cancel_event, log_path=log_path)
2092
+ if boot["ok"]:
2093
+ cmd, uv_missing = _resolve_mgmt_tool(cmd, op)
2094
+ if uv_missing:
2095
+ return {"ok": False, "uv_missing": True,
2096
+ "error": "自动安装 uv 未成功(%s)。请手动安装:%s;"
2097
+ "装完重启 CodeBee,再点一次安装。"
2098
+ % (boot["detail"], _uv_install_hint())}
2099
+ # uv 全家桶命令挂镜像兜底:Python 独立构建/包索引国内直连常断(只补缺不覆盖)
2003
2100
  res = runner.run_process(shell_cmd=cmd, cwd=str(paths.ROOT),
2004
- timeout=1800, cancel_event=cancel_event, log_path=log_path)
2101
+ timeout=1800, cancel_event=cancel_event, log_path=log_path,
2102
+ env=_uv_mirror_env() if _cmd_head_is_uv(cmd) else None)
2005
2103
  winget_noop = _winget_already_ok(cmd, op, res)
2006
2104
  detect_all(force=True)
2007
2105
  with _LOCK:
@@ -2177,6 +2177,20 @@ def _model_bindable(prov, model):
2177
2177
  return True
2178
2178
 
2179
2179
 
2180
+ def _usable_default_model(prov, model=""):
2181
+ """默认模型键位(model / model_easy / model_hard / 编排者配置)对账启用名单。
2182
+
2183
+ 默认/难度模型是自由文本,与 models[] 的启停开关互不联动——键位指向已
2184
+ 停用/已删除的模型时不能冒充可用:回落该厂商启用名单首个;全停用返回
2185
+ 空串(调用方按「无默认模型」处理)。空键位本身合法(用网关默认),
2186
+ 也返回空。"""
2187
+ model = (model or "").strip()
2188
+ if model and _model_bindable(prov, model):
2189
+ return model
2190
+ names = [m.get("name") for m in _enabled_models(prov) if m.get("name")]
2191
+ return names[0] if names else ""
2192
+
2193
+
2180
2194
  def _model_image_in(prov, model):
2181
2195
  """模型是否声明支持图片输入:models[] 条目 image_in;缺省/查不到=False(纯文本)。"""
2182
2196
  for m in ((prov or {}).get("models") or []):
@@ -2591,7 +2605,7 @@ def resolve_binding(agent_kind_or_id, difficulty="default"):
2591
2605
  if len(entries) >= MAX_CHAIN_ATTEMPTS:
2592
2606
  break
2593
2607
  entries.append(_chain_entry_env(
2594
- prov, model or prov.get("model") or "",
2608
+ prov, model or _usable_default_model(prov, prov.get("model")),
2595
2609
  target=agent_kind_or_id, endpoint=ep, key=kk["key"],
2596
2610
  key_id=kk.get("id") or "", provider_id=pid))
2597
2611
  if len(entries) >= MAX_CHAIN_ATTEMPTS:
@@ -2662,6 +2676,8 @@ def resolve_binding(agent_kind_or_id, difficulty="default"):
2662
2676
  model = prov.get("model_" + tier) or "" if (routing and tier) else ""
2663
2677
  if not model:
2664
2678
  model = prov.get("model") or ""
2679
+ if model and not _model_bindable(prov, model):
2680
+ model = "" # 默认/难度模型键位指向停用或已删模型:不冒充可用,回落启用名单
2665
2681
  if not model and names:
2666
2682
  model = names[0] if (not routing or difficulty != "easy") else names[-1]
2667
2683
  if not _evaluation_is_usable(pid, model):
@@ -2724,7 +2740,9 @@ def recommend_binding(agent_kind_or_id, difficulty="default", task_type="", role
2724
2740
  names = [m.get("name") for m in _enabled_models(prov)
2725
2741
  if isinstance(m, dict) and m.get("name")]
2726
2742
  if not names and prov.get("model"):
2727
- names = [str(prov.get("model"))]
2743
+ dflt = str(prov.get("model"))
2744
+ if _model_bindable(prov, dflt): # 默认模型被停用时不再充作唯一候选
2745
+ names = [dflt]
2728
2746
  # 候选阶段不能先截前三个:便宜档往往排在供应商列表后面,easy 任务
2729
2747
  # 需要看到完整启用列表后再按成本与档位评分。最终调用链仍受上限约束。
2730
2748
  for model in names:
@@ -3618,9 +3636,7 @@ def resolve_orchestrator():
3618
3636
  if not prov or not prov.get("enabled", True) or not prov.get("api_key"):
3619
3637
  return None
3620
3638
  model = (cfg.get("model") or "").strip() or prov.get("model") or ""
3621
- if not model:
3622
- names = _enabled_models(prov)
3623
- model = names[0]["name"] if names else ""
3639
+ model = _usable_default_model(prov, model) # 配置的模型被停用时回落启用名单,不冒充可用
3624
3640
  if not model:
3625
3641
  return None
3626
3642
  if not _evaluation_is_usable(pid, model):
@@ -574,25 +574,38 @@ def _external_sandbox_block_reason(agent, workdir, sandbox):
574
574
  """Reject task policies that this CLI launcher cannot enforce.
575
575
 
576
576
  Codex is currently the only supported external runner with an OS-backed
577
- workspace write sandbox. Other CLI integrations have no equivalent boundary
578
- in this launcher and must use the in-process agent/tool executor instead.
577
+ workspace write sandbox. Other CLI integrations run with their natural
578
+ permission set (workdir read/write, network allowed): they are rejected
579
+ only when the task policy is *narrower* than that — fail closed for
580
+ narrowed policies, not for defaults(实案:create_task 改为强制落默认
581
+ 策略后,本闸把 89—96 批的连载起草整批拒成「没有经验证的 OS 文件隔
582
+ 离」;默认策略与外部 CLI 的天然权限一致,不属于收窄).
579
583
  """
580
584
  if (agent or {}).get("mode") == "mock":
581
585
  return ""
582
586
  from . import policy
583
- if (agent or {}).get("kind") != "codex":
584
- return "沙箱拒绝:该外部 CLI 没有经验证的 OS 文件隔离;请改用内置 Agent"
585
587
  root = os.path.realpath(workdir or ".")
586
588
  normalized = policy.normalize_sandbox(sandbox, root)
587
- roots = [os.path.realpath(x) for x in normalized.get("allowed_roots") or []]
589
+ if (agent or {}).get("kind") == "codex":
590
+ if normalized.get("network") is False:
591
+ return "沙箱拒绝:该 CLI 后端未接入网络隔离,拒绝运行网络受限任务"
592
+ if [os.path.realpath(x) for x in normalized.get("allowed_roots") or []] != [root]:
593
+ return "沙箱拒绝:该 CLI 后端未接入允许目录隔离,拒绝运行目录受限任务"
594
+ if normalized.get("disabled_tools"):
595
+ return "权限拒绝:该 CLI 后端无法执行 CodeBee 工具禁用策略,拒绝运行"
596
+ if normalized.get("env_allowlist"):
597
+ return "沙箱拒绝:该 CLI 后端未接入环境变量白名单,拒绝运行受限任务"
598
+ return ""
599
+ # 非 codex 外部 CLI:默认策略放行,收窄策略拒绝
588
600
  if normalized.get("network") is False:
589
- return "沙箱拒绝:该 CLI 后端未接入网络隔离,拒绝运行网络受限任务"
590
- if roots != [root]:
591
- return "沙箱拒绝:该 CLI 后端未接入允许目录隔离,拒绝运行目录受限任务"
601
+ return "沙箱拒绝:该任务要求禁网,外部 CLI 无法执行网络受限任务"
592
602
  if normalized.get("disabled_tools"):
593
- return "权限拒绝:该 CLI 后端无法执行 CodeBee 工具禁用策略,拒绝运行"
603
+ return "权限拒绝:该任务带工具禁用策略,外部 CLI 无法执行"
594
604
  if normalized.get("env_allowlist"):
595
- return "沙箱拒绝:该 CLI 后端未接入环境变量白名单,拒绝运行受限任务"
605
+ return "沙箱拒绝:该任务带环境变量白名单,外部 CLI 无法执行受限任务"
606
+ roots = [os.path.realpath(x) for x in normalized.get("allowed_roots") or []]
607
+ if roots and root not in roots:
608
+ return "沙箱拒绝:该任务目录白名单不含工作目录,外部 CLI 无法保证隔离范围"
596
609
  return ""
597
610
 
598
611
 
@@ -615,14 +628,16 @@ def _create_task_from_builtin(payload):
615
628
 
616
629
 
617
630
  def _run_builtin_step(run_id, role, bi, prompt, workdir, ev, note="", images=None,
618
- followups=False):
631
+ followups=False, sandbox=None):
619
632
  """内置智能体步骤:直连模型 API + 工具循环(builtin_agent),不经 CLI 进程。
620
633
 
621
634
  与 _run_step 对齐的三件事:暂停/取消闸门、运行中指令 drain 注入、重复调用
622
635
  守门;结果同样经 _finish_step_result 落步骤(output=干净回答)并入用量台账。
623
636
  日志只有「迭代/工具」摘要行——对话视图吃 output,日志抽屉看工具轨迹。
624
637
  followups=True 时从回答末尾解析「建议追问」块(直连对话专用协议):
625
- 剥离出结构化列表落步骤记录,正文保持干净。"""
638
+ 剥离出结构化列表落步骤记录,正文保持干净。
639
+ sandbox:任务沙箱策略由调用方透传(本函数无 task 上下文),builtin_agent
640
+ 内部 normalize 后在工具执行期强制。"""
626
641
  _wait_gate(run_id, ev)
627
642
  deadline = _ensure_budget(run_id)
628
643
  step, log_abs = store.add_step(run_id, role, "builtin", "CodeBee", note=note,
@@ -676,7 +691,7 @@ def _run_builtin_step(run_id, role, bi, prompt, workdir, ev, note="", images=Non
676
691
  on_reason=_on_reason, on_stream=_on_stream,
677
692
  on_activity=_on_activity,
678
693
  task_creator=_create_task_from_builtin,
679
- sandbox=task.get("sandbox"), checkpoint_run_id=run_id)
694
+ sandbox=sandbox, checkpoint_run_id=run_id)
680
695
  if followups and res.get("ok"):
681
696
  clean, fups = _parse_followups(res.get("text") or "")
682
697
  if fups:
@@ -2321,7 +2336,7 @@ def _run_direct(run, task, agents, ev, stats, mode):
2321
2336
  if bi is not None:
2322
2337
  res = _run_builtin_step(run_id, "direct" if first else "chat", bi, prompt,
2323
2338
  step_wd, ev=ev, note=note, images=images,
2324
- followups=True)
2339
+ followups=True, sandbox=task.get("sandbox"))
2325
2340
  else:
2326
2341
  res = _run_step(run_id, "direct" if first else "chat", impl, prompt, step_wd,
2327
2342
  readonly=False, ev=ev, note=note,