Feat/支持fnm nvm #663

Merged
k88936 merged 2 commits from feat/support-more-node into master 2026-10-07 13:08:51 +08:00
Member

close #648

close #648
k88936 added 2 commits 2026-10-07 11:33:39 +08:00
- 新增共享窄叶校验 validate_node_installation_prefix,只接受含 bin/node 与 lib/node_modules/npm/bin/npm-cli.js 的完整 Node 安装前缀,拒绝 HOME、管理器根、宽泛目录和逃逸 symlink
- 开发构建枚举 fnm node-versions/<ver>/installation 与 nvm versions/node/<ver>,按 .nvmrc / .node-version 权威 pin、PATH、engines.node 偏好、活动版本、默认别名、最高版本选择
- .nvmrc / .node-version 能理解但未安装时返回 node-version-pinned-not-installed 失败关闭;engines.node 与无法解析的 pin 不阻塞
- Linux 命令沙箱只读挂载通过校验的完整 Node 安装前缀,并在内联环境剔除 FNM_MULTISHELL_PATH
- Linux npm 改以受信任 node <npm-cli.js> 启动,npm install 联网判定跟随真实启动形态
- 非 Node 程序在 Linux 受信任 PATH 前置 Node 工具链 bin,npx / corepack 等随 node 同版本
- 同步技术方案 V1.11.2、实施计划切片、里程碑规范、decision-log 与 pitfalls
修复nvm默认别名并补真实nvm沙箱验证
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m41s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 7m1s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m46s
Project CI / Frontend tests (pull_request) Successful in 3m17s
Project CI / Backend tests (pull_request) Successful in 7m25s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m52s
Project CI / Repository checks (pull_request) Successful in 6m9s
Project CI / Native shell tests (pull_request) Successful in 8m28s
7ed3696959
- nvm 的 alias/default 只写主版本号(如 22),改为按同一 pin 子集在已安装版本里选最高匹配,避免按 (22,0,0) 精确匹配永远落空
- 新增 opt-in 真机测试 command_sandbox_real_linux_opt_in_runs_nvm_installation_prefix,在 bwrap 内跑真实 nvm 安装前缀的 node 与 npm
- 新增单测 nvm_major_only_default_alias_selects_the_installed_patch 守住主版本号别名
- 里程碑、decision-log 与 pitfalls 记录真实 nvm v0.40.8 + Node v22.23.3 验证与 default 别名口径
Author
Member
  • 1 environment_check.rs:565 lts/<codename> 被当成匹配任意版本

    • 原评审:.nvmrc 写 lts/iron 时,因为任意 lts/ 前缀都短路成 Some(true),会对每个已安装版本都“匹配”;而版本文件 pin 是权威的,于是静默选中最高安装版本(如 22)。建议区分 lts/* 与 lts/<codename>。
    • 现状:version_matches_pin 里 matches!(..., "lts/*") || lower.starts_with("lts/") 一起 return Some(true);select_pinned_installation 从 Some(true) 的候选里取最高版本,resolve_pinned_managed_runtime 对 Matched 直接采用。所以 lts/iron 确实会选中已安装最高版本,而不是 Node 20。
    • 判定:成立,属小范围修复。
    • 已改:lts/* 仍匹配任意;lts/<codename> 返回 None(按“未 pin”回退到 PATH / 回退链)。没有硬编码 codename → 版本行映射表(该表会过期,是另一个决策)。
    • 提交:d905fa1c0;单测覆盖 lts/iron 回退、lts/* 仍匹配,并把 lts/iron 加入“不支持 pin 回退”用例。
  • 2 command_exec.rs:2645 新增测试 npm_command_targets_node_plus_npm_cli_on_linux 非 hermetic

    • 原评审:该测试走 resolve_node_runtime,需要真实宿主 Node;在没有 Node 的 Linux CI runner 上 .unwrap() 会 panic。建议像其它真机测试一样早退。
    • 现状:测试确实依赖宿主 Node。但这不是本改动独有的:process_session/tests.rs 有约 10 处非 opt-in 测试同样 resolve_project_command_spec_at(..., "npm", ...) 并 unwrap(),整个 shell bin 的 Linux 单测早已依赖宿主 Node。
    • 判定:不成立(针对本仓库)。CI 的 Rust lane 由 npm run check:native-shells:agc-rust-shard-* 驱动,runner 必有 Node;只给这一条测试加早退,会把同一个依赖静默漏掉、降低信号,也与既有测试口径不一致。
    • 若你仍想要测试级安全网:把该测试改成 let Ok(spec) = ... else { return } 即可;但要连同 process_session 一起处理才有实际意义。
    • 提交:无(保持现状)。
  • 3 command_sandbox.rs:563 npm install 联网判定只看第一个参数的 basename(安全)

    • 原评审:Linux 上 command.exec 对非 node 程序的参数不做路径校验,node 本身只约束 args[1..];因此第一个参数可以是一个叫 npm-cli.js 的项目文件,后面跟 install,就会给非可信 npm CLI 打开 --share-net。建议绑定到 ProjectCommandSpec::node_launcher 的真实 npm_cli 路径。
    • 现状:command_sandbox_requests_npm_install(executable, arguments) 对非 npm 可执行文件仅做 arguments[0].file_name() == "npm-cli.js" + arguments[1] == "install";prepare_linux_command_sandbox_launch / build_linux_bwrap_launch 用它决定是否 --share-net。
    • 核实:成立且可达。Linux 的 project_command_validate_arguments 在 #[cfg(target_os = "linux")] 分支直接 Ok(())(外部路径 / 敏感路径校验只在非 Linux 生效),project_command_actual_arguments 在 Linux 也原样返回 spec.arguments。所以 program="node" + args=["/项目/game/npm-cli.js","install"] 会被判为联网;项目根可写,模型能创建这个文件。
    • 注意:原评审内联给的 .filter(|path| path.is_absolute()) 不够——绝对路径的项目内 npm-cli.js 仍会绕过,必须做前缀 / 身份校验。
    • 已按方案 1 落地:prepare_command_sandbox_launch 新增 trusted_npm_install: Option<(&Path, &Path)>,prepare_project_command_launch_spec 把 spec.node_launcher 的 (node, npm_cli) 透传给沙箱;command_sandbox_requests_npm_install 只在 executable、首个参数、install 子命令都与可信对逐字一致时返回 true,拿不到可信对时一律不放网;LinuxSandboxPlan 用 npm_install_network 承载结果,build_linux_bwrap_launch 直接采用。
    • 提交:f6e3e19cd(同步技术方案 V1.11.2、decision-log、pitfalls;全量 shell bin 单测 1972 passed,opt-in 真机 fnm 用例通过)。
  • 4 environment_check.rs:679 fnm default 别名

    • 原评审:fnm 把 default 存成内容为版本字符串(如 v20.11.0 / 20)的文件,不是路径 / 符号链接;validate_node_installation_prefix 会拒绝它,于是 default 别名被静默忽略、总回退到最高版本。建议像 nvm 分支那样读文件内容再 select_pinned_installation。
    • 核实:前提有误(至少 fnm 1.39.0)。实测 fnm/aliases/default 是符号链接:default -> /root/.local/share/fnm/node-versions/v22.23.3/installation(file 报 “symbolic link to ...”,readlink -f 落到 installation)。validate_node_installation_prefix 会 canonicalize 成 installation,bin/node 与 lib/node_modules/npm/bin/npm-cli.js 都在,校验通过,随后 installed.iter().find(prefix == ...) 命中——default 别名是生效的。
    • 判定:不成立。评审建议的“只读文件内容”反而会让这条路径失效:fs::read_to_string 对“指向目录的符号链接”会报错并跳过,等于把能用的 default 别名改坏。
    • 已做:不改生产逻辑;新增回归测试 fnm_default_alias_symlink_selects_the_linked_installation,断言 default 跟随符号链接选中 v20 而非最高的 v22。
    • 提交:6f4667d96。
    • 备注:若你确实在某平台见过 fnm 把 default 写成普通文件的形态(例如 Windows 无符号链接权限),告诉我,我再加“先按链接校验、失败再读文件内容”的双形态兼容;当前没有证据,先不加投机分支。
  • 5 environment_check.rs:658 FNM_MULTISHELL_PATH 检测不到活动 fnm 版本

    • 原评审:FNM_MULTISHELL_PATH 指向一堆符号链接二进制,不是含 bin/ + lib/node_modules/npm 的安装前缀;validate_node_installation_prefix 总是拒绝它,因此永远检测不到活动 fnm 版本。建议解析其中的 node 符号链接并上溯到安装前缀。
    • 核实:结论对一半,根因与建议都不对。
      • 根因错:fnm 1.39 的 multishell 目录是完整镜像,实测含 bin/node(普通文件)与 lib/node_modules/npm/bin/npm-cli.js,所以 validate_node_installation_prefix 是通过的,不是“总是拒绝”。
      • 建议不可行:bin/node 不是符号链接(ls -la 显示为普通文件),canonicalize 不会回到 installation;bin/npm 是相对符号链接,也停在 multishell 内。
      • 真正原因在下一句:active_managed_prefix() 返回的 multishell 前缀与 installed_node_versions 枚举出的 node-versions/<ver>/installation 前缀不相等,随后的 installed.iter().find(|node| node.prefix == prefix) 落空,于是继续走默认别名 / 最高版本。
    • 影响:只在 resolve_managed_fallback_runtime(先看 PATH,PATH 没有再走这里)里体现;正常情况下 PATH 里的 fnm multishell 已由 resolve_host_path_runtime 解析到真实安装,所以实际影响低。
    • 已按你选的 B 处理:删除 active_managed_prefix() 及其调用分支,回退链收敛为 engines 最高匹配 > 默认别名 > 已安装最高版本;活动版本不单独查询,因为激活时它已经在 PATH 里。
    • 提交:deecebf0b(同步 decision-log、里程碑与两处技术方案口径;全量 shell bin 单测 1971 passed)。
- [x] 1 `environment_check.rs:565` `lts/<codename>` 被当成匹配任意版本 - 原评审:`.nvmrc` 写 `lts/iron` 时,因为任意 `lts/` 前缀都短路成 `Some(true)`,会对每个已安装版本都“匹配”;而版本文件 pin 是权威的,于是静默选中最高安装版本(如 22)。建议区分 `lts/*` 与 `lts/<codename>`。 - 现状:`version_matches_pin` 里 `matches!(..., "lts/*") || lower.starts_with("lts/")` 一起 `return Some(true)`;`select_pinned_installation` 从 `Some(true)` 的候选里取最高版本,`resolve_pinned_managed_runtime` 对 `Matched` 直接采用。所以 `lts/iron` 确实会选中已安装最高版本,而不是 Node 20。 - 判定:**成立**,属小范围修复。 - 已改:`lts/*` 仍匹配任意;`lts/<codename>` 返回 `None`(按“未 pin”回退到 PATH / 回退链)。没有硬编码 codename → 版本行映射表(该表会过期,是另一个决策)。 - 提交:`d905fa1c0`;单测覆盖 `lts/iron` 回退、`lts/*` 仍匹配,并把 `lts/iron` 加入“不支持 pin 回退”用例。 - [x] 2 `command_exec.rs:2645` 新增测试 `npm_command_targets_node_plus_npm_cli_on_linux` 非 hermetic - 原评审:该测试走 `resolve_node_runtime`,需要真实宿主 Node;在没有 Node 的 Linux CI runner 上 `.unwrap()` 会 panic。建议像其它真机测试一样早退。 - 现状:测试确实依赖宿主 Node。但这不是本改动独有的:`process_session/tests.rs` 有约 10 处非 opt-in 测试同样 `resolve_project_command_spec_at(..., "npm", ...)` 并 `unwrap()`,整个 shell bin 的 Linux 单测早已依赖宿主 Node。 - 判定:**不成立(针对本仓库)**。CI 的 Rust lane 由 `npm run check:native-shells:agc-rust-shard-*` 驱动,runner 必有 Node;只给这一条测试加早退,会把同一个依赖静默漏掉、降低信号,也与既有测试口径不一致。 - 若你仍想要测试级安全网:把该测试改成 `let Ok(spec) = ... else { return }` 即可;但要连同 `process_session` 一起处理才有实际意义。 - 提交:无(保持现状)。 - [x] 3 `command_sandbox.rs:563` npm install 联网判定只看第一个参数的 basename(安全) - 原评审:Linux 上 `command.exec` 对非 `node` 程序的参数不做路径校验,`node` 本身只约束 `args[1..]`;因此第一个参数可以是一个叫 `npm-cli.js` 的项目文件,后面跟 `install`,就会给非可信 npm CLI 打开 `--share-net`。建议绑定到 `ProjectCommandSpec::node_launcher` 的真实 `npm_cli` 路径。 - 现状:`command_sandbox_requests_npm_install(executable, arguments)` 对非 npm 可执行文件仅做 `arguments[0].file_name() == "npm-cli.js"` + `arguments[1] == "install"`;`prepare_linux_command_sandbox_launch` / `build_linux_bwrap_launch` 用它决定是否 `--share-net`。 - 核实:**成立且可达**。Linux 的 `project_command_validate_arguments` 在 `#[cfg(target_os = "linux")]` 分支直接 `Ok(())`(外部路径 / 敏感路径校验只在非 Linux 生效),`project_command_actual_arguments` 在 Linux 也原样返回 `spec.arguments`。所以 `program="node"` + `args=["/项目/game/npm-cli.js","install"]` 会被判为联网;项目根可写,模型能创建这个文件。 - 注意:原评审内联给的 `.filter(|path| path.is_absolute())` **不够**——绝对路径的项目内 `npm-cli.js` 仍会绕过,必须做前缀 / 身份校验。 - 已按方案 1 落地:`prepare_command_sandbox_launch` 新增 `trusted_npm_install: Option<(&Path, &Path)>`,`prepare_project_command_launch_spec` 把 `spec.node_launcher` 的 `(node, npm_cli)` 透传给沙箱;`command_sandbox_requests_npm_install` 只在 executable、首个参数、`install` 子命令都与可信对逐字一致时返回 true,拿不到可信对时一律不放网;`LinuxSandboxPlan` 用 `npm_install_network` 承载结果,`build_linux_bwrap_launch` 直接采用。 - 提交:`f6e3e19cd`(同步技术方案 V1.11.2、decision-log、pitfalls;全量 shell bin 单测 1972 passed,opt-in 真机 fnm 用例通过)。 - [x] 4 `environment_check.rs:679` fnm default 别名 - 原评审:fnm 把 `default` 存成内容为版本字符串(如 `v20.11.0` / `20`)的文件,不是路径 / 符号链接;`validate_node_installation_prefix` 会拒绝它,于是 default 别名被静默忽略、总回退到最高版本。建议像 nvm 分支那样读文件内容再 `select_pinned_installation`。 - 核实:**前提有误(至少 fnm 1.39.0)**。实测 `fnm/aliases/default` 是符号链接:`default -> /root/.local/share/fnm/node-versions/v22.23.3/installation`(`file` 报 “symbolic link to ...”,`readlink -f` 落到 installation)。`validate_node_installation_prefix` 会 canonicalize 成 installation,`bin/node` 与 `lib/node_modules/npm/bin/npm-cli.js` 都在,**校验通过**,随后 `installed.iter().find(prefix == ...)` 命中——default 别名是生效的。 - 判定:**不成立**。评审建议的“只读文件内容”反而会让这条路径失效:`fs::read_to_string` 对“指向目录的符号链接”会报错并跳过,等于把能用的 default 别名改坏。 - 已做:不改生产逻辑;新增回归测试 `fnm_default_alias_symlink_selects_the_linked_installation`,断言 default 跟随符号链接选中 v20 而非最高的 v22。 - 提交:`6f4667d96`。 - 备注:若你确实在某平台见过 fnm 把 default 写成普通文件的形态(例如 Windows 无符号链接权限),告诉我,我再加“先按链接校验、失败再读文件内容”的双形态兼容;当前没有证据,先不加投机分支。 - [x] 5 `environment_check.rs:658` `FNM_MULTISHELL_PATH` 检测不到活动 fnm 版本 - 原评审:`FNM_MULTISHELL_PATH` 指向一堆符号链接二进制,不是含 `bin/` + `lib/node_modules/npm` 的安装前缀;`validate_node_installation_prefix` 总是拒绝它,因此永远检测不到活动 fnm 版本。建议解析其中的 `node` 符号链接并上溯到安装前缀。 - 核实:**结论对一半,根因与建议都不对**。 - 根因错:fnm 1.39 的 multishell 目录是**完整镜像**,实测含 `bin/node`(普通文件)与 `lib/node_modules/npm/bin/npm-cli.js`,所以 `validate_node_installation_prefix` 是**通过**的,不是“总是拒绝”。 - 建议不可行:`bin/node` 不是符号链接(`ls -la` 显示为普通文件),`canonicalize` 不会回到 installation;`bin/npm` 是相对符号链接,也停在 multishell 内。 - 真正原因在下一句:`active_managed_prefix()` 返回的 multishell 前缀与 `installed_node_versions` 枚举出的 `node-versions/<ver>/installation` 前缀**不相等**,随后的 `installed.iter().find(|node| node.prefix == prefix)` 落空,于是继续走默认别名 / 最高版本。 - 影响:只在 `resolve_managed_fallback_runtime`(先看 PATH,PATH 没有再走这里)里体现;正常情况下 PATH 里的 fnm multishell 已由 `resolve_host_path_runtime` 解析到真实安装,所以实际影响低。 - 已按你选的 B 处理:删除 `active_managed_prefix()` 及其调用分支,回退链收敛为 `engines` 最高匹配 > 默认别名 > 已安装最高版本;活动版本不单独查询,因为激活时它已经在 `PATH` 里。 - 提交:`deecebf0b`(同步 decision-log、里程碑与两处技术方案口径;全量 shell bin 单测 1971 passed)。
k88936 marked the pull request as ready for review 2026-10-07 13:08:31 +08:00
k88936 merged commit 441be8d621 into master 2026-10-07 13:08:51 +08:00
k88936 deleted branch feat/support-more-node 2026-10-07 13:08:51 +08:00
Sign in to join this conversation.