From 7867f85bf783e57cba89f282e1cb904bc0aa717b Mon Sep 17 00:00:00 2001 From: Jared Davenport Date: Tue, 7 Jul 2026 16:50:48 -0400 Subject: [PATCH] Add FedRAMP API Codex binary --- .github/workflows/rust-release-windows.yml | 6 +- codex-rs/cli/BUILD.bazel | 1 + codex-rs/cli/Cargo.toml | 5 + codex-rs/cli/src/fedramp_api.rs | 168 +++++++++++++++++ codex-rs/cli/src/fedramp_api_defaults.toml | 50 +++++ codex-rs/cli/src/fedramp_api_tests.rs | 204 +++++++++++++++++++++ codex-rs/cli/src/main.rs | 6 +- 7 files changed, 436 insertions(+), 4 deletions(-) create mode 100644 codex-rs/cli/src/fedramp_api.rs create mode 100644 codex-rs/cli/src/fedramp_api_defaults.toml create mode 100644 codex-rs/cli/src/fedramp_api_tests.rs diff --git a/.github/workflows/rust-release-windows.yml b/.github/workflows/rust-release-windows.yml index ccb2ee13f7..76139d0cc4 100644 --- a/.github/workflows/rust-release-windows.yml +++ b/.github/workflows/rust-release-windows.yml @@ -4,7 +4,7 @@ on: workflow_call: env: - WINDOWS_BINARIES: "codex codex-code-mode-host codex-responses-api-proxy codex-windows-sandbox-setup codex-command-runner codex-app-server" + WINDOWS_BINARIES: "codex codex-fedramp-api codex-code-mode-host codex-responses-api-proxy codex-windows-sandbox-setup codex-command-runner codex-app-server" jobs: build-windows-binaries: @@ -25,14 +25,14 @@ jobs: - runner: windows-x64 target: x86_64-pc-windows-msvc bundle: primary - binaries: "codex codex-code-mode-host codex-responses-api-proxy" + binaries: "codex codex-fedramp-api codex-code-mode-host codex-responses-api-proxy" runs_on: group: ${{ github.event.repository.name }}-runners labels: ${{ github.event.repository.name }}-windows-x64 - runner: windows-arm64 target: aarch64-pc-windows-msvc bundle: primary - binaries: "codex codex-code-mode-host codex-responses-api-proxy" + binaries: "codex codex-fedramp-api codex-code-mode-host codex-responses-api-proxy" runs_on: group: ${{ github.event.repository.name }}-runners labels: ${{ github.event.repository.name }}-windows-arm64 diff --git a/codex-rs/cli/BUILD.bazel b/codex-rs/cli/BUILD.bazel index 97c868845b..85ed8ccfe9 100644 --- a/codex-rs/cli/BUILD.bazel +++ b/codex-rs/cli/BUILD.bazel @@ -3,6 +3,7 @@ load("//bazel/platforms:release_binaries.bzl", "multiplatform_binaries") codex_rust_crate( name = "cli", + compile_data = ["src/fedramp_api_defaults.toml"], crate_name = "codex_cli", extra_binaries = [ "//codex-rs/bwrap:bwrap", diff --git a/codex-rs/cli/Cargo.toml b/codex-rs/cli/Cargo.toml index 9a8312ced2..a71b8b3855 100644 --- a/codex-rs/cli/Cargo.toml +++ b/codex-rs/cli/Cargo.toml @@ -9,6 +9,11 @@ build = "build.rs" name = "codex" path = "src/main.rs" +[[bin]] +name = "codex-fedramp-api" +path = "src/main.rs" +test = false + [lib] name = "codex_cli" path = "src/lib.rs" diff --git a/codex-rs/cli/src/fedramp_api.rs b/codex-rs/cli/src/fedramp_api.rs new file mode 100644 index 0000000000..95bcb19f7a --- /dev/null +++ b/codex-rs/cli/src/fedramp_api.rs @@ -0,0 +1,168 @@ +use anyhow::bail; +use codex_utils_cli::CliConfigOverrides; +use std::ffi::OsString; + +use super::LoginSubcommand; +use super::Subcommand; + +const FEDRAMP_API_BINARY_NAME: &str = "codex-fedramp-api"; +const FEDRAMP_API_DEFAULTS_TOML: &str = include_str!("fedramp_api_defaults.toml"); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum CodexFlavor { + Normal, + FedRAMPApi, +} + +impl CodexFlavor { + pub(crate) fn compiled() -> Self { + Self::from_compiled_binary_name(option_env!("CARGO_BIN_NAME")) + } + + fn from_compiled_binary_name(binary_name: Option<&str>) -> Self { + match binary_name { + Some(FEDRAMP_API_BINARY_NAME) => Self::FedRAMPApi, + _ => Self::Normal, + } + } + + fn is_fedramp_api(self) -> bool { + self == Self::FedRAMPApi + } +} + +pub(crate) fn apply( + flavor: CodexFlavor, + root_config_overrides: &mut CliConfigOverrides, + subcommand: Option<&Subcommand>, +) -> anyhow::Result<()> { + if !flavor.is_fedramp_api() { + return Ok(()); + } + + reject_unsafe_cli_args(std::env::args_os().skip(1))?; + reject_unsupported_subcommand(subcommand)?; + root_config_overrides + .raw_overrides + .extend(blessed_config_overrides()?); + Ok(()) +} + +fn blessed_config_overrides() -> anyhow::Result> { + let config: toml::Value = toml::from_str(FEDRAMP_API_DEFAULTS_TOML)?; + let mut overrides = Vec::new(); + flatten_toml_value(&config, None, &mut overrides); + Ok(overrides) +} + +fn flatten_toml_value(value: &toml::Value, prefix: Option<&str>, overrides: &mut Vec) { + if let toml::Value::Table(table) = value { + if table.is_empty() { + if let Some(prefix) = prefix { + overrides.push(format!("{prefix}={{}}")); + } + return; + } + + for (key, child) in table { + let path = match prefix { + Some(prefix) => format!("{prefix}.{key}"), + None => key.clone(), + }; + flatten_toml_value(child, Some(&path), overrides); + } + return; + } + + if let Some(prefix) = prefix { + overrides.push(format!("{prefix}={value}")); + } +} + +fn reject_unsafe_cli_args(args: impl Iterator) -> anyhow::Result<()> { + for arg in args { + let arg = arg.to_string_lossy(); + if arg == "--" { + break; + } + + if matches!( + arg.as_ref(), + "-c" | "--config" + | "--enable" + | "--disable" + | "-p" + | "--profile" + | "--oss" + | "--local-provider" + | "--remote" + | "--remote-auth-token-env" + ) || arg.starts_with("--config=") + || arg.starts_with("-c") + || arg.starts_with("--enable=") + || arg.starts_with("--disable=") + || arg.starts_with("--profile=") + || arg.starts_with("-p") + || arg.starts_with("--local-provider=") + || arg.starts_with("--remote=") + || arg.starts_with("--remote-auth-token-env=") + { + bail!("{arg} is not supported by codex-fedramp-api"); + } + } + + Ok(()) +} + +fn reject_unsupported_subcommand(subcommand: Option<&Subcommand>) -> anyhow::Result<()> { + match subcommand { + None + | Some(Subcommand::Exec(_)) + | Some(Subcommand::Review(_)) + | Some(Subcommand::Logout(_)) + | Some(Subcommand::Completion(_)) + | Some(Subcommand::Doctor(_)) + | Some(Subcommand::Sandbox(_)) + | Some(Subcommand::Debug(_)) + | Some(Subcommand::Execpolicy(_)) + | Some(Subcommand::Apply(_)) + | Some(Subcommand::Resume(_)) + | Some(Subcommand::Archive(_)) + | Some(Subcommand::Delete(_)) + | Some(Subcommand::Unarchive(_)) + | Some(Subcommand::Fork(_)) => Ok(()), + Some(Subcommand::Login(login)) => { + if matches!(login.action, Some(LoginSubcommand::Status)) + || (login.with_api_key + && !login.with_access_token + && !login.use_device_code + && login.api_key.is_none()) + { + Ok(()) + } else { + bail!("codex-fedramp-api only supports API-key login and login status"); + } + } + Some(Subcommand::Mcp(_)) + | Some(Subcommand::Plugin(_)) + | Some(Subcommand::McpServer(_)) + | Some(Subcommand::AppServer(_)) + | Some(Subcommand::RemoteControl(_)) + | Some(Subcommand::Update) + | Some(Subcommand::Cloud(_)) + | Some(Subcommand::ResponsesApiProxy(_)) + | Some(Subcommand::StdioToUds(_)) + | Some(Subcommand::ExecServer(_)) + | Some(Subcommand::Features(_)) => { + bail!("this command is not supported by codex-fedramp-api"); + } + #[cfg(any(target_os = "macos", target_os = "windows"))] + Some(Subcommand::App(_)) => { + bail!("this command is not supported by codex-fedramp-api"); + } + } +} + +#[cfg(test)] +#[path = "fedramp_api_tests.rs"] +mod tests; diff --git a/codex-rs/cli/src/fedramp_api_defaults.toml b/codex-rs/cli/src/fedramp_api_defaults.toml new file mode 100644 index 0000000000..c82619261e --- /dev/null +++ b/codex-rs/cli/src/fedramp_api_defaults.toml @@ -0,0 +1,50 @@ +forced_login_method = "api" +openai_base_url = "https://gov.api.openai.com/v1" +model_provider = "openai" +mcp_servers = {} +model_providers = {} +plugins = {} + +[features] +apps = false +enable_mcp_apps = false +plugins = false +remote_plugin = false +tool_suggest = false +memories = false +memory_tool = false +multi_agent = false +multi_agent_mode = false +multi_agent_v2 = false +remote_control = false +plugin_hooks = false +plugin_sharing = false +skill_mcp_dependency_install = false +mentions_v2 = false + +[memories] +generate_memories = false +use_memories = false +dedicated_tools = false + +[skills] +include_instructions = false +config = [] + +[skills.bundled] +enabled = false + +[orchestrator.skills] +enabled = false + +[orchestrator.mcp] +enabled = false + +[analytics] +enabled = false + +[otel] +exporter = "none" +trace_exporter = "none" +metrics_exporter = "none" +log_user_prompt = false diff --git a/codex-rs/cli/src/fedramp_api_tests.rs b/codex-rs/cli/src/fedramp_api_tests.rs new file mode 100644 index 0000000000..3694e25646 --- /dev/null +++ b/codex-rs/cli/src/fedramp_api_tests.rs @@ -0,0 +1,204 @@ +use super::CodexFlavor; +use super::FEDRAMP_API_DEFAULTS_TOML; +use super::blessed_config_overrides; +use super::reject_unsafe_cli_args; +use codex_utils_cli::CliConfigOverrides; +use pretty_assertions::assert_eq; + +#[test] +fn blessed_defaults_are_valid_toml() { + let parsed: toml::Value = + toml::from_str(FEDRAMP_API_DEFAULTS_TOML).expect("blessed defaults should parse"); + let table = parsed + .as_table() + .expect("blessed defaults should be a TOML table"); + assert_eq!( + table.get("openai_base_url"), + Some(&toml::Value::String( + "https://gov.api.openai.com/v1".to_string() + )) + ); + assert_eq!( + table.get("forced_login_method"), + Some(&toml::Value::String("api".to_string())) + ); + assert_eq!( + table.get("model_provider"), + Some(&toml::Value::String("openai".to_string())) + ); + assert_eq!( + table.get("mcp_servers"), + Some(&toml::Value::Table(toml::Table::new())) + ); + assert_eq!( + table.get("model_providers"), + Some(&toml::Value::Table(toml::Table::new())) + ); + assert_eq!( + table.get("plugins"), + Some(&toml::Value::Table(toml::Table::new())) + ); + + let features = table + .get("features") + .and_then(toml::Value::as_table) + .expect("features should be a TOML table"); + for key in [ + "apps", + "enable_mcp_apps", + "plugins", + "remote_plugin", + "tool_suggest", + "memories", + "memory_tool", + "multi_agent", + "multi_agent_mode", + "multi_agent_v2", + "remote_control", + "plugin_hooks", + "plugin_sharing", + "skill_mcp_dependency_install", + "mentions_v2", + ] { + assert_eq!( + features.get(key), + Some(&toml::Value::Boolean(false)), + "{key} should be disabled" + ); + } + + let memories = table + .get("memories") + .and_then(toml::Value::as_table) + .expect("memories should be a TOML table"); + for key in ["generate_memories", "use_memories", "dedicated_tools"] { + assert_eq!( + memories.get(key), + Some(&toml::Value::Boolean(false)), + "{key} should be disabled" + ); + } + + let analytics = table + .get("analytics") + .and_then(toml::Value::as_table) + .expect("analytics should be a TOML table"); + assert_eq!(analytics.get("enabled"), Some(&toml::Value::Boolean(false))); + + let otel = table + .get("otel") + .and_then(toml::Value::as_table) + .expect("otel should be a TOML table"); + for key in ["exporter", "trace_exporter", "metrics_exporter"] { + assert_eq!( + otel.get(key), + Some(&toml::Value::String("none".to_string())), + "{key} should be disabled" + ); + } + assert_eq!( + otel.get("log_user_prompt"), + Some(&toml::Value::Boolean(false)) + ); +} + +#[test] +fn blessed_defaults_override_user_values() { + let mut config = toml::Value::Table(toml::Table::from_iter([ + ( + "openai_base_url".to_string(), + toml::Value::String("https://api.openai.com/v1".to_string()), + ), + ( + "model_provider".to_string(), + toml::Value::String("ollama".to_string()), + ), + ( + "mcp_servers".to_string(), + toml::Value::Table(toml::Table::from_iter([( + "attacker".to_string(), + toml::Value::Table(toml::Table::from_iter([( + "command".to_string(), + toml::Value::String("evil".to_string()), + )])), + )])), + ), + ( + "skills".to_string(), + toml::Value::Table(toml::Table::from_iter([( + "config".to_string(), + toml::Value::Array(vec![toml::Value::Table(toml::Table::from_iter([ + ( + "name".to_string(), + toml::Value::String("attacker".to_string()), + ), + ("enabled".to_string(), toml::Value::Boolean(true)), + ]))]), + )])), + ), + ])); + let overrides = CliConfigOverrides { + raw_overrides: blessed_config_overrides().expect("blessed defaults should flatten"), + }; + overrides + .apply_on_value(&mut config) + .expect("blessed defaults should apply"); + + assert_eq!( + config.get("openai_base_url"), + Some(&toml::Value::String( + "https://gov.api.openai.com/v1".to_string() + )) + ); + assert_eq!( + config.get("model_provider"), + Some(&toml::Value::String("openai".to_string())) + ); + assert_eq!( + config.get("mcp_servers"), + Some(&toml::Value::Table(toml::Table::new())) + ); + assert_eq!( + config + .get("skills") + .and_then(toml::Value::as_table) + .and_then(|skills| skills.get("config")), + Some(&toml::Value::Array(Vec::new())) + ); +} + +#[test] +fn rejects_unsafe_config_and_provider_flags() { + for args in [ + vec!["-c", "openai_base_url=\"https://api.openai.com/v1\""], + vec!["--config=openai_base_url=\"https://api.openai.com/v1\""], + vec!["--enable=plugins"], + vec!["--profile", "custom"], + vec!["--oss"], + vec!["--local-provider=ollama"], + vec!["--remote=wss://example.com"], + ] { + let args = args.into_iter().map(Into::into); + assert!(reject_unsafe_cli_args(args).is_err()); + } +} + +#[test] +fn allows_prompt_after_argument_separator() { + let args = vec!["--", "--enable", "plugins"] + .into_iter() + .map(Into::into); + assert!(reject_unsafe_cli_args(args).is_ok()); +} + +#[test] +fn compiled_binary_name_selects_flavor() { + assert_eq!( + CodexFlavor::from_compiled_binary_name(Some("codex-fedramp-api")), + CodexFlavor::FedRAMPApi + ); + assert_eq!( + CodexFlavor::from_compiled_binary_name(Some("codex")), + CodexFlavor::Normal + ); +} diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index c766baa048..15e4545cd4 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -50,6 +50,7 @@ mod app_cmd; mod desktop_app; mod doctor; mod exec_server_telemetry; +mod fedramp_api; mod marketplace_cmd; mod mcp_cmd; mod plugin_cmd; @@ -954,9 +955,10 @@ fn stage_str(stage: Stage) -> &'static str { } fn main() -> anyhow::Result<()> { + let flavor = fedramp_api::CodexFlavor::compiled(); let remote_control_disabled = codex_app_server::take_remote_control_disabled_env(); arg0_dispatch_or_else(move |arg0_paths: Arg0DispatchPaths| async move { - cli_main(arg0_paths, remote_control_disabled).await?; + cli_main(arg0_paths, remote_control_disabled, flavor).await?; Ok(()) }) } @@ -964,6 +966,7 @@ fn main() -> anyhow::Result<()> { async fn cli_main( arg0_paths: Arg0DispatchPaths, remote_control_disabled: bool, + flavor: fedramp_api::CodexFlavor, ) -> anyhow::Result<()> { let MultitoolCli { config_overrides: mut root_config_overrides, @@ -976,6 +979,7 @@ async fn cli_main( // Fold --enable/--disable into config overrides so they flow to all subcommands. let toggle_overrides = feature_toggles.to_overrides()?; root_config_overrides.raw_overrides.extend(toggle_overrides); + fedramp_api::apply(flavor, &mut root_config_overrides, subcommand.as_ref())?; let root_remote = remote.remote; let root_remote_auth_token_env = remote.remote_auth_token_env; let root_strict_config = interactive.strict_config;