将npm install联网放行绑定到可信node launcher
Project CI / Backend tests (pull_request) Failing after 35s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 5m43s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 7m2s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m6s
Project CI / Repository checks (pull_request) Failing after 21s
Project CI / Frontend tests (pull_request) Successful in 2m31s
Project CI / Native shell tests (pull_request) Successful in 10m18s
Project CI / AI game creator shell web tests (pull_request) Successful in 4m18s
Project CI / Backend tests (pull_request) Failing after 35s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 5m43s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 7m2s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m6s
Project CI / Repository checks (pull_request) Failing after 21s
Project CI / Frontend tests (pull_request) Successful in 2m31s
Project CI / Native shell tests (pull_request) Successful in 10m18s
Project CI / AI game creator shell web tests (pull_request) Successful in 4m18s
- prepare_command_sandbox_launch 新增 trusted_npm_install 参数,command_exec 把 spec.node_launcher 的 (node, npm_cli) 透传给沙箱 - command_sandbox_requests_npm_install 改为与可信 (node, npm_cli) 精确比对(executable、首个参数、install 子命令),删除只看 basename 的判定;拿不到可信对时一律不放网 - LinuxSandboxPlan 增加 npm_install_network,build_linux_bwrap_launch 直接采用,避免重复推导 - 新增/更新单测:项目同名 npm-cli.js 不放网、无可信 launcher 不放网、--share-net 开关、可信 (node, npm_cli) 与实际目标逐字一致 - 同步技术方案 V1.11.2、decision-log 与 pitfalls 记录该安全边界
This commit is contained in:
@@ -1661,6 +1661,9 @@ pub(crate) fn prepare_project_command_launch_spec(
|
||||
&target_arguments,
|
||||
&spec.cwd,
|
||||
&environment,
|
||||
spec.node_launcher
|
||||
.as_ref()
|
||||
.map(|(node, npm_cli)| (node.as_path(), npm_cli.as_path())),
|
||||
)
|
||||
.map_err(|error| {
|
||||
ProjectCommandError::new(
|
||||
@@ -2664,6 +2667,17 @@ mod tests {
|
||||
assert!(arguments[0].ends_with("npm-cli.js"), "{arguments:?}");
|
||||
assert_eq!(&arguments[1..], ["run".to_string(), "build".to_string()]);
|
||||
|
||||
// 沙箱联网判定要求目标与解析层给出的可信 (node, npm_cli) 精确一致。
|
||||
let (trusted_node, trusted_npm_cli) = spec
|
||||
.node_launcher
|
||||
.as_ref()
|
||||
.expect("Linux npm spec must carry the trusted node launcher");
|
||||
assert_eq!(trusted_node, &executable);
|
||||
assert_eq!(
|
||||
trusted_npm_cli.to_string_lossy().as_ref(),
|
||||
arguments[0].as_str()
|
||||
);
|
||||
|
||||
let bootstrap = resolve_project_bootstrap_spec_at(root.path(), 30).unwrap();
|
||||
let (_, arguments) = project_command_actual_target(&bootstrap);
|
||||
assert!(arguments[0].ends_with("npm-cli.js"), "{arguments:?}");
|
||||
|
||||
@@ -110,6 +110,9 @@ pub(crate) fn command_sandbox_platform_metadata() -> CommandSandboxMetadata {
|
||||
|
||||
/// Builds a fail-closed launcher for a direct executable plus structured argv.
|
||||
/// It never falls back to launching the original command on the host.
|
||||
///
|
||||
/// `trusted_npm_install` 是解析层给出的可信 `(node, npm_cli)`;只有目标命令与它精确一致
|
||||
/// 且子命令是 `install` 时才放网,避免同名项目文件冒充 npm。
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) fn prepare_command_sandbox_launch(
|
||||
root: &Path,
|
||||
@@ -117,8 +120,16 @@ pub(crate) fn prepare_command_sandbox_launch(
|
||||
arguments: &[String],
|
||||
cwd: &Path,
|
||||
environment: &[(OsString, OsString)],
|
||||
trusted_npm_install: Option<(&Path, &Path)>,
|
||||
) -> Result<CommandSandboxLaunch, CommandSandboxError> {
|
||||
prepare_linux_command_sandbox_launch(root, executable, arguments, cwd, environment)
|
||||
prepare_linux_command_sandbox_launch(
|
||||
root,
|
||||
executable,
|
||||
arguments,
|
||||
cwd,
|
||||
environment,
|
||||
trusted_npm_install,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -202,6 +213,7 @@ mod linux {
|
||||
cwd: PathBuf,
|
||||
executable: PathBuf,
|
||||
arguments: Vec<String>,
|
||||
npm_install_network: bool,
|
||||
target_environment: Vec<(OsString, OsString)>,
|
||||
merged_usr_links: Vec<(OsString, PathBuf)>,
|
||||
protected_read_only: Vec<PathBuf>,
|
||||
@@ -293,9 +305,12 @@ mod linux {
|
||||
arguments: &[String],
|
||||
cwd: &Path,
|
||||
environment: &[(OsString, OsString)],
|
||||
trusted_npm_install: Option<(&Path, &Path)>,
|
||||
) -> Result<CommandSandboxLaunch, CommandSandboxError> {
|
||||
let mut metadata = CommandSandboxMetadata::enforced_linux();
|
||||
if command_sandbox_requests_npm_install(executable, arguments) {
|
||||
let npm_install_network =
|
||||
command_sandbox_requests_npm_install(executable, arguments, trusted_npm_install);
|
||||
if npm_install_network {
|
||||
metadata.network = "enabled";
|
||||
}
|
||||
let bwrap = find_trusted_bwrap().map_err(|error| {
|
||||
@@ -372,6 +387,7 @@ mod linux {
|
||||
cwd,
|
||||
executable,
|
||||
arguments: arguments.to_vec(),
|
||||
npm_install_network,
|
||||
target_environment,
|
||||
merged_usr_links,
|
||||
protected_read_only,
|
||||
@@ -546,26 +562,21 @@ mod linux {
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// `npm install` 是唯一允许联网的受控入口。Linux 以 `node <npm-cli.js> install` 启动时
|
||||
/// executable 是 node,网络判定不能只看 executable 文件名。
|
||||
fn command_sandbox_requests_npm_install(executable: &Path, arguments: &[String]) -> bool {
|
||||
let npm_name = executable
|
||||
.file_name()
|
||||
.and_then(OsStr::to_str)
|
||||
.is_some_and(|name| {
|
||||
name.eq_ignore_ascii_case("npm") || name.eq_ignore_ascii_case("npm.cmd")
|
||||
});
|
||||
if npm_name {
|
||||
return arguments
|
||||
/// `npm install` 是唯一允许联网的受控入口。Linux 上 npm 以 `node <npm-cli.js> install`
|
||||
/// 启动,只看 executable 或第一个参数的 basename 会被项目里同名的文件冒充,因此必须与解析层
|
||||
/// 给出的可信 `(node, npm_cli)` 精确比对;拿不到可信对时一律不放网。
|
||||
fn command_sandbox_requests_npm_install(
|
||||
executable: &Path,
|
||||
arguments: &[String],
|
||||
trusted_npm_install: Option<(&Path, &Path)>,
|
||||
) -> bool {
|
||||
let Some((trusted_node, trusted_npm_cli)) = trusted_npm_install else {
|
||||
return false;
|
||||
};
|
||||
executable == trusted_node
|
||||
&& arguments
|
||||
.first()
|
||||
.is_some_and(|argument| argument == "install");
|
||||
}
|
||||
arguments
|
||||
.first()
|
||||
.map(Path::new)
|
||||
.and_then(Path::file_name)
|
||||
.and_then(OsStr::to_str)
|
||||
.is_some_and(|name| name.eq_ignore_ascii_case("npm-cli.js"))
|
||||
.is_some_and(|argument| Path::new(argument) == trusted_npm_cli)
|
||||
&& arguments
|
||||
.get(1)
|
||||
.is_some_and(|argument| argument == "install")
|
||||
@@ -713,10 +724,7 @@ mod linux {
|
||||
|
||||
fn build_linux_bwrap_launch(plan: LinuxSandboxPlan) -> CommandSandboxLaunch {
|
||||
let mut args = Vec::<OsString>::new();
|
||||
push_namespace_arguments(
|
||||
&mut args,
|
||||
command_sandbox_requests_npm_install(&plan.executable, &plan.arguments),
|
||||
);
|
||||
push_namespace_arguments(&mut args, plan.npm_install_network);
|
||||
push_ro_bind(&mut args, Path::new("/usr"), Path::new("/usr"));
|
||||
for (target, destination) in &plan.merged_usr_links {
|
||||
push_option(
|
||||
@@ -1056,6 +1064,16 @@ mod linux {
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn namespace_arguments_share_net_only_when_trusted() {
|
||||
let mut shared = Vec::new();
|
||||
push_namespace_arguments(&mut shared, true);
|
||||
assert!(shared.contains(&OsString::from("--share-net")));
|
||||
let mut isolated = Vec::new();
|
||||
push_namespace_arguments(&mut isolated, false);
|
||||
assert!(!isolated.contains(&OsString::from("--share-net")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pure_builder_constructs_empty_workspace_namespace_and_private_environment() {
|
||||
let launch = build_linux_bwrap_launch(LinuxSandboxPlan {
|
||||
@@ -1064,6 +1082,7 @@ mod linux {
|
||||
cwd: PathBuf::from("/workspace/project/game"),
|
||||
executable: PathBuf::from("/opt/toolchain/bin/tool"),
|
||||
arguments: vec!["check".to_string(), "--flag".to_string()],
|
||||
npm_install_network: false,
|
||||
target_environment: vec![
|
||||
(
|
||||
OsString::from("HOME"),
|
||||
@@ -1188,6 +1207,7 @@ mod linux {
|
||||
&["-c".to_string(), script],
|
||||
&root,
|
||||
&[(OsString::from("PATH"), OsString::from("/usr/bin"))],
|
||||
None,
|
||||
)
|
||||
.expect_err("invalid protected path must fail closed");
|
||||
assert!(error.to_string().contains(".codex"));
|
||||
@@ -1332,25 +1352,49 @@ mod linux {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn npm_install_network_detection_supports_node_launcher() {
|
||||
fn npm_install_network_detection_requires_the_trusted_node_launcher() {
|
||||
let node = Path::new("/opt/node/bin/node");
|
||||
let npm_cli = Path::new("/opt/node/lib/node_modules/npm/bin/npm-cli.js");
|
||||
let trusted = Some((node, npm_cli));
|
||||
let install_arguments = vec![
|
||||
npm_cli.to_string_lossy().into_owned(),
|
||||
"install".to_string(),
|
||||
];
|
||||
assert!(command_sandbox_requests_npm_install(
|
||||
Path::new("/opt/node/bin/node"),
|
||||
node,
|
||||
&install_arguments,
|
||||
trusted,
|
||||
));
|
||||
// 项目里同名的 npm-cli.js 不能再冒充。
|
||||
assert!(!command_sandbox_requests_npm_install(
|
||||
node,
|
||||
&[
|
||||
"/opt/node/lib/node_modules/npm/bin/npm-cli.js".to_string(),
|
||||
"/workspace/game/npm-cli.js".to_string(),
|
||||
"install".to_string(),
|
||||
],
|
||||
trusted,
|
||||
));
|
||||
// executable 与可信 node 不一致时不放网。
|
||||
assert!(!command_sandbox_requests_npm_install(
|
||||
Path::new("/opt/node/bin/node"),
|
||||
Path::new("/opt/node/bin/other"),
|
||||
&install_arguments,
|
||||
trusted,
|
||||
));
|
||||
// 非 install 子命令不放网。
|
||||
assert!(!command_sandbox_requests_npm_install(
|
||||
node,
|
||||
&[
|
||||
"/opt/node/lib/node_modules/npm/bin/npm-cli.js".to_string(),
|
||||
npm_cli.to_string_lossy().into_owned(),
|
||||
"run".to_string(),
|
||||
"build".to_string(),
|
||||
],
|
||||
trusted,
|
||||
));
|
||||
assert!(command_sandbox_requests_npm_install(
|
||||
// 没有可信 launcher(例如直接 npm shim)时不放网。
|
||||
assert!(!command_sandbox_requests_npm_install(
|
||||
Path::new("/usr/bin/npm"),
|
||||
&["install".to_string()],
|
||||
None,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1451,6 +1495,7 @@ print("SANDBOX_OK")
|
||||
&["-c".to_string(), script],
|
||||
&root,
|
||||
&environment,
|
||||
None,
|
||||
)
|
||||
.expect("prepare real Linux sandbox");
|
||||
|
||||
@@ -1509,6 +1554,7 @@ print("SANDBOX_OK")
|
||||
arguments,
|
||||
&root,
|
||||
&environment,
|
||||
None,
|
||||
)
|
||||
.expect("prepare node sandbox");
|
||||
let output = Command::new(&launch.executable)
|
||||
@@ -1578,9 +1624,15 @@ print("SANDBOX_OK")
|
||||
(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 launch = prepare_command_sandbox_launch(
|
||||
&root,
|
||||
&node,
|
||||
arguments,
|
||||
&root,
|
||||
&environment,
|
||||
None,
|
||||
)
|
||||
.expect("prepare nvm sandbox");
|
||||
let output = Command::new(&launch.executable)
|
||||
.args(&launch.arguments)
|
||||
.current_dir(&launch.cwd)
|
||||
@@ -1635,6 +1687,7 @@ print("SANDBOX_OK")
|
||||
&arguments,
|
||||
&root,
|
||||
&environment,
|
||||
None,
|
||||
)
|
||||
.expect("prepare real Linux sandbox");
|
||||
let target_arguments = arguments.iter().map(OsString::from).collect::<Vec<_>>();
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
- 背景:开发构建里 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` 最高匹配 > 默认别名 > 已安装最高版本);活动版本不单独查询(`FNM_MULTISHELL_PATH` / `NVM_BIN`),因为激活时它已在 `PATH` 里,回退链无需重复一份可能失效的副本;只实现文档化比较子集(精确三元组、major、`>=` / `>` / `<=` / `<`、`^`、`~`、`x` / `*`、`lts/*`;`lts/<codename>` 需要 codename → 版本行映射,当前按未 pin 回退而不猜);`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`。
|
||||
- 决策(沙箱内启动形态与联网放行):Linux npm 改为 `node <npm-cli.js>`,因为单文件挂载 npm 软链必然丢 `../lib/cli.js`;只读挂载整棵已验证的安装前缀(不是整个 HOME、`FNM_DIR` 或 `NVM_DIR`)。`npm install` 的联网放行只认解析层给出的可信 `(node, npm_cli)` 精确匹配(executable 是对应 node、首个参数是对应 `npm-cli.js`、子命令是 `install`),不再按 basename 判定:项目里同名的 `npm-cli.js` 可写,只加「路径必须绝对」也能绕过;拿不到可信对时一律不放网。
|
||||
- 决策(范围与非目标):托管版本管理器发现只在 `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` 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。
|
||||
- 影响范围:`apps/ai-game-creator-shell/src-tauri/src/environment_check.rs`(窄叶校验、托管安装枚举、pin / engines 解析、选择与回退)、`command_sandbox.rs`(Node 工具链挂载收集与合并、`prepare_command_sandbox_launch` 透传可信 `(node, npm_cli)`、`LinuxSandboxPlan::npm_install_network`、`FNM_MULTISHELL_PATH` 清理)、`command_exec.rs`(Linux npm `node_launcher`、`project_command_actual_target`、`prepare_project_command_launch_spec` 把可信 launcher 交给沙箱、非 Node 程序 PATH 前置工具链 bin)、`process_session_bridge.rs`(`ProcessSessionLaunchPlan::from_launch` 改用实际目标)。
|
||||
- 验证方式:`environment_check` 25 passed、`command_sandbox` 15 passed、`command_exec` 19 passed、`process_session` 27 passed(均 `--test-threads=1`);`npm_install_network_detection_requires_the_trusted_node_launcher` 覆盖「项目同名 `npm-cli.js` 不放网、无可信 launcher 不放网、executable 不匹配不放网」,`namespace_arguments_share_net_only_when_trusted` 覆盖 `--share-net` 开关,`npm_command_targets_node_plus_npm_cli_on_linux` 断言解析层可信 `(node, npm_cli)` 与实际目标逐字一致;`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 自带白名单不再共享
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
- **根因 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 / 不支持写法 / 宽叶与逃逸前缀 / 联网判定。
|
||||
- **根因 4(联网放行只看 basename 会被同名文件冒充)**:`npm install` 是唯一允许联网的入口,最初只按「首个参数 basename 是 `npm-cli.js` 且第二个参数是 `install`」放行。Linux 上 `command.exec` 不对 `node` 的脚本参数做路径校验,项目根又可写,于是在项目里放一个自写的 `npm-cli.js` 再 `node 项目/npm-cli.js install` 就能拿到 `--share-net`。修法是让联网放行与解析层给出的可信 `(node, npm_cli)` 精确比对,拿不到可信对时一律不放网;只加「路径必须绝对」不够,绝对路径的项目内文件照样绕过。
|
||||
- **现行口径**:宿主发现与沙箱挂载共用 `validate_node_installation_prefix`;`.nvmrc` / `.node-version` 权威、`engines.node` 偏好;Linux npm 以 `node <npm-cli.js>` 启动,`npm install` 联网放行只认解析层给出的可信 `(node, npm_cli)` 精确匹配。契约见技术方案 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 / 不支持写法 / 宽叶与逃逸前缀 / 可信 launcher 联网放行。
|
||||
|
||||
## 2026-10-07 cargo 目标目录里被"刷新 mtime"的陈旧 shared-contracts 会让编译报源文件里明明存在的字段缺失
|
||||
|
||||
|
||||
@@ -623,7 +623,7 @@ Runner-kill E2E 不再以 latest task 或单个 process record 推断整体恢
|
||||
- 版本来源是机器上可枚举的托管安装:fnm 的 `node-versions/<version>/installation`(含 `aliases/default` 指向的默认别名)与 nvm 的 `versions/node/<version>`。宿主发现和沙箱只读挂载共用同一套窄叶校验,不允许两处信任口径漂移。
|
||||
- 解析优先级为:`.nvmrc` / `.node-version` 的权威 pin 命中 > 宿主 `PATH` 能解析出的可用 Node > 版本管理器回退链(`package.json` `engines.node` 偏好中的最高匹配 > 默认别名 > 已安装最高版本)。不单独查询活动版本(`FNM_MULTISHELL_PATH` / `NVM_BIN`):激活时它已经在 `PATH` 里,回退链重复一份可能失效的副本只会带来误导。`.nvmrc` / `.node-version` 能理解但未安装时必须失败关闭(`node-version-pinned-not-installed`),不得静默回退;`engines.node` 只是偏好,任何情况下都不阻塞。只实现文档化的比较子集(精确三元组、major、`>=` / `>` / `<=` / `<`、`^`、`~`、`x` / `*` 通配、`lts/*`);不支持或无法解析的写法按「未 pin」处理并回退。
|
||||
- 只读挂载只允许通过窄叶校验的完整安装前缀(同时含 `bin/node` 与 npm 的 `npm-cli.js`)。HOME、`FNM_DIR` / `NVM_DIR` 根、`aliases` 目录、宽泛用户目录、不完整前缀,以及 canonicalize 后逃逸出受控前缀的 symlink 全部拒绝并失败关闭;不得为了兼容而挂载整个用户 HOME 或版本管理器数据目录。
|
||||
- Linux 上 npm 不再直接执行 npm shim,而是以受信任的 `node <npm-cli.js> ...` 启动;`npm install` 的联网判定必须跟随这条真实启动形态,不能因为包装方式变化而丢失联网或反向放开。
|
||||
- Linux 上 npm 不再直接执行 npm shim,而是以受信任的 `node <npm-cli.js> ...` 启动;`npm install` 的联网放行必须与解析层给出的可信 `(node, npm_cli)` 精确一致(executable 是对应的 node、首个参数是对应的 `npm-cli.js`、子命令是 `install`),不能只匹配 basename——项目根可写,项目里同名的 `npm-cli.js` 会骗到 `--share-net`;拿不到可信对时一律不放网。
|
||||
- 沙箱内联环境不继承宿主活动版本管理器的临时变量(如 fnm multishell 路径);版本管理器 CLI(`fnm` / `nvm`)本身不需要在沙箱内可用。
|
||||
|
||||
## V1.12 受控本地 Git 提交
|
||||
|
||||
Reference in New Issue
Block a user