From eeb834bf02d9727596f04decd48912c1be74f08a Mon Sep 17 00:00:00 2001 From: liuwenhao <897210338@qq.com> Date: Tue, 22 Sep 2026 10:57:51 +0800 Subject: [PATCH 1/2] fix(process_env): mirror carrier writes into the real env on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `EnvUpdate::commit` gated the real-environment write-through behind `#[cfg(unix)]`, so on Windows every carrier write — settings.json `env` included — stayed invisible to the raw `std::env` readers. That is why `ANTHROPIC_AUTH_TOKEN` from settings.json never reached the API client (`services/api/client.rs:84`) and every launch without a pre-exported token failed with "Could not resolve authentication method". The carrier is a one-time copy of the real environment (`process_env.rs:189-204`), so without this bridge the two diverge and 516 raw `std::env` call sites read stale values while only the 29 carrier readers stay correct. Widen the eight `#[cfg(unix)]` gates that carry the write-through — the `os_operations` field, its two queue sites, `begin_update`'s initializer, the `OsOperation` enum, `EnvUpdate::set`/`remove`, and the `commit` loop — to `#[cfg(any(unix, windows))]`. The operation log was already platform-ready: `truncate_at_nul`/`contains_equals`/`keys_equal`/`array_index` all carry Windows arms, and Windows `set_var` is documented sound, the same premise `entrypoints/cli.rs:19-23` already relies on. The Unix arm is unchanged, so Unix codegen is byte-identical and the existing `cfg(unix)` mirroring assertions still hold. Invert the Windows test that codified the defect as intent: `windows_carrier_operations_leave_real_environment_unchanged` asserted the real environment stays untouched, so it had to become `windows_carrier_operations_mirror_into_real_environment` and assert the carrier write lands. Verified on Windows against three probes with the parent `ANTHROPIC_AUTH_TOKEN` unset, so the credential could only come from a settings file: - settings-provided `ANTHROPIC_CUSTOM_HEADERS` went `present: false` -> `present: true`, and the request followed the settings-provided `ANTHROPIC_BASE_URL` rather than the inherited one; - `--settings ` — Flag source — returned a real completion end to end; - `CLAUDE_CONFIG_DIR` — User source — stopped failing with the SDK's "Could not resolve authentication method". Co-Authored-By: Claude --- src/utils/process_env.rs | 66 ++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/src/utils/process_env.rs b/src/utils/process_env.rs index 0e09431..76f4489 100644 --- a/src/utils/process_env.rs +++ b/src/utils/process_env.rs @@ -268,7 +268,7 @@ pub(crate) fn begin_update() -> EnvUpdate<'static> { EnvUpdate { guard, staged, - #[cfg(unix)] + #[cfg(any(unix, windows))] os_operations: Vec::new(), } } @@ -305,7 +305,7 @@ pub(crate) fn restore_entry(saved: EnvEntryRestore) { .entries .partition_point(|candidate| candidate.insertion_ordinal < entry.insertion_ordinal); table.entries.insert(position, entry.clone()); - #[cfg(unix)] + #[cfg(any(unix, windows))] update .os_operations .push(OsOperation::Set(entry.key, entry.value)); @@ -313,7 +313,7 @@ pub(crate) fn restore_entry(saved: EnvEntryRestore) { None => { if let Some(current) = current { Arc::make_mut(&mut update.staged).entries.remove(current); - #[cfg(unix)] + #[cfg(any(unix, windows))] update.os_operations.push(OsOperation::Remove(saved.key)); } } @@ -321,7 +321,7 @@ pub(crate) fn restore_entry(saved: EnvEntryRestore) { update.commit(); } -#[cfg(unix)] +#[cfg(any(unix, windows))] enum OsOperation { Set(OsString, OsString), Remove(OsString), @@ -332,7 +332,7 @@ enum OsOperation { pub(crate) struct EnvUpdate<'a> { guard: RwLockWriteGuard<'a, Arc>, staged: Arc, - #[cfg(unix)] + #[cfg(any(unix, windows))] os_operations: Vec, } @@ -348,7 +348,7 @@ impl EnvUpdate<'_> { return; }; Arc::make_mut(&mut self.staged).insert(key.clone(), value.clone()); - #[cfg(unix)] + #[cfg(any(unix, windows))] self.os_operations.push(OsOperation::Set(key, value)); } @@ -360,7 +360,7 @@ impl EnvUpdate<'_> { return; } Arc::make_mut(&mut self.staged).remove(&key); - #[cfg(unix)] + #[cfg(any(unix, windows))] self.os_operations.push(OsOperation::Remove(key)); } @@ -376,16 +376,23 @@ impl EnvUpdate<'_> { } /// Publishes the complete staged version exactly once. Until production - /// raw readers are migrated, Unix mirrors the already-normalized operations - /// into the real environment at this boundary only. - #[allow(clippy::disallowed_methods)] // Transitional carrier-owned Unix write-through. + /// raw readers are migrated, every platform mirrors the already-normalized + /// operations into the real environment at this boundary only. A build + /// without this write-through strands every carrier-only write where the + /// raw `std::env` readers cannot see it — settings.json `env` included, + /// which is how `ANTHROPIC_AUTH_TOKEN` stopped reaching auth + /// (`services/api/client.rs:84`). + #[allow(clippy::disallowed_methods)] // Transitional carrier-owned real-env write-through. pub(crate) fn commit(mut self) -> EnvSnapshot { - #[cfg(unix)] + #[cfg(any(unix, windows))] for operation in &self.os_operations { - // SAFETY: normalization removes NUL/`=` cases that make std's API - // panic, but this transitional write-through remains unsound with - // concurrent raw OS readers/writers and is not atomic with carrier - // publication. The final integration child removes the bridge. + // SAFETY: `normalize_assignment`/`normalize_key` strip the NUL and + // `=` shapes that make std's API panic. Windows `set_var` is + // documented as sound — the same premise `entrypoints/cli.rs:19-23` + // already relies on. On Unix the write-through stays unsound with + // concurrent raw OS readers and is not atomic with carrier + // publication, so the integration child removes this bridge only + // after the raw readers are migrated. unsafe { match operation { OsOperation::Set(key, value) => std::env::set_var(key, value), @@ -829,30 +836,35 @@ mod tests { assert!(!keys_equal(&first, &second)); } - /// The Windows carrier is the L1 owner for ordinary CC `process.env` - /// assignments (`cli/structuredIO.ts:348-360`); only the later bootstrap - /// hardening owner may mutate the real Windows environment. + /// The carrier mirrors ordinary CC `process.env` assignments into the real + /// environment (`cli/structuredIO.ts:348-360`). Node has ONE `process.env` + /// object, so a carrier-only write strands settings `env` where the raw + /// `std::env` readers cannot see it — which is exactly how + /// `ANTHROPIC_AUTH_TOKEN` from settings.json stopped reaching auth + /// (`services/api/client.rs:84`) before this write-through covered Windows. #[cfg(windows)] #[test] - fn windows_carrier_operations_leave_real_environment_unchanged() { + fn windows_carrier_operations_mirror_into_real_environment() { let _lock = crate::utils::env_utils::TEST_ENV_LOCK.lock().unwrap(); - let key = "COMETIX_PROCESS_ENV_WINDOWS_OS_UNCHANGED"; - let real_before = std::env::vars_os().collect::>(); - let raw_value_before = std::env::var_os(key); + let key = "COMETIX_PROCESS_ENV_WINDOWS_OS_MIRRORED"; let _carrier = EnvVarGuard::unset(key); - assert_eq!(std::env::vars_os().collect::>(), real_before); - assert_eq!(std::env::var_os(key), raw_value_before); + // Names normalization rejects reach neither the carrier nor the OS. let before_invalid = snapshot(); set("", "ignored"); set("BAD=KEY", "ignored"); assert!(before_invalid.same_version(&snapshot())); - assert_eq!(std::env::vars_os().collect::>(), real_before); set(key, "valid\0truncated"); assert_eq!(var(key).as_deref(), Some("valid")); - assert_eq!(std::env::vars_os().collect::>(), real_before); + assert_eq!( + std::env::var(key).as_deref(), + Ok("valid"), + "a carrier write must reach the real Windows environment" + ); + remove(key); - assert_eq!(std::env::vars_os().collect::>(), real_before); + assert_eq!(var(key), None); + assert_eq!(std::env::var_os(key), None); } } From 6a191ebfcfa99b3cc4b12d100973596cebc68c52 Mon Sep 17 00:00:00 2001 From: liuwenhao <897210338@qq.com> Date: Tue, 22 Sep 2026 10:57:51 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E4=BF=AE=E5=A4=8D(process=5Fenv):=20Window?= =?UTF-8?q?s=20=E4=B8=8A=E6=8A=8A=20carrier=20=E5=86=99=E5=85=A5=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E5=88=B0=E7=9C=9F=E5=AE=9E=E8=BF=9B=E7=A8=8B=E7=8E=AF?= =?UTF-8?q?=E5=A2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `EnvUpdate::commit` 的真实环境写回被 `#[cfg(unix)]` 门控,于是 Windows 上所有 carrier 写入——包括 settings.json 的 `env`——对裸 `std::env` 读取者都不可见。 这正是 settings.json 里的 `ANTHROPIC_AUTH_TOKEN` 始终到不了 API 客户端 (`services/api/client.rs:84`)、凡是没有预先导出该变量的启动一律报 "Could not resolve authentication method" 的原因。carrier 只是真实环境的一次性 拷贝(`process_env.rs:189-204`),缺了这座桥就会与真实环境分叉:516 处裸 `std::env` 读取点读到的是过期值,只有 29 处 carrier 读取者正确。 把承载写回的 8 个 `#[cfg(unix)]` 门放宽为 `#[cfg(any(unix, windows))]`: `os_operations` 字段、两处入队点、`begin_update` 里的初始化、`OsOperation` 枚举、`EnvUpdate::set`/`remove`,以及 `commit` 的写回循环。操作日志本身早已 平台就绪——`truncate_at_nul`/`contains_equals`/`keys_equal`/`array_index` 都带 Windows 分支;而 Windows 的 `set_var` 被文档保证安全,这正是 `entrypoints/cli.rs:19-23` 已经在依赖的前提。Unix 分支逐字未改,故 Unix 生成 代码等价,既有的 `cfg(unix)` 镜像断言依然成立。 反转那个把缺陷固化成预期的 Windows 测试: `windows_carrier_operations_leave_real_environment_unchanged` 断言真实环境保持 不变,必须改为 `windows_carrier_operations_mirror_into_real_environment`,并 断言 carrier 写入确实落地。 Windows 上以三个探针验收(父进程的 `ANTHROPIC_AUTH_TOKEN` 已 unset,凭据只能 来自 settings 文件): - settings 提供的 `ANTHROPIC_CUSTOM_HEADERS` 由 `present: false` 变为 `present: true`,且请求按 settings 提供的 `ANTHROPIC_BASE_URL` 发出,而非 继承来的地址; - `--settings <备用 settings.json>`(Flag 源)端到端返回真实回复; - `CLAUDE_CONFIG_DIR`(User 源)不再报 SDK 的 "Could not resolve authentication method"。 Co-Authored-By: Claude --- src/utils/process_env.rs | 66 ++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/src/utils/process_env.rs b/src/utils/process_env.rs index 0e09431..76f4489 100644 --- a/src/utils/process_env.rs +++ b/src/utils/process_env.rs @@ -268,7 +268,7 @@ pub(crate) fn begin_update() -> EnvUpdate<'static> { EnvUpdate { guard, staged, - #[cfg(unix)] + #[cfg(any(unix, windows))] os_operations: Vec::new(), } } @@ -305,7 +305,7 @@ pub(crate) fn restore_entry(saved: EnvEntryRestore) { .entries .partition_point(|candidate| candidate.insertion_ordinal < entry.insertion_ordinal); table.entries.insert(position, entry.clone()); - #[cfg(unix)] + #[cfg(any(unix, windows))] update .os_operations .push(OsOperation::Set(entry.key, entry.value)); @@ -313,7 +313,7 @@ pub(crate) fn restore_entry(saved: EnvEntryRestore) { None => { if let Some(current) = current { Arc::make_mut(&mut update.staged).entries.remove(current); - #[cfg(unix)] + #[cfg(any(unix, windows))] update.os_operations.push(OsOperation::Remove(saved.key)); } } @@ -321,7 +321,7 @@ pub(crate) fn restore_entry(saved: EnvEntryRestore) { update.commit(); } -#[cfg(unix)] +#[cfg(any(unix, windows))] enum OsOperation { Set(OsString, OsString), Remove(OsString), @@ -332,7 +332,7 @@ enum OsOperation { pub(crate) struct EnvUpdate<'a> { guard: RwLockWriteGuard<'a, Arc>, staged: Arc, - #[cfg(unix)] + #[cfg(any(unix, windows))] os_operations: Vec, } @@ -348,7 +348,7 @@ impl EnvUpdate<'_> { return; }; Arc::make_mut(&mut self.staged).insert(key.clone(), value.clone()); - #[cfg(unix)] + #[cfg(any(unix, windows))] self.os_operations.push(OsOperation::Set(key, value)); } @@ -360,7 +360,7 @@ impl EnvUpdate<'_> { return; } Arc::make_mut(&mut self.staged).remove(&key); - #[cfg(unix)] + #[cfg(any(unix, windows))] self.os_operations.push(OsOperation::Remove(key)); } @@ -376,16 +376,23 @@ impl EnvUpdate<'_> { } /// Publishes the complete staged version exactly once. Until production - /// raw readers are migrated, Unix mirrors the already-normalized operations - /// into the real environment at this boundary only. - #[allow(clippy::disallowed_methods)] // Transitional carrier-owned Unix write-through. + /// raw readers are migrated, every platform mirrors the already-normalized + /// operations into the real environment at this boundary only. A build + /// without this write-through strands every carrier-only write where the + /// raw `std::env` readers cannot see it — settings.json `env` included, + /// which is how `ANTHROPIC_AUTH_TOKEN` stopped reaching auth + /// (`services/api/client.rs:84`). + #[allow(clippy::disallowed_methods)] // Transitional carrier-owned real-env write-through. pub(crate) fn commit(mut self) -> EnvSnapshot { - #[cfg(unix)] + #[cfg(any(unix, windows))] for operation in &self.os_operations { - // SAFETY: normalization removes NUL/`=` cases that make std's API - // panic, but this transitional write-through remains unsound with - // concurrent raw OS readers/writers and is not atomic with carrier - // publication. The final integration child removes the bridge. + // SAFETY: `normalize_assignment`/`normalize_key` strip the NUL and + // `=` shapes that make std's API panic. Windows `set_var` is + // documented as sound — the same premise `entrypoints/cli.rs:19-23` + // already relies on. On Unix the write-through stays unsound with + // concurrent raw OS readers and is not atomic with carrier + // publication, so the integration child removes this bridge only + // after the raw readers are migrated. unsafe { match operation { OsOperation::Set(key, value) => std::env::set_var(key, value), @@ -829,30 +836,35 @@ mod tests { assert!(!keys_equal(&first, &second)); } - /// The Windows carrier is the L1 owner for ordinary CC `process.env` - /// assignments (`cli/structuredIO.ts:348-360`); only the later bootstrap - /// hardening owner may mutate the real Windows environment. + /// The carrier mirrors ordinary CC `process.env` assignments into the real + /// environment (`cli/structuredIO.ts:348-360`). Node has ONE `process.env` + /// object, so a carrier-only write strands settings `env` where the raw + /// `std::env` readers cannot see it — which is exactly how + /// `ANTHROPIC_AUTH_TOKEN` from settings.json stopped reaching auth + /// (`services/api/client.rs:84`) before this write-through covered Windows. #[cfg(windows)] #[test] - fn windows_carrier_operations_leave_real_environment_unchanged() { + fn windows_carrier_operations_mirror_into_real_environment() { let _lock = crate::utils::env_utils::TEST_ENV_LOCK.lock().unwrap(); - let key = "COMETIX_PROCESS_ENV_WINDOWS_OS_UNCHANGED"; - let real_before = std::env::vars_os().collect::>(); - let raw_value_before = std::env::var_os(key); + let key = "COMETIX_PROCESS_ENV_WINDOWS_OS_MIRRORED"; let _carrier = EnvVarGuard::unset(key); - assert_eq!(std::env::vars_os().collect::>(), real_before); - assert_eq!(std::env::var_os(key), raw_value_before); + // Names normalization rejects reach neither the carrier nor the OS. let before_invalid = snapshot(); set("", "ignored"); set("BAD=KEY", "ignored"); assert!(before_invalid.same_version(&snapshot())); - assert_eq!(std::env::vars_os().collect::>(), real_before); set(key, "valid\0truncated"); assert_eq!(var(key).as_deref(), Some("valid")); - assert_eq!(std::env::vars_os().collect::>(), real_before); + assert_eq!( + std::env::var(key).as_deref(), + Ok("valid"), + "a carrier write must reach the real Windows environment" + ); + remove(key); - assert_eq!(std::env::vars_os().collect::>(), real_before); + assert_eq!(var(key), None); + assert_eq!(std::env::var_os(key), None); } }