From b7ee9cc8b3be6cceaaa867085d9314214438c114 Mon Sep 17 00:00:00 2001 From: "fineas-bot[bot]" <258147136+fineas-bot[bot]@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:46:11 -0400 Subject: [PATCH 1/2] feat: add workflow-host-only env passthrough Allow operators to copy explicitly named API process variables into workflow-host sandboxes without exposing them to interactive session sandboxes. Preserve the existing shared session passthrough contract. --- docs/pages/reference/configuration.mdx | 2 + docs/public/md/reference/configuration.md | 3 + .../crates/centaur-api-server/src/args.rs | 97 ++++++++++++++++++- 3 files changed, 101 insertions(+), 1 deletion(-) diff --git a/docs/pages/reference/configuration.mdx b/docs/pages/reference/configuration.mdx index 80cb29b24..479271030 100644 --- a/docs/pages/reference/configuration.mdx +++ b/docs/pages/reference/configuration.mdx @@ -83,6 +83,8 @@ Optional required-by-mode variables: | `OTEL_TRACES_EXPORTER` | `apiRs.extraEnv`. | Set to `otlp` to force OTLP trace export, or `none`/`off` to disable it. | | `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | `apiRs.extraEnv`. | Enables OTLP trace export to Tempo, Jaeger, or another OTLP collector. | | `apiRs.workflowHostSandbox`, `WORKFLOW_HOST_SANDBOX` | Helm value, default `true`; override with `apiRs.extraEnv`. | Runs workflow hosts in Kubernetes sandboxes instead of the api-rs process. Required for workflow-scoped principals. | +| `WORKFLOW_HOST_PASSTHROUGH_ENV` | `apiRs.extraEnv`; comma-delimited variable names. | Copies named variables from the api-rs process into workflow-host sandboxes only. Names listed only here are never copied into interactive session sandboxes; duplicate names are collapsed. | +| `SESSION_SANDBOX_PASSTHROUGH_ENV` | `apiRs.extraEnv`; comma-delimited variable names. | Copies named variables from the api-rs process into interactive session sandboxes and, for backwards compatibility, workflow-host sandboxes. | | `apiRs.metrics.scrapeAnnotations` | Helm value, default `true`. | Adds Prometheus scrape annotations to the API-RS Pod template and Service. | | `apiRs.metrics.path` | Helm value, default `/metrics`. | Metrics scrape path for annotation-based discovery. | | `apiRs.metrics.annotations` | Helm value. | Additional scrape annotations for Prometheus-compatible collectors. | diff --git a/docs/public/md/reference/configuration.md b/docs/public/md/reference/configuration.md index 53841c94f..479271030 100644 --- a/docs/public/md/reference/configuration.md +++ b/docs/public/md/reference/configuration.md @@ -82,6 +82,9 @@ Optional required-by-mode variables: | `CENTAUR_ENVIRONMENT`, `DEPLOY_ENV`, `ENVIRONMENT` | `apiRs.extraEnv` or deployment env. | Deployment environment resource attribute for telemetry. | | `OTEL_TRACES_EXPORTER` | `apiRs.extraEnv`. | Set to `otlp` to force OTLP trace export, or `none`/`off` to disable it. | | `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | `apiRs.extraEnv`. | Enables OTLP trace export to Tempo, Jaeger, or another OTLP collector. | +| `apiRs.workflowHostSandbox`, `WORKFLOW_HOST_SANDBOX` | Helm value, default `true`; override with `apiRs.extraEnv`. | Runs workflow hosts in Kubernetes sandboxes instead of the api-rs process. Required for workflow-scoped principals. | +| `WORKFLOW_HOST_PASSTHROUGH_ENV` | `apiRs.extraEnv`; comma-delimited variable names. | Copies named variables from the api-rs process into workflow-host sandboxes only. Names listed only here are never copied into interactive session sandboxes; duplicate names are collapsed. | +| `SESSION_SANDBOX_PASSTHROUGH_ENV` | `apiRs.extraEnv`; comma-delimited variable names. | Copies named variables from the api-rs process into interactive session sandboxes and, for backwards compatibility, workflow-host sandboxes. | | `apiRs.metrics.scrapeAnnotations` | Helm value, default `true`. | Adds Prometheus scrape annotations to the API-RS Pod template and Service. | | `apiRs.metrics.path` | Helm value, default `/metrics`. | Metrics scrape path for annotation-based discovery. | | `apiRs.metrics.annotations` | Helm value. | Additional scrape annotations for Prometheus-compatible collectors. | diff --git a/services/api-rs/crates/centaur-api-server/src/args.rs b/services/api-rs/crates/centaur-api-server/src/args.rs index bb1504bec..d23b83276 100644 --- a/services/api-rs/crates/centaur-api-server/src/args.rs +++ b/services/api-rs/crates/centaur-api-server/src/args.rs @@ -668,6 +668,15 @@ struct SandboxArgs { value_delimiter = ',' )] passthrough_env: Vec, + /// Comma-delimited API process variables copied only into workflow-host + /// sandboxes. Unlike SESSION_SANDBOX_PASSTHROUGH_ENV, these values are not + /// exposed to interactive session sandboxes. + #[arg( + long = "workflow-host-passthrough-env", + env = "WORKFLOW_HOST_PASSTHROUGH_ENV", + value_delimiter = ',' + )] + workflow_host_passthrough_env: Vec, /// Operator-supplied sandbox env as a JSON list of `{"name","value"}` /// objects — the chart renders `sandbox.extraEnv` into this (the same /// contract as the Python control plane's `KUBERNETES_SANDBOX_EXTRA_ENV`). @@ -1255,7 +1264,13 @@ impl SandboxArgs { envs.push(("TOOLS_OVERLAY_PATH".to_owned(), value)); } - for name in self.passthrough_env_names() { + // Keep SESSION_SANDBOX_PASSTHROUGH_ENV available here for backwards + // compatibility, then layer workflow-host-only names on top. Upserting + // ensures duplicate and overlapping allowlist entries produce one env. + for name in self + .passthrough_env_names() + .chain(self.workflow_host_passthrough_env_names()) + { if let Ok(value) = env::var(name) { if let Some((_, existing_value)) = envs .iter_mut() @@ -1287,6 +1302,14 @@ impl SandboxArgs { .filter(|name| !name.is_empty()) } + fn workflow_host_passthrough_env_names(&self) -> impl Iterator { + self.workflow_host_passthrough_env + .iter() + .flat_map(|entry| entry.split(',')) + .map(str::trim) + .filter(|name| !name.is_empty()) + } + fn discover_tool_proxy_fragment( &self, ) -> Result, ServerError> { @@ -2935,6 +2958,78 @@ mod tests { ); } + #[test] + fn workflow_host_passthrough_env_is_workflow_only_and_deduplicated() { + let _lock = ENV_LOCK.lock().unwrap(); + let _env = EnvGuard::set(&[ + ("SESSION_SHARED", "session-value"), + ("WORKFLOW_ONLY", "workflow-value"), + ("OVERLAPPING_ENV", "shared-value"), + ]); + let args = Args::try_parse_from([ + "centaur-api-server", + "--database-url", + "postgres://postgres:postgres@localhost/centaur", + "--session-sandbox-backend", + "agent-k8s", + "--session-sandbox-workload", + "codex-app-server", + "--session-sandbox-passthrough-env", + "SESSION_SHARED,OVERLAPPING_ENV", + "--workflow-host-passthrough-env", + "WORKFLOW_ONLY,WORKFLOW_ONLY,OVERLAPPING_ENV", + "--kubernetes-sandbox-iron-proxy-mode", + "disabled", + ]) + .unwrap(); + + let workflow_spec = args.sandbox.workflow_host_spec(None).unwrap(); + let session_env = args.sandbox.codex_app_server_env_template().unwrap(); + + let workflow_value = |key: &str| { + workflow_spec + .env + .iter() + .find(|env| env.name == key) + .map(|env| env.value.as_str()) + }; + let session_value = |key: &str| { + session_env + .iter() + .find(|(name, _)| name == key) + .map(|(_, value)| value.as_str()) + }; + + // Preserve the existing shared passthrough behavior. + assert_eq!(workflow_value("SESSION_SHARED"), Some("session-value")); + assert_eq!(session_value("SESSION_SHARED"), Some("session-value")); + assert_eq!(session_value("OVERLAPPING_ENV"), Some("shared-value")); + + // The new allowlist is workflow-host-only. + assert_eq!(workflow_value("WORKFLOW_ONLY"), Some("workflow-value")); + assert_eq!(session_value("WORKFLOW_ONLY"), None); + + // Duplicate entries within the new list and overlaps with the shared + // list are collapsed without changing the API process value. + assert_eq!( + workflow_spec + .env + .iter() + .filter(|env| env.name == "WORKFLOW_ONLY") + .count(), + 1 + ); + assert_eq!( + workflow_spec + .env + .iter() + .filter(|env| env.name == "OVERLAPPING_ENV") + .count(), + 1 + ); + assert_eq!(workflow_value("OVERLAPPING_ENV"), Some("shared-value")); + } + #[test] fn codex_app_server_env_template_injects_auth_mode_and_placeholder() { let args = Args::try_parse_from([ From 71affb2e32547c19e67ea8e0c69943951fba664e Mon Sep 17 00:00:00 2001 From: "fineas-bot[bot]" <258147136+fineas-bot[bot]@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:47:48 -0400 Subject: [PATCH 2/2] test: cover workflow env precedence Verify workflow-only values replace workflow-host template placeholders without changing the corresponding interactive sandbox placeholder. --- services/api-rs/crates/centaur-api-server/src/args.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/services/api-rs/crates/centaur-api-server/src/args.rs b/services/api-rs/crates/centaur-api-server/src/args.rs index d23b83276..28256e22d 100644 --- a/services/api-rs/crates/centaur-api-server/src/args.rs +++ b/services/api-rs/crates/centaur-api-server/src/args.rs @@ -2965,6 +2965,7 @@ mod tests { ("SESSION_SHARED", "session-value"), ("WORKFLOW_ONLY", "workflow-value"), ("OVERLAPPING_ENV", "shared-value"), + (GITHUB_TOKEN_ENV, "workflow-token-value"), ]); let args = Args::try_parse_from([ "centaur-api-server", @@ -2977,7 +2978,7 @@ mod tests { "--session-sandbox-passthrough-env", "SESSION_SHARED,OVERLAPPING_ENV", "--workflow-host-passthrough-env", - "WORKFLOW_ONLY,WORKFLOW_ONLY,OVERLAPPING_ENV", + "WORKFLOW_ONLY,WORKFLOW_ONLY,OVERLAPPING_ENV,GITHUB_TOKEN", "--kubernetes-sandbox-iron-proxy-mode", "disabled", ]) @@ -3009,6 +3010,14 @@ mod tests { assert_eq!(workflow_value("WORKFLOW_ONLY"), Some("workflow-value")); assert_eq!(session_value("WORKFLOW_ONLY"), None); + // Workflow-only passthrough is applied after the workflow template, + // while the interactive template retains its proxy placeholder. + assert_eq!( + workflow_value(GITHUB_TOKEN_ENV), + Some("workflow-token-value") + ); + assert_eq!(session_value(GITHUB_TOKEN_ENV), Some(GITHUB_TOKEN_ENV)); + // Duplicate entries within the new list and overlaps with the shared // list are collapsed without changing the API process value. assert_eq!(