29th platform `openai-codex` (uses_oauth, Responses wire). PKCE-localhost login at auth.openai.com (client app_EMoamEEZ73f0CkXaXp7hrann, redirect localhost:1455/auth/ callback, form token exchange, fresh-random state) reusing the claude-pro-max flow; OAuthFlow::PkceLocalhost gained a redirect_path and OAuthConfig an authorize_extra (empty elsewhere, so claude/xai/copilot authorize URLs stay byte-identical). Codex-specific: the access token is a JWT carrying chatgpt_account_id, which becomes the `chatgpt-account-id` inference header. It is derived STATELESSLY from whichever bearer rides each request (so a rotated token needs no persisted field), and BOTH login and refresh fail fast when the claim is absent — gated on the explicit OAuthConfig.requires_chatgpt_account_id fact, never inferred from the token-body encoding (a plain form endpoint is the OAuth norm and must not inherit this). Inference rides the existing Responses wire at chatgpt.com/backend-api/codex → /responses, with codex headers (chatgpt-account-id, originator, OpenAI-Beta responses=experimental, codex UA) gated on SamplerConfig.openai_codex so API-key `openai` stays byte-identical; store:false was already the global Responses default. Catalog is HARDCODED (no live endpoint exists for this backend; read from the official Codex CLI's model cache): gpt-5.6-sol/terra/luna + gpt-5.5, ctx 272000, each with its real reasoning levels (low..ultra — ReasoningEffort gained Ultra). Excluded: gpt-5.3-codex-spark (supported_in_api=false), gpt-5.4/-mini and codex-auto-review (hidden) — they would list but fail at inference. The fetch short-circuits before any HTTP; Kigi never shells out to the codex CLI or reads ~/.codex. Security review fixes: redact any `account-id` header from request logs (it was reaching debug logs), strict 3-segment JWT check (fail closed), refresh no longer fails open on a missing claim. Inherits leak-safe pooled routing (scope oauth/openai-codex) — never the Kimi token. Full gate green (234 suites, 0 warnings).
204 lines
7.6 KiB
Rust
204 lines
7.6 KiB
Rust
use crate::agent::subagent::SubagentSpawnContext;
|
|
use crate::session::SessionCommand;
|
|
use agent_client_protocol as acp;
|
|
use kigi_acp_lib::AcpAgentGatewaySender as GatewaySender;
|
|
use kigi_tools::implementations::kigi::task::types::{SubagentRequest, SubagentResult};
|
|
use std::collections::HashMap;
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
use tokio::sync::{mpsc, oneshot};
|
|
pub(crate) type GatewayOut = <acp::AgentSide as kigi_acp_lib::AcpSide>::OutMessage;
|
|
pub(crate) fn test_gateway() -> GatewaySender {
|
|
let (tx, _rx) = mpsc::unbounded_channel();
|
|
GatewaySender::new(tx)
|
|
}
|
|
/// Like `test_gateway` but returns the receiver; keep it alive for the test.
|
|
pub(crate) fn test_gateway_with_receiver() -> (GatewaySender, mpsc::UnboundedReceiver<GatewayOut>) {
|
|
let (tx, rx) = mpsc::unbounded_channel();
|
|
(GatewaySender::new(tx), rx)
|
|
}
|
|
/// `ctx_with_toggle` with a wired `parent_cmd_tx`.
|
|
pub(crate) fn ctx_with_toggle_and_cmd_tx(
|
|
toggle: HashMap<String, bool>,
|
|
) -> (
|
|
SubagentSpawnContext,
|
|
mpsc::UnboundedReceiver<SessionCommand>,
|
|
) {
|
|
let mut ctx = ctx_with_toggle(toggle);
|
|
let (tx, rx) = mpsc::unbounded_channel();
|
|
ctx.parent_cmd_tx = Some(tx);
|
|
(ctx, rx)
|
|
}
|
|
pub(crate) fn ctx_with_toggle(toggle: HashMap<String, bool>) -> SubagentSpawnContext {
|
|
let (tx, _rx) = mpsc::unbounded_channel();
|
|
SubagentSpawnContext {
|
|
lsp: None,
|
|
parent_max_turns: None,
|
|
gateway: test_gateway(),
|
|
client_hooks: Default::default(),
|
|
sampling_config: kigi_sampler::SamplerConfig {
|
|
api_key: None,
|
|
base_url: String::new(),
|
|
model: String::new(),
|
|
max_completion_tokens: None,
|
|
temperature: None,
|
|
top_p: None,
|
|
api_backend: Default::default(),
|
|
chat_compat: Default::default(),
|
|
auth_scheme: Default::default(),
|
|
anthropic_oauth: false,
|
|
github_copilot: false,
|
|
openai_codex: false,
|
|
extra_headers: Default::default(),
|
|
context_window: 256_000,
|
|
force_http1: false,
|
|
max_retries: None,
|
|
stream_tool_calls: false,
|
|
idle_timeout_secs: None,
|
|
reasoning_effort: None,
|
|
origin_client: None,
|
|
attribution_callback: None,
|
|
bearer_resolver: None,
|
|
supports_backend_search: false,
|
|
compactions_remaining: None,
|
|
compaction_at_tokens: None,
|
|
doom_loop_recovery: None,
|
|
header_injector: None,
|
|
},
|
|
alpha_test_key: None,
|
|
auth_method_id: acp::AuthMethodId::new("test"),
|
|
model_id: acp::ModelId::new("test"),
|
|
storage_mode: crate::config::StorageMode::Local,
|
|
auth: None,
|
|
parent_cwd: PathBuf::from("/tmp"),
|
|
parent_session_id: "test-parent".into(),
|
|
yolo_mode: false,
|
|
subagent_event_tx: tx,
|
|
hunk_tracker_handle: kigi_hunk_tracker::HunkTrackerHandle::noop(),
|
|
hunk_tracking_enabled: false,
|
|
fs: Arc::new(kigi_workspace::file_system::LocalFs::new(PathBuf::from(
|
|
"/tmp",
|
|
))),
|
|
terminal: Arc::new(crate::terminal::TerminalRunner::new(
|
|
Arc::new(test_gateway()),
|
|
acp::SessionId::new("test"),
|
|
)),
|
|
session_env: Arc::new(HashMap::new()),
|
|
memory_config: None,
|
|
web_search_config: Default::default(),
|
|
web_fetch_config: Default::default(),
|
|
app_builder_deployer_config: Default::default(),
|
|
write_file_enabled: true,
|
|
goal_enabled: false,
|
|
ask_user_question_enabled: true,
|
|
parent_cmd_tx: None,
|
|
parent_session_info: None,
|
|
subagent_roles: HashMap::new(),
|
|
subagent_personas: HashMap::new(),
|
|
persona_io_summaries: Vec::new(),
|
|
parent_chat_state: None,
|
|
available_models: indexmap::IndexMap::new(),
|
|
subagent_model_overrides: HashMap::new(),
|
|
subagent_toggle: toggle,
|
|
disable_web_search: false,
|
|
todo_gate: false,
|
|
remote_settings: None,
|
|
laziness_debug_log: None,
|
|
backend_tools_enabled: true,
|
|
respect_gitignore: false,
|
|
path_not_found_hints: false,
|
|
plugin_registry: None,
|
|
models_manager: Default::default(),
|
|
file_tool_overrides: None,
|
|
agent_config: None,
|
|
hook_registry: None,
|
|
hook_workspace_root: String::new(),
|
|
parent_depth: 0,
|
|
inference_idle_timeout_secs: 600,
|
|
auto_compact_threshold_tiers: crate::agent::subagent::AutoCompactThresholdTiers::default(),
|
|
permission_handle: None,
|
|
worktree_type: crate::util::config::WorktreeType::Linked,
|
|
api_key_provider: None,
|
|
image_description_model: crate::test_support::TEST_MODEL.to_owned(),
|
|
workspace_ops: kigi_workspace::WorkspaceOps::for_test(),
|
|
auth_manager: Arc::new(crate::auth::AuthManager::new(
|
|
std::path::Path::new("/tmp/nonexistent-kigi-test"),
|
|
crate::auth::KimiCodeConfig::default(),
|
|
)),
|
|
attribution_callback: None,
|
|
parent_agent_name: None,
|
|
parent_model_agent_type: None,
|
|
allowed_subagent_types: None,
|
|
parent_mcp_configs: vec![],
|
|
parent_mcp_pool: None,
|
|
parent_tool_snapshot: None,
|
|
parent_skills: None,
|
|
parent_skills_config: kigi_agent::prompt::skills::SkillsConfig::default(),
|
|
parent_compat: kigi_tools::types::compat::CompatConfig::default(),
|
|
auto_wake_delivered: None,
|
|
task_output_tool_name: kigi_tools::reminders::task_completion::DEFAULT_TASK_OUTPUT_TOOL
|
|
.to_string(),
|
|
auto_wake_enabled: true,
|
|
goal_loop_active: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
|
parent_blocking_wait_depth: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)),
|
|
parent_terminal_backend: None,
|
|
parent_notification_handle: None,
|
|
parent_scheduler_handle: None,
|
|
}
|
|
}
|
|
pub(crate) fn make_request(
|
|
subagent_type: &str,
|
|
) -> (SubagentRequest, oneshot::Receiver<SubagentResult>) {
|
|
let (tx, rx) = oneshot::channel();
|
|
let req = SubagentRequest {
|
|
id: uuid::Uuid::now_v7().to_string(),
|
|
prompt: "do something".into(),
|
|
description: "test task".into(),
|
|
subagent_type: subagent_type.into(),
|
|
parent_session_id: "test-parent".into(),
|
|
parent_prompt_id: Some("parent-prompt".into()),
|
|
resume_from: None,
|
|
cwd: None,
|
|
runtime_overrides: Default::default(),
|
|
run_in_background: false,
|
|
surface_completion: true,
|
|
fork_context: false,
|
|
result_tx: tx,
|
|
};
|
|
(req, rx)
|
|
}
|
|
#[derive(Default)]
|
|
pub(crate) struct DummyLspDispatch;
|
|
#[async_trait::async_trait]
|
|
impl kigi_tools::implementations::lsp::LspBackend for DummyLspDispatch {
|
|
fn ensure_started_background(&self) {}
|
|
async fn ensure_ready(&self) -> Result<(), String> {
|
|
Ok(())
|
|
}
|
|
fn is_ready(&self) -> bool {
|
|
true
|
|
}
|
|
async fn dispatch(
|
|
&self,
|
|
_input: &kigi_tools::implementations::lsp::LspToolInput,
|
|
) -> kigi_tools::implementations::lsp::LspToolResult {
|
|
kigi_tools::implementations::lsp::LspToolResult {
|
|
text: String::new(),
|
|
is_error: false,
|
|
}
|
|
}
|
|
async fn drain_diagnostics(
|
|
&self,
|
|
_timeout: std::time::Duration,
|
|
) -> Option<kigi_tools::implementations::lsp::DiagnosticsSummary> {
|
|
None
|
|
}
|
|
async fn notify_file_changed(&self, _path: &std::path::Path, _content: &str) {}
|
|
async fn read_diagnostics(
|
|
&self,
|
|
_paths: &[std::path::PathBuf],
|
|
) -> Vec<kigi_tools::implementations::lsp::FileDiagnosticEntry> {
|
|
vec![]
|
|
}
|
|
}
|