27th registry variant, 2nd subscription-OAuth provider. Log in with a Claude
Pro/Max subscription via PKCE authorization-code + S256 (loopback callback on
127.0.0.1:53692, with a manual code-paste fallback), then use it against
api.anthropic.com — reusing the existing Anthropic Messages wire + Anthropic
listing + the multi-provider OAuth foundation (dbce6bf). Sourced from Pi
(earendil-works/pi auth/oauth/anthropic.ts): client 9d1c250a..., authorize
claude.ai/oauth/authorize, token platform.claude.com/v1/oauth/token, scope
'…user:inference user:sessions:claude_code…'.
New machinery (foundation handles token routing — claude-pro-max is a
uses_oauth platform so its bearer/refresh/api_key already route to its own
pooled manager, never Kimi):
- OAuthConfig gains flow{DeviceCode|PkceLocalhost} + token_host + token_body
{Form|JSON}; xai/kimi rows unchanged (DeviceCode/Form).
- auth/oauth_pkce.rs: PKCE S256 wire — loopback listener with STRICT state
validation (CSRF, fail-closed), manual-paste fallback, JSON code→token
exchange + rotating-refresh. Never logs code/verifier/tokens.
- Messages OAuth adaptation gated on SamplerConfig.anthropic_oauth (true only
for a claude-pro-max managed key): Authorization: Bearer + anthropic-beta
oauth + user-agent claude-cli + x-app cli, and the required 'You are Claude
Code' system prefix. API-key anthropic/minimax Messages requests are
BYTE-IDENTICAL (regression-guarded).
- Live /models under the OAuth Bearer + oauth-beta headers (Anthropic listing,
enriched from models.dev anthropic); persistent 401 → 0 models + WARN, NO
hardcoded fallback list (honest failure).
Adversarial review: no blocking findings (secret handling, CSRF/state, the
anthropic_oauth gate, token routing, non-regression all CONFIRMED). Full gate
green. Registry at 27; picker updated. Residual (unverifiable without a real
Claude Pro/Max account): whether GET /v1/models accepts the OAuth bearer, and
the real endpoint's acceptance of the OAuth Messages request.
202 lines
7.5 KiB
Rust
202 lines
7.5 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,
|
|
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![]
|
|
}
|
|
}
|