Files
Kigi-CLI/crates/codegen/kigi-shell/src/test_support/lsp_runtime.rs
T
ZacharyZhang-NY 422e241e13 feat(providers): add ChatGPT/Codex subscription OAuth (PKCE + account-id, hardcoded catalog)
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).
2026-07-22 08:28:03 -04:00

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![]
}
}