use std::collections::BTreeMap; use std::collections::HashMap; use std::io::IsTerminal; use std::io::Read; use std::io::Write; use std::sync::Arc; use anyhow::Context; use anyhow::bail; use clap::Parser; use codex_core_api::AbsolutePathBuf; use codex_core_api::AltScreenMode; use codex_core_api::ApprovalsReviewer; use codex_core_api::Arg0DispatchPaths; use codex_core_api::AskForApproval; use codex_core_api::AuthKeyringBackendKind; use codex_core_api::AuthManager; use codex_core_api::AutoCompactTokenLimitScope; use codex_core_api::CodexAppsToolsCache; use codex_core_api::CodexHomeUserInstructionsProvider; use codex_core_api::CodexThread; use codex_core_api::Config; use codex_core_api::ConfigLayerStack; use codex_core_api::ConfigLoadOptions; use codex_core_api::ConfigRequirements; use codex_core_api::ConfigRequirementsToml; use codex_core_api::Constrained; use codex_core_api::EnvironmentManager; use codex_core_api::EventMsg; use codex_core_api::ExecServerRuntimeOptions; use codex_core_api::ExtensionRegistryBuilder; use codex_core_api::Feature; use codex_core_api::Features; use codex_core_api::GhostSnapshotConfig; use codex_core_api::History; use codex_core_api::MemoriesConfig; use codex_core_api::ModelAvailabilityNuxConfig; use codex_core_api::MultiAgentV2Config; use codex_core_api::NewThread; use codex_core_api::Notice; use codex_core_api::OPENAI_PROVIDER_ID; use codex_core_api::OtelConfig; use codex_core_api::PermissionProfile; use codex_core_api::Permissions; use codex_core_api::ProjectConfig; use codex_core_api::RealtimeAudioConfig; use codex_core_api::RealtimeConfig; use codex_core_api::SessionPickerViewMode; use codex_core_api::SessionSource; use codex_core_api::SqliteConfig; use codex_core_api::StartIfIdleSubmission; use codex_core_api::StartThreadOptions; use codex_core_api::TerminalResizeReflowConfig; use codex_core_api::ThreadManager; use codex_core_api::ThreadStoreConfig; use codex_core_api::ToolSuggestConfig; use codex_core_api::TuiKeymap; use codex_core_api::TuiNotificationSettings; use codex_core_api::TuiPetAnchor; use codex_core_api::TurnInputRequest; use codex_core_api::UriBasedFileOpener; use codex_core_api::UserInput; use codex_core_api::WebSearchMode; use codex_core_api::arg0_dispatch_or_else; use codex_core_api::bootstrap_auth_config; use codex_core_api::build_models_manager; use codex_core_api::built_in_model_providers; use codex_core_api::find_codex_home; use codex_core_api::init_state_db; use codex_core_api::install_image_generation_extension; use codex_core_api::item_event_to_server_notification; use codex_core_api::load_config_toml_with_layer_stack; use codex_core_api::local_agent_graph_store_from_state_db; use codex_core_api::passthrough_image_store; use codex_core_api::resolve_bootstrap_respect_system_proxy; use codex_core_api::resolve_installation_id; use codex_core_api::set_default_originator; use codex_core_api::thread_store_from_config; #[derive(Debug, Parser)] #[command( name = "codex-thread-manager-sample", about = "Run one Codex turn through ThreadManager and print mapped notifications as newline-delimited JSON." )] struct Args { /// Override the model for this run. #[arg(long, value_name = "MODEL")] model: Option, /// Prompt text. If omitted, the prompt is read from piped stdin. #[arg(value_name = "PROMPT", num_args = 0.., trailing_var_arg = true)] prompt: Vec, } fn main() -> anyhow::Result<()> { arg0_dispatch_or_else(run_main) } async fn run_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> { if let Err(err) = set_default_originator("codex_thread_manager_sample".to_string()) { tracing::warn!("failed to set originator: {err:?}"); } let args = Args::parse(); let prompt = if args.prompt.is_empty() { if std::io::stdin().is_terminal() { bail!("no prompt provided; pass a prompt argument or pipe one into stdin"); } let mut prompt = String::new(); std::io::stdin() .read_to_string(&mut prompt) .context("read prompt from stdin")?; let prompt = prompt.replace("\r\n", "\n").replace('\r', "\n"); if prompt.trim().is_empty() { bail!("no prompt provided via stdin"); } prompt } else { args.prompt.join(" ") }; let config = new_config(args.model, arg0_paths).await?; let state_db = init_state_db(&config).await; let auth_manager = AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false).await?; let local_runtime_paths = ExecServerRuntimeOptions::from_optional_paths( config.codex_self_exe.clone(), config.codex_linux_sandbox_exe.clone(), )?; let thread_store = thread_store_from_config(&config, state_db.clone()); let environment_manager = Arc::new( EnvironmentManager::from_codex_home( config.codex_home.clone(), Some(local_runtime_paths), config.http_client_factory(), ) .await?, ); let installation_id = resolve_installation_id(&config.codex_home).await?; let user_instructions_provider = Arc::new(CodexHomeUserInstructionsProvider::new( config.codex_home.clone(), )); let mut extensions = ExtensionRegistryBuilder::::new(); install_image_generation_extension(&mut extensions, auth_manager.clone(), |config: &Config| { Some(config.codex_home.clone()) }); let thread_manager = ThreadManager::new( &config, Arc::clone(&auth_manager), build_models_manager(&config, auth_manager), CodexAppsToolsCache::default(), SessionSource::Exec, environment_manager, Arc::new(extensions.build()), user_instructions_provider, /*analytics_events_client*/ None, passthrough_image_store(), Arc::clone(&thread_store), local_agent_graph_store_from_state_db(state_db.as_ref()), installation_id, /*attestation_provider*/ None, /*external_time_provider*/ None, ); let NewThread { thread_id, thread, .. } = thread_manager .start_thread(StartThreadOptions::new(config)) .await .context("start Codex thread")?; let thread_id_string = thread_id.to_string(); let turn_output = run_turn(&thread, &thread_id_string, prompt).await; let shutdown_result = thread.shutdown_and_wait().await; let _ = thread_manager.remove_thread(&thread_id).await; turn_output?; shutdown_result.context("shut down Codex thread")?; Ok(()) } async fn new_config( model: Option, arg0_paths: Arg0DispatchPaths, ) -> anyhow::Result { let codex_home = find_codex_home().context("find Codex home")?; let cwd = AbsolutePathBuf::current_dir().context("resolve current directory")?; // Resolve auth through the shared bootstrap path so cargo-run builds retain the configured // secure store instead of the full Config loader's local-development file-store override. let bootstrap = load_config_toml_with_layer_stack( codex_home.as_path(), Some(&cwd), Vec::new(), ConfigLoadOptions::default(), ) .await .context("load authentication configuration")?; let auth_config = bootstrap_auth_config(codex_home.as_path(), &bootstrap)?; let requirements = bootstrap.config_layer_stack.requirements(); let requirements_toml = bootstrap.config_layer_stack.requirements_toml(); // Preserve auth restrictions for every manager rebuilt from Config without importing // execution settings, plugins, or hooks from the bootstrap configuration. let config_layer_stack = ConfigLayerStack::new( Vec::new(), ConfigRequirements { allowed_login_methods: requirements.allowed_login_methods.clone(), allowed_chatgpt_workspaces: requirements.allowed_chatgpt_workspaces.clone(), ..Default::default() }, ConfigRequirementsToml { allowed_login_methods: requirements_toml.allowed_login_methods.clone(), allowed_chatgpt_workspaces: requirements_toml.allowed_chatgpt_workspaces.clone(), ..Default::default() }, )?; let mcp_oauth_credentials_store_mode = bootstrap .config_toml .mcp_oauth_credentials_store .unwrap_or_default(); let respect_system_proxy = resolve_bootstrap_respect_system_proxy( &bootstrap.config_toml, bootstrap .config_layer_stack .requirements() .feature_requirements .as_ref(), )?; let model_provider_id = OPENAI_PROVIDER_ID.to_string(); let model_providers = built_in_model_providers(/*openai_base_url*/ None); let model_provider = model_providers .get(&model_provider_id) .context("OpenAI model provider should be available")? .clone(); let mut config = Config { application_network_policy: Default::default(), application_auth_route_config: Some(auth_config.auth_route_config.clone()), config_layer_stack, startup_warnings: Vec::new(), bypass_hook_trust: false, model, daybreak_enabled: false, service_tier: None, review_model: None, model_context_window: None, model_auto_compact_token_limit: None, model_auto_compact_token_limit_scope: AutoCompactTokenLimitScope::Total, model_post_turn_compact_threshold_percent: 0, model_provider_id, model_provider, personality: None, permissions: Permissions::from_approval_and_profile( Constrained::allow_any(AskForApproval::Never), Constrained::allow_any(PermissionProfile::read_only()), )?, explicit_permission_profile_mode: false, custom_permission_profiles: Vec::new(), approvals_reviewer: ApprovalsReviewer::User, enforce_residency: Constrained::allow_any(/*initial_value*/ None), hide_agent_reasoning: false, show_raw_agent_reasoning: false, base_instructions: None, base_instructions_provenance: None, developer_instructions: None, guardian_policy_config: None, guardian_extra_policy: None, guardian_policy_template: None, guardian_conversation_history_prompt: None, guardian_conversation_history_max_output_tokens: None, guardian_circuit_break_action: Default::default(), include_permissions_instructions: false, include_apps_instructions: false, include_collaboration_mode_instructions: false, include_skill_instructions: false, skill_max_context_tokens: None, cloud_skill_enabled: false, orchestrator_mcp_enabled: false, include_environment_context: false, compact_prompt: None, notify: None, tui_notifications: TuiNotificationSettings::default(), animations: true, tui_effects: Default::default(), tui_rendering: Default::default(), show_tooltips: true, tui_show_server_version_notice: true, tui_auto_recap: true, model_availability_nux: ModelAvailabilityNuxConfig::default(), tui_fullscreen_transcript: false, tui_mouse_scroll_speed: None, tui_copy_on_select: Default::default(), tui_right_click_paste: Default::default(), tui_alternate_screen: AltScreenMode::Auto, tui_status_line: None, tui_status_line_use_colors: true, tui_terminal_title: None, tui_theme: None, tui_raw_output_mode: false, tui_pet: None, tui_pet_anchor: TuiPetAnchor::Composer, terminal_resize_reflow: TerminalResizeReflowConfig::default(), tui_keymap: TuiKeymap::default(), tui_session_picker_view: SessionPickerViewMode::Dense, tui_resume_cwd: None, tui_vim_mode_default: false, tui_question_esc_back: true, cwd: cwd.clone(), workspace_roots: vec![cwd], workspace_roots_explicit: false, cli_auth_credentials_store_mode: auth_config.auth_credentials_store_mode, mcp_servers: Constrained::allow_any(HashMap::new()), mcp_enterprise_managed_auth: None, non_prefixed_mcp_tool_servers: None, mcp_oauth_credentials_store_mode, mcp_oauth_callback_port: None, mcp_oauth_callback_url: None, mcp_optional_startup_grace: std::time::Duration::from_secs(1), model_providers, project_doc_max_bytes: 32 * 1024, project_doc_fallback_filenames: Vec::new(), tool_output_token_limit: None, agents_enabled: true, agent_max_threads: Some(6), agent_default_subagent_model: None, agent_default_subagent_reasoning_effort: None, agent_interrupt_message_enabled: false, agent_max_depth: 1, agent_roles: BTreeMap::new(), memories: MemoriesConfig::default(), sqlite: SqliteConfig::from_sqlite_home(codex_home.clone()), log_dir: codex_home.join("log").to_path_buf(), codex_home, history: History::default(), ephemeral: true, extra_config: None, file_opener: UriBasedFileOpener::VsCode, codex_self_exe: arg0_paths.codex_self_exe, codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe, main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe, zsh_path: None, model_reasoning_effort: None, plan_mode_reasoning_effort: None, model_reasoning_summary: None, model_catalog: None, model_verbosity: None, chatgpt_base_url: auth_config .chatgpt_base_url .clone() .unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string()), respect_system_proxy, apps_mcp_product_sku: None, responses_api_metadata: BTreeMap::new(), realtime_audio: RealtimeAudioConfig::default(), experimental_realtime_ws_base_url: None, experimental_realtime_webrtc_call_base_url: None, experimental_realtime_ws_model: None, realtime: RealtimeConfig::default(), experimental_realtime_ws_backend_prompt: None, experimental_realtime_ws_startup_context: None, experimental_realtime_start_instructions: None, experimental_thread_store: ThreadStoreConfig::Local, forced_chatgpt_workspace_id: auth_config.forced_chatgpt_workspace_id.clone(), forced_login_method: auth_config.forced_login_method, web_search_mode: Constrained::allow_any(WebSearchMode::Disabled), web_search_config: None, experimental_request_user_input_enabled: true, update_plan_enabled: true, tool_registry: Default::default(), code_mode: Default::default(), background_terminal_max_timeout: 300_000, thread_unload_delay: std::time::Duration::from_secs(60), ghost_snapshot: GhostSnapshotConfig::default(), multi_agent_v2: MultiAgentV2Config::default(), max_goal_token_budget: None, token_budget: None, token_budget_startup_config: None, rollout_budget: None, current_time_reminder: None, sleep_tool_mode: Default::default(), features: Default::default(), prefer_mxc: false, runtime_feature_defaults: Default::default(), suppress_unstable_features_warning: false, active_project: ProjectConfig { trust_level: None }, notices: Notice::default(), check_for_update_on_startup: false, disable_paste_burst: false, analytics_enabled: Some(false), feedback_enabled: false, tool_suggest: ToolSuggestConfig::default(), otel: OtelConfig::default(), }; config .features .set(Features::with_defaults()) .context("configure default features")?; // AuthManager and MCP both derive their credential backend from Config. config.features.set_enabled( Feature::SecretAuthStorage, auth_config.keyring_backend_kind == AuthKeyringBackendKind::Secrets, )?; Ok(config) } async fn run_turn(thread: &CodexThread, thread_id: &str, prompt: String) -> anyhow::Result<()> { let submission = thread .start_turn_if_idle(TurnInputRequest::user_input(vec![UserInput::Text { text: prompt, text_elements: Vec::new(), }])) .await .context("submit user input")?; if let StartIfIdleSubmission::NotSubmitted { reason } = submission { bail!("turn input was not submitted: {reason:?}"); } let mut current_turn_id: Option = None; let mut stdout = std::io::stdout().lock(); loop { let event = thread.next_event().await.context("read Codex event")?; let notification = match &event.msg { EventMsg::TurnStarted(event) => { current_turn_id = Some(event.turn_id.clone()); None } EventMsg::DynamicToolCallResponse(_) | EventMsg::McpToolCallBegin(_) | EventMsg::McpToolCallEnd(_) | EventMsg::CollabAgentSpawnBegin(_) | EventMsg::CollabAgentSpawnEnd(_) | EventMsg::CollabAgentInteractionBegin(_) | EventMsg::CollabAgentInteractionEnd(_) | EventMsg::CollabWaitingBegin(_) | EventMsg::CollabWaitingEnd(_) | EventMsg::CollabCloseBegin(_) | EventMsg::CollabCloseEnd(_) | EventMsg::CollabResumeBegin(_) | EventMsg::CollabResumeEnd(_) | EventMsg::SubAgentActivity(_) | EventMsg::AgentMessageContentDelta(_) | EventMsg::PlanDelta(_) | EventMsg::ReasoningContentDelta(_) | EventMsg::ReasoningRawContentDelta(_) | EventMsg::AgentReasoningSectionBreak(_) | EventMsg::ItemStarted(_) | EventMsg::ItemCompleted(_) | EventMsg::PatchApplyBegin(_) | EventMsg::PatchApplyUpdated(_) | EventMsg::TerminalInteraction(_) | EventMsg::ExecCommandBegin(_) | EventMsg::ExecCommandOutputDelta(_) | EventMsg::ExecCommandEnd(_) => Some(item_event_to_server_notification( event.msg.clone(), thread_id, current_turn_id .as_deref() .context("mapped notification arrived before turn started")?, )), _ => None, }; if let Some(notification) = notification { serde_json::to_writer(&mut stdout, ¬ification) .context("serialize mapped notification")?; stdout .write_all(b"\n") .context("write notification newline")?; stdout.flush().context("flush notification output")?; } match event.msg { EventMsg::TurnComplete(_) => { return Ok(()); } EventMsg::Error(event) => { bail!(event.message); } EventMsg::TurnAborted(_) => { bail!("turn aborted"); } EventMsg::ExecApprovalRequest(_) => { bail!("turn requested exec approval"); } EventMsg::ApplyPatchApprovalRequest(_) => { bail!("turn requested patch approval"); } EventMsg::RequestPermissions(_) => { bail!("turn requested permissions"); } EventMsg::RequestUserInput(_) => { bail!("turn requested user input"); } EventMsg::DynamicToolCallRequest(_) => { bail!("turn requested a dynamic tool call"); } _ => {} } } }