diff --git a/crates/agent-artifact-admission/src/pypi_global_option_authority.rs b/crates/agent-artifact-admission/src/pypi_global_option_authority.rs index 715aa215..024211d8 100644 --- a/crates/agent-artifact-admission/src/pypi_global_option_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_global_option_authority.rs @@ -1,6 +1,7 @@ use crate::InstallIntent; use crate::pypi_certificate_store_authority::matches_pip_certificate_store_abbreviation; use crate::pypi_client_certificate_authority::matches_pip_client_certificate_option; +use crate::pypi_log_output_authority::matches_pip_log_option; use crate::pypi_proxy_authority::{ is_attached_direct_pip_proxy_selector, is_direct_pip_proxy_value_selector, }; @@ -123,6 +124,26 @@ pub(crate) fn normalize_reviewed_direct_pip_global_options( continue; } + if matches_pip_log_option(argument) { + if let Some((_, value)) = argument.split_once('=') { + if value.is_empty() { + return None; + } + reviewed_global_arguments.push(arguments[index].clone()); + index += 1; + } else { + // A separate log path is consumed General Option grammar. Attach + // it only in the internal policy copy so it cannot masquerade as + // an artifact operand; the submitted argv/hash stay authoritative. + push_attached_normalized_value_argument( + arguments, + &mut reviewed_global_arguments, + &mut index, + )?; + } + continue; + } + if matches_pip_python_interpreter_option(argument) { let value = if let Some((_, value)) = argument.split_once('=') { if value.is_empty() { diff --git a/crates/agent-artifact-admission/src/pypi_log_output_authority.rs b/crates/agent-artifact-admission/src/pypi_log_output_authority.rs index da8c9174..6a57838d 100644 --- a/crates/agent-artifact-admission/src/pypi_log_output_authority.rs +++ b/crates/agent-artifact-admission/src/pypi_log_output_authority.rs @@ -27,7 +27,7 @@ pub(crate) fn requests_unapproved_pypi_log_output_authority(intent: &InstallInte /// pip uses Python optparse, which accepts unambiguous long-option prefixes. /// Keep this accepted-language set explicit so an ambiguous prefix such as /// `--lo` is not reinterpreted by Wardnet as valid caller authority. -fn matches_pip_log_option(argument: &str) -> bool { +pub(crate) fn matches_pip_log_option(argument: &str) -> bool { let option = argument.split_once('=').map_or(argument, |(name, _)| name); matches!( option, diff --git a/crates/agent-artifact-admission/tests/pypi_log_output_authority_contract.rs b/crates/agent-artifact-admission/tests/pypi_log_output_authority_contract.rs index da42a545..c905e980 100644 --- a/crates/agent-artifact-admission/tests/pypi_log_output_authority_contract.rs +++ b/crates/agent-artifact-admission/tests/pypi_log_output_authority_contract.rs @@ -1,6 +1,6 @@ use wardnet_agent_artifact_admission::{ AdmissionPolicy, ApprovedArtifact, ApprovedManifest, ArtifactCoordinate, DecisionKind, - InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, + InstallIntent, InstructionSource, InstructionSourceKind, admission_decision, sha256_hex, }; #[test] @@ -50,6 +50,66 @@ fn approved_pip_install_cannot_gain_caller_selected_log_write_authority() { } } +#[test] +fn valid_global_pip_log_options_remain_causal_write_authority_evidence() { + for executable in ["pip", "pip3"] { + let (policy, control_intent) = approved_pip_install(executable); + + for global_log_arguments in [ + vec!["--log=/tmp/wardnet-pip.log"], + vec!["--log", "/tmp/wardnet-pip.log"], + vec!["--log-file=/tmp/wardnet-pip.log"], + vec!["--log-file", "/tmp/wardnet-pip.log"], + vec!["--local-log=/tmp/wardnet-pip.log"], + vec!["--local-log", "/tmp/wardnet-pip.log"], + vec!["--log-f", "/tmp/wardnet-pip.log"], + vec!["--loc=/tmp/wardnet-pip.log"], + ] { + let mut intent = control_intent.clone(); + let mut argv = Vec::with_capacity(intent.argv.len() + global_log_arguments.len()); + argv.push(executable.to_string()); + argv.extend( + global_log_arguments + .iter() + .map(|argument| (*argument).to_string()), + ); + argv.extend(control_intent.argv.iter().skip(1).cloned()); + intent.argv = argv; + let submitted_argv = intent.argv.clone(); + + let decision = admission_decision(&policy, &intent); + assert_eq!( + decision.decision, + DecisionKind::Block, + "parser-valid global {executable} {} must fail closed", + global_log_arguments.join(" ") + ); + assert!( + decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "alternate_install_root"), + "parser-valid global {executable} {} must retain causal log-write evidence: {:?}", + global_log_arguments.join(" "), + decision.reason_codes + ); + assert!( + !decision + .reason_codes + .iter() + .any(|reason| reason.as_str() == "artifact_not_approved"), + "the log path consumed by pip General Options must not masquerade as an artifact operand: {:?}", + decision.reason_codes + ); + assert_eq!( + decision.command_sha256, + sha256_hex(submitted_argv.join("\u{1f}").as_bytes()), + "policy normalization must preserve the exact submitted argv audit identity" + ); + } + } +} + fn approved_pip_install(executable: &str) -> (AdmissionPolicy, InstallIntent) { let artifact = ArtifactCoordinate { ecosystem: "pypi".to_string(),