diff --git a/doc/sandbox-work-folders.md b/doc/sandbox-work-folders.md index 91638f93c9..77152315c0 100644 --- a/doc/sandbox-work-folders.md +++ b/doc/sandbox-work-folders.md @@ -51,8 +51,8 @@ in the sandbox home. For API-key Codex ACP runs, the adapter writes the explicit key to an owner-only login file in the staging copy; host credentials stay unchanged. Per-run GitHub launchers declare their own CommonJS package scope so warm runs inside ES-module repositories can still execute Git and GitHub CLI commands. Native runner launches carry the validated scoped paths -through runnerd to ACPX, including the explicitly controller-projected GitHub -broker environment. ACPX does not inherit ambient host GitHub credentials or +through both runnerd’s Rust sidecar filter and the ACPX JavaScript launch filter, +including the explicitly controller-projected GitHub broker environment. ACPX does not inherit ambient host GitHub credentials or shell startup hooks. CLI configuration remains in its private runtime directories. Warm sandbox task bindings persist independently of the experimental isolated workspace setting. Only the active host run can establish that binding; the diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs index ddaf3adcf7..f85b8f0b9d 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs @@ -13,7 +13,7 @@ use crate::generated_acpx_sidecar_contract::{ use crate::local_runner::LocalRunnerError; use crate::process_supervisor::{ BoundedLogBuffer, ProcessOutput, SupervisedProcess, VerifiedProcessLaunch, - WORK_FOLDER_ENVIRONMENT_KEYS, + GITHUB_CREDENTIAL_ENVIRONMENT_KEYS, WORK_FOLDER_ENVIRONMENT_KEYS, }; use crate::stable_identity::{is_stable_id, DURABLE_STABLE_ID_CHARS, SHORT_STABLE_ID_CHARS}; @@ -126,6 +126,7 @@ impl AcpxSidecarTransport { ]; keys.extend_from_slice(credential_keys); keys.extend_from_slice(WORK_FOLDER_ENVIRONMENT_KEYS); + keys.extend_from_slice(GITHUB_CREDENTIAL_ENVIRONMENT_KEYS); Self::start_with_environment_keys(config, &keys) } @@ -674,6 +675,58 @@ fn response_error_classification(error: &ResponseError) -> &'static str { mod tests { use super::*; + #[test] + #[cfg(unix)] + fn forwards_projected_github_environment_through_actual_sidecar_spawn() { + const CHILD: &str = "PAPERCLIP_ACPX_ENV_TEST_CHILD"; + if std::env::var(CHILD).as_deref() != Ok("1") { + // Set synthetic credentials on a separate test process. Do not + // mutate the shared environment of parallel Rust tests. + let result = std::process::Command::new(std::env::current_exe().unwrap()) + .args([ + "--exact", + "acpx_sidecar_transport::tests::forwards_projected_github_environment_through_actual_sidecar_spawn", + "--nocapture", + ]) + .env(CHILD, "1") + .env("PAPERCLIP_GITHUB_BROKER_TOKEN", "test-run-capability") + .env("BASH_ENV", "/dev/null") + .env("GIT_CONFIG_COUNT", "1") + .env("GIT_CONFIG_KEY_0", "credential.helper") + .env("GIT_CONFIG_VALUE_0", "test-helper") + .env("GIT_CONFIG_KEY_32", "outside-ceiling") + .env("UNRELATED_TEST_SECRET", "must-not-cross") + .output() + .unwrap(); + assert!( + result.status.success(), + "{}\n{}", + String::from_utf8_lossy(&result.stdout), + String::from_utf8_lossy(&result.stderr) + ); + return; + } + let config = AcpxSidecarTransportConfig { + command: PathBuf::from("/bin/sh"), + args: vec!["-c".to_owned(), r#"printf '%s\n' "$PAPERCLIP_GITHUB_BROKER_TOKEN|$BASH_ENV|$GIT_CONFIG_COUNT|$GIT_CONFIG_KEY_0|$GIT_CONFIG_VALUE_0|$GIT_CONFIG_KEY_32|$UNRELATED_TEST_SECRET""#.to_owned()], + verified_launch: None, + request_timeout: Duration::from_secs(3), + shutdown_grace: Duration::from_millis(100), + }; + for agent in ["claude", "codex", "pi"] { + let mut transport = AcpxSidecarTransport::start_for_agent(&config, agent).unwrap(); + let line = transport + .receive_stdout_line(Duration::from_secs(3), "environment probe") + .unwrap(); + assert_eq!( + line.as_deref(), + Some("test-run-capability|/dev/null|1|credential.helper|test-helper||"), + "{agent}" + ); + transport.shutdown().unwrap(); + } + } + #[test] fn parse_frame_does_not_echo_untrusted_deserialization_details() { let cases = [ diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/codex_provider.rs b/packages/paperclip-runner/runner/crates/runner-core/src/codex_provider.rs index 477446a855..1e7254be4f 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/codex_provider.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/codex_provider.rs @@ -16,7 +16,8 @@ use crate::durable::{redact_text, OpenCodeLaunchProfile}; use crate::local_runner::LocalRunnerError; use crate::process_supervisor::{ is_node_interpreter, BoundedLogBuffer, ProcessOutput, SupervisedProcess, - VerifiedProcessArgument, VerifiedProcessLaunch, WORK_FOLDER_ENVIRONMENT_KEYS, + VerifiedProcessArgument, VerifiedProcessLaunch, GITHUB_CREDENTIAL_ENVIRONMENT_KEYS, + WORK_FOLDER_ENVIRONMENT_KEYS, }; use crate::provider_bridge::{AuthorizedTool, DurableReplayFilter, ToolResult}; use crate::provider_events::normalized_codex_terminal_event_type; @@ -545,103 +546,6 @@ pub struct CodexProvider { permission_profile: &'static str, } -// The controller accepts at most 32 process-scoped Git config entries and -// projects only these exact GitHub credential names into runnerd. Keep the -// provider child boundary equally explicit: runnerd may inherit a configured -// entry from this static ceiling, but cannot introduce another environment -// variable by changing GIT_CONFIG_COUNT. -const GITHUB_CREDENTIAL_ENVIRONMENT_KEYS: &[&str] = &[ - "ZDOTDIR", - "BASH_ENV", - "PAPERCLIP_GITHUB_BROKER_URL", - "PAPERCLIP_GITHUB_BROKER_TOKEN", - "PAPERCLIP_GITHUB_LAUNCHER_DIR", - "GH_CONFIG_DIR", - "GH_ENTERPRISE_TOKEN", - "GITHUB_ENTERPRISE_TOKEN", - "GIT_CONFIG_GLOBAL", - "GIT_CONFIG_SYSTEM", - "GIT_CONFIG_NOSYSTEM", - "GIT_ASKPASS", - "SSH_ASKPASS", - "SSH_AUTH_SOCK", - "GIT_SSH_COMMAND", - "PAPERCLIP_GITHUB_BRIDGE_TOKEN", - "GH_TOKEN", - "GITHUB_TOKEN", - "PAPERCLIP_GIT_TOKEN", - "GIT_TERMINAL_PROMPT", - "GIT_CONFIG_COUNT", - "GIT_AUTHOR_NAME", - "GIT_AUTHOR_EMAIL", - "GIT_COMMITTER_NAME", - "GIT_COMMITTER_EMAIL", - "GIT_CONFIG_KEY_0", - "GIT_CONFIG_VALUE_0", - "GIT_CONFIG_KEY_1", - "GIT_CONFIG_VALUE_1", - "GIT_CONFIG_KEY_2", - "GIT_CONFIG_VALUE_2", - "GIT_CONFIG_KEY_3", - "GIT_CONFIG_VALUE_3", - "GIT_CONFIG_KEY_4", - "GIT_CONFIG_VALUE_4", - "GIT_CONFIG_KEY_5", - "GIT_CONFIG_VALUE_5", - "GIT_CONFIG_KEY_6", - "GIT_CONFIG_VALUE_6", - "GIT_CONFIG_KEY_7", - "GIT_CONFIG_VALUE_7", - "GIT_CONFIG_KEY_8", - "GIT_CONFIG_VALUE_8", - "GIT_CONFIG_KEY_9", - "GIT_CONFIG_VALUE_9", - "GIT_CONFIG_KEY_10", - "GIT_CONFIG_VALUE_10", - "GIT_CONFIG_KEY_11", - "GIT_CONFIG_VALUE_11", - "GIT_CONFIG_KEY_12", - "GIT_CONFIG_VALUE_12", - "GIT_CONFIG_KEY_13", - "GIT_CONFIG_VALUE_13", - "GIT_CONFIG_KEY_14", - "GIT_CONFIG_VALUE_14", - "GIT_CONFIG_KEY_15", - "GIT_CONFIG_VALUE_15", - "GIT_CONFIG_KEY_16", - "GIT_CONFIG_VALUE_16", - "GIT_CONFIG_KEY_17", - "GIT_CONFIG_VALUE_17", - "GIT_CONFIG_KEY_18", - "GIT_CONFIG_VALUE_18", - "GIT_CONFIG_KEY_19", - "GIT_CONFIG_VALUE_19", - "GIT_CONFIG_KEY_20", - "GIT_CONFIG_VALUE_20", - "GIT_CONFIG_KEY_21", - "GIT_CONFIG_VALUE_21", - "GIT_CONFIG_KEY_22", - "GIT_CONFIG_VALUE_22", - "GIT_CONFIG_KEY_23", - "GIT_CONFIG_VALUE_23", - "GIT_CONFIG_KEY_24", - "GIT_CONFIG_VALUE_24", - "GIT_CONFIG_KEY_25", - "GIT_CONFIG_VALUE_25", - "GIT_CONFIG_KEY_26", - "GIT_CONFIG_VALUE_26", - "GIT_CONFIG_KEY_27", - "GIT_CONFIG_VALUE_27", - "GIT_CONFIG_KEY_28", - "GIT_CONFIG_VALUE_28", - "GIT_CONFIG_KEY_29", - "GIT_CONFIG_VALUE_29", - "GIT_CONFIG_KEY_30", - "GIT_CONFIG_VALUE_30", - "GIT_CONFIG_KEY_31", - "GIT_CONFIG_VALUE_31", -]; - const CODEX_PROVIDER_ENVIRONMENT_KEYS: &[&str] = &[ "CODEX_HOME", "OPENAI_API_KEY", diff --git a/packages/paperclip-runner/runner/crates/runner-core/src/process_supervisor.rs b/packages/paperclip-runner/runner/crates/runner-core/src/process_supervisor.rs index d191f94805..a7c3caccb2 100644 --- a/packages/paperclip-runner/runner/crates/runner-core/src/process_supervisor.rs +++ b/packages/paperclip-runner/runner/crates/runner-core/src/process_supervisor.rs @@ -26,6 +26,103 @@ use sha2::{Digest, Sha256}; use crate::local_runner::LocalRunnerError; const PROCESS_OUTPUT_QUEUE_CAPACITY: usize = 256; +// The controller accepts at most 32 process-scoped Git config entries and +// projects only these exact GitHub credential names into runnerd. Keep the +// provider child boundary equally explicit: runnerd may inherit a configured +// entry from this static ceiling, but cannot introduce another environment +// variable by changing GIT_CONFIG_COUNT. +pub(crate) const GITHUB_CREDENTIAL_ENVIRONMENT_KEYS: &[&str] = &[ + "ZDOTDIR", + "BASH_ENV", + "PAPERCLIP_GITHUB_BROKER_URL", + "PAPERCLIP_GITHUB_BROKER_TOKEN", + "PAPERCLIP_GITHUB_LAUNCHER_DIR", + "GH_CONFIG_DIR", + "GH_ENTERPRISE_TOKEN", + "GITHUB_ENTERPRISE_TOKEN", + "GIT_CONFIG_GLOBAL", + "GIT_CONFIG_SYSTEM", + "GIT_CONFIG_NOSYSTEM", + "GIT_ASKPASS", + "SSH_ASKPASS", + "SSH_AUTH_SOCK", + "GIT_SSH_COMMAND", + "PAPERCLIP_GITHUB_BRIDGE_TOKEN", + "GH_TOKEN", + "GITHUB_TOKEN", + "PAPERCLIP_GIT_TOKEN", + "GIT_TERMINAL_PROMPT", + "GIT_CONFIG_COUNT", + "GIT_AUTHOR_NAME", + "GIT_AUTHOR_EMAIL", + "GIT_COMMITTER_NAME", + "GIT_COMMITTER_EMAIL", + "GIT_CONFIG_KEY_0", + "GIT_CONFIG_VALUE_0", + "GIT_CONFIG_KEY_1", + "GIT_CONFIG_VALUE_1", + "GIT_CONFIG_KEY_2", + "GIT_CONFIG_VALUE_2", + "GIT_CONFIG_KEY_3", + "GIT_CONFIG_VALUE_3", + "GIT_CONFIG_KEY_4", + "GIT_CONFIG_VALUE_4", + "GIT_CONFIG_KEY_5", + "GIT_CONFIG_VALUE_5", + "GIT_CONFIG_KEY_6", + "GIT_CONFIG_VALUE_6", + "GIT_CONFIG_KEY_7", + "GIT_CONFIG_VALUE_7", + "GIT_CONFIG_KEY_8", + "GIT_CONFIG_VALUE_8", + "GIT_CONFIG_KEY_9", + "GIT_CONFIG_VALUE_9", + "GIT_CONFIG_KEY_10", + "GIT_CONFIG_VALUE_10", + "GIT_CONFIG_KEY_11", + "GIT_CONFIG_VALUE_11", + "GIT_CONFIG_KEY_12", + "GIT_CONFIG_VALUE_12", + "GIT_CONFIG_KEY_13", + "GIT_CONFIG_VALUE_13", + "GIT_CONFIG_KEY_14", + "GIT_CONFIG_VALUE_14", + "GIT_CONFIG_KEY_15", + "GIT_CONFIG_VALUE_15", + "GIT_CONFIG_KEY_16", + "GIT_CONFIG_VALUE_16", + "GIT_CONFIG_KEY_17", + "GIT_CONFIG_VALUE_17", + "GIT_CONFIG_KEY_18", + "GIT_CONFIG_VALUE_18", + "GIT_CONFIG_KEY_19", + "GIT_CONFIG_VALUE_19", + "GIT_CONFIG_KEY_20", + "GIT_CONFIG_VALUE_20", + "GIT_CONFIG_KEY_21", + "GIT_CONFIG_VALUE_21", + "GIT_CONFIG_KEY_22", + "GIT_CONFIG_VALUE_22", + "GIT_CONFIG_KEY_23", + "GIT_CONFIG_VALUE_23", + "GIT_CONFIG_KEY_24", + "GIT_CONFIG_VALUE_24", + "GIT_CONFIG_KEY_25", + "GIT_CONFIG_VALUE_25", + "GIT_CONFIG_KEY_26", + "GIT_CONFIG_VALUE_26", + "GIT_CONFIG_KEY_27", + "GIT_CONFIG_VALUE_27", + "GIT_CONFIG_KEY_28", + "GIT_CONFIG_VALUE_28", + "GIT_CONFIG_KEY_29", + "GIT_CONFIG_VALUE_29", + "GIT_CONFIG_KEY_30", + "GIT_CONFIG_VALUE_30", + "GIT_CONFIG_KEY_31", + "GIT_CONFIG_VALUE_31", +]; + // Host-validated scoped paths, carried only through sandbox provider launches. pub(crate) const WORK_FOLDER_ENVIRONMENT_KEYS: &[&str] = &[ "PAPERCLIP_RUNNER_EXTERNAL_SANDBOX",