diff --git a/.bazelrc b/.bazelrc index 192f4e3aef..964dd2f57d 100644 --- a/.bazelrc +++ b/.bazelrc @@ -47,6 +47,9 @@ build:windows --action_env=PATH # Windows runfiles default to a manifest; suites read fixtures and spawn # binaries through the runfiles tree. build:windows --enable_runfiles +# Git for Windows exports its installation root to Git Bash. Preserve gix's +# native discovery fast path in tests instead of launching Git per hook. +test:windows --test_env=EXEPATH test --test_output=errors # Suites spawn child services as well as the bounded Rust test harness. diff --git a/crates/tracedecay-agent-hosts/BUILD.bazel b/crates/tracedecay-agent-hosts/BUILD.bazel index 37b5b97776..469fc6ed29 100644 --- a/crates/tracedecay-agent-hosts/BUILD.bazel +++ b/crates/tracedecay-agent-hosts/BUILD.bazel @@ -141,7 +141,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-agent-hosts/src/agents/context_scout/decision_engine.rs b/crates/tracedecay-agent-hosts/src/agents/context_scout/decision_engine.rs index 9675113355..b403255505 100644 --- a/crates/tracedecay-agent-hosts/src/agents/context_scout/decision_engine.rs +++ b/crates/tracedecay-agent-hosts/src/agents/context_scout/decision_engine.rs @@ -634,6 +634,7 @@ pub mod test_server { #[cfg(test)] #[allow(clippy::unwrap_used)] mod tests { + use std::io::Read as _; use std::time::{Duration, Instant}; use super::test_server::{ @@ -1059,12 +1060,16 @@ mod tests { } #[test] - fn an_unreachable_engine_is_unreachable() { - let port = std::net::TcpListener::bind("127.0.0.1:0") - .unwrap() - .local_addr() - .unwrap() - .port(); + fn a_disconnected_engine_is_unreachable() { + let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + let port = listener.local_addr().unwrap().port(); + // A closed port can spend the entire request deadline in Windows' + // connect retries. Close an established request to observe transport + // loss independently of that platform's refusal timing. + let peer = std::thread::spawn(move || { + let (mut stream, _) = listener.accept().unwrap(); + stream.read_exact(&mut [0_u8; 1]).unwrap(); + }); let mut configured = config(&format!("http://127.0.0.1:{port}")); configured.timeout = Duration::from_millis(500); assert_eq!( @@ -1073,6 +1078,7 @@ mod tests { .ask_blocking(&request("s", vec![noul("q")])), DecisionEngineOutcomeV1::Unreachable ); + peer.join().unwrap(); } #[test] diff --git a/crates/tracedecay-agent-hosts/src/agents/host_bundle/tests.rs b/crates/tracedecay-agent-hosts/src/agents/host_bundle/tests.rs index 04875d9264..b6c734cf66 100644 --- a/crates/tracedecay-agent-hosts/src/agents/host_bundle/tests.rs +++ b/crates/tracedecay-agent-hosts/src/agents/host_bundle/tests.rs @@ -12,6 +12,45 @@ use super::doctor::doctor_artifact_state; use super::planner::plan_artifact_action; use super::*; +#[cfg(windows)] +#[test] +fn publication_survives_a_transient_windows_reader() { + let temp = tempfile::tempdir().unwrap(); + let destination = temp.path().join("artifact"); + drop(tracedecay_private_fs::create_private_file(&destination).unwrap()); + fs::write(&destination, b"before").unwrap(); + let reader = + tracedecay_private_fs::windows::open_or_create_private_lock_file(&destination).unwrap(); + let probe = temp.path().join("probe"); + fs::write(&probe, b"probe").unwrap(); + let error = fs::rename(&probe, &destination).unwrap_err(); + assert!(matches!(error.raw_os_error(), Some(5 | 32))); + fs::remove_file(probe).unwrap(); + let parent = + cap_std::fs::Dir::open_ambient_dir(temp.path(), cap_std::ambient_authority()).unwrap(); + std::thread::scope(|scope| { + scope.spawn(move || { + std::thread::sleep(std::time::Duration::from_millis(40)); + drop(reader); + }); + super::writer::atomic_write_nofollow(&parent, "artifact", b"after", true).unwrap(); + }); + assert_eq!(fs::read(destination).unwrap(), b"after"); + assert_eq!(fs::read_dir(temp.path()).unwrap().count(), 1); + + let destination = temp.path().join("artifact"); + tracedecay_private_fs::make_private_file(&destination).unwrap(); + let reader = + tracedecay_private_fs::windows::open_or_create_private_lock_file(&destination).unwrap(); + assert!(matches!( + super::writer::atomic_write_nofollow(&parent, "artifact", b"denied", true), + Err(HostBundleError::StorageFailure(_)) + )); + assert_eq!(fs::read(destination).unwrap(), b"after"); + assert_eq!(fs::read_dir(temp.path()).unwrap().count(), 1); + drop(reader); +} + #[derive(Clone)] struct FirstPartyVerifier([u8; 32]); diff --git a/crates/tracedecay-agent-hosts/src/agents/host_bundle/writer.rs b/crates/tracedecay-agent-hosts/src/agents/host_bundle/writer.rs index 91cc11b5e8..26775ece28 100644 --- a/crates/tracedecay-agent-hosts/src/agents/host_bundle/writer.rs +++ b/crates/tracedecay-agent-hosts/src/agents/host_bundle/writer.rs @@ -12,6 +12,10 @@ use cap_std::ambient_authority; use cap_std::fs::{Dir, OpenOptions as CapOpenOptions}; use sha2::{Digest, Sha256}; use tracedecay_host_integration::host_bundle_storage_failure; +#[cfg(windows)] +use tracedecay_private_fs::capability_dir::rename_noreplace; +use tracedecay_private_fs::capability_dir::replace_file; +use tracedecay_runtime_core::storage::retry_transient_file_op; use super::control::{ HOST_BUNDLE_CONTROL_DIR, HOST_BUNDLE_LOCK_FILE, MAX_CONTROL_FILE_BYTES, @@ -829,21 +833,29 @@ pub(super) fn atomic_write_nofollow( // a final symlink; the preflight and capability parent prevent // traversal through any descendant component. if replace_existing { - parent - .rename(&temporary, parent, name) + // Retain the staged bytes across transient failures under + // the shared bounded file policy. + retry_transient_file_op(|| replace_file(parent, temporary.as_ref(), name.as_ref())) .map_err(|_| host_bundle_storage_failure!())?; } else { - parent - .hard_link(&temporary, parent, name) - .map_err(|error| { - if error.kind() == io::ErrorKind::AlreadyExists { - HostBundleError::OwnershipConflict(format!( - "{name}: a file appeared at this deploy path mid-write" - )) - } else { - host_bundle_storage_failure!() - } - })?; + #[cfg(windows)] + // First publication needs the same namespace durability as + // replacement, without ever displacing a raced entry. + let publication = retry_transient_file_op(|| { + rename_noreplace(parent, temporary.as_ref(), parent, name.as_ref()) + }); + #[cfg(not(windows))] + let publication = parent.hard_link(&temporary, parent, name); + publication.map_err(|error| { + if error.kind() == io::ErrorKind::AlreadyExists { + HostBundleError::OwnershipConflict(format!( + "{name}: a file appeared at this deploy path mid-write" + )) + } else { + host_bundle_storage_failure!() + } + })?; + #[cfg(not(windows))] parent .remove_file(&temporary) .map_err(|_| host_bundle_storage_failure!())?; diff --git a/crates/tracedecay-api/BUILD.bazel b/crates/tracedecay-api/BUILD.bazel index e0ba6c6208..b300f46109 100644 --- a/crates/tracedecay-api/BUILD.bazel +++ b/crates/tracedecay-api/BUILD.bazel @@ -110,7 +110,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-application/BUILD.bazel b/crates/tracedecay-application/BUILD.bazel index 4132335dc7..8bcfb5826b 100644 --- a/crates/tracedecay-application/BUILD.bazel +++ b/crates/tracedecay-application/BUILD.bazel @@ -150,7 +150,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-application/src/lsp_runtime/semantic/tests.rs b/crates/tracedecay-application/src/lsp_runtime/semantic/tests.rs index f230ae4b56..a39fc58b38 100644 --- a/crates/tracedecay-application/src/lsp_runtime/semantic/tests.rs +++ b/crates/tracedecay-application/src/lsp_runtime/semantic/tests.rs @@ -103,11 +103,18 @@ impl PendingAnalyzerRequest { async fn assert_stopped(mut self) { let mut byte = [0_u8]; - let bytes = tokio::time::timeout(TEST_BOUND, self.stream.read(&mut byte)) + let stopped = tokio::time::timeout(TEST_BOUND, self.stream.read(&mut byte)) .await - .expect("cancelled analyzer work must release its process") - .unwrap(); - assert_eq!(bytes, 0); + .expect("cancelled analyzer work must release its process"); + match stopped { + Ok(0) => {} + Err(error) + if matches!( + error.kind(), + std::io::ErrorKind::ConnectionReset | std::io::ErrorKind::ConnectionAborted + ) => {} + other => panic!("cancelled analyzer connection must close: {other:?}"), + } } } diff --git a/crates/tracedecay-application/tests/application_suite/registered_scope_route.rs b/crates/tracedecay-application/tests/application_suite/registered_scope_route.rs index fe89227a3a..1109b76bfd 100644 --- a/crates/tracedecay-application/tests/application_suite/registered_scope_route.rs +++ b/crates/tracedecay-application/tests/application_suite/registered_scope_route.rs @@ -2,12 +2,13 @@ use std::path::Path; use std::process::Command; use tracedecay_domain::ProjectId; +use tracedecay_runtime_core::path_safety::plain_git_args; use tracedecay_session_memory::context::RegisteredScopeResolver; fn git(root: &Path, args: &[&str]) { let status = Command::new("git") .current_dir(root) - .args(args) + .args(plain_git_args(args)) .status() .expect("git command starts"); assert!(status.success(), "git command failed: {args:?}"); diff --git a/crates/tracedecay-automation-runtime/BUILD.bazel b/crates/tracedecay-automation-runtime/BUILD.bazel index e64cef019e..47d98aad7f 100644 --- a/crates/tracedecay-automation-runtime/BUILD.bazel +++ b/crates/tracedecay-automation-runtime/BUILD.bazel @@ -113,7 +113,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-automation-runtime/src/automation/scheduler.rs b/crates/tracedecay-automation-runtime/src/automation/scheduler.rs index 602996d84c..ba5ab0b961 100644 --- a/crates/tracedecay-automation-runtime/src/automation/scheduler.rs +++ b/crates/tracedecay-automation-runtime/src/automation/scheduler.rs @@ -1020,6 +1020,7 @@ mod tests { SESSION_EVIDENCE_BUDGET_EXHAUSTED, SESSION_EVIDENCE_BUDGET_SUPPRESSED, }; use tracedecay_private_fs::framed_log::{DirectorySyncPolicy, with_owned_temp_publish}; + use tracedecay_private_fs::{create_private_file, open_private_file}; #[cfg(unix)] use tracedecay_runtime_core::storage::with_durable_namespace_sync_fault_for_test; @@ -1899,13 +1900,13 @@ evidence about it", for command in std::io::stdin().lock().lines() { let answer = match command.unwrap().as_str() { "prepare" if old => { - let file = std::fs::OpenOptions::new() - .read(true) - .write(true) - .create(true) - .truncate(false) - .open(&coordination_path) - .unwrap(); + let file = match create_private_file(&coordination_path) { + Ok(file) => file, + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => { + open_private_file(&coordination_path).unwrap() + } + Err(error) => panic!("create old coordination: {error}"), + }; match file.try_lock() { Ok(()) => { coordination = Some(FileLease::held(file, "test.old-task")); @@ -1921,11 +1922,7 @@ evidence about it", // The production publisher hard-linked a synced staging // file under this lock. Keep that publication ordering. let staging = lock_dir.join("old-owner-staging"); - let mut file = std::fs::OpenOptions::new() - .write(true) - .create_new(true) - .open(&staging) - .unwrap(); + let mut file = create_private_file(&staging).unwrap(); writeln!(file, "pid={}\ncreated_at=0", std::process::id()).unwrap(); file.sync_all().unwrap(); std::fs::hard_link(&staging, &lock_path).unwrap(); @@ -2244,12 +2241,16 @@ evidence about it", fn write_pre_lease_lock(lock_dir: &Path, key: &str, payload: &[u8]) -> PathBuf { tracedecay_runtime_core::storage::PrivateStoreIo::create_dir_all_durable(lock_dir).unwrap(); let lock_path = lock_dir.join(format!("{key}.lock")); - std::fs::write(&lock_path, payload).unwrap(); - std::fs::write( - tracedecay_runtime_core::storage::append_lock_path(&lock_path), - b"", - ) - .unwrap(); + create_private_file(&lock_path) + .unwrap() + .write_all(payload) + .unwrap(); + drop( + create_private_file(&tracedecay_runtime_core::storage::append_lock_path( + &lock_path, + )) + .unwrap(), + ); lock_path } diff --git a/crates/tracedecay-automation/BUILD.bazel b/crates/tracedecay-automation/BUILD.bazel index f72467d8fc..26cd75529a 100644 --- a/crates/tracedecay-automation/BUILD.bazel +++ b/crates/tracedecay-automation/BUILD.bazel @@ -66,7 +66,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-capture/BUILD.bazel b/crates/tracedecay-capture/BUILD.bazel index 8ecf26fe2f..40113da1bf 100644 --- a/crates/tracedecay-capture/BUILD.bazel +++ b/crates/tracedecay-capture/BUILD.bazel @@ -95,7 +95,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-cli/BUILD.bazel b/crates/tracedecay-cli/BUILD.bazel index 4a771bf949..99b0f055c1 100644 --- a/crates/tracedecay-cli/BUILD.bazel +++ b/crates/tracedecay-cli/BUILD.bazel @@ -230,7 +230,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-code-extraction/BUILD.bazel b/crates/tracedecay-code-extraction/BUILD.bazel index 238c27bbbe..60975f1e71 100644 --- a/crates/tracedecay-code-extraction/BUILD.bazel +++ b/crates/tracedecay-code-extraction/BUILD.bazel @@ -169,7 +169,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-code-index-retention/BUILD.bazel b/crates/tracedecay-code-index-retention/BUILD.bazel index 4b7fca3c8f..4146bc0eae 100644 --- a/crates/tracedecay-code-index-retention/BUILD.bazel +++ b/crates/tracedecay-code-index-retention/BUILD.bazel @@ -113,7 +113,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-code-index-runtime/BUILD.bazel b/crates/tracedecay-code-index-runtime/BUILD.bazel index 99a07c7b7c..b1eb89bfb5 100644 --- a/crates/tracedecay-code-index-runtime/BUILD.bazel +++ b/crates/tracedecay-code-index-runtime/BUILD.bazel @@ -97,7 +97,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-code-index/BUILD.bazel b/crates/tracedecay-code-index/BUILD.bazel index 4c6b9717b3..fa69088409 100644 --- a/crates/tracedecay-code-index/BUILD.bazel +++ b/crates/tracedecay-code-index/BUILD.bazel @@ -300,7 +300,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-configuration/BUILD.bazel b/crates/tracedecay-configuration/BUILD.bazel index 8e072250ae..4658dac582 100644 --- a/crates/tracedecay-configuration/BUILD.bazel +++ b/crates/tracedecay-configuration/BUILD.bazel @@ -113,7 +113,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-contracts/BUILD.bazel b/crates/tracedecay-contracts/BUILD.bazel index fbf4207d7a..597e29d055 100644 --- a/crates/tracedecay-contracts/BUILD.bazel +++ b/crates/tracedecay-contracts/BUILD.bazel @@ -80,7 +80,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-daemon-control/BUILD.bazel b/crates/tracedecay-daemon-control/BUILD.bazel index 04caea2f93..6705462af0 100644 --- a/crates/tracedecay-daemon-control/BUILD.bazel +++ b/crates/tracedecay-daemon-control/BUILD.bazel @@ -65,7 +65,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-daemon-identity/BUILD.bazel b/crates/tracedecay-daemon-identity/BUILD.bazel index e1787bca22..449cc0c6fd 100644 --- a/crates/tracedecay-daemon-identity/BUILD.bazel +++ b/crates/tracedecay-daemon-identity/BUILD.bazel @@ -65,7 +65,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-daemon-protocol/BUILD.bazel b/crates/tracedecay-daemon-protocol/BUILD.bazel index 2065de454b..18d5119641 100644 --- a/crates/tracedecay-daemon-protocol/BUILD.bazel +++ b/crates/tracedecay-daemon-protocol/BUILD.bazel @@ -65,7 +65,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-daemon-protocol/src/transport.rs b/crates/tracedecay-daemon-protocol/src/transport.rs index df59006a87..59a91263a3 100644 --- a/crates/tracedecay-daemon-protocol/src/transport.rs +++ b/crates/tracedecay-daemon-protocol/src/transport.rs @@ -156,9 +156,11 @@ impl DaemonClientStream { } // Boxed: the connect state machine would otherwise inflate // every caller's future past the large-future lint. - Ok(Self::Tcp( - Box::pin(asupersync::net::TcpStream::connect(*address)).await?, - )) + let stream = Box::pin(asupersync::net::TcpStream::connect(*address)).await?; + // Control frames and their terminators must not wait for a + // previous small write's acknowledgement on loopback. + stream.set_nodelay(true)?; + Ok(Self::Tcp(stream)) } } } @@ -516,7 +518,11 @@ impl BrokerListener { match self { #[cfg(unix)] Self::Unix(listener) => Ok(BrokerStream::Unix(listener.accept().await?.0)), - Self::Tcp(listener) => Ok(BrokerStream::Tcp(listener.accept().await?.0)), + Self::Tcp(listener) => { + let (stream, _) = listener.accept().await?; + stream.set_nodelay(true)?; + Ok(BrokerStream::Tcp(stream)) + } } } } @@ -725,6 +731,13 @@ mod tests { let mut clients = tokio::task::JoinSet::new(); for _ in 0..CLIENTS { let stream = listener.accept().await.unwrap(); + match &stream { + BrokerStream::Tcp(stream) => { + assert!(socket2::SockRef::from(stream).tcp_nodelay().unwrap()); + } + #[cfg(unix)] + BrokerStream::Unix(_) => panic!("expected loopback transport"), + } clients.spawn(async move { let mut reader = BufReader::new(stream); let mut line = String::new(); @@ -744,10 +757,15 @@ mod tests { clients.spawn(async move { use asupersync::io::AsyncWriteExt; - let (_reader, mut writer) = DaemonClientStream::connect(&endpoint) - .await - .unwrap() - .into_split(); + let stream = DaemonClientStream::connect(&endpoint).await.unwrap(); + match &stream { + DaemonClientStream::Tcp(stream) => { + assert!(socket2::SockRef::from(stream).tcp_nodelay().unwrap()); + } + #[cfg(unix)] + DaemonClientStream::Unix(_) => panic!("expected loopback transport"), + } + let (_reader, mut writer) = stream.into_split(); let line = DaemonAuthPreface::new(TOKEN).to_line().unwrap(); writer.write_all(line.as_bytes()).await.unwrap(); writer.write_all(b"\n").await.unwrap(); diff --git a/crates/tracedecay-daemon-service/BUILD.bazel b/crates/tracedecay-daemon-service/BUILD.bazel index 82a5916c20..2bbaf7a566 100644 --- a/crates/tracedecay-daemon-service/BUILD.bazel +++ b/crates/tracedecay-daemon-service/BUILD.bazel @@ -128,7 +128,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-daemon-service/src/remote_http_transport.rs b/crates/tracedecay-daemon-service/src/remote_http_transport.rs index 9ad88d4ec5..901430055c 100644 --- a/crates/tracedecay-daemon-service/src/remote_http_transport.rs +++ b/crates/tracedecay-daemon-service/src/remote_http_transport.rs @@ -1119,12 +1119,10 @@ mod tests { let certificate_chain = directory.join("chain.pem"); let private_key = directory.join("key.pem"); std::fs::write(&certificate_chain, CERTIFICATE_CHAIN).unwrap(); - std::fs::write(&private_key, PRIVATE_KEY).unwrap(); - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(&private_key, std::fs::Permissions::from_mode(0o600)).unwrap(); - } + tracedecay_private_fs::create_private_file(&private_key) + .unwrap() + .write_all(PRIVATE_KEY) + .unwrap(); let config = RemoteBrainTlsConfig::from_optional_parts( Some(SocketAddr::from(([127, 0, 0, 1], 0))), Some(certificate_chain), diff --git a/crates/tracedecay-dashboard-api/BUILD.bazel b/crates/tracedecay-dashboard-api/BUILD.bazel index 2aef6b2247..7c65637ecc 100644 --- a/crates/tracedecay-dashboard-api/BUILD.bazel +++ b/crates/tracedecay-dashboard-api/BUILD.bazel @@ -97,7 +97,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-domain/BUILD.bazel b/crates/tracedecay-domain/BUILD.bazel index 9fa68a88c0..3a95939e58 100644 --- a/crates/tracedecay-domain/BUILD.bazel +++ b/crates/tracedecay-domain/BUILD.bazel @@ -110,7 +110,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-framing/BUILD.bazel b/crates/tracedecay-framing/BUILD.bazel index 674dc9fc89..fd37410507 100644 --- a/crates/tracedecay-framing/BUILD.bazel +++ b/crates/tracedecay-framing/BUILD.bazel @@ -65,7 +65,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-global-db/BUILD.bazel b/crates/tracedecay-global-db/BUILD.bazel index f611b9e94e..ac196f875c 100644 --- a/crates/tracedecay-global-db/BUILD.bazel +++ b/crates/tracedecay-global-db/BUILD.bazel @@ -184,7 +184,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-global-db/src/observation_projection/state.rs b/crates/tracedecay-global-db/src/observation_projection/state.rs index cf9333f022..000b8b83ea 100644 --- a/crates/tracedecay-global-db/src/observation_projection/state.rs +++ b/crates/tracedecay-global-db/src/observation_projection/state.rs @@ -1,5 +1,7 @@ use std::borrow::Cow; use std::collections::{BTreeSet, HashMap}; +#[cfg(windows)] +use std::path::{Component, Path, Prefix}; use tracedecay_domain::{ CanonicalObservationIdV1, DurableObservationV1, ObservationSourceGenerationV1, PayloadDigestV1, @@ -18,6 +20,8 @@ use tracedecay_lcm::{LcmError, LcmStorageKind}; use tracedecay_runtime_core::db::Database; use tracedecay_runtime_core::db::engine::{Executor, IntoParams, QueryExecutor, Row, params}; use tracedecay_sessions::runtime::shared::durable_project_path_key; +#[cfg(windows)] +use tracedecay_sessions::runtime::shared::path_identity_key; use tracedecay_sessions::runtime::store_access::{ message_record_from_row, session_record_from_row, }; @@ -1237,11 +1241,19 @@ pub(super) fn reconcile_session_rows_detailed( .chain(expected.started_at) .min(), ended_at: actual.ended_at.into_iter().chain(expected.ended_at).max(), - transcript_path: reconcile_optional( - "transcript_path", - actual.transcript_path.as_ref(), - expected.transcript_path.as_ref(), - )?, + transcript_path: match (&actual.transcript_path, &expected.transcript_path) { + #[cfg(windows)] + (Some(actual), Some(expected)) + if actual != expected && existing_windows_transcript_alias(actual, expected) => + { + Some(actual.clone()) + } + _ => reconcile_optional( + "transcript_path", + actual.transcript_path.as_ref(), + expected.transcript_path.as_ref(), + )?, + }, metadata_json: reconcile_metadata( actual.metadata_json.as_ref(), expected.metadata_json.as_ref(), @@ -1269,6 +1281,33 @@ fn project_key_is_directory(session: &SessionRecord) -> bool { session.project_key == session.project_path } +#[cfg(windows)] +fn existing_windows_transcript_alias(actual: &str, expected: &str) -> bool { + if path_identity_key(actual) != path_identity_key(expected) { + return false; + } + let actual = Path::new(actual); + let expected = Path::new(expected); + // The lexical key only selects candidates; it cannot prove identity. + // Do not turn arbitrary UNC annotations into network probes. + if ![actual, expected].into_iter().all(|path| { + path.is_absolute() + && matches!(path.components().next(), Some(Component::Prefix(prefix)) + if matches!(prefix.kind(), Prefix::Disk(_) | Prefix::VerbatimDisk(_))) + }) { + return false; + } + // Keep the native canonical spelling. Stripping its verbatim prefix can + // reinterpret literal Windows names before the identity comparison. + match (actual.canonicalize(), expected.canonicalize()) { + (Ok(actual), Ok(expected)) if actual.as_os_str() == expected.as_os_str() => { + actual.is_file() + } + // Missing evidence follows the ordinary typed collision refusal. + _ => false, + } +} + fn reconcile_optional( field: &'static str, actual: Option<&T>, @@ -1461,6 +1500,8 @@ pub(super) async fn protected_message_rows_compatible( #[allow(clippy::unwrap_used)] mod reconcile_tests { use std::collections::BTreeSet; + #[cfg(windows)] + use std::path::Path; use crate::tests::harness::RegisteredGlobalDbHarness; use tracedecay_domain::{ @@ -1472,6 +1513,9 @@ mod reconcile_tests { }; #[cfg(unix)] use tracedecay_runtime_core::db::engine::params; + #[cfg(windows)] + use tracedecay_runtime_core::path_safety::plain_host_path; + use tracedecay_sessions::runtime::shared::path_identity_key; use tracedecay_store::{ ObservationProjection, ProjectionStoreError, SessionMessageRecord, SessionRecord, session_project_fields, @@ -1701,6 +1745,234 @@ mod reconcile_tests { ); } + #[test] + fn transcript_paths_require_exact_stored_evidence() { + let sparse = record("user"); + let mut actual = sparse.clone(); + actual.transcript_path = Some(r"C:\sessions\rollout.jsonl".to_owned()); + assert_eq!( + reconcile_session_rows_detailed(&actual, &actual).unwrap(), + actual + ); + for (stored, expected) in [(&sparse, &actual), (&actual, &sparse)] { + assert_eq!( + reconcile_session_rows_detailed(stored, expected).unwrap(), + actual + ); + } + + let mut conflicting = actual.clone(); + conflicting.transcript_path = Some("c:/sessions/other.jsonl".to_owned()); + assert_eq!( + reconcile_session_rows_detailed(&actual, &conflicting) + .unwrap_err() + .field(), + "transcript_path" + ); + } + + #[cfg(windows)] + fn windows_transcript_spellings(root: &Path, name: &str) -> (String, String) { + let verbatim = root.canonicalize().unwrap().join(name); + ( + plain_host_path(&verbatim).to_str().unwrap().to_owned(), + verbatim.to_str().unwrap().to_owned(), + ) + } + + #[cfg(windows)] + #[test] + fn existing_transcript_disk_aliases_retain_the_stored_spelling() { + let fixture = tempfile::tempdir().unwrap(); + let (native, verbatim) = windows_transcript_spellings(fixture.path(), "rollout.jsonl"); + std::fs::write(&verbatim, b"transcript").unwrap(); + let actual = SessionRecord { + transcript_path: Some(native.clone()), + ..record("user") + }; + let expected = SessionRecord { + transcript_path: Some(verbatim.clone()), + ..actual.clone() + }; + let slash_spelling = SessionRecord { + transcript_path: Some(native.replace('\\', "/")), + ..actual.clone() + }; + for (stored, incoming) in [ + (&actual, &expected), + (&expected, &actual), + (&actual, &slash_spelling), + (&slash_spelling, &actual), + ] { + assert_eq!( + reconcile_session_rows_detailed(stored, incoming) + .unwrap() + .transcript_path, + stored.transcript_path + ); + } + + std::fs::remove_file(&verbatim).unwrap(); + assert_eq!( + reconcile_session_rows_detailed(&actual, &expected) + .unwrap_err() + .field(), + "transcript_path" + ); + std::fs::create_dir(&verbatim).unwrap(); + assert_eq!( + reconcile_session_rows_detailed(&actual, &expected) + .unwrap_err() + .field(), + "transcript_path" + ); + } + + #[cfg(windows)] + #[test] + fn dos_and_verbatim_trailing_dot_paths_to_distinct_files_collide() { + let fixture = tempfile::tempdir().unwrap(); + let (native, literal) = windows_transcript_spellings(fixture.path(), "rollout.jsonl."); + let ordinary = fixture.path().join("rollout.jsonl"); + std::fs::write(&ordinary, b"ordinary").unwrap(); + std::fs::write(&literal, b"literal dot").unwrap(); + assert_eq!(std::fs::read(&native).unwrap(), b"ordinary"); + assert_eq!(std::fs::read(&literal).unwrap(), b"literal dot"); + let actual = SessionRecord { + transcript_path: Some(native), + ..record("user") + }; + let expected = SessionRecord { + transcript_path: Some(literal.clone()), + ..actual.clone() + }; + for (stored, incoming) in [(&actual, &expected), (&expected, &actual)] { + assert_eq!( + reconcile_session_rows_detailed(stored, incoming) + .unwrap_err() + .field(), + "transcript_path" + ); + } + std::fs::remove_file(literal).unwrap(); + } + + #[test] + fn distinct_transcript_spellings_with_the_same_path_key_collide() { + #[cfg(windows)] + let fixture = tempfile::tempdir().unwrap(); + for (actual_path, expected_path) in [ + ( + r"C:\sessions\rollout.jsonl", + r"\\?\C:\sessions\rollout.jsonl", + ), + ( + r"C:\sessions\rollout.jsonl.", + r"\\?\C:\sessions\rollout.jsonl.", + ), + ( + r"C:\sessions\rollout.jsonl ", + r"\\?\C:\sessions\rollout.jsonl ", + ), + ( + r"//sessions/archive\rollout.jsonl", + "//sessions/archive/rollout.jsonl", + ), + ] { + #[cfg(windows)] + let (actual_path, expected_path) = match actual_path.strip_prefix("C:\\sessions\\") { + Some(name) => windows_transcript_spellings(fixture.path(), name), + None => (actual_path.to_owned(), expected_path.to_owned()), + }; + #[cfg(not(windows))] + let (actual_path, expected_path) = (actual_path.to_owned(), expected_path.to_owned()); + assert_ne!(actual_path, expected_path); + assert_eq!( + path_identity_key(&actual_path), + path_identity_key(&expected_path), + "the lexical key is not evidence of native transcript identity" + ); + let actual = SessionRecord { + transcript_path: Some(actual_path.to_owned()), + ..record("user") + }; + let expected = SessionRecord { + transcript_path: Some(expected_path.to_owned()), + ..actual.clone() + }; + for (stored, incoming) in [(&actual, &expected), (&expected, &actual)] { + assert_eq!( + reconcile_session_rows_detailed(stored, incoming) + .unwrap_err() + .field(), + "transcript_path" + ); + } + } + } + + #[asupersync::test] + async fn applying_transcript_key_collision_preserves_the_stored_session() { + let harness = RegisteredGlobalDbHarness::open("transcript-key-collision").await; + #[cfg(windows)] + let fixture = tempfile::tempdir().unwrap(); + #[cfg(windows)] + let (original_path, incoming_path) = + windows_transcript_spellings(fixture.path(), "rollout.jsonl."); + #[cfg(not(windows))] + let (original_path, incoming_path) = ( + r"C:\sessions\rollout.jsonl.".to_owned(), + r"\\?\C:\sessions\rollout.jsonl.".to_owned(), + ); + let original = SessionRecord { + transcript_path: Some(original_path), + ..record("user") + }; + let transaction = harness.registered.begin_write_transaction().await.unwrap(); + super::super::apply::apply_session(&transaction, &original) + .await + .unwrap(); + transaction.commit().await.unwrap(); + let persisted = harness + .registered + .get_session(&original.provider, &original.session_id) + .await + .unwrap() + .unwrap(); + assert_eq!(persisted, original); + + let incoming = SessionRecord { + transcript_path: Some(incoming_path), + title: Some("Rejected transcript annotation".to_owned()), + ended_at: Some(3), + ..original.clone() + }; + let transaction = harness.registered.begin_write_transaction().await.unwrap(); + let error = super::super::apply::apply_session(&transaction, &incoming) + .await + .expect_err("distinct transcript evidence must not enrich the stored session"); + assert!(matches!( + error, + ProjectionStoreError::SessionOutputCollision { + provider, + session_id, + field, + } if provider == original.provider + && session_id == original.session_id + && field == "transcript_path" + )); + // Commit so rollback cannot hide a partial update before the refusal. + transaction.commit().await.unwrap(); + + let after = harness + .registered + .get_session(&original.provider, &original.session_id) + .await + .unwrap() + .unwrap(); + assert_eq!(after, persisted); + } + #[test] fn a_scope_shell_row_takes_the_rollout_working_directory() { for (scope, key) in [ diff --git a/crates/tracedecay-global-db/src/project_registry/tests.rs b/crates/tracedecay-global-db/src/project_registry/tests.rs index b40bb4c825..5e634b9a1c 100644 --- a/crates/tracedecay-global-db/src/project_registry/tests.rs +++ b/crates/tracedecay-global-db/src/project_registry/tests.rs @@ -12,6 +12,7 @@ use std::path::{Path, PathBuf}; use tracedecay_runtime_core::db::engine::{ Executor, IntoParams, QueryExecutor, Result as EngineResult, Rows, TestConnection, params, }; +use tracedecay_runtime_core::path_safety::plain_git_args; use crate::RegisteredGlobalDb; use crate::tests::harness::{RegisteredGlobalDbHarness, RegisteredGlobalDbTestRuntime}; @@ -657,7 +658,7 @@ async fn dashboard_membership_returns_only_requested_registered_projects() { fn git(dir: &Path, args: &[&str]) { let output = std::process::Command::new("git") - .args(args) + .args(plain_git_args(args)) .current_dir(dir) .env("GIT_CONFIG_GLOBAL", "/dev/null") .env("GIT_CONFIG_SYSTEM", "/dev/null") @@ -770,7 +771,11 @@ async fn full_clone_adopts_a_shallow_first_project_and_enriches_its_history() { let (original, full) = cloned_repository(&harness, "shallow-first"); std::fs::write(original.join("lib.rs"), "pub fn next() {}\n").unwrap(); git(&original, &["commit", "-q", "-am", "advance history"]); - let url = format!("file://{}/", original.display()); + let origin = tracedecay_runtime_core::path_safety::plain_host_path(&original); + let url = format!( + "file:///{}/", + tracedecay_domain::forward_slash_path(&origin).trim_start_matches('/') + ); git(&full, &["remote", "set-url", "origin", &url]); let shallow = original.with_file_name("shallow-first-shallow"); git( diff --git a/crates/tracedecay-graph-db/BUILD.bazel b/crates/tracedecay-graph-db/BUILD.bazel index d4ef5fe73f..eae8c4e32a 100644 --- a/crates/tracedecay-graph-db/BUILD.bazel +++ b/crates/tracedecay-graph-db/BUILD.bazel @@ -262,7 +262,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-graph-query/BUILD.bazel b/crates/tracedecay-graph-query/BUILD.bazel index 6319b428e7..d0bfd6d2c5 100644 --- a/crates/tracedecay-graph-query/BUILD.bazel +++ b/crates/tracedecay-graph-query/BUILD.bazel @@ -113,7 +113,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-hooks/BUILD.bazel b/crates/tracedecay-hooks/BUILD.bazel index e144c111ca..cc9eb69c9e 100644 --- a/crates/tracedecay-hooks/BUILD.bazel +++ b/crates/tracedecay-hooks/BUILD.bazel @@ -80,7 +80,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-host-admission/BUILD.bazel b/crates/tracedecay-host-admission/BUILD.bazel index 01831693bf..64c9ae8ea3 100644 --- a/crates/tracedecay-host-admission/BUILD.bazel +++ b/crates/tracedecay-host-admission/BUILD.bazel @@ -128,7 +128,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-host-integration/BUILD.bazel b/crates/tracedecay-host-integration/BUILD.bazel index 0a58ff4b83..5d41670cbe 100644 --- a/crates/tracedecay-host-integration/BUILD.bazel +++ b/crates/tracedecay-host-integration/BUILD.bazel @@ -65,7 +65,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-lcm/BUILD.bazel b/crates/tracedecay-lcm/BUILD.bazel index 8571a6d369..b8d21146b9 100644 --- a/crates/tracedecay-lcm/BUILD.bazel +++ b/crates/tracedecay-lcm/BUILD.bazel @@ -96,7 +96,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-lsp/BUILD.bazel b/crates/tracedecay-lsp/BUILD.bazel index f4a6ebc631..4ec5da2321 100644 --- a/crates/tracedecay-lsp/BUILD.bazel +++ b/crates/tracedecay-lsp/BUILD.bazel @@ -96,7 +96,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-maintenance/BUILD.bazel b/crates/tracedecay-maintenance/BUILD.bazel index e888a32ce8..18a8ca4efb 100644 --- a/crates/tracedecay-maintenance/BUILD.bazel +++ b/crates/tracedecay-maintenance/BUILD.bazel @@ -97,7 +97,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-maintenance/src/telemetry.rs b/crates/tracedecay-maintenance/src/telemetry.rs index b96225957b..52b5741bb6 100644 --- a/crates/tracedecay-maintenance/src/telemetry.rs +++ b/crates/tracedecay-maintenance/src/telemetry.rs @@ -1398,6 +1398,7 @@ mod store_file_identity_tests { .collect() } + #[cfg(not(windows))] #[asupersync::test] async fn a_mounted_store_path_reports_replacement_and_removal() { let temporary = tempfile::tempdir().expect("identity fixture root"); @@ -1438,4 +1439,34 @@ mod store_file_identity_tests { [(database_path.clone(), StoreFileIdentityStateV1::Missing)] ); } + + #[cfg(windows)] + #[asupersync::test] + async fn a_mounted_windows_store_refuses_replacement_and_removal() { + let temporary = tempfile::tempdir().expect("identity fixture root"); + let database_path = temporary.path().join("project.db"); + let (database, scope) = super::table_growth_tests::growth_fixture(&database_path).await; + let registry = StoreTelemetrySamplingRegistry::default(); + assert!(registry.register_port(&database_path, &scope, || { + database.storage_telemetry_handle() + })); + let replacement = temporary.path().join("replacement.db"); + std::fs::write(&replacement, b"replacement").expect("replacement file"); + // SQLite's Windows handles exclude deletion while the store is open. + let replacement_error = std::fs::rename(&replacement, &database_path).unwrap_err(); + assert!( + matches!(replacement_error.raw_os_error(), Some(5 | 32)), + "{replacement_error}" + ); + let removal_error = std::fs::remove_file(&database_path).unwrap_err(); + assert!( + matches!(removal_error.raw_os_error(), Some(5 | 32)), + "{removal_error}" + ); + assert_eq!( + state_of(®istry, &database_path), + [(database_path, StoreFileIdentityStateV1::Current)] + ); + assert_eq!(std::fs::read(replacement).unwrap(), b"replacement"); + } } diff --git a/crates/tracedecay-mcp-catalog/BUILD.bazel b/crates/tracedecay-mcp-catalog/BUILD.bazel index c5824c1f91..2a93cc658e 100644 --- a/crates/tracedecay-mcp-catalog/BUILD.bazel +++ b/crates/tracedecay-mcp-catalog/BUILD.bazel @@ -65,7 +65,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-mcp/BUILD.bazel b/crates/tracedecay-mcp/BUILD.bazel index 06b3fc7d7b..779871bbd3 100644 --- a/crates/tracedecay-mcp/BUILD.bazel +++ b/crates/tracedecay-mcp/BUILD.bazel @@ -171,7 +171,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-mcp/src/scope.rs b/crates/tracedecay-mcp/src/scope.rs index 48d916006b..a1fd1a9c8d 100644 --- a/crates/tracedecay-mcp/src/scope.rs +++ b/crates/tracedecay-mcp/src/scope.rs @@ -172,6 +172,7 @@ mod tests { use std::process::Command; use tempfile::TempDir; + use tracedecay_runtime_core::path_safety::{canonical_existing_identity, plain_git_args}; use super::{QueryScopeError, resolve_query_scope as resolve_query_scope_with_root}; use tracedecay_global_db::{CodeProjectRecord, ProjectRegistryContext}; @@ -186,7 +187,7 @@ mod tests { fn git(root: &Path, args: &[&str]) { let output = Command::new("git") .current_dir(root) - .args(args) + .args(plain_git_args(args)) .output() .expect("git runs"); assert!( @@ -236,7 +237,7 @@ mod tests { #[test] fn subdirectory_request_converges_to_registered_canonical_root() { let temp = TempDir::new().unwrap(); - let root = temp.path().canonicalize().unwrap(); + let root = canonical_existing_identity(temp.path()).unwrap(); init_repo(&root); write_identity_marker(&root, "project.mcp-scope-test"); let subdir = root.join("src/deep"); @@ -282,7 +283,7 @@ mod tests { .unwrap() .unwrap(); let (root, scope) = resolve_query_scope_with_root(&owner, &subdirectory).unwrap(); - assert_eq!(root, package); + assert_eq!(root, canonical_existing_identity(&package).unwrap()); assert_eq!(scope.project_id.as_str(), project_id); assert_eq!(scope, resolve_query_scope(&owner, &package).unwrap()); assert_ne!( @@ -366,7 +367,7 @@ mod tests { "feature", ], ); - let linked = linked.canonicalize().unwrap(); + let linked = canonical_existing_identity(&linked).unwrap(); let owner = owner_for(&root, "project.mcp-scope-test"); let main_scope = resolve_query_scope(&owner, &root).unwrap(); diff --git a/crates/tracedecay-policy/BUILD.bazel b/crates/tracedecay-policy/BUILD.bazel index 6159ec7c50..3b6c7f8f0e 100644 --- a/crates/tracedecay-policy/BUILD.bazel +++ b/crates/tracedecay-policy/BUILD.bazel @@ -80,7 +80,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-privacy/BUILD.bazel b/crates/tracedecay-privacy/BUILD.bazel index c7130dc4d9..cccdaf2358 100644 --- a/crates/tracedecay-privacy/BUILD.bazel +++ b/crates/tracedecay-privacy/BUILD.bazel @@ -80,7 +80,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-private-fs/BUILD.bazel b/crates/tracedecay-private-fs/BUILD.bazel index b40493ae65..f95a5d5f59 100644 --- a/crates/tracedecay-private-fs/BUILD.bazel +++ b/crates/tracedecay-private-fs/BUILD.bazel @@ -95,7 +95,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-private-fs/src/capability_dir.rs b/crates/tracedecay-private-fs/src/capability_dir.rs index b8a8369153..c0c846fb8f 100644 --- a/crates/tracedecay-private-fs/src/capability_dir.rs +++ b/crates/tracedecay-private-fs/src/capability_dir.rs @@ -1,6 +1,6 @@ //! Capability-relative durable directory primitives shared by the quarantine //! and retirement authorities: atomic no-replace rename between already-open -//! parent capabilities, directory metadata sync, recursive no-follow removal, +//! parent capabilities, sibling replacement, directory metadata sync, recursive no-follow removal, //! and create-or-open of one entry that survives the Darwin create race. //! Every mutation is relative to an open `Dir` handle so a parent path //! swapped for a symlink cannot redirect the operation. @@ -117,6 +117,35 @@ fn dir_entry_path(parent: &Dir, name: &OsStr) -> io::Result Ok(dir_path(parent)?.join(name)) } +/// Atomically replaces a sibling file beneath a held parent capability. +/// Windows uses the shared namespace-durable rename authority, including its +/// NTFS fallback for replacement while delete-sharing readers remain open. +pub fn replace_file(parent: &Dir, source: &OsStr, destination: &OsStr) -> io::Result<()> { + for name in [source, destination] { + let mut components = std::path::Path::new(name).components(); + if !matches!(components.next(), Some(std::path::Component::Normal(_))) + || components.next().is_some() + { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "file replacement requires sibling entry names", + )); + } + } + #[cfg(windows)] + { + let source = dir_entry_path(parent, source)?; + let destination = dir_entry_path(parent, destination)?; + crate::windows::rename_paths_durable( + &source, + &destination, + crate::windows::RenameMode::Replace, + ) + } + #[cfg(not(windows))] + parent.rename(source, parent, destination) +} + /// Resolves an open directory capability back to its live filesystem path so /// path-based Win32 primitives can address entries below it. #[cfg(windows)] @@ -206,6 +235,74 @@ pub fn remove_open_dir_all_nofollow(directory: Dir) -> io::Result<()> { } } +#[cfg(test)] +mod replacement_tests { + use std::io::Read; + + use super::*; + + #[test] + fn replacement_publishes_complete_bytes_and_refuses_parent_traversal() { + let root = tempfile::tempdir().unwrap(); + let parent = Dir::open_ambient_dir(root.path(), cap_std::ambient_authority()).unwrap(); + std::fs::write(root.path().join("published"), b"before").unwrap(); + std::fs::write(root.path().join("staged"), b"after").unwrap(); + let mut reader = std::fs::File::open(root.path().join("published")).unwrap(); + let error = + replace_file(&parent, OsStr::new("staged"), OsStr::new("../escape")).unwrap_err(); + assert_eq!(error.kind(), io::ErrorKind::InvalidInput); + assert_eq!( + std::fs::read(root.path().join("published")).unwrap(), + b"before" + ); + assert_eq!(std::fs::read(root.path().join("staged")).unwrap(), b"after"); + replace_file(&parent, OsStr::new("staged"), OsStr::new("published")).unwrap(); + assert_eq!( + std::fs::read(root.path().join("published")).unwrap(), + b"after" + ); + let mut displaced = Vec::new(); + reader.read_to_end(&mut displaced).unwrap(); + assert_eq!(displaced, b"before"); + assert!(!root.path().join("staged").exists()); + } + + #[cfg(windows)] + #[test] + fn no_replace_publication_preserves_an_occupied_entry() { + let root = tempfile::tempdir().unwrap(); + let parent = Dir::open_ambient_dir(root.path(), cap_std::ambient_authority()).unwrap(); + std::fs::write(root.path().join("staged"), b"owned").unwrap(); + std::fs::write(root.path().join("published"), b"foreign").unwrap(); + let error = rename_noreplace( + &parent, + OsStr::new("staged"), + &parent, + OsStr::new("published"), + ) + .unwrap_err(); + assert_eq!(error.kind(), io::ErrorKind::AlreadyExists); + assert_eq!(std::fs::read(root.path().join("staged")).unwrap(), b"owned"); + assert_eq!( + std::fs::read(root.path().join("published")).unwrap(), + b"foreign" + ); + std::fs::remove_file(root.path().join("published")).unwrap(); + rename_noreplace( + &parent, + OsStr::new("staged"), + &parent, + OsStr::new("published"), + ) + .unwrap(); + assert_eq!( + std::fs::read(root.path().join("published")).unwrap(), + b"owned" + ); + assert!(!root.path().join("staged").exists()); + } +} + #[cfg(all( test, any(target_os = "macos", all(target_os = "linux", target_env = "gnu")) diff --git a/crates/tracedecay-private-fs/src/windows.rs b/crates/tracedecay-private-fs/src/windows.rs index 0ee2c63dff..8530636607 100644 --- a/crates/tracedecay-private-fs/src/windows.rs +++ b/crates/tracedecay-private-fs/src/windows.rs @@ -600,6 +600,11 @@ fn open_with_private_creation_acl( fn protect_existing(file: &File, path: &Path, kind: PathKind) -> io::Result<()> { let current_user = current_user_sid() .map_err(|error| wrap_error("resolve current Windows user SID", path, error))?; + // Rewriting an already exact DACL performs filesystem metadata writes on + // every hook callback. Inspect the opened object before hardening it. + if private_security_is_exact(&security_snapshot(file, path, ¤t_user)?, kind) { + return Ok(()); + } let acl = private_acl(¤t_user, kind.inheritance()) .map_err(|error| wrap_error("build private Windows DACL", path, error))?; // SAFETY: the file handle, SID, and ACL are valid and remain live for the call. @@ -1080,6 +1085,16 @@ fn validate_private_security( current_user: &OwnedSid, ) -> io::Result<()> { let snapshot = security_snapshot(file, path, current_user)?; + if !private_security_is_exact(&snapshot, kind) { + return Err(rejected( + path, + &format!("private Windows DACL validation failed: {snapshot:?}"), + )); + } + Ok(()) +} + +fn private_security_is_exact(snapshot: &SecuritySnapshot, kind: PathKind) -> bool { let acl = AclSnapshot { ace_count: snapshot.ace_count, ace_is_allowed: snapshot.ace_is_allowed, @@ -1087,16 +1102,9 @@ fn validate_private_security( ace_inheritance: snapshot.ace_inheritance, trustee_is_current_user: snapshot.trustee_is_current_user, }; - let valid = snapshot.owner_is_current_user + snapshot.owner_is_current_user && snapshot.dacl_is_protected - && acl_is_exact(&acl, kind.inheritance()); - if !valid { - return Err(rejected( - path, - &format!("private Windows DACL validation failed: {snapshot:?}"), - )); - } - Ok(()) + && acl_is_exact(&acl, kind.inheritance()) } fn security_snapshot( @@ -1480,6 +1488,50 @@ mod tests { drop(open_private_directory(&path).unwrap()); } + #[test] + fn exact_private_security_needs_no_metadata_write_access() { + let temp = tempfile::tempdir().unwrap(); + for kind in [PathKind::Directory, PathKind::File] { + let path = temp.path().join(kind.description()); + match kind { + PathKind::Directory => create_private_directory(&path).unwrap(), + PathKind::File => drop(create_private_file(&path).unwrap()), + } + let before = snapshot(&path, kind); + let read_only = open_handle_with_share( + &path, + OPEN_EXISTING, + SECURITY_ACCESS, + null(), + SHARE_READ_WRITE_DELETE, + ) + .unwrap(); + // This handle cannot set ownership or the DACL. A redundant + // SetSecurityInfo would fail even though the object is private. + protect_existing(&read_only, &path, kind).unwrap(); + assert_eq!(snapshot(&path, kind), before); + } + } + + #[test] + fn nonprivate_security_still_requires_metadata_write_authority() { + let temp = tempfile::tempdir().unwrap(); + let path = temp.path().join("ordinary"); + std::fs::write(&path, b"secret").unwrap(); + let before = snapshot(&path, PathKind::File); + let read_only = open_handle_with_share( + &path, + OPEN_EXISTING, + SECURITY_ACCESS, + null(), + SHARE_READ_WRITE_DELETE, + ) + .unwrap(); + assert!(protect_existing(&read_only, &path, PathKind::File).is_err()); + assert_eq!(snapshot(&path, PathKind::File), before); + assert!(validate_private_file(&path).is_err()); + } + #[test] fn permissive_existing_file_is_rejected_without_acl_rewrite() { let temp = tempfile::tempdir().unwrap(); diff --git a/crates/tracedecay-project/BUILD.bazel b/crates/tracedecay-project/BUILD.bazel index 4399e856fc..e96b9c7d10 100644 --- a/crates/tracedecay-project/BUILD.bazel +++ b/crates/tracedecay-project/BUILD.bazel @@ -98,7 +98,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-query/BUILD.bazel b/crates/tracedecay-query/BUILD.bazel index 77a562baa4..d176717b29 100644 --- a/crates/tracedecay-query/BUILD.bazel +++ b/crates/tracedecay-query/BUILD.bazel @@ -297,7 +297,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-runtime-core/BUILD.bazel b/crates/tracedecay-runtime-core/BUILD.bazel index 148a2deb5b..e2f4c82f40 100644 --- a/crates/tracedecay-runtime-core/BUILD.bazel +++ b/crates/tracedecay-runtime-core/BUILD.bazel @@ -213,7 +213,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-runtime-core/src/db/connection.rs b/crates/tracedecay-runtime-core/src/db/connection.rs index a6c0478c64..00c0ff22a4 100644 --- a/crates/tracedecay-runtime-core/src/db/connection.rs +++ b/crates/tracedecay-runtime-core/src/db/connection.rs @@ -136,7 +136,7 @@ static DATABASE_HEALTH_GATE: OnceLock>> = OnceLo /// lane. It is another capability over the same physical attachment, never a /// second path-derived `SQLite` open. pub struct DatabaseWriterConnection<'a> { - _guard: asupersync::sync::OwnedMutexGuard<()>, + _guard: asupersync::sync::OwnedSemaphorePermit, conn: Connection, _client_guard: DatabaseClientGuardV1, _database: std::marker::PhantomData<&'a Database>, @@ -241,7 +241,7 @@ pub enum DatabaseMemoryTransaction<'a> { /// transaction commits, rolls back, or is dropped. pub struct DatabaseWriteTransaction<'a> { transaction: Transaction, - guard: asupersync::sync::OwnedMutexGuard<()>, + guard: asupersync::sync::OwnedSemaphorePermit, _client_guard: DatabaseClientGuardV1, _database: std::marker::PhantomData<&'a Database>, } diff --git a/crates/tracedecay-runtime-core/src/db/connection/query_write.rs b/crates/tracedecay-runtime-core/src/db/connection/query_write.rs index 70239cac64..ef5e4a038b 100644 --- a/crates/tracedecay-runtime-core/src/db/connection/query_write.rs +++ b/crates/tracedecay-runtime-core/src/db/connection/query_write.rs @@ -172,7 +172,7 @@ impl Database { &self, ) -> std::pin::Pin< Box< - dyn std::future::Future>> + dyn std::future::Future> + Send + '_, >, @@ -181,12 +181,16 @@ impl Database { let cx = crate::native_runtime::cx().ok_or_else(|| TraceDecayError::Config { message: "database writer admission requires an Asupersync context".to_owned(), })?; - asupersync::sync::OwnedMutexGuard::lock(std::sync::Arc::clone(&self.inner.writer), &cx) - .await - .map_err(|error| TraceDecayError::Database { - message: format!("database writer admission failed: {error}"), - operation: "acquire database writer lane".to_owned(), - }) + asupersync::sync::OwnedSemaphorePermit::acquire( + std::sync::Arc::clone(&self.inner.writer), + &cx, + 1, + ) + .await + .map_err(|error| TraceDecayError::Database { + message: format!("database writer admission failed: {error}"), + operation: "acquire database writer lane".to_owned(), + }) }) } diff --git a/crates/tracedecay-runtime-core/src/db/connection/registry.rs b/crates/tracedecay-runtime-core/src/db/connection/registry.rs index be699cfd76..b02e24030a 100644 --- a/crates/tracedecay-runtime-core/src/db/connection/registry.rs +++ b/crates/tracedecay-runtime-core/src/db/connection/registry.rs @@ -249,8 +249,9 @@ pub(super) struct DatabaseInner { runtime: DatabaseRuntimeAttachment, /// Descriptor-derived identity reported by the physical attachment. pub(super) opened_file_identity: u64, - /// Serializes logical writers sharing this canonical database slot. - pub(super) writer: Arc>, + /// Serializes admission, while each transaction owns its SQL state and rollback. + /// A caller panic must release capacity rather than poison a unit-valued lock. + pub(super) writer: Arc, /// Coalesces and retains background memory-graph catch-up for this exact /// relational attachment. pub(super) memory_graph_reconciliation: MemoryGraphReconciliationCoordinatorV1, @@ -379,7 +380,7 @@ impl DatabaseInner { canonical_path: runtime.client().canonical_path().to_path_buf(), runtime, opened_file_identity, - writer: Arc::new(asupersync::sync::Mutex::new(())), + writer: Arc::new(asupersync::sync::Semaphore::new(1)), memory_graph_reconciliation: MemoryGraphReconciliationCoordinatorV1::default(), memory_graph_reconciliation_telemetry: Arc::new( ProjectMemoryReconciliationTelemetryV1::default(), diff --git a/crates/tracedecay-runtime-core/src/db/connection/tests.rs b/crates/tracedecay-runtime-core/src/db/connection/tests.rs index dd0fa2e074..49caed124a 100644 --- a/crates/tracedecay-runtime-core/src/db/connection/tests.rs +++ b/crates/tracedecay-runtime-core/src/db/connection/tests.rs @@ -536,6 +536,75 @@ async fn cancelled_write_transaction_rolls_back_before_releasing_lane() { transaction.commit().await.unwrap(); } +#[asupersync::test] +async fn panicked_write_transaction_rolls_back_and_releases_admission() { + let temp = tempfile::tempdir().unwrap(); + let path = temp.path().join("graph.db"); + let authority = DatabaseAuthority::acquire_test(&path, "panicked writer").unwrap(); + let (db, _) = Database::publish_fixture_runtime( + &path, + &authority, + TestDatabaseRuntimeMode::Initialize, + TestRuntimeShardFamilyV1::Code, + None, + ) + .await + .unwrap(); + db.writer_connection("seed panicked writer") + .await + .unwrap() + .execute_batch("CREATE TABLE panicked_writer (value INTEGER NOT NULL); INSERT INTO panicked_writer VALUES (0);") + .await + .unwrap(); + let caller_database = db.clone(); + let caller: crate::native_runtime::NativeTask<()> = + crate::native_runtime::NativeTask::spawn(async move { + let transaction = caller_database + .begin_write_transaction("panicked update") + .await + .unwrap(); + transaction + .execute("UPDATE panicked_writer SET value = 1", ()) + .await + .unwrap(); + panic!("injected writer caller panic"); + }) + .unwrap(); + assert!(matches!( + caller.wait().await, + crate::native_runtime::NativeTaskOutcome::Panicked(_) + )); + + let transaction = asupersync::time::timeout( + Cx::current().unwrap().now(), + std::time::Duration::from_secs(2), + db.begin_write_transaction("writer after panic"), + ) + .await + .expect("writer admission must recover after caller panic") + .unwrap(); + let mut rows = transaction + .query("SELECT value FROM panicked_writer", ()) + .await + .unwrap(); + assert_eq!( + rows.next().await.unwrap().unwrap().get::(0).unwrap(), + 0 + ); + drop(rows); + transaction + .execute("UPDATE panicked_writer SET value = 2", ()) + .await + .unwrap(); + transaction.commit().await.unwrap(); + assert_eq!( + db.query_scalar_i64("read recovered writer", "SELECT value FROM panicked_writer") + .await + .unwrap(), + 2 + ); +} + #[asupersync::test] async fn checkpoint_waits_for_shared_writer_lane() { let temp = tempfile::tempdir().unwrap(); diff --git a/crates/tracedecay-runtime-core/src/git.rs b/crates/tracedecay-runtime-core/src/git.rs index 3a11ece085..451637ad8a 100644 --- a/crates/tracedecay-runtime-core/src/git.rs +++ b/crates/tracedecay-runtime-core/src/git.rs @@ -767,8 +767,11 @@ pub fn normalize_git_remote_url(remote: &str) -> Option { /// names, or `None` for a network remote. #[must_use] pub fn local_remote_path(remote: &str) -> Option { - let remote = remote.trim(); - let path = Path::new(remote.strip_prefix("file://").unwrap_or(remote)); + let remote = gix::url::parse(remote.trim()).ok()?; + if remote.scheme != gix::url::Scheme::File || remote.host.is_some() { + return None; + } + let path = Path::new(std::str::from_utf8(&remote.path).ok()?); path.is_absolute().then(|| path.to_path_buf()) } diff --git a/crates/tracedecay-runtime-core/src/storage/identity_tests.rs b/crates/tracedecay-runtime-core/src/storage/identity_tests.rs index 04e6b00510..39e722b922 100644 --- a/crates/tracedecay-runtime-core/src/storage/identity_tests.rs +++ b/crates/tracedecay-runtime-core/src/storage/identity_tests.rs @@ -345,12 +345,28 @@ mod identity_anchor_tests { crate::git::normalize_git_remote_url("file:///srv/repos/remote.git/"), crate::git::normalize_git_remote_url("/srv/repos/remote.git"), ); + } + + #[test] + fn local_origins_resolve_native_absolute_paths_and_file_urls() { + #[cfg(unix)] + let (path, url) = ("/srv/repos/checkout", "file:///srv/repos/checkout"); + #[cfg(windows)] + let (path, url) = ("C:/repos/checkout", "file:///C:/repos/checkout"); + for spelling in [path, url] { + assert_eq!( + crate::git::local_remote_path(spelling), + Some(PathBuf::from(path)), + "{spelling}" + ); + } assert_eq!( - crate::git::local_remote_path("file:///srv/repos/checkout"), - Some(PathBuf::from("/srv/repos/checkout")) + crate::git::local_remote_path("git@github.com:owner/repo.git"), + None ); + assert_eq!(crate::git::local_remote_path("relative/checkout"), None); assert_eq!( - crate::git::local_remote_path("git@github.com:owner/repo.git"), + crate::git::local_remote_path("file://server/repos/checkout"), None ); } diff --git a/crates/tracedecay-runtime-core/src/storage/paths_and_io.rs b/crates/tracedecay-runtime-core/src/storage/paths_and_io.rs index 75d4530d7d..bcb7981c4f 100644 --- a/crates/tracedecay-runtime-core/src/storage/paths_and_io.rs +++ b/crates/tracedecay-runtime-core/src/storage/paths_and_io.rs @@ -1,10 +1,14 @@ use std::ffi::{OsStr, OsString}; use std::fs; use std::io::{self, Write}; +#[cfg(windows)] +use std::io::{Seek, SeekFrom}; use std::path::{Component, Path, PathBuf}; use tracedecay_private_fs::FileLease; use tracedecay_private_fs::framed_log::{DirectorySyncPolicy, set_owner_private_file_mode}; +#[cfg(windows)] +use tracedecay_private_fs::{create_private_file, make_private_file, open_private_file}; use crate::config; use tracedecay_domain::errors::{Result, TraceDecayError}; @@ -212,14 +216,37 @@ impl PrivateStoreIo { /// (see [`append_line_locked`]). fn append_line_data(path: &Path, line: &str) -> io::Result<()> { reject_symlink_components(path, "private store file")?; - let mut options = fs::OpenOptions::new(); - options.append(true); - // One canonical handle: the append file itself, no buffer wrapper. - let mut file = Self::open_private(path, &mut options)?; + #[cfg(windows)] + let mut file = { + // Harden before writing and retain that exact handle. Reopening a + // just-modified file for WRITE_OWNER/WRITE_DAC stalls native hooks. + let mut file = match open_private_file(path) { + Ok(file) => file, + Err(error) if error.kind() == io::ErrorKind::NotFound => create_private_file(path)?, + Err(error) if error.kind() == io::ErrorKind::PermissionDenied => { + make_private_file(path)? + } + Err(error) => return Err(error), + }; + // The shared sidecar lease serializes this seek and append. + file.seek(SeekFrom::End(0))?; + file + }; + #[cfg(not(windows))] + let mut file = { + let mut options = fs::OpenOptions::new(); + options.append(true); + Self::open_private(path, &mut options)? + }; file.write_all(format!("{line}\n").as_bytes())?; file.flush()?; - drop(file); - set_owner_private_file_mode(path) + #[cfg(not(windows))] + { + drop(file); + set_owner_private_file_mode(path) + } + #[cfg(windows)] + Ok(()) } /// Opens `path` for writing, creating it if missing with owner-only diff --git a/crates/tracedecay-runtime-core/src/storage/tests.rs b/crates/tracedecay-runtime-core/src/storage/tests.rs index 50c3f2ca48..f93fc04a37 100644 --- a/crates/tracedecay-runtime-core/src/storage/tests.rs +++ b/crates/tracedecay-runtime-core/src/storage/tests.rs @@ -503,6 +503,8 @@ mod tests { !meta.permissions().readonly(), "appended data file must stay writable" ); + #[cfg(windows)] + tracedecay_private_fs::validate_private_file(&path).unwrap(); #[cfg(unix)] { @@ -518,4 +520,19 @@ mod tests { PrivateStoreIo::append_line(&path, "{\"a\":3}").unwrap(); assert_eq!(std::fs::read_to_string(&path).unwrap().lines().count(), 3); } + + #[cfg(windows)] + #[test] + fn append_line_hardens_an_existing_windows_file_and_preserves_its_bytes() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().canonicalize().unwrap().join("legacy.jsonl"); + std::fs::write(&path, "{\"before\":true}\n").unwrap(); + assert!(tracedecay_private_fs::validate_private_file(&path).is_err()); + PrivateStoreIo::append_line(&path, "{\"after\":true}").unwrap(); + tracedecay_private_fs::validate_private_file(&path).unwrap(); + assert_eq!( + std::fs::read_to_string(&path).unwrap(), + "{\"before\":true}\n{\"after\":true}\n" + ); + } } diff --git a/crates/tracedecay-rusqlite-runtime/BUILD.bazel b/crates/tracedecay-rusqlite-runtime/BUILD.bazel index 4523831b08..9a94627e72 100644 --- a/crates/tracedecay-rusqlite-runtime/BUILD.bazel +++ b/crates/tracedecay-rusqlite-runtime/BUILD.bazel @@ -125,7 +125,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-sdk/BUILD.bazel b/crates/tracedecay-sdk/BUILD.bazel index 8bacfecee7..b3c8cbdf1a 100644 --- a/crates/tracedecay-sdk/BUILD.bazel +++ b/crates/tracedecay-sdk/BUILD.bazel @@ -109,7 +109,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-search-eval/BUILD.bazel b/crates/tracedecay-search-eval/BUILD.bazel index e09aae133c..b7bbea00e9 100644 --- a/crates/tracedecay-search-eval/BUILD.bazel +++ b/crates/tracedecay-search-eval/BUILD.bazel @@ -155,7 +155,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-session-memory/BUILD.bazel b/crates/tracedecay-session-memory/BUILD.bazel index 8b73cf0a94..6b3054d9f8 100644 --- a/crates/tracedecay-session-memory/BUILD.bazel +++ b/crates/tracedecay-session-memory/BUILD.bazel @@ -113,7 +113,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-session-memory/src/context/registered_scope.rs b/crates/tracedecay-session-memory/src/context/registered_scope.rs index d3e1b85649..ccdf555512 100644 --- a/crates/tracedecay-session-memory/src/context/registered_scope.rs +++ b/crates/tracedecay-session-memory/src/context/registered_scope.rs @@ -77,6 +77,7 @@ fn resolve_authorized_root( requested_root.display() )) })?; + let checkout = canonical_root(&checkout, "Git checkout")?; // An independently enrolled package owns its path-local identity; // discovery of the enclosing repository cannot widen that scope. if requested_root.starts_with(®istered_root) diff --git a/crates/tracedecay-session-runtime/BUILD.bazel b/crates/tracedecay-session-runtime/BUILD.bazel index c63058c622..4f828cab2c 100644 --- a/crates/tracedecay-session-runtime/BUILD.bazel +++ b/crates/tracedecay-session-runtime/BUILD.bazel @@ -249,7 +249,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-session-temporal-store/BUILD.bazel b/crates/tracedecay-session-temporal-store/BUILD.bazel index 9115bc930c..585440d73f 100644 --- a/crates/tracedecay-session-temporal-store/BUILD.bazel +++ b/crates/tracedecay-session-temporal-store/BUILD.bazel @@ -114,7 +114,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-sessions/BUILD.bazel b/crates/tracedecay-sessions/BUILD.bazel index 4c18689377..ce842c49d8 100644 --- a/crates/tracedecay-sessions/BUILD.bazel +++ b/crates/tracedecay-sessions/BUILD.bazel @@ -122,7 +122,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-sessions/src/runtime/hosts/claude/cursor.rs b/crates/tracedecay-sessions/src/runtime/hosts/claude/cursor.rs index c372523168..98530c80c9 100644 --- a/crates/tracedecay-sessions/src/runtime/hosts/claude/cursor.rs +++ b/crates/tracedecay-sessions/src/runtime/hosts/claude/cursor.rs @@ -1,5 +1,7 @@ use std::ffi::OsStr; use std::path::Path; +#[cfg(windows)] +use std::path::PathBuf; use tracedecay_capture::claude::{ encode_cursor_key, encode_source_id, observation_path_source_id, observation_source_id, @@ -13,6 +15,10 @@ pub(super) fn claude_source_id(path: &Path) -> Option { pub(super) fn claude_observation_path_source_id(projects_dir: &Path, path: &Path) -> String { let relative = path.strip_prefix(projects_dir).unwrap_or(path); + // Windows discovery uses native separators; caller spellings must name + // the same source without changing the discovered path's durable bytes. + #[cfg(windows)] + let relative = relative.components().collect::(); observation_path_source_id(&tracedecay_runtime_core::os_str_bytes::native_os_str_bytes( relative.as_os_str(), )) diff --git a/crates/tracedecay-sessions/src/runtime/hosts/claude/tests.rs b/crates/tracedecay-sessions/src/runtime/hosts/claude/tests.rs index 81afac59b8..6f674b6af9 100644 --- a/crates/tracedecay-sessions/src/runtime/hosts/claude/tests.rs +++ b/crates/tracedecay-sessions/src/runtime/hosts/claude/tests.rs @@ -186,6 +186,27 @@ fn non_utf8_paths_that_render_identically_have_distinct_cursor_keys() { assert_ne!(first_identity.source_id, second_identity.source_id); } +#[cfg(windows)] +#[test] +fn observation_source_identity_preserves_native_bytes_across_separator_spellings() { + let root = Path::new(r"C:\profiles\.claude\projects"); + let native = root.join("account").join("session.jsonl"); + let alternate = PathBuf::from(native.to_str().unwrap().replace('\\', "/")); + let identity = identify_claude_source(root, &native).unwrap(); + assert_eq!( + identify_claude_source(root, &alternate).unwrap().source_id, + identity.source_id + ); + assert_eq!( + identity.source_id, + tracedecay_capture::claude::observation_path_source_id( + &tracedecay_runtime_core::os_str_bytes::native_os_str_bytes( + Path::new(r"account\session.jsonl").as_os_str() + ) + ) + ); +} + #[cfg(unix)] #[test] fn observation_source_ids_are_private_and_follow_the_projects_relative_path() { diff --git a/crates/tracedecay-sessions/src/runtime/hosts/claude_observation/tests.rs b/crates/tracedecay-sessions/src/runtime/hosts/claude_observation/tests.rs index c321a62261..3f9719650b 100644 --- a/crates/tracedecay-sessions/src/runtime/hosts/claude_observation/tests.rs +++ b/crates/tracedecay-sessions/src/runtime/hosts/claude_observation/tests.rs @@ -725,8 +725,9 @@ async fn protected_source_identity_reuses_cursor_across_admission_handoff() { .expect("reopened protected source cursor"); assert!(cursor.byte_offset() > 0); let durable = serde_json::to_string(cursor.source()).unwrap(); + let relative = PathBuf::from("project-scope").join(format!("{raw_session_id}.jsonl")); let path_key = tracedecay_capture::claude::observation_path_source_id( - format!("project-scope/{raw_session_id}.jsonl").as_bytes(), + &tracedecay_runtime_core::os_str_bytes::native_os_str_bytes(relative.as_os_str()), ); assert_eq!(identity.source_id, path_key); assert_eq!( diff --git a/crates/tracedecay-sessions/src/runtime/hosts/codex/observation/scope_resolution_tests.rs b/crates/tracedecay-sessions/src/runtime/hosts/codex/observation/scope_resolution_tests.rs index 0b17cedc1d..9f445ed29b 100644 --- a/crates/tracedecay-sessions/src/runtime/hosts/codex/observation/scope_resolution_tests.rs +++ b/crates/tracedecay-sessions/src/runtime/hosts/codex/observation/scope_resolution_tests.rs @@ -9,6 +9,7 @@ use tempfile::TempDir; use tracedecay_runtime_core::git_discovery::{ GitRepositoryIdentityOutcome, discover_repository_identity_cli_first, }; +use tracedecay_runtime_core::path_safety::plain_git_args; use super::*; use crate::admission::test_support::MemoryHostAdmission; @@ -181,7 +182,7 @@ async fn scope_cache_retains_captured_absence_across_nested_profile_scopes() { fn git_fixture(project: &Path, args: &[&str]) { let output = std::process::Command::new(tracedecay_runtime_core::git::try_git_program().unwrap()) - .args(args) + .args(plain_git_args(args)) .current_dir(project) .output() .unwrap(); diff --git a/crates/tracedecay-sessions/src/runtime/hosts/pi.rs b/crates/tracedecay-sessions/src/runtime/hosts/pi.rs index ebee062b68..8e50ad9f86 100644 --- a/crates/tracedecay-sessions/src/runtime/hosts/pi.rs +++ b/crates/tracedecay-sessions/src/runtime/hosts/pi.rs @@ -104,7 +104,10 @@ fn file_stamp(metadata: &std::fs::Metadata) -> PathFileStamp { let (mtime_secs, inode) = { use std::os::unix::fs::MetadataExt; ( - metadata.mtime().unsigned_abs().saturating_mul(1_000_000_000) + metadata + .mtime() + .unsigned_abs() + .saturating_mul(1_000_000_000) + metadata.mtime_nsec().unsigned_abs(), metadata.ino(), ) @@ -923,8 +926,12 @@ async fn admit_scheduled_file( // re-enter admission, or every restart re-pays the whole corpus. let stamp = { let path_buf = path.to_path_buf(); - run_blocking_transcript_section("pi", move || std::fs::metadata(&path_buf).ok().map(|meta| file_stamp(&meta))) - .await? + run_blocking_transcript_section("pi", move || { + std::fs::metadata(&path_buf) + .ok() + .map(|meta| file_stamp(&meta)) + }) + .await? }; if let Some(stamp) = stamp { if path_memo_matches(facade, scope, PI_COMPLETE_MEMO_PREFIX, path, stamp).await { diff --git a/crates/tracedecay-sessions/src/runtime/ingest/tests.rs b/crates/tracedecay-sessions/src/runtime/ingest/tests.rs index dae95cbca8..0c77d203e6 100644 --- a/crates/tracedecay-sessions/src/runtime/ingest/tests.rs +++ b/crates/tracedecay-sessions/src/runtime/ingest/tests.rs @@ -617,6 +617,8 @@ async fn unchanged_codex_contract_failure_is_quarantined_without_reopening() { std::fs::create_dir_all(&dir).unwrap(); let path = dir.join("rollout-2026-10-09T10-00-00-invalid.jsonl"); std::fs::write(&path, "{}\n").unwrap(); + // Directory discovery emits native components, including on Windows. + let path = path.components().collect::(); let settled = crate::runtime::source::spin_until_jsonl_change_settled(&path); let hub = crate::runtime::hosts::codex::CodexDiscoveryHub::default(); hub.register("project", Some(home.path())); diff --git a/crates/tracedecay-sessions/src/runtime/source/jsonl.rs b/crates/tracedecay-sessions/src/runtime/source/jsonl.rs index 9e0f6c6ca2..c01c44006b 100644 --- a/crates/tracedecay-sessions/src/runtime/source/jsonl.rs +++ b/crates/tracedecay-sessions/src/runtime/source/jsonl.rs @@ -384,11 +384,7 @@ impl HoldUnchangedGenerationCache { #[cfg(test)] pub(in crate::runtime) fn spin_until_jsonl_change_settled(path: &Path) -> bool { for _ in 0..10_000_000u32 { - let Ok(file) = std::fs::File::open(path) else { - std::thread::yield_now(); - continue; - }; - let Ok(metadata) = file.metadata() else { + let Ok(metadata) = std::fs::metadata(path) else { std::thread::yield_now(); continue; }; diff --git a/crates/tracedecay-source-edit/BUILD.bazel b/crates/tracedecay-source-edit/BUILD.bazel index b4923fd412..42369756ac 100644 --- a/crates/tracedecay-source-edit/BUILD.bazel +++ b/crates/tracedecay-source-edit/BUILD.bazel @@ -97,7 +97,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-store-runtime/BUILD.bazel b/crates/tracedecay-store-runtime/BUILD.bazel index 758283fce3..07dfd2afbb 100644 --- a/crates/tracedecay-store-runtime/BUILD.bazel +++ b/crates/tracedecay-store-runtime/BUILD.bazel @@ -97,7 +97,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-store-runtime/src/remote_replay_transaction/tests.rs b/crates/tracedecay-store-runtime/src/remote_replay_transaction/tests.rs index 99aecfe594..18f207a386 100644 --- a/crates/tracedecay-store-runtime/src/remote_replay_transaction/tests.rs +++ b/crates/tracedecay-store-runtime/src/remote_replay_transaction/tests.rs @@ -183,6 +183,7 @@ fn admitted( matches!(outcome, Ok(RemoteReplayTransactionOutcomeV1::Admitted(_))) } +#[cfg(target_os = "linux")] fn remote_replay_threads() -> usize { std::fs::read_dir("/proc/self/task") .expect("thread listing") @@ -242,6 +243,7 @@ async fn replay_and_fence_read_awaited_on_a_native_worker_never_park_it() { reader_waits, "no reader wait parked a native worker" ); + #[cfg(target_os = "linux")] assert_eq!( remote_replay_threads(), 0, diff --git a/crates/tracedecay-store/BUILD.bazel b/crates/tracedecay-store/BUILD.bazel index 4ded0bc74b..78609cff1a 100644 --- a/crates/tracedecay-store/BUILD.bazel +++ b/crates/tracedecay-store/BUILD.bazel @@ -81,7 +81,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-temporal-query/BUILD.bazel b/crates/tracedecay-temporal-query/BUILD.bazel index 81cd5b1a5d..7b80d00686 100644 --- a/crates/tracedecay-temporal-query/BUILD.bazel +++ b/crates/tracedecay-temporal-query/BUILD.bazel @@ -65,7 +65,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-tokenizer/BUILD.bazel b/crates/tracedecay-tokenizer/BUILD.bazel index 6fea40a96f..478398f45a 100644 --- a/crates/tracedecay-tokenizer/BUILD.bazel +++ b/crates/tracedecay-tokenizer/BUILD.bazel @@ -65,7 +65,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay-tool-catalog/BUILD.bazel b/crates/tracedecay-tool-catalog/BUILD.bazel index d2f9c8c20c..f49a7568d8 100644 --- a/crates/tracedecay-tool-catalog/BUILD.bazel +++ b/crates/tracedecay-tool-catalog/BUILD.bazel @@ -80,7 +80,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay/BUILD.bazel b/crates/tracedecay/BUILD.bazel index 5748e8d7a0..ff030c60f0 100644 --- a/crates/tracedecay/BUILD.bazel +++ b/crates/tracedecay/BUILD.bazel @@ -489,7 +489,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/crates/tracedecay/src/daemon/hook_v2_replay_consumer.rs b/crates/tracedecay/src/daemon/hook_v2_replay_consumer.rs index e5dd4ba6bf..c4456b06fd 100644 --- a/crates/tracedecay/src/daemon/hook_v2_replay_consumer.rs +++ b/crates/tracedecay/src/daemon/hook_v2_replay_consumer.rs @@ -95,9 +95,8 @@ struct PendingWorkRedriveState { terminal: bool, } -static PENDING_WORK_REDRIVE_GATE: std::sync::OnceLock< - std::sync::Mutex, -> = std::sync::OnceLock::new(); +static PENDING_WORK_REDRIVE_GATE: std::sync::OnceLock> = + std::sync::OnceLock::new(); fn pending_work_redrive_gate() -> &'static std::sync::Mutex { PENDING_WORK_REDRIVE_GATE @@ -393,8 +392,9 @@ async fn redrive_pending_work( ); } } else { - state.next_eligible_micros = - now.0.saturating_add(pending_work_redrive_backoff(state.attempts)); + state.next_eligible_micros = now + .0 + .saturating_add(pending_work_redrive_backoff(state.attempts)); } } else { let mut gate = pending_work_redrive_gate() diff --git a/crates/tracedecay/src/daemon/maintenance.rs b/crates/tracedecay/src/daemon/maintenance.rs index 3fbaf49efe..30aebd8aee 100644 --- a/crates/tracedecay/src/daemon/maintenance.rs +++ b/crates/tracedecay/src/daemon/maintenance.rs @@ -1537,7 +1537,7 @@ mod tests { } #[tokio::test] - async fn a_mounted_store_deleted_under_the_daemon_requires_a_restart() { + async fn mounted_store_identity_tracks_native_removal_outcomes() { let temporary = tempfile::tempdir().expect("store identity fixture root"); let profile_root = temporary.path().join("profile"); let identity = tracedecay_daemon_identity::profile_identity::load_or_create(&profile_root) @@ -1594,13 +1594,25 @@ mod tests { "a live store is not lost" ); - std::fs::remove_file(&database_path).expect("delete the mounted store"); - super::record_lost_store_files(&sampling, ®istry); - assert_eq!( - lost(®istry), - [tracedecay_domain::errors::DAEMON_RESTART_COMMAND.to_owned()], - "a store deleted under the daemon must be reset-required, naming the restart" - ); + #[cfg(windows)] + { + let error = std::fs::remove_file(&database_path) + .expect_err("Windows must refuse deletion of a mounted SQLite store"); + assert!(matches!(error.raw_os_error(), Some(5 | 32)), "{error}"); + super::record_lost_store_files(&sampling, ®istry); + assert!(database_path.is_file()); + assert!(lost(®istry).is_empty()); + } + #[cfg(not(windows))] + { + std::fs::remove_file(&database_path).expect("delete the mounted store"); + super::record_lost_store_files(&sampling, ®istry); + assert_eq!( + lost(®istry), + [tracedecay_domain::errors::DAEMON_RESTART_COMMAND.to_owned()], + "a store deleted under the daemon must be reset-required, naming the restart" + ); + } } #[tokio::test] diff --git a/crates/tracedecay/src/mcp/server/hook_boundary_failure_matrix_tests.rs b/crates/tracedecay/src/mcp/server/hook_boundary_failure_matrix_tests.rs index 964eafe370..32635f1ac9 100644 --- a/crates/tracedecay/src/mcp/server/hook_boundary_failure_matrix_tests.rs +++ b/crates/tracedecay/src/mcp/server/hook_boundary_failure_matrix_tests.rs @@ -284,7 +284,9 @@ async fn matrix_unavailable_then_success_keeps_sticky_retained_failure_frontier( #[tokio::test] async fn after_edit_hook_delivers_touched_paths_to_code_index_sink() { let (cg, project, authority) = init_indexed_repo().await; - let expected_root = cg.project_root().to_path_buf(); + let expected_root = + tracedecay_runtime_core::path_safety::canonical_existing_identity(cg.project_root()) + .unwrap(); type RecordedDeliveries = Arc)>>>; let recorded: RecordedDeliveries = Arc::new(Mutex::new(Vec::new())); let sink_recorded = Arc::clone(&recorded); diff --git a/crates/tracedecay/src/mcp/server/hook_dispatch.rs b/crates/tracedecay/src/mcp/server/hook_dispatch.rs index 326180247a..2c85d3d7d1 100644 --- a/crates/tracedecay/src/mcp/server/hook_dispatch.rs +++ b/crates/tracedecay/src/mcp/server/hook_dispatch.rs @@ -2,6 +2,7 @@ //! and hook-event plan execution. use super::*; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; impl McpServer { /// Authorizes and admits one branch reconciliation without waiting for indexing. @@ -85,6 +86,8 @@ impl McpServer { scope: &tracedecay_contracts::ResolvedScope, ) -> std::result::Result { + let effect_root = canonical_existing_identity(effect_root) + .map_err(|_| HostAdmissionOutcome::degraded("project_route_stale"))?; let Some(registry) = self.registry_db.as_deref() else { return Err(HostAdmissionOutcome::retained_unavailable( "project_registry_route_unavailable", @@ -103,13 +106,13 @@ impl McpServer { } }; let (root, current_scope) = - match tracedecay_mcp::scope::resolve_query_scope(&owner, effect_root) { + match tracedecay_mcp::scope::resolve_query_scope(&owner, &effect_root) { Ok(resolved) => resolved, Err(_) => return Err(HostAdmissionOutcome::degraded("project_route_stale")), }; // Ref is mutable checkout state. Fixed route identity must still match // exactly; indexing reconciles the canonical current branch below. - if root.as_path() != effect_root || !current_scope.identifies_same_checkout(scope) { + if root != effect_root || !current_scope.identifies_same_checkout(scope) { return Err(HostAdmissionOutcome::degraded("project_route_stale")); } let resolved = match crate::mcp::project_route::resolve_registered_project_route( @@ -177,7 +180,11 @@ impl McpServer { ); } let (cg, branch) = server.reopen_if_branch_drifted_memoized().await; - if cg.project_root() != root { + let graph_root = match canonical_existing_identity(cg.project_root()) { + Ok(root) => root, + Err(_) => return HostAdmissionOutcome::degraded("project_route_stale"), + }; + if graph_root != root { return HostAdmissionOutcome::degraded("project_route_stale"); } if let Some(seq) = admission_seq { diff --git a/crates/tracedecay/src/mcp/server/host_admission_tests.rs b/crates/tracedecay/src/mcp/server/host_admission_tests.rs index f6c689c697..c82e25ba32 100644 --- a/crates/tracedecay/src/mcp/server/host_admission_tests.rs +++ b/crates/tracedecay/src/mcp/server/host_admission_tests.rs @@ -21,6 +21,7 @@ use tracedecay_host_admission::{ }; use tracedecay_private_fs::framed_log::sync_latency; use tracedecay_project::test_support::host_admission::HostAdmissionTestRuntimeV1; +use tracedecay_runtime_core::path_safety::canonical_existing_identity; use tracedecay_session_memory::event_lane::{self, ActivityFamilyV1}; use tracedecay_sessions::admission::{ HostAdmissionOutcome, HostAdmissionScope, HostAdmissionStatus, @@ -194,6 +195,7 @@ async fn file_edit_commit_failure_queues_no_sync() { #[tokio::test] async fn file_edit_request_acks_durable_work_while_effect_is_held() { let (cg, project, authority) = init_indexed_repo().await; + let project_root = canonical_existing_identity(project.path()).unwrap(); let mut live_activity = event_lane::subscribe().unwrap(); let spool = TempDir::new().unwrap(); let broker = default_spool_broker(spool.path()); @@ -210,7 +212,7 @@ async fn file_edit_request_acks_durable_work_while_effect_is_held() { let entered = Arc::clone(&entered); let release = Arc::clone(&release); let effects = Arc::clone(&effects); - let expected_root = project.path().canonicalize().unwrap(); + let expected_root = project_root.clone(); Arc::new(move |root, paths| { assert_eq!(root, expected_root); assert_eq!(paths, ["src/lib.rs"]); @@ -263,7 +265,7 @@ async fn file_edit_request_acks_durable_work_while_effect_is_held() { let mut queued_activity = Vec::new(); while let Ok(record) = live_activity.try_recv() { if record.pulse.family == ActivityFamilyV1::CodeIndex - && record.pulse.project_root == project.path() + && record.pulse.project_root == project_root { queued_activity.push(record); } @@ -291,7 +293,7 @@ async fn incremental_path_hints_emit_activity_only_for_queued_paths() { .with_code_index_reconcile_sink(success_reconcile_sink()) .with_code_index_hook_sink({ let effects = Arc::clone(&effects); - let root = project.path().to_path_buf(); + let root = canonical_existing_identity(project.path()).unwrap(); Arc::new(move |effect_root, paths| { assert_eq!(effect_root, root); assert_eq!(paths, ["src/lib.rs"]); @@ -328,7 +330,7 @@ async fn retained_file_edits_reconcile_branch_drift_without_blocking_the_source( .unwrap(); let effects = Arc::new(AtomicUsize::new(0)); let available = Arc::new(AtomicBool::new(false)); - let root = project.path().canonicalize().unwrap(); + let root = canonical_existing_identity(project.path()).unwrap(); let mut context = registered_context(cg, &authority); context.host_admission_broker = Some(Arc::clone(&broker)); context = context.with_code_index_hook_sink({ @@ -518,7 +520,7 @@ async fn retained_file_edits_replay_once_on_each_exact_linked_worktree() { assert_eq!(decoded_scope, scope); broker.admit(name, &encoded).await.unwrap(); plans.push(decoded); - roots.push(root); + roots.push(canonical_existing_identity(&root).unwrap()); } let mut context = registered_context(cg, &authority); context.host_admission_broker = Some(Arc::clone(&broker)); diff --git a/crates/tracedecay/tests/automation_runner_test/jobs.rs b/crates/tracedecay/tests/automation_runner_test/jobs.rs index 3c693a72df..0aefb1149a 100644 --- a/crates/tracedecay/tests/automation_runner_test/jobs.rs +++ b/crates/tracedecay/tests/automation_runner_test/jobs.rs @@ -624,7 +624,14 @@ async fn user_job_pre_run_command_runs_from_project_root() { fs::write(project_root.join("marker.txt"), "project-marker").unwrap(); let mut job = sample_job("cmd-cwd"); - job.pre_run_command = Some("cat marker.txt".to_string()); + job.pre_run_command = Some( + if cfg!(windows) { + "type marker.txt" + } else { + "cat marker.txt" + } + .to_string(), + ); struct AssertProjectCommandOutputBackend; impl AgentTaskBackend for AssertProjectCommandOutputBackend { diff --git a/crates/tracedecay/tests/hooks_lsp_suite/hook_spool_drain_test.rs b/crates/tracedecay/tests/hooks_lsp_suite/hook_spool_drain_test.rs index 668cf7fe58..c0c4f03676 100644 --- a/crates/tracedecay/tests/hooks_lsp_suite/hook_spool_drain_test.rs +++ b/crates/tracedecay/tests/hooks_lsp_suite/hook_spool_drain_test.rs @@ -17,7 +17,7 @@ use tracedecay_hooks::{ HookDeliverySourceReceiptV1, hook_delivery_receipt_spool_root, }; use tracedecay_runtime_core::logging::format_daemon_log_line; -use tracedecay_runtime_core::path_safety::canonical_existing_identity; +use tracedecay_runtime_core::path_safety::{canonical_existing_identity, plain_host_path}; use tracedecay_runtime_core::storage::default_profile_sharded_layout; use crate::common::{ @@ -518,7 +518,7 @@ fn admitted_edit_sequence(log: &str, root: &Path) -> Option { ("project", root.display().to_string()), ( "paths_digest", - sha256_hex(format!("{:?}", ["src/lib.rs"]).as_bytes()), + sha256_hex(format!("{:?}", [Path::new("src").join("lib.rs")]).as_bytes()), ), ], ); @@ -544,7 +544,7 @@ fn an_edit_in_a_never_opened_linked_worktree_binds_it_and_reaches_the_daemon() { assert!( Command::new(git_program()) .args(["worktree", "add", "-q", "-b", branch]) - .arg(root) + .arg(plain_host_path(root)) .current_dir(&project) .status() .unwrap() diff --git a/scripts/BUILD.bazel b/scripts/BUILD.bazel index a5a420707a..acafb70fad 100644 --- a/scripts/BUILD.bazel +++ b/scripts/BUILD.bazel @@ -208,7 +208,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"], diff --git a/scripts/bazel/gen_builds.py b/scripts/bazel/gen_builds.py index 489e3cbeaf..90d475d6da 100644 --- a/scripts/bazel/gen_builds.py +++ b/scripts/bazel/gen_builds.py @@ -1084,6 +1084,9 @@ def formatting_rules(package, sources_by_edition): # PowerShell executes the generated command as a script. Invoke # the formatter per file there to stay below Windows' argv limit. powershell = ( + # Bazel strips PATHEXT. PowerShell otherwise treats .exe paths + # as documents and leaves LASTEXITCODE unset instead of waiting. + "$$env:PATHEXT += ';.EXE'; " f"foreach ($$source in ('$(locations :{sources})' -split ' ')) {{ " f'& "$(RUSTFMT)" {args} $$source; ' 'if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; ' diff --git a/sdks/codegen/BUILD.bazel b/sdks/codegen/BUILD.bazel index e201407e05..dbf00d0f70 100644 --- a/sdks/codegen/BUILD.bazel +++ b/sdks/codegen/BUILD.bazel @@ -51,7 +51,7 @@ genrule( srcs = [":rustfmt_sources_2024", "//:rustfmt.toml"], outs = ["rustfmt_check_2024.ok"], cmd = "\"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $(locations :rustfmt_sources_2024) && touch \"$@\"", - cmd_ps = "foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", + cmd_ps = "$$env:PATHEXT += ';.EXE'; foreach ($$source in ('$(locations :rustfmt_sources_2024)' -split ' ')) { & \"$(RUSTFMT)\" --config-path \"$(location //:rustfmt.toml)\" --edition 2024 --config skip_children=true --check $$source; if ($$LASTEXITCODE -ne 0) { exit $$LASTEXITCODE } }; New-Item -ItemType File -Path \"$@\" -Force | Out-Null", tools = ["//:rustfmt_toolchain"], toolchains = ["//:rustfmt_toolchain"], tags = ["manual"],