修复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
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
- 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 别名口径
This commit is contained in:
@@ -1543,6 +1543,74 @@ print("SANDBOX_OK")
|
||||
assert!(!npm_version.is_empty(), "npm --version returned nothing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_sandbox_real_linux_opt_in_runs_nvm_installation_prefix() {
|
||||
if std::env::var_os("GENARRATIVE_COMMAND_SANDBOX_REAL_TEST").is_none() {
|
||||
return;
|
||||
}
|
||||
let Some(nvm_dir) = std::env::var_os("NVM_DIR").map(PathBuf::from) else {
|
||||
return;
|
||||
};
|
||||
let versions_dir = nvm_dir.join("versions").join("node");
|
||||
let Ok(entries) = std::fs::read_dir(&versions_dir) else {
|
||||
return;
|
||||
};
|
||||
let mut versions = entries
|
||||
.flatten()
|
||||
.filter_map(|entry| entry.file_name().to_str().map(str::to_string))
|
||||
.collect::<Vec<_>>();
|
||||
versions.sort();
|
||||
let Some(version) = versions.pop() else {
|
||||
return;
|
||||
};
|
||||
let prefix = crate::environment_check::validate_node_installation_prefix(
|
||||
&versions_dir.join(&version),
|
||||
)
|
||||
.expect("nvm 安装前缀应通过窄叶校验");
|
||||
let node = prefix.join("bin").join("node");
|
||||
let npm_cli = prefix.join("lib/node_modules/npm/bin/npm-cli.js");
|
||||
|
||||
let tree = unique_temp_tree();
|
||||
let root = tree.0.join("workspace-nvm-runtime");
|
||||
std::fs::create_dir_all(root.join(".agent")).expect("create runtime control");
|
||||
let environment = vec![
|
||||
(OsString::from("PATH"), prefix.join("bin").into_os_string()),
|
||||
(OsString::from("HOME"), OsString::from("/host/home")),
|
||||
];
|
||||
let run = |arguments: &[String]| {
|
||||
let launch =
|
||||
prepare_command_sandbox_launch(&root, &node, arguments, &root, &environment)
|
||||
.expect("prepare nvm sandbox");
|
||||
let output = Command::new(&launch.executable)
|
||||
.args(&launch.arguments)
|
||||
.current_dir(&launch.cwd)
|
||||
.env_clear()
|
||||
.envs(launch.environment.iter().cloned())
|
||||
.output()
|
||||
.expect("run nvm sandbox");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"stderr={}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
String::from_utf8_lossy(&output.stdout).trim().to_string()
|
||||
};
|
||||
|
||||
let version = run(&[
|
||||
"-e".to_string(),
|
||||
"process.stdout.write(process.version)".to_string(),
|
||||
]);
|
||||
assert!(
|
||||
version.starts_with('v'),
|
||||
"unexpected node version: {version}"
|
||||
);
|
||||
let npm_version = run(&[
|
||||
npm_cli.to_string_lossy().into_owned(),
|
||||
"--version".to_string(),
|
||||
]);
|
||||
assert!(!npm_version.is_empty(), "npm --version returned nothing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_sandbox_staged_gate_real_linux_opt_in_blocks_until_commit() {
|
||||
if std::env::var_os("GENARRATIVE_COMMAND_SANDBOX_REAL_TEST").is_none() {
|
||||
|
||||
@@ -687,10 +687,9 @@ fn default_managed_installation<'a>(
|
||||
let Ok(content) = fs::read_to_string(root.join("alias").join("default")) else {
|
||||
continue;
|
||||
};
|
||||
let Some(version) = parse_numeric_version(content.trim()) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(node) = installed.iter().find(|node| node.version == version) {
|
||||
// nvm 的 default 别名常写成 `22`、`v22.23.3` 或 `lts/*`,不是完整三元组;按 pin 的
|
||||
// 同一子集在已安装版本里选最高匹配,避免 `22` 被当成 (22,0,0) 而永远匹配不到。
|
||||
if let PinSelection::Matched(node) = select_pinned_installation(installed, content.trim()) {
|
||||
return Some(node);
|
||||
}
|
||||
}
|
||||
@@ -1274,6 +1273,24 @@ mod tests {
|
||||
assert!(runtime.node.to_string_lossy().contains("v22.23.3"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nvm_major_only_default_alias_selects_the_installed_patch() {
|
||||
let nvm_root = tempfile::tempdir().unwrap();
|
||||
install_nvm_version(nvm_root.path(), "v20.11.0");
|
||||
let expected = install_nvm_version(nvm_root.path(), "v22.23.3");
|
||||
let alias_dir = nvm_root.path().join("alias");
|
||||
fs::create_dir_all(&alias_dir).unwrap();
|
||||
// `nvm alias default 22` 写的是主版本号,不是完整三元组。
|
||||
fs::write(alias_dir.join("default"), "22\n").unwrap();
|
||||
let installed = installed_node_versions(&[], &[nvm_root.path().to_path_buf()]);
|
||||
assert_eq!(installed.len(), 2);
|
||||
let default =
|
||||
default_managed_installation(&installed, &[], &[nvm_root.path().to_path_buf()])
|
||||
.expect("major-only default alias should resolve");
|
||||
assert_eq!(default.prefix, expected.canonicalize().unwrap());
|
||||
assert_eq!(default.version, (22, 23, 3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn version_file_pin_is_authoritative_and_blocks_when_not_installed() {
|
||||
let fnm_root = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
- [x] 只有通过窄叶校验的完整安装前缀会被只读挂载;HOME、管理器根、`aliases`、宽泛目录、不完整前缀和逃逸 symlink 全部失败关闭。
|
||||
- [x] npm 在 Linux 上以受信任 `node <npm-cli.js> ...` 启动,`npm install` 仍被判定为联网命令,普通 `npm run` 仍离线。
|
||||
- [x] 宿主发现与沙箱挂载共用同一套窄叶校验,不存在第二份信任口径。
|
||||
- [ ] 真实 nvm 安装前缀的端到端沙箱运行(本机未安装 nvm,改用临时目录夹具覆盖布局与解析)。
|
||||
- [x] 真实 nvm(v0.40.8 + Node v22.23.3)安装前缀的端到端沙箱运行,以及在仅 nvm 环境(`NVM_DIR` + 空 PATH + 临时 HOME)下的托管解析;同时修正 nvm `alias/default` 只写主版本号(如 `22`)时被当作 `(22,0,0)` 而匹配不到已安装补丁版本的问题。
|
||||
|
||||
## 证据要求
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
- `cargo test --locked -p genarrative-ai-game-creator-shell --bin genarrative-ai-game-creator-shell -- environment_check:: --test-threads=1`
|
||||
- `cargo test --locked -p genarrative-ai-game-creator-shell --bin genarrative-ai-game-creator-shell -- command_sandbox:: command_exec:: process_session:: --test-threads=1`
|
||||
- 可选真机:`GENARRATIVE_COMMAND_SANDBOX_REAL_TEST=1 cargo test … -- command_sandbox_real_linux_opt_in_runs_host_node_and_npm_cli`
|
||||
- 可选真机 nvm:`NVM_DIR=<nvm 目录> GENARRATIVE_COMMAND_SANDBOX_REAL_TEST=1 cargo test … -- command_sandbox_real_linux_opt_in_runs_nvm_installation_prefix`;仅 nvm 解析:`env -i HOME=<临时家目录> NVM_DIR=<nvm 目录> PATH=/nonexistent GENARRATIVE_COMMAND_SANDBOX_REAL_TEST=1 <测试二进制> --exact environment_check::tests::real_node_npm_environment_versions --ignored`
|
||||
- `cargo fmt --check`、`npm run check:encoding`、`git diff --check`。
|
||||
- 运行时:真实 fnm v22 前缀下,bwrap 内 `node -e 'process.stdout.write(process.version)'` 与 `node <npm-cli.js> --version` 均成功且版本为 v22。
|
||||
- 边界:`.nvmrc` pin 未安装、`engines.node` 不匹配、不支持 pin、宽泛 / 不完整前缀、逃逸 symlink、`npm run` 离线与 `npm install` 联网。
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
|
||||
- 背景:开发构建里 AGC 让命令沙箱执行 `npm run build` / `npm install` 时,宿主 Node 由 fnm 托管,`node` / `npm` 实际是随 shell 会话变化的 fnm multishell 目录里的 shim;bwrap `--tmpfs /run` 会抹掉该路径,而只按单文件挂载 `<前缀>/bin/npm`(它软链到 `lib/node_modules/npm/bin/npm-cli.js`)会因 `Cannot find module '../lib/cli.js'` 失败。此前把宿主 `node` / `npm` / `npx` shim 指到 `/usr/bin/*` 是错误取舍:系统 Node 26 默认启用实验性 Web Storage,会顶掉 vitest 0.34 jsdom 的 localStorage,使 AGC 测试套件在 HEAD 即失败(见 `pitfalls.md` 2026-10-03 条)。
|
||||
- 决策(宿主发现与沙箱挂载共用窄叶校验):新增 `validate_node_installation_prefix`,canonicalize 后拒绝 `/home`、`/root`、`/tmp`、`/var`、`/etc`、`/proc`、`/dev`、`/run`、`/sys`、`/boot`、`/srv` 根、HOME 及其祖先和相对路径,并要求前缀同时含 `bin/node` 与 `lib/node_modules/npm/bin/npm-cli.js`(bundle 形态为 `<前缀>/node` + `node_modules/npm/bin/npm-cli.js`)。宿主版本枚举与 Linux 沙箱只读挂载都调用它,避免两处信任口径漂移。
|
||||
- 决策(版本选择):`.nvmrc` / `.node-version` 是权威 pin,能理解但未安装时返回 `node-version-pinned-not-installed` 失败关闭;`package.json` `engines.node` 只是偏好,永不阻塞;不支持的写法(`iojs`、`||`、部分 `>` / `<=`、hyphen range、prerelease)按未 pin 回退。整体顺序为 pin 命中 > PATH 可解析的可用 Node > 版本管理器回退链(`engines` 最高匹配 > 活动版本 > 默认别名 > 已安装最高版本),只实现文档化比较子集(精确三元组、major、`>=` / `>` / `<=` / `<`、`^`、`~`、`x` / `*`、`lts/*`);`v22.23.3` 这类带 `v` 的 `.nvmrc` 必须先剥前缀。
|
||||
- 决策(版本选择):`.nvmrc` / `.node-version` 是权威 pin,能理解但未安装时返回 `node-version-pinned-not-installed` 失败关闭;`package.json` `engines.node` 只是偏好,永不阻塞;不支持的写法(`iojs`、`||`、部分 `>` / `<=`、hyphen range、prerelease)按未 pin 回退。整体顺序为 pin 命中 > PATH 可解析的可用 Node > 版本管理器回退链(`engines` 最高匹配 > 活动版本 > 默认别名 > 已安装最高版本),只实现文档化比较子集(精确三元组、major、`>=` / `>` / `<=` / `<`、`^`、`~`、`x` / `*`、`lts/*`);`v22.23.3` 这类带 `v` 的 `.nvmrc` 必须先剥前缀。nvm 的 `alias/default` 常只写主版本号(如 `22`),必须按同一 pin 子集在已安装版本里选最高匹配,不能当成完整三元组 `(22,0,0)`。
|
||||
- 决策(沙箱内启动形态):Linux npm 改为 `node <npm-cli.js>`,因为单文件挂载 npm 软链必然丢 `../lib/cli.js`;只读挂载整棵已验证的安装前缀(不是整个 HOME、`FNM_DIR` 或 `NVM_DIR`)。`npm install` 的联网判定跟随真实启动形态(`node` + `npm-cli.js` + `install`),不因包装变化丢 `--share-net`。
|
||||
- 决策(范围与非目标):托管版本管理器发现只在 `debug_assertions` / development 生效,发布构建继续只认随包 bundle;不把 fnm / nvm CLI 做成沙箱内工具;Windows 不变;不新增 fnm / nvm 之外的版本管理器。
|
||||
- 影响范围:`apps/ai-game-creator-shell/src-tauri/src/environment_check.rs`(窄叶校验、托管安装枚举、pin / engines 解析、选择与回退)、`command_sandbox.rs`(Node 工具链挂载收集与合并、`command_sandbox_requests_npm_install`、`FNM_MULTISHELL_PATH` 清理)、`command_exec.rs`(Linux npm `node_launcher`、`project_command_actual_target`、非 Node 程序 PATH 前置工具链 bin)、`process_session_bridge.rs`(`ProcessSessionLaunchPlan::from_launch` 改用实际目标)。
|
||||
- 验证方式:`environment_check` 23 passed、`command_sandbox` 13 passed、`command_exec` 19 passed、`process_session` 27 passed(均 `--test-threads=1`);`GENARRATIVE_COMMAND_SANDBOX_REAL_TEST=1` 真机 bwrap 内 fnm v22.23.3 的 `node --version` 与 npm 10.9.9 通过;`cargo fmt --check`、`npm run check:encoding`、`git diff --check` 通过。
|
||||
- 边界:本机未安装 nvm,nvm 布局由临时目录夹具覆盖;真实 nvm 端到端留待有 nvm 的机器。真实验收前不宣称发布构建也支持 fnm / nvm。
|
||||
- 验证方式:`environment_check` 24 passed、`command_sandbox` 14 passed、`command_exec` 19 passed、`process_session` 27 passed(均 `--test-threads=1`);`GENARRATIVE_COMMAND_SANDBOX_REAL_TEST=1` 真机 bwrap 内 fnm v22.23.3 的 `node --version` 与 npm 10.9.9 通过;另装 nvm v0.40.8 + Node v22.23.3,`command_sandbox_real_linux_opt_in_runs_nvm_installation_prefix` 证明真实 nvm 前缀可在 bwrap 内跑 node / npm,`real_node_npm_environment_versions`(仅 `NVM_DIR` + 空 PATH + 临时 HOME)证明托管解析确实选中 nvm;`cargo fmt --check`、`npm run check:encoding`、`git diff --check` 通过。验证后 fnm 仍是宿主默认,nvm 未写入任何 shell profile。
|
||||
- 边界:nvm 已在验证主机安装(v0.40.8 + Node v22.23.3)并跑通真实前缀与仅 nvm 解析;CI 仍由临时目录夹具覆盖 fnm / nvm 布局,真实安装路径测试保持 opt-in。真实验收前不宣称发布构建也支持 fnm / nvm。
|
||||
|
||||
## 2026-10-06 小红书导出 validate/pack:Chrome 61 能力按硬性 ERROR 拦下,pack 自带白名单不再共享
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
- **根因 1(单文件挂载软链前缀)**:fnm / nvm 的 `<前缀>/bin/npm` 是指向 `<前缀>/lib/node_modules/npm/bin/npm-cli.js` 的软链。bwrap `--ro-bind <前缀>/bin/npm <前缀>/bin/npm` 只挂载这一个文件,`npm-cli.js` 里的 `require('../lib/cli.js')` 找不到同安装内的相对目标,于是报错;必须整棵只读挂载通过窄叶校验的完整安装前缀(含 `bin/node`、`lib/node_modules/npm`),不能只挂 shim 或 `bin/`。
|
||||
- **根因 2(`--tmpfs /run` 抹掉活动版本)**:fnm 的活动 `PATH` 项是 `/run/user/<uid>/fnm_multishells/<pid>/bin`;sandbox 的 `--tmpfs /run` 会清空该目录,sandbox 内解析到的 `node` 随之失效或退回系统版本。不要依赖宿主 `PATH` 原样进入沙箱:canonicalize 路径,并把活动版本管理器变量(如 `FNM_MULTISHELL_PATH`)从 sandbox 环境里剔除。
|
||||
- **根因 3(错误取舍会打穿测试)**:把宿主 `node` / `npm` / `npx` shim 指到 `/usr/bin/*` 能让沙箱借用系统 Node,但在本机系统 Node 26 上会默认启用实验性 Web Storage,顶掉 vitest 0.34 jsdom 的 localStorage,AGC 测试在 HEAD 即红(见下方 2026-10-03「AGC 测试不在任何 tsconfig 里」条的环境提示)。正确方向是原生支持托管安装,而不是改宿主 shim。
|
||||
- **补充(nvm default 别名是主版本号)**:`nvm alias default 22` 写进 `$NVM_DIR/alias/default` 的内容是 `22`,不是完整三元组。若按精确 `(22,0,0)` 去匹配 `versions/node/v22.23.3` 会永远落空,默认别名形同不存在;必须用与 `.nvmrc` 相同的比较子集在已安装版本里选最高匹配。
|
||||
- **现行口径**:宿主发现与沙箱挂载共用 `validate_node_installation_prefix`;`.nvmrc` / `.node-version` 权威、`engines.node` 偏好;Linux npm 以 `node <npm-cli.js>` 启动并保留 `npm install` 联网判定。契约见技术方案 V1.11.2。
|
||||
- **验证**:`GENARRATIVE_COMMAND_SANDBOX_REAL_TEST=1` 跑 `command_sandbox_real_linux_opt_in_runs_host_node_and_npm_cli`,在真实 fnm v22 前缀下 bwrap 内 `node --version` 与 `node <npm-cli.js> --version` 均通过;单元用例覆盖 pin / engines / 不支持写法 / 宽叶与逃逸前缀 / 联网判定。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user