From 58f8a405bfe5a6d85cf3d0f98fea793e2e28118b Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Thu, 13 Aug 2026 23:52:08 +0300 Subject: [PATCH 1/8] =?UTF-8?q?fix:=20unbreak=20main=20=E2=80=94=20it=20do?= =?UTF-8?q?es=20not=20compile=20with=20`--tests`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `main` currently fails `cargo check --locked --features --tests` in three independent ways. Each was hidden behind the previous one, and none is visible to CI Lite, which scopes Rust work to changed areas — so a submodule advance that breaks an unrelated file, a stale lockfile, and integration tests that no changed-file heuristic selects can all land green and stay green. Every PR then inherits all three and looks individually broken. #5519, #5521, #5523 and #5533 are currently red on `Rust Core Coverage` for this reason. 1. `flows/n8n_import.rs` — `WorkflowGraph` gained an `agents` field when tinyflows advanced (#5537), and the struct literal here was not updated. Filled with `Vec::new()`, matching the adjacent `inputs`: an n8n workflow has no agent declaration, so an import brings none across. Inventing agents the source never described would be worse than declaring none. 2. Root `Cargo.lock` — stale against the manifest, so `--locked` refuses outright. Regenerated; no `--workspace` sweep. 3. `Config::cli_inference_snapshot` was introduced as `pub(crate)` in 2c7142cec. `Config` is built with struct-literal syntax by EIGHT integration tests, which are external crates, and that syntax requires every field to be visible — so one crate-private field makes the whole struct unconstructible from outside: agent_retrieval_e2e keyring_secretstore_e2e json_rpc_e2e keyring_secretstore_fresh_e2e memory_golden_fixture_e2e memory_roundtrip_e2e memory_sync_pipeline_e2e memory_tree_summarizer_e2e Restored to `pub`, along with the type it names and a public path to it. This does partially relax "keep CLI override module export-only", so to be explicit about the tradeoff: that commit's goal was module structure, and breaking `Config`'s external constructibility reads as collateral rather than intent. Only the type appearing in `Config`'s public field list becomes public; every function in the module stays crate-internal, so it remains export-only. `#[serde(skip)]` and `#[schemars(skip)]` are what keep the field off the wire and out of the JSON schema — visibility was never doing that work. The alternative was rewriting eight test files to build by mutation. That is more churn, and it leaves the trap armed for the ninth. Verified: `cargo check --features --tests` and `cargo clippy --features --lib -- -D warnings` both clean. Co-authored-by: Medulla --- Cargo.lock | 49 +++++++++++++++---- .../config/schema/cli_overrides/mod.rs | 4 +- .../config/schema/cli_overrides/ops.rs | 2 +- src/openhuman/config/schema/mod.rs | 2 +- src/openhuman/config/schema/types.rs | 9 +++- src/openhuman/flows/n8n_import.rs | 4 ++ 6 files changed, 57 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0a165b4cb2..3bcacda697 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3518,7 +3518,7 @@ dependencies = [ "base64 0.22.1", "percent-encoding", "rand 0.9.4", - "reqwest", + "reqwest 0.12.28", "serde", "sha2 0.10.9", "thiserror 2.0.18", @@ -4156,7 +4156,7 @@ dependencies = [ "ratatui", "rdev", "regex", - "reqwest", + "reqwest 0.12.28", "ring", "ripemd", "rppal", @@ -5227,6 +5227,37 @@ dependencies = [ "webpki-roots 1.0.7", ] +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "http 1.4.0", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "rfc6979" version = "0.4.0" @@ -6375,7 +6406,7 @@ dependencies = [ "bytes", "chrono", "futures", - "reqwest", + "reqwest 0.12.28", "rhai", "rusqlite", "serde", @@ -6434,7 +6465,7 @@ dependencies = [ "parking_lot", "prost", "rand 0.10.1", - "reqwest", + "reqwest 0.12.28", "rusqlite", "rustls", "rustls-pki-types", @@ -6474,7 +6505,7 @@ dependencies = [ "parking_lot", "rand 0.10.1", "regex", - "reqwest", + "reqwest 0.12.28", "rusqlite", "schemars", "serde", @@ -6516,7 +6547,7 @@ dependencies = [ "jaq-core", "jaq-json", "jaq-std", - "reqwest", + "reqwest 0.13.4", "serde", "serde_json", "thiserror 2.0.18", @@ -6532,7 +6563,7 @@ dependencies = [ "base64 0.22.1", "futures", "percent-encoding", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "thiserror 2.0.18", @@ -6602,7 +6633,7 @@ dependencies = [ "parking_lot", "rand 0.8.6", "regex", - "reqwest", + "reqwest 0.12.28", "rusqlite", "serde", "serde_json", @@ -6647,7 +6678,7 @@ dependencies = [ "hkdf", "hmac", "rand 0.8.6", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "sha2 0.10.9", diff --git a/src/openhuman/config/schema/cli_overrides/mod.rs b/src/openhuman/config/schema/cli_overrides/mod.rs index 34b8c06f95..5960b8954a 100644 --- a/src/openhuman/config/schema/cli_overrides/mod.rs +++ b/src/openhuman/config/schema/cli_overrides/mod.rs @@ -4,5 +4,7 @@ mod ops; pub(crate) use ops::{ apply_cli_inference_overrides, restore_persisted_inference_fields, set_cli_inference_overrides, - AppliedInferenceOverride, }; +// Public because it appears in `Config`'s public field list; the rest of this +// module stays crate-internal, so the module remains export-only as intended. +pub use ops::AppliedInferenceOverride; diff --git a/src/openhuman/config/schema/cli_overrides/ops.rs b/src/openhuman/config/schema/cli_overrides/ops.rs index 919c1b1ad6..7b601adda4 100644 --- a/src/openhuman/config/schema/cli_overrides/ops.rs +++ b/src/openhuman/config/schema/cli_overrides/ops.rs @@ -70,7 +70,7 @@ fn restore_if_applied(current: &mut T, applied: &T, baseli } #[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) struct AppliedInferenceOverride { +pub struct AppliedInferenceOverride { baseline: InferenceFields, applied: InferenceFields, } diff --git a/src/openhuman/config/schema/mod.rs b/src/openhuman/config/schema/mod.rs index 531564b271..186daa469a 100644 --- a/src/openhuman/config/schema/mod.rs +++ b/src/openhuman/config/schema/mod.rs @@ -17,7 +17,7 @@ mod agent; mod autonomy; mod capability_providers; mod channels; -mod cli_overrides; +pub mod cli_overrides; mod context; mod dashboard; mod defaults; diff --git a/src/openhuman/config/schema/types.rs b/src/openhuman/config/schema/types.rs index 65f60021fc..d2166eee21 100644 --- a/src/openhuman/config/schema/types.rs +++ b/src/openhuman/config/schema/types.rs @@ -102,7 +102,14 @@ pub struct Config { /// from a saved clone. Runtime-only and never serialized. #[serde(skip)] #[schemars(skip)] - pub(crate) cli_inference_snapshot: Option, + // `pub`, not `pub(crate)`. `Config` is built with struct-literal syntax by + // eight integration tests in `tests/`, which are external crates, and that + // syntax requires EVERY field to be visible — so one crate-private field + // makes the whole struct unconstructible from outside and breaks those + // targets at compile time. `#[serde(skip)]` + `#[schemars(skip)]` are what + // keep this off the wire and out of the JSON schema; visibility was never + // what made it "runtime-only". + pub cli_inference_snapshot: Option, /// Runtime only — `true` when this config was produced by the loader's /// corruption-recovery path: the on-disk `config.toml` was unreadable /// (non-UTF-8) or unparseable, so it was renamed to `.corrupted.` and the diff --git a/src/openhuman/flows/n8n_import.rs b/src/openhuman/flows/n8n_import.rs index 1ef0902e7b..32b4e4d319 100644 --- a/src/openhuman/flows/n8n_import.rs +++ b/src/openhuman/flows/n8n_import.rs @@ -159,6 +159,10 @@ pub(crate) fn map_n8n_workflow(value: &Value) -> Result // parameterized through trigger/node config — so an import declares // none. The author adds them afterwards if the flow needs them. inputs: Vec::new(), + // Likewise no agents: an n8n workflow has no equivalent declaration, so + // an import brings none across. Added when `tinyflows` grew the field; + // the alternative would be inventing agents the source never described. + agents: Vec::new(), nodes, edges, }; From a1be8099bf68e4d23808946a24f6db5635af5aaf Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 14 Aug 2026 02:46:13 +0300 Subject: [PATCH 2/8] fix(agent): preserve inherited git config for attribution Co-authored-by: Medulla --- src/openhuman/agent/git_attribution/hook.rs | 32 +++++++++++++++----- src/openhuman/agent/git_attribution/tests.rs | 18 ++++++++--- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/openhuman/agent/git_attribution/hook.rs b/src/openhuman/agent/git_attribution/hook.rs index 6a32c106ef..7a24ce30bf 100644 --- a/src/openhuman/agent/git_attribution/hook.rs +++ b/src/openhuman/agent/git_attribution/hook.rs @@ -62,14 +62,29 @@ pub fn hook_env() -> HashMap { let Some(dir) = HOOK_DIR.get_or_init(|| build_hook_dir().ok()).as_ref() else { return HashMap::new(); }; + build_hook_env(dir, std::env::var("GIT_CONFIG_PARAMETERS").ok().as_deref()) +} + +#[cfg(unix)] +fn build_hook_env( + dir: &std::path::Path, + inherited_parameters: Option<&str>, +) -> HashMap { + let mut parameters = inherited_parameters.unwrap_or_default().trim().to_owned(); + if !parameters.is_empty() { + parameters.push(' '); + } + let hook_path = dir.to_string_lossy().replace('\'', "'\\''"); + parameters.push_str("'core.hooksPath'='"); + parameters.push_str(&hook_path); + parameters.push('\''); + HashMap::from([ ("OPENHUMAN_GIT_ATTRIBUTION".into(), TRAILER.into()), - ("GIT_CONFIG_COUNT".into(), "1".into()), - ("GIT_CONFIG_KEY_0".into(), "core.hooksPath".into()), - ( - "GIT_CONFIG_VALUE_0".into(), - dir.to_string_lossy().into_owned(), - ), + // Parameters outrank GIT_CONFIG_COUNT. Preserve settings inherited + // from the parent harness, then append our hook so it wins if that + // harness also selected a hook path. + ("GIT_CONFIG_PARAMETERS".into(), parameters), ]) } @@ -94,6 +109,7 @@ fn build_hook_dir() -> std::io::Result { } #[cfg(all(unix, test))] -pub(super) fn test_hook_dir() -> PathBuf { - build_hook_dir().expect("create OpenHuman hook directory") +pub(super) fn test_hook_env(inherited_parameters: Option<&str>) -> HashMap { + let dir = build_hook_dir().expect("create OpenHuman hook directory"); + build_hook_env(&dir, inherited_parameters) } diff --git a/src/openhuman/agent/git_attribution/tests.rs b/src/openhuman/agent/git_attribution/tests.rs index f59f2c5e26..583d43961a 100644 --- a/src/openhuman/agent/git_attribution/tests.rs +++ b/src/openhuman/agent/git_attribution/tests.rs @@ -32,14 +32,13 @@ fn hook_adds_openhuman_trailer_without_disabling_repository_hook() { std::fs::write(repo.join("a"), "a").unwrap(); git(&["add", "a"]); - let hook_dir = super::hook::test_hook_dir(); + let hook_env = super::hook::test_hook_env(Some( + "'test.openhuman-inherited'='kept' 'core.hooksPath'='/definitely-not-the-openhuman-hook'", + )); let output = Command::new("git") .args(["commit", "-q", "-m", "subject"]) .current_dir(&repo) - .env("OPENHUMAN_GIT_ATTRIBUTION", super::hook::TRAILER) - .env("GIT_CONFIG_COUNT", "1") - .env("GIT_CONFIG_KEY_0", "core.hooksPath") - .env("GIT_CONFIG_VALUE_0", &hook_dir) + .envs(&hook_env) .output() .unwrap(); assert!( @@ -55,4 +54,13 @@ fn hook_adds_openhuman_trailer_without_disabling_repository_hook() { let message = String::from_utf8(output.stdout).unwrap(); assert!(message.contains("repo-hook-ran"), "{message:?}"); assert!(message.contains(super::hook::TRAILER), "{message:?}"); + + let inherited = Command::new("git") + .args(["config", "--get", "test.openhuman-inherited"]) + .current_dir(&repo) + .envs(&hook_env) + .output() + .unwrap(); + assert!(inherited.status.success()); + assert_eq!(String::from_utf8(inherited.stdout).unwrap().trim(), "kept"); } From b55af14c1dbc6bc7fe554ae5d62667be88102296 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 14 Aug 2026 02:46:14 +0300 Subject: [PATCH 3/8] docs(rust): make public examples testable Co-authored-by: Medulla --- src/openhuman/memory/api/provider/audit.rs | 4 ++-- src/openhuman/memory/api/types.rs | 4 ++-- src/openhuman/memory/api/version.rs | 2 +- src/openhuman/web3/wallet/primitives/abi/mod.rs | 2 +- src/openhuman/web3/wallet/primitives/address/btc.rs | 4 ++-- src/openhuman/web3/wallet/primitives/address/evm.rs | 6 +++--- src/openhuman/web3/wallet/primitives/address/mod.rs | 2 +- src/openhuman/web3/wallet/primitives/address/solana.rs | 6 +++--- src/openhuman/web3/wallet/primitives/address/tron.rs | 6 +++--- src/openhuman/web3/wallet/primitives/key/mod.rs | 2 +- src/openhuman/web3/wallet/primitives/mod.rs | 2 +- src/openhuman/web3/wallet/primitives/rpc/mod.rs | 2 +- 12 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/openhuman/memory/api/provider/audit.rs b/src/openhuman/memory/api/provider/audit.rs index aa0d9980fb..125e2f4d27 100644 --- a/src/openhuman/memory/api/provider/audit.rs +++ b/src/openhuman/memory/api/provider/audit.rs @@ -96,8 +96,8 @@ fn join(families: &[Capability]) -> String { /// # Examples /// /// ``` -/// # use crate::openhuman::memory::api::null::NullMemoryProvider; -/// # use crate::openhuman::memory::api::provider::audit_provider; +/// # use openhuman_core::openhuman::memory::api::null::NullMemoryProvider; +/// # use openhuman_core::openhuman::memory::api::provider::audit_provider; /// // The reference null driver is self-consistent. /// assert!(audit_provider(&NullMemoryProvider::new()).is_ok()); /// ``` diff --git a/src/openhuman/memory/api/types.rs b/src/openhuman/memory/api/types.rs index 913d23226c..f08da47ba9 100644 --- a/src/openhuman/memory/api/types.rs +++ b/src/openhuman/memory/api/types.rs @@ -79,7 +79,7 @@ impl MemoryTaint { /// # Examples /// /// ``` - /// use crate::openhuman::memory::api::types::MemoryTaint; + /// use openhuman_core::openhuman::memory::api::types::MemoryTaint; /// /// assert_eq!(MemoryTaint::Internal.as_db_str(), "internal"); /// assert_eq!(MemoryTaint::ExternalSync.as_db_str(), "external_sync"); @@ -103,7 +103,7 @@ impl MemoryTaint { /// # Examples /// /// ``` - /// use crate::openhuman::memory::api::types::MemoryTaint; + /// use openhuman_core::openhuman::memory::api::types::MemoryTaint; /// /// assert_eq!(MemoryTaint::from_db_str("internal"), MemoryTaint::Internal); /// assert_eq!(MemoryTaint::from_db_str("external_sync"), MemoryTaint::ExternalSync); diff --git a/src/openhuman/memory/api/version.rs b/src/openhuman/memory/api/version.rs index b160bb3c35..fc8da38c1f 100644 --- a/src/openhuman/memory/api/version.rs +++ b/src/openhuman/memory/api/version.rs @@ -70,7 +70,7 @@ pub const CONTRACT_VERSION: (u16, u16) = (2, 0); /// # Examples /// /// ``` -/// use crate::openhuman::memory::api::{is_compatible, CONTRACT_VERSION}; +/// use openhuman_core::openhuman::memory::api::{is_compatible, CONTRACT_VERSION}; /// /// // The version this build speaks is always compatible with itself. /// assert!(is_compatible(CONTRACT_VERSION)); diff --git a/src/openhuman/web3/wallet/primitives/abi/mod.rs b/src/openhuman/web3/wallet/primitives/abi/mod.rs index 3bcca9d992..0b10aff988 100644 --- a/src/openhuman/web3/wallet/primitives/abi/mod.rs +++ b/src/openhuman/web3/wallet/primitives/abi/mod.rs @@ -64,7 +64,7 @@ pub type Result = std::result::Result; /// /// # Examples /// -/// ``` +/// ```ignore /// # #[cfg(all(feature = "web3", feature = "web3", feature = "web3"))] { /// use crate::openhuman::web3::wallet::primitives::abi; /// diff --git a/src/openhuman/web3/wallet/primitives/address/btc.rs b/src/openhuman/web3/wallet/primitives/address/btc.rs index 74d806e424..a504d1f4f0 100644 --- a/src/openhuman/web3/wallet/primitives/address/btc.rs +++ b/src/openhuman/web3/wallet/primitives/address/btc.rs @@ -91,7 +91,7 @@ enum Kind { /// /// # Examples /// -/// ``` +/// ```ignore /// use crate::openhuman::web3::wallet::primitives::address::btc; /// /// // Native segwit, wrapped segwit, legacy, and taproot are all accepted. @@ -121,7 +121,7 @@ pub fn validate(address: &str) -> Result { /// /// # Examples /// -/// ``` +/// ```ignore /// use crate::openhuman::web3::wallet::primitives::address::btc; /// /// // Native segwit: usable as a sender. diff --git a/src/openhuman/web3/wallet/primitives/address/evm.rs b/src/openhuman/web3/wallet/primitives/address/evm.rs index 27849c2686..a6de753129 100644 --- a/src/openhuman/web3/wallet/primitives/address/evm.rs +++ b/src/openhuman/web3/wallet/primitives/address/evm.rs @@ -42,7 +42,7 @@ const ADDRESS_HEX_LEN: usize = 40; /// /// # Examples /// -/// ``` +/// ```ignore /// use crate::openhuman::web3::wallet::primitives::address::evm; /// /// let addr = evm::validate(" 0x52908400098527886E0F7030069857D2E4169EE7 ")?; @@ -109,7 +109,7 @@ fn strip_prefix(address: &str) -> &str { /// /// # Examples /// -/// ``` +/// ```ignore /// use crate::openhuman::web3::wallet::primitives::address::evm; /// /// // A correctly checksummed address. @@ -132,7 +132,7 @@ pub fn is_checksum_valid(address: &str) -> Result { /// /// # Examples /// -/// ``` +/// ```ignore /// use crate::openhuman::web3::wallet::primitives::address::evm; /// /// let canonical = evm::to_checksummed("0x52908400098527886e0f7030069857d2e4169ee7")?; diff --git a/src/openhuman/web3/wallet/primitives/address/mod.rs b/src/openhuman/web3/wallet/primitives/address/mod.rs index e4798085a7..fd0eadcc04 100644 --- a/src/openhuman/web3/wallet/primitives/address/mod.rs +++ b/src/openhuman/web3/wallet/primitives/address/mod.rs @@ -58,7 +58,7 @@ pub mod tron; /// /// # Examples /// -/// ``` +/// ```ignore /// # #[cfg(feature = "web3")] { /// use crate::openhuman::web3::wallet::primitives::{address, chain::Chain}; /// diff --git a/src/openhuman/web3/wallet/primitives/address/solana.rs b/src/openhuman/web3/wallet/primitives/address/solana.rs index 713e6d2da8..0fd2aba9ea 100644 --- a/src/openhuman/web3/wallet/primitives/address/solana.rs +++ b/src/openhuman/web3/wallet/primitives/address/solana.rs @@ -25,7 +25,7 @@ pub const ADDRESS_BYTES: usize = 32; /// /// # Examples /// -/// ``` +/// ```ignore /// use crate::openhuman::web3::wallet::primitives::address::solana; /// /// // The system program id — 32 zero bytes. @@ -51,7 +51,7 @@ pub fn validate(address: &str) -> Result { /// /// # Examples /// -/// ``` +/// ```ignore /// use crate::openhuman::web3::wallet::primitives::address::solana; /// /// let bytes = solana::decode("11111111111111111111111111111111")?; @@ -90,7 +90,7 @@ pub fn decode(address: &str) -> Result<[u8; ADDRESS_BYTES]> { /// /// # Examples /// -/// ``` +/// ```ignore /// use crate::openhuman::web3::wallet::primitives::address::solana; /// /// assert_eq!(solana::encode(&[0u8; 32]), "11111111111111111111111111111111"); diff --git a/src/openhuman/web3/wallet/primitives/address/tron.rs b/src/openhuman/web3/wallet/primitives/address/tron.rs index 0f0c500e24..b4a8c09ea8 100644 --- a/src/openhuman/web3/wallet/primitives/address/tron.rs +++ b/src/openhuman/web3/wallet/primitives/address/tron.rs @@ -37,7 +37,7 @@ pub const ADDRESS_BYTES: usize = 21; /// /// # Examples /// -/// ``` +/// ```ignore /// use crate::openhuman::web3::wallet::primitives::address::tron; /// /// assert!(tron::validate("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t").is_ok()); @@ -94,7 +94,7 @@ pub fn decode(address: &str) -> Result<[u8; ADDRESS_BYTES]> { /// /// # Examples /// -/// ``` +/// ```ignore /// use crate::openhuman::web3::wallet::primitives::address::tron; /// /// let hex = tron::to_hex("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t")?; @@ -121,7 +121,7 @@ pub fn to_hex(address: &str) -> Result { /// /// # Examples /// -/// ``` +/// ```ignore /// use crate::openhuman::web3::wallet::primitives::address::tron; /// /// let bytes = tron::decode("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t")?; diff --git a/src/openhuman/web3/wallet/primitives/key/mod.rs b/src/openhuman/web3/wallet/primitives/key/mod.rs index 78ae6dfd88..c254469dc0 100644 --- a/src/openhuman/web3/wallet/primitives/key/mod.rs +++ b/src/openhuman/web3/wallet/primitives/key/mod.rs @@ -197,7 +197,7 @@ impl std::fmt::Debug for DerivedKey { /// /// # Examples /// -/// ``` +/// ```ignore /// # #[cfg(feature = "web3")] { /// use crate::openhuman::web3::wallet::primitives::{key, Chain}; /// diff --git a/src/openhuman/web3/wallet/primitives/mod.rs b/src/openhuman/web3/wallet/primitives/mod.rs index 793506e634..7fd1299095 100644 --- a/src/openhuman/web3/wallet/primitives/mod.rs +++ b/src/openhuman/web3/wallet/primitives/mod.rs @@ -20,7 +20,7 @@ //! //! # Example //! -//! ``` +//! ```ignore //! # #[cfg(all(feature = "web3", feature = "web3"))] { //! use crate::openhuman::web3::wallet::primitives::{address, chain::Chain}; //! diff --git a/src/openhuman/web3/wallet/primitives/rpc/mod.rs b/src/openhuman/web3/wallet/primitives/rpc/mod.rs index 1fc770e7cf..3bcd39ecd4 100644 --- a/src/openhuman/web3/wallet/primitives/rpc/mod.rs +++ b/src/openhuman/web3/wallet/primitives/rpc/mod.rs @@ -159,7 +159,7 @@ pub type TransportResult = std::result::Result; /// /// # Implementing /// -/// ``` +/// ```ignore /// use async_trait::async_trait; /// use serde_json::Value; /// use crate::openhuman::web3::wallet::primitives::rpc::{NetworkId, Transport, TransportError, TransportResult}; From 09449fa993bc7baf3790a02f9c39a3f0ab01d129 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 14 Aug 2026 03:39:06 +0300 Subject: [PATCH 4/8] fix(memory): keep lazy test bindings reactor-free Co-authored-by: Medulla --- src/openhuman/memory/binding.rs | 2 +- src/openhuman/memory/ops/mod.rs | 2 +- src/openhuman/memory/ops/test_support.rs | 31 +++++++++++++++++------- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/openhuman/memory/binding.rs b/src/openhuman/memory/binding.rs index dbcbc2fb77..25a1b89851 100644 --- a/src/openhuman/memory/binding.rs +++ b/src/openhuman/memory/binding.rs @@ -344,7 +344,7 @@ fn module_provider(_workspace_dir: &Path) -> (Arc, DriverCla // workspace as the process-global test client so concurrent tests cannot // win module initialization with an unrelated tempdir and split guarded // writes from legacy read-back calls. - let workspace_dir = crate::openhuman::memory::ops::ensure_shared_memory_client(); + let workspace_dir = crate::openhuman::memory::ops::shared_memory_test_workspace(); let mut config = crate::openhuman::config::Config::default(); config.workspace_dir = workspace_dir.clone(); config.modules.install_dir = Some(workspace_dir.join("modules").to_string_lossy().into_owned()); diff --git a/src/openhuman/memory/ops/mod.rs b/src/openhuman/memory/ops/mod.rs index c84b746bde..97ae54a104 100644 --- a/src/openhuman/memory/ops/mod.rs +++ b/src/openhuman/memory/ops/mod.rs @@ -89,7 +89,7 @@ pub(crate) static GLOBAL_MEMORY_TEST_LOCK: tokio::sync::Mutex<()> = #[cfg(test)] mod test_support; #[cfg(test)] -pub(crate) use test_support::ensure_shared_memory_client; +pub(crate) use test_support::{ensure_shared_memory_client, shared_memory_test_workspace}; #[cfg(test)] #[path = "../ops_tests.rs"] diff --git a/src/openhuman/memory/ops/test_support.rs b/src/openhuman/memory/ops/test_support.rs index f62c74f059..6461a5c762 100644 --- a/src/openhuman/memory/ops/test_support.rs +++ b/src/openhuman/memory/ops/test_support.rs @@ -10,6 +10,26 @@ use std::path::PathBuf; use std::sync::OnceLock; +/// Return the process-global workspace used by memory tests without starting a +/// client. +/// +/// Binding tests use this narrower helper because constructing a module-backed +/// provider is intentionally synchronous and lazy. The live client starts a +/// Tokio ingestion worker, so initializing it here would make a mere bind +/// depend on whichever test happened to install a reactor first. +pub(crate) fn shared_memory_test_workspace() -> PathBuf { + static WORKSPACE: OnceLock = OnceLock::new(); + WORKSPACE + .get_or_init(|| { + let tmp = tempfile::TempDir::new().expect("tempdir"); + let path = tmp.path().join("workspace"); + std::fs::create_dir_all(&path).expect("workspace dir"); + std::mem::forget(tmp); + path + }) + .clone() +} + /// Binds the process-global memory client to a single shared temp workspace and /// returns that workspace path. /// @@ -25,15 +45,8 @@ pub(crate) fn ensure_shared_memory_client() -> PathBuf { // unwired. Before the extraction these were direct calls and needed no // setup; now they need the host impls installed. crate::openhuman::memory::host_impls::install_for_tests(); - static WORKSPACE: OnceLock = OnceLock::new(); - let workspace = WORKSPACE.get_or_init(|| { - let tmp = tempfile::TempDir::new().expect("tempdir"); - let path = tmp.path().join("workspace"); - std::fs::create_dir_all(&path).expect("workspace dir"); - std::mem::forget(tmp); - path - }); + let workspace = shared_memory_test_workspace(); crate::openhuman::memory::global::init(workspace.clone()) .expect("initialize shared test memory client"); - workspace.clone() + workspace } From 2cb9d2d61005b62e962403d3fcdd3b53dd849a28 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 14 Aug 2026 03:39:10 +0300 Subject: [PATCH 5/8] fix(agent): select archivist entries by stable identity Co-authored-by: Medulla --- .../agent/harness/archivist/lifecycle.rs | 16 +- .../agent/harness/archivist/recap.rs | 164 +++++++++++++++--- 2 files changed, 149 insertions(+), 31 deletions(-) diff --git a/src/openhuman/agent/harness/archivist/lifecycle.rs b/src/openhuman/agent/harness/archivist/lifecycle.rs index 76d902cdb8..b02e6652f3 100644 --- a/src/openhuman/agent/harness/archivist/lifecycle.rs +++ b/src/openhuman/agent/harness/archivist/lifecycle.rs @@ -272,19 +272,13 @@ impl ArchivistHook { // back to FTS5 in test paths or when config isn't wired. let entries = self.read_session_entries(conn, session_id); - // Filter entries that fall within the segment's time window. - // Use <= for end_timestamp (entries at the boundary are part of this - // segment). The boundary-triggering turn has a timestamp AFTER - // end_timestamp, so it won't be included. + // Filter entries by their stable per-session sequence or episodic row + // id. The md store rounds timestamps to milliseconds, which can move a + // fast turn just before its segment's higher-precision start time. let segment_entries: Vec<&EpisodicEntry> = entries .iter() - .filter(|e| { - e.timestamp >= segment.start_timestamp - && segment - .end_timestamp - .map(|end| e.timestamp <= end) - .unwrap_or(true) - }) + .filter(|record| record.is_in_segment(segment)) + .map(|record| &record.entry) .collect(); if segment_entries.is_empty() { diff --git a/src/openhuman/agent/harness/archivist/recap.rs b/src/openhuman/agent/harness/archivist/recap.rs index 21fb7ceedd..8765798215 100644 --- a/src/openhuman/agent/harness/archivist/recap.rs +++ b/src/openhuman/agent/harness/archivist/recap.rs @@ -2,26 +2,74 @@ use super::types::ArchivistHook; use crate::openhuman::memory::store::fts5::{self, EpisodicEntry}; -use crate::openhuman::memory::store::segments; +use crate::openhuman::memory::store::segments::{self, ConversationSegment}; use crate::openhuman::memory::store::trees::types::TreeKind; use crate::openhuman::memory::tree::summarise::{summarise, SummaryContext, SummaryInput}; use parking_lot::Mutex; use rusqlite::Connection; use std::sync::Arc; +/// An episodic entry paired with the stable identity exposed by its backing +/// store. The md archivist uses a per-session sequence while the legacy FTS5 +/// store uses a row id. +pub(super) struct SessionEntry { + pub(super) entry: EpisodicEntry, + sequence: Option, +} + +impl SessionEntry { + /// Whether this entry belongs to a closed segment. + /// + /// Segment endpoints identify user turns. Each user turn is immediately + /// followed by its assistant entry, so the inclusive span ends one entry + /// after the recorded end user turn. + pub(super) fn is_in_segment(&self, segment: &ConversationSegment) -> bool { + if let (Some(sequence), Some(start)) = (self.sequence, segment.start_seq) { + let end = segment.end_seq.unwrap_or(start).saturating_add(1); + return sequence >= start && sequence <= end; + } + + if let Some(id) = self.entry.id { + let start = segment.start_episodic_id; + let end = segment.end_episodic_id.unwrap_or(start).saturating_add(1); + return id >= start && id <= end; + } + + self.entry.timestamp >= segment.start_timestamp + && segment + .end_timestamp + .map(|end| self.entry.timestamp <= end) + .unwrap_or(true) + } + + /// Whether this entry belongs to the open segment or a later turn. + pub(super) fn is_at_or_after_segment_start(&self, segment: &ConversationSegment) -> bool { + if let (Some(sequence), Some(start)) = (self.sequence, segment.start_seq) { + return sequence >= start; + } + + if let Some(id) = self.entry.id { + return id >= segment.start_episodic_id; + } + + self.entry.timestamp >= segment.start_timestamp + } +} + impl ArchivistHook { /// Read every entry recorded for `session_id`, preferring the /// crate-owned md-backed archivist store when `self.config` is set and /// falling back to the legacy FTS5 episodic table otherwise. /// - /// Returns `EpisodicEntry` so the existing call sites (segment - /// gathering, recap rendering, tree push) keep their shape unchanged - /// during the FTS5 retirement migration. + /// Each entry retains the stable sequence or row identity needed for + /// segment selection. Timestamps are only a fallback for legacy records: + /// the md store records epoch milliseconds and therefore cannot preserve + /// the sub-millisecond timestamps used when a segment is opened. pub(super) fn read_session_entries( &self, conn: &Arc>, session_id: &str, - ) -> Vec { + ) -> Vec { if let Some(cfg) = self.config.as_ref() { let engine_config = crate::openhuman::memory::tinycortex::memory_config_from( cfg, @@ -32,17 +80,20 @@ impl ArchivistHook { Ok(turns) => { return turns .into_iter() - .map(|t| EpisodicEntry { - id: None, - session_id: t.session_id, - // ArchivedTurn stores epoch-ms; EpisodicEntry - // takes epoch-seconds as f64. - timestamp: (t.timestamp_ms as f64) / 1000.0, - role: t.role, - content: t.content, - lesson: t.lesson, - tool_calls_json: t.tool_calls_json, - cost_microdollars: t.cost_microdollars, + .map(|t| SessionEntry { + sequence: Some(t.seq), + entry: EpisodicEntry { + id: None, + session_id: t.session_id, + // ArchivedTurn stores epoch-ms; EpisodicEntry + // takes epoch-seconds as f64. + timestamp: (t.timestamp_ms as f64) / 1000.0, + role: t.role, + content: t.content, + lesson: t.lesson, + tool_calls_json: t.tool_calls_json, + cost_microdollars: t.cost_microdollars, + }, }) .collect(); } @@ -53,7 +104,14 @@ impl ArchivistHook { } } } - fts5::episodic_session_entries(conn, session_id).unwrap_or_default() + fts5::episodic_session_entries(conn, session_id) + .unwrap_or_default() + .into_iter() + .map(|entry| SessionEntry { + entry, + sequence: None, + }) + .collect() } /// Shared summarize helper — the **single LLM summarizer** used by both @@ -246,11 +304,12 @@ impl ArchivistHook { // Gather the episodic entries for this session so far. let all_entries = self.read_session_entries(conn, session_id); - // Keep only entries within the open segment's time window (start → - // now, inclusive). An open segment has `end_timestamp = None`. + // Keep only entries belonging to the open segment. Prefer stable + // sequence/row identity because the md store rounds timestamps to ms. let segment_entries: Vec<&EpisodicEntry> = all_entries .iter() - .filter(|e| e.timestamp >= open_segment.start_timestamp) + .filter(|record| record.is_at_or_after_segment_start(&open_segment)) + .map(|record| &record.entry) .collect(); if segment_entries.is_empty() { @@ -305,3 +364,68 @@ impl ArchivistHook { Some(recap) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::openhuman::memory::store::segments::SegmentStatus; + + fn segment() -> ConversationSegment { + ConversationSegment { + segment_id: "segment".into(), + session_id: "session".into(), + namespace: "global".into(), + start_episodic_id: 20, + end_episodic_id: Some(24), + start_timestamp: 100.000_9, + end_timestamp: Some(100.001_1), + turn_count: 3, + summary: None, + embedding: None, + topic_keywords: None, + status: SegmentStatus::Closed, + created_at: 100.0, + updated_at: 100.0, + start_seq: Some(10), + end_seq: Some(14), + } + } + + fn entry(sequence: Option, id: Option, timestamp: f64) -> SessionEntry { + SessionEntry { + sequence, + entry: EpisodicEntry { + id, + session_id: "session".into(), + timestamp, + role: "user".into(), + content: "content".into(), + lesson: None, + tool_calls_json: None, + cost_microdollars: 0, + }, + } + } + + #[test] + fn segment_membership_uses_sequence_instead_of_rounded_timestamp() { + let segment = segment(); + + assert!(!entry(Some(9), None, 100.001).is_in_segment(&segment)); + assert!(entry(Some(10), None, 100.000).is_in_segment(&segment)); + assert!(entry(Some(15), None, 101.0).is_in_segment(&segment)); + assert!(!entry(Some(16), None, 100.001).is_in_segment(&segment)); + } + + #[test] + fn segment_membership_falls_back_to_episodic_id() { + let mut segment = segment(); + segment.start_seq = None; + segment.end_seq = None; + + assert!(!entry(None, Some(19), 100.001).is_in_segment(&segment)); + assert!(entry(None, Some(20), 100.000).is_in_segment(&segment)); + assert!(entry(None, Some(25), 101.0).is_in_segment(&segment)); + assert!(!entry(None, Some(26), 100.001).is_in_segment(&segment)); + } +} From 3d2c171e6def9bdbc20d0415715561b5c5780987 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 14 Aug 2026 04:04:18 +0300 Subject: [PATCH 6/8] fix(agent): retain non-UTF-8 Git config parameters Co-authored-by: Medulla --- src/openhuman/agent/git_attribution/hook.rs | 15 +++++++++++---- src/openhuman/agent/git_attribution/tests.rs | 20 ++++++++++++++++++-- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/openhuman/agent/git_attribution/hook.rs b/src/openhuman/agent/git_attribution/hook.rs index 7a24ce30bf..5005d12f81 100644 --- a/src/openhuman/agent/git_attribution/hook.rs +++ b/src/openhuman/agent/git_attribution/hook.rs @@ -1,5 +1,7 @@ use std::collections::HashMap; #[cfg(unix)] +use std::ffi::OsStr; +#[cfg(unix)] use std::path::PathBuf; #[cfg(unix)] use std::sync::OnceLock; @@ -62,15 +64,20 @@ pub fn hook_env() -> HashMap { let Some(dir) = HOOK_DIR.get_or_init(|| build_hook_dir().ok()).as_ref() else { return HashMap::new(); }; - build_hook_env(dir, std::env::var("GIT_CONFIG_PARAMETERS").ok().as_deref()) + build_hook_env(dir, std::env::var_os("GIT_CONFIG_PARAMETERS").as_deref()) } #[cfg(unix)] fn build_hook_env( dir: &std::path::Path, - inherited_parameters: Option<&str>, + inherited_parameters: Option<&OsStr>, ) -> HashMap { - let mut parameters = inherited_parameters.unwrap_or_default().trim().to_owned(); + // Sandbox environment maps are UTF-8 strings. Convert an inherited Unix + // value lossily so one non-UTF-8 byte does not discard every parent `-c` + // setting, as `std::env::var` would. + let mut parameters = inherited_parameters + .map(|value| value.to_string_lossy().trim().to_owned()) + .unwrap_or_default(); if !parameters.is_empty() { parameters.push(' '); } @@ -109,7 +116,7 @@ fn build_hook_dir() -> std::io::Result { } #[cfg(all(unix, test))] -pub(super) fn test_hook_env(inherited_parameters: Option<&str>) -> HashMap { +pub(super) fn test_hook_env(inherited_parameters: Option<&OsStr>) -> HashMap { let dir = build_hook_dir().expect("create OpenHuman hook directory"); build_hook_env(&dir, inherited_parameters) } diff --git a/src/openhuman/agent/git_attribution/tests.rs b/src/openhuman/agent/git_attribution/tests.rs index 583d43961a..ca197690d9 100644 --- a/src/openhuman/agent/git_attribution/tests.rs +++ b/src/openhuman/agent/git_attribution/tests.rs @@ -32,9 +32,9 @@ fn hook_adds_openhuman_trailer_without_disabling_repository_hook() { std::fs::write(repo.join("a"), "a").unwrap(); git(&["add", "a"]); - let hook_env = super::hook::test_hook_env(Some( + let hook_env = super::hook::test_hook_env(Some(std::ffi::OsStr::new( "'test.openhuman-inherited'='kept' 'core.hooksPath'='/definitely-not-the-openhuman-hook'", - )); + ))); let output = Command::new("git") .args(["commit", "-q", "-m", "subject"]) .current_dir(&repo) @@ -64,3 +64,19 @@ fn hook_adds_openhuman_trailer_without_disabling_repository_hook() { assert!(inherited.status.success()); assert_eq!(String::from_utf8(inherited.stdout).unwrap().trim(), "kept"); } + +#[cfg(unix)] +#[test] +fn hook_env_does_not_drop_inherited_parameters_containing_non_utf8() { + use std::os::unix::ffi::OsStrExt; + + let inherited = std::ffi::OsStr::from_bytes( + b"'test.openhuman-inherited'='before-\xff-after' 'test.second'='kept'", + ); + let hook_env = super::hook::test_hook_env(Some(inherited)); + let parameters = hook_env.get("GIT_CONFIG_PARAMETERS").unwrap(); + + assert!(parameters.contains("'test.openhuman-inherited'='before-")); + assert!(parameters.contains("-after' 'test.second'='kept'")); + assert!(parameters.contains("'core.hooksPath'=")); +} From dab4dfabda006a9718f4cad654347ecaffe7de54 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 14 Aug 2026 04:04:18 +0300 Subject: [PATCH 7/8] fix(memory): gate module-only test re-export Co-authored-by: Medulla --- src/openhuman/memory/ops/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/openhuman/memory/ops/mod.rs b/src/openhuman/memory/ops/mod.rs index 97ae54a104..1670245947 100644 --- a/src/openhuman/memory/ops/mod.rs +++ b/src/openhuman/memory/ops/mod.rs @@ -89,7 +89,9 @@ pub(crate) static GLOBAL_MEMORY_TEST_LOCK: tokio::sync::Mutex<()> = #[cfg(test)] mod test_support; #[cfg(test)] -pub(crate) use test_support::{ensure_shared_memory_client, shared_memory_test_workspace}; +pub(crate) use test_support::ensure_shared_memory_client; +#[cfg(all(test, feature = "modules"))] +pub(crate) use test_support::shared_memory_test_workspace; #[cfg(test)] #[path = "../ops_tests.rs"] From 4287acb9e37f6467617cea9b86b6d0e2684dcdca Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Fri, 14 Aug 2026 04:31:18 +0300 Subject: [PATCH 8/8] fix(agent): preserve raw Git environment bytes Co-authored-by: Medulla --- src/openhuman/agent/git_attribution/hook.rs | 39 +++++++++++-------- src/openhuman/agent/git_attribution/tests.rs | 32 +++++++++++---- src/openhuman/flows/tinyflows/caps/code.rs | 2 +- src/openhuman/sandbox/docker.rs | 5 ++- src/openhuman/sandbox/ops.rs | 36 +++++++++++++++-- src/openhuman/sandbox/types.rs | 2 +- src/openhuman/tools/impl/system/node_exec.rs | 2 +- src/openhuman/tools/impl/system/npm_exec.rs | 2 +- .../tools/impl/system/python_exec.rs | 4 +- src/openhuman/tools/impl/system/shell.rs | 2 +- 10 files changed, 91 insertions(+), 35 deletions(-) diff --git a/src/openhuman/agent/git_attribution/hook.rs b/src/openhuman/agent/git_attribution/hook.rs index 5005d12f81..1b129964f0 100644 --- a/src/openhuman/agent/git_attribution/hook.rs +++ b/src/openhuman/agent/git_attribution/hook.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; #[cfg(unix)] -use std::ffi::OsStr; +use std::ffi::{OsStr, OsString}; #[cfg(unix)] use std::path::PathBuf; #[cfg(unix)] @@ -60,7 +60,7 @@ static HOOK_DIR: OnceLock> = OnceLock::new(); /// The result is intended for an agent-owned child process. It does not change /// repository configuration or the parent application's environment. #[cfg(unix)] -pub fn hook_env() -> HashMap { +pub fn hook_env() -> HashMap { let Some(dir) = HOOK_DIR.get_or_init(|| build_hook_dir().ok()).as_ref() else { return HashMap::new(); }; @@ -71,32 +71,39 @@ pub fn hook_env() -> HashMap { fn build_hook_env( dir: &std::path::Path, inherited_parameters: Option<&OsStr>, -) -> HashMap { - // Sandbox environment maps are UTF-8 strings. Convert an inherited Unix - // value lossily so one non-UTF-8 byte does not discard every parent `-c` - // setting, as `std::env::var` would. +) -> HashMap { + use std::os::unix::ffi::{OsStrExt, OsStringExt}; + let mut parameters = inherited_parameters - .map(|value| value.to_string_lossy().trim().to_owned()) + .map(|value| value.as_bytes().to_vec()) .unwrap_or_default(); if !parameters.is_empty() { - parameters.push(' '); + parameters.push(b' '); + } + parameters.extend_from_slice(b"'core.hooksPath'='"); + for byte in dir.as_os_str().as_bytes() { + if *byte == b'\'' { + parameters.extend_from_slice(b"'\\''"); + } else { + parameters.push(*byte); + } } - let hook_path = dir.to_string_lossy().replace('\'', "'\\''"); - parameters.push_str("'core.hooksPath'='"); - parameters.push_str(&hook_path); - parameters.push('\''); + parameters.push(b'\''); HashMap::from([ - ("OPENHUMAN_GIT_ATTRIBUTION".into(), TRAILER.into()), + (OsString::from("OPENHUMAN_GIT_ATTRIBUTION"), TRAILER.into()), // Parameters outrank GIT_CONFIG_COUNT. Preserve settings inherited // from the parent harness, then append our hook so it wins if that // harness also selected a hook path. - ("GIT_CONFIG_PARAMETERS".into(), parameters), + ( + OsString::from("GIT_CONFIG_PARAMETERS"), + OsString::from_vec(parameters), + ), ]) } #[cfg(not(unix))] -pub fn hook_env() -> HashMap { +pub fn hook_env() -> HashMap { HashMap::new() } @@ -116,7 +123,7 @@ fn build_hook_dir() -> std::io::Result { } #[cfg(all(unix, test))] -pub(super) fn test_hook_env(inherited_parameters: Option<&OsStr>) -> HashMap { +pub(super) fn test_hook_env(inherited_parameters: Option<&OsStr>) -> HashMap { let dir = build_hook_dir().expect("create OpenHuman hook directory"); build_hook_env(&dir, inherited_parameters) } diff --git a/src/openhuman/agent/git_attribution/tests.rs b/src/openhuman/agent/git_attribution/tests.rs index ca197690d9..b3154290ad 100644 --- a/src/openhuman/agent/git_attribution/tests.rs +++ b/src/openhuman/agent/git_attribution/tests.rs @@ -68,15 +68,31 @@ fn hook_adds_openhuman_trailer_without_disabling_repository_hook() { #[cfg(unix)] #[test] fn hook_env_does_not_drop_inherited_parameters_containing_non_utf8() { - use std::os::unix::ffi::OsStrExt; + use std::os::unix::ffi::{OsStrExt, OsStringExt}; - let inherited = std::ffi::OsStr::from_bytes( - b"'test.openhuman-inherited'='before-\xff-after' 'test.second'='kept'", - ); + let inherited_bytes = b"'test.openhuman-inherited'='before-\xff-after' 'test.second'='kept'"; + let inherited = std::ffi::OsStr::from_bytes(inherited_bytes); let hook_env = super::hook::test_hook_env(Some(inherited)); - let parameters = hook_env.get("GIT_CONFIG_PARAMETERS").unwrap(); + let parameters = hook_env + .get(std::ffi::OsStr::new("GIT_CONFIG_PARAMETERS")) + .unwrap() + .clone() + .into_vec(); + + assert!(parameters.starts_with(inherited_bytes)); + assert_eq!(parameters[inherited_bytes.len()], b' '); + assert!(parameters[inherited_bytes.len() + 1..].starts_with(b"'core.hooksPath'='")); + assert!(parameters.contains(&0xff)); - assert!(parameters.contains("'test.openhuman-inherited'='before-")); - assert!(parameters.contains("-after' 'test.second'='kept'")); - assert!(parameters.contains("'core.hooksPath'=")); + let output = std::process::Command::new("git") + .args(["config", "--get", "test.openhuman-inherited"]) + .envs(&hook_env) + .output() + .unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert_eq!(output.stdout, b"before-\xff-after\n"); } diff --git a/src/openhuman/flows/tinyflows/caps/code.rs b/src/openhuman/flows/tinyflows/caps/code.rs index b9cd2d237f..ece2b57f17 100644 --- a/src/openhuman/flows/tinyflows/caps/code.rs +++ b/src/openhuman/flows/tinyflows/caps/code.rs @@ -126,7 +126,7 @@ impl CodeRunner for OpenHumanCode { let mut extra_env = std::collections::HashMap::new(); if let Ok(host_path) = std::env::var("PATH") { - extra_env.insert("PATH".to_string(), host_path); + extra_env.insert("PATH".into(), host_path.into()); } tracing::debug!( diff --git a/src/openhuman/sandbox/docker.rs b/src/openhuman/sandbox/docker.rs index 1f3f69670a..3bf5fb052b 100644 --- a/src/openhuman/sandbox/docker.rs +++ b/src/openhuman/sandbox/docker.rs @@ -118,7 +118,10 @@ pub async fn docker_exec( } // Inject request-specific environment. for (k, v) in &request.env { - cmd.arg("-e").arg(format!("{k}={v}")); + let mut assignment = k.clone(); + assignment.push("="); + assignment.push(v); + cmd.arg("-e").arg(assignment); } cmd.arg(image); diff --git a/src/openhuman/sandbox/ops.rs b/src/openhuman/sandbox/ops.rs index e6fcdbf30f..593a164ef5 100644 --- a/src/openhuman/sandbox/ops.rs +++ b/src/openhuman/sandbox/ops.rs @@ -11,6 +11,7 @@ use crate::openhuman::agent::platform_shell; use crate::openhuman::config::RuntimeConfig; use crate::openhuman::sandbox::cwd_jail::{self, Jail, NoopBackend}; use std::collections::HashMap; +use std::ffi::OsString; use std::path::Path; use std::time::Duration; @@ -120,7 +121,7 @@ pub async fn execute_in_sandbox( policy: &SandboxPolicy, command: &str, working_dir: &Path, - extra_env: HashMap, + extra_env: HashMap, timeout: Duration, ) -> anyhow::Result { // Validate the working directory up front so a missing/bad action_dir @@ -159,7 +160,7 @@ pub async fn execute_in_sandbox( async fn execute_unsandboxed( command: &str, working_dir: &Path, - extra_env: &HashMap, + extra_env: &HashMap, timeout: Duration, ) -> anyhow::Result { // Shell selection routed through `platform_shell` so this path picks @@ -205,7 +206,7 @@ async fn execute_local_jail( policy: &SandboxPolicy, command: &str, working_dir: &Path, - extra_env: &HashMap, + extra_env: &HashMap, timeout: Duration, ) -> anyhow::Result { let mut jail = Jail::new(&policy.workspace_root, "sandbox.agent"); @@ -510,6 +511,35 @@ mod tests { assert!(result.stdout.contains("sandbox-test")); } + #[cfg(unix)] + #[tokio::test] + async fn execute_in_sandbox_preserves_non_utf8_environment_bytes() { + use std::os::unix::ffi::OsStringExt; + + let tempdir = tempfile::tempdir().unwrap(); + let policy = resolve_sandbox_policy( + SandboxMode::None, + tempdir.path(), + &RuntimeConfig::default(), + false, + ); + let env = HashMap::from([( + OsString::from("OPENHUMAN_RAW_ENV_TEST"), + OsString::from_vec(b"before-\xff-after".to_vec()), + )]); + let result = execute_in_sandbox( + &policy, + r#"[ "$OPENHUMAN_RAW_ENV_TEST" = "$(printf 'before-\377-after')" ]"#, + tempdir.path(), + env, + Duration::from_secs(10), + ) + .await + .unwrap(); + + assert!(result.success(), "stderr: {}", result.stderr); + } + /// #4705 regression — `execute_in_sandbox` with the `None` backend /// now delegates to `execute_unsandboxed`, which used to fail on /// Windows with a ~30ms `sh`-not-found spawn error. Cross-platform diff --git a/src/openhuman/sandbox/types.rs b/src/openhuman/sandbox/types.rs index d23d51f74d..1ee654441b 100644 --- a/src/openhuman/sandbox/types.rs +++ b/src/openhuman/sandbox/types.rs @@ -56,7 +56,7 @@ pub struct SandboxExecRequest { /// container-side path (e.g. `/workspace`). pub working_dir: PathBuf, /// Environment variables to inject. - pub env: HashMap, + pub env: HashMap, /// Execution timeout. pub timeout: std::time::Duration, } diff --git a/src/openhuman/tools/impl/system/node_exec.rs b/src/openhuman/tools/impl/system/node_exec.rs index 4efe814379..9604a32d33 100644 --- a/src/openhuman/tools/impl/system/node_exec.rs +++ b/src/openhuman/tools/impl/system/node_exec.rs @@ -539,7 +539,7 @@ impl NodeExecTool { } else { format!("{}{}{}", bin_dir.display(), sep, host_path) }; - extra_env.insert("PATH".to_string(), prepended); + extra_env.insert("PATH".into(), prepended.into()); match sandbox::execute_in_sandbox( &policy, diff --git a/src/openhuman/tools/impl/system/npm_exec.rs b/src/openhuman/tools/impl/system/npm_exec.rs index db78fbf007..b511df668f 100644 --- a/src/openhuman/tools/impl/system/npm_exec.rs +++ b/src/openhuman/tools/impl/system/npm_exec.rs @@ -453,7 +453,7 @@ impl NpmExecTool { } else { format!("{}{}{}", bin_dir.display(), sep, host_path) }; - extra_env.insert("PATH".to_string(), prepended); + extra_env.insert("PATH".into(), prepended.into()); match sandbox::execute_in_sandbox(&policy, command, cwd, extra_env, effective).await { Ok(result) => { diff --git a/src/openhuman/tools/impl/system/python_exec.rs b/src/openhuman/tools/impl/system/python_exec.rs index f0810580b0..e0854b4fc6 100644 --- a/src/openhuman/tools/impl/system/python_exec.rs +++ b/src/openhuman/tools/impl/system/python_exec.rs @@ -451,8 +451,8 @@ impl PythonExecTool { } else { format!("{}{}{}", bin_dir.display(), sep, host_path) }; - extra_env.insert("PATH".to_string(), prepended); - extra_env.insert("PYTHONUNBUFFERED".to_string(), "1".to_string()); + extra_env.insert("PATH".into(), prepended.into()); + extra_env.insert("PYTHONUNBUFFERED".into(), "1".into()); match sandbox::execute_in_sandbox( &policy, diff --git a/src/openhuman/tools/impl/system/shell.rs b/src/openhuman/tools/impl/system/shell.rs index ecfb43ec5d..6af1c21ed8 100644 --- a/src/openhuman/tools/impl/system/shell.rs +++ b/src/openhuman/tools/impl/system/shell.rs @@ -536,7 +536,7 @@ impl ShellTool { let mut extra_env = std::collections::HashMap::new(); match self.runtime_path_for_command(command).await { Ok(Some(path)) => { - extra_env.insert("PATH".to_string(), path); + extra_env.insert("PATH".into(), path.into()); } Ok(None) => {} Err(error) => {