fix(kiro): 修复 Kiro WebSearch MCP 调用 (#344)

* fix(kiro): 接入 Kiro MCP web_search 工具调用

* fix(kiro): 对齐 Kiro IDE MCP 鉴权与 profileArn 头部

* fix(responses): 保留 Responses 嵌套与自定义工具参数

* test(ci): 通过 Rust Action 三项检查
This commit is contained in:
Entropy.Xu
2026-04-27 12:20:26 +08:00
committed by GitHub
parent 488bd08f04
commit 6e1eaf8aec
11 changed files with 1917 additions and 11 deletions

View File

@@ -15,7 +15,10 @@ pub use auth::{
};
pub use converter::convert_claude_messages_to_conversation_state;
pub use credentials::{generate_machine_id, normalize_machine_id, KiroAuthConfig};
pub use headers::{build_generate_assistant_headers, AWS_EVENTSTREAM_CONTENT_TYPE};
pub use headers::{
build_generate_assistant_headers, build_mcp_headers, AWS_EVENTSTREAM_CONTENT_TYPE,
KIRO_EXTERNAL_IDP_TOKEN_TYPE, KIRO_PROFILE_ARN_HEADER, KIRO_TOKEN_TYPE_HEADER,
};
pub use policy::{
local_kiro_request_transport_unsupported_reason_with_network,
supports_local_kiro_request_transport, supports_local_kiro_request_transport_with_network,
@@ -28,6 +31,7 @@ pub use request::{
KiroProviderHeadersInput,
};
pub use url::{
build_kiro_generate_assistant_response_url, resolve_kiro_base_url,
GENERATE_ASSISTANT_RESPONSE_PATH, KIRO_ENVELOPE_NAME,
build_kiro_generate_assistant_response_url, build_kiro_mcp_url,
build_kiro_mcp_url_from_resolved_url, resolve_kiro_base_url, GENERATE_ASSISTANT_RESPONSE_PATH,
KIRO_ENVELOPE_NAME, MCP_PATH, MCP_STREAM_PATH,
};

View File

@@ -3,7 +3,7 @@ use sha2::{Digest, Sha256};
use std::time::{SystemTime, UNIX_EPOCH};
pub const DEFAULT_REGION: &str = "us-east-1";
pub const DEFAULT_KIRO_VERSION: &str = "0.8.0";
pub const DEFAULT_KIRO_VERSION: &str = "0.3.210";
pub const DEFAULT_NODE_VERSION: &str = "22.21.1";
pub const DEFAULT_SYSTEM_VERSION: &str = "other#unknown";
@@ -158,7 +158,7 @@ impl KiroAuthConfig {
.map(normalize_auth_method)
.unwrap_or_else(|| "social".to_string());
if explicit_method != "social" {
return explicit_method == "idc";
return matches!(explicit_method.as_str(), "idc" | "external_idp");
}
self.client_id
.as_deref()
@@ -173,6 +173,14 @@ impl KiroAuthConfig {
.is_some()
}
pub fn uses_external_idp_token_type(&self) -> bool {
self.auth_method
.as_deref()
.map(normalize_auth_method)
.as_deref()
== Some("external_idp")
}
pub fn profile_arn_for_payload(&self) -> Option<&str> {
if self.is_idc_auth() {
return None;
@@ -183,6 +191,13 @@ impl KiroAuthConfig {
.filter(|value| !value.is_empty())
}
pub fn profile_arn_for_mcp(&self) -> Option<&str> {
self.profile_arn
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
}
pub fn can_refresh_access_token(&self) -> bool {
let refresh_token = self
.refresh_token
@@ -309,6 +324,7 @@ fn normalize_auth_method(raw: &str) -> String {
| "identity_center"
| "identitycenter"
| "idc" => "idc".to_string(),
"external-idp" | "external_idp" | "externalidp" => "external_idp".to_string(),
_ => value,
}
}
@@ -394,6 +410,29 @@ mod tests {
assert_eq!(DEFAULT_REGION, "us-east-1");
}
#[test]
fn preserves_external_idp_auth_method_for_header_selection() {
let auth_config = KiroAuthConfig::from_raw_json(Some(
r#"{
"authMethod":"external_idp",
"refreshToken":"rt-1",
"clientId":"cid",
"clientSecret":"secret",
"profileArn":"arn:aws:bedrock:demo"
}"#,
))
.expect("auth config should parse");
assert_eq!(auth_config.auth_method.as_deref(), Some("external_idp"));
assert!(auth_config.is_idc_auth());
assert!(auth_config.uses_external_idp_token_type());
assert!(auth_config.profile_arn_for_payload().is_none());
assert_eq!(
auth_config.profile_arn_for_mcp(),
Some("arn:aws:bedrock:demo")
);
}
#[test]
fn infers_idc_when_client_credentials_exist() {
let auth_config = KiroAuthConfig::from_raw_json(Some(
@@ -407,7 +446,12 @@ mod tests {
.expect("auth config should parse");
assert!(auth_config.is_idc_auth());
assert!(!auth_config.uses_external_idp_token_type());
assert!(auth_config.profile_arn_for_payload().is_none());
assert_eq!(
auth_config.profile_arn_for_mcp(),
Some("arn:aws:bedrock:demo")
);
}
#[test]

View File

@@ -5,6 +5,9 @@ use uuid::Uuid;
use super::credentials::KiroAuthConfig;
pub const AWS_EVENTSTREAM_CONTENT_TYPE: &str = "application/vnd.amazon.eventstream";
pub const KIRO_PROFILE_ARN_HEADER: &str = "x-amzn-kiro-profile-arn";
pub const KIRO_TOKEN_TYPE_HEADER: &str = "TokenType";
pub const KIRO_EXTERNAL_IDP_TOKEN_TYPE: &str = "EXTERNAL_IDP";
const AWS_SDK_JS_MAIN_VERSION: &str = "1.0.27";
const CODEWHISPERER_OPTOUT: &str = "true";
const KIRO_AGENT_MODE: &str = "vibe";
@@ -81,10 +84,55 @@ pub fn build_generate_assistant_headers(
])
}
pub fn build_mcp_headers(
auth_config: &KiroAuthConfig,
machine_id: &str,
) -> BTreeMap<String, String> {
let kiro_version = auth_config.effective_kiro_version();
let system_version = auth_config.effective_system_version();
let node_version = auth_config.effective_node_version();
let region = auth_config.effective_api_region();
let host = format!("q.{region}.amazonaws.com");
let mut headers = BTreeMap::from([
("accept".to_string(), "application/json".to_string()),
(
"amz-sdk-invocation-id".to_string(),
Uuid::new_v4().to_string(),
),
(
"amz-sdk-request".to_string(),
"attempt=1; max=3".to_string(),
),
("connection".to_string(), "close".to_string()),
("content-type".to_string(), "application/json".to_string()),
("host".to_string(), host),
(
"user-agent".to_string(),
build_user_agent_main(system_version, node_version, kiro_version, machine_id),
),
(
"x-amz-user-agent".to_string(),
build_x_amz_user_agent_main(kiro_version, machine_id),
),
(
"x-amzn-codewhisperer-optout".to_string(),
CODEWHISPERER_OPTOUT.to_string(),
),
]);
if let Some(profile_arn) = auth_config.profile_arn_for_mcp() {
headers.insert(KIRO_PROFILE_ARN_HEADER.to_string(), profile_arn.to_string());
}
headers
}
#[cfg(test)]
mod tests {
use super::super::credentials::KiroAuthConfig;
use super::{build_generate_assistant_headers, AWS_EVENTSTREAM_CONTENT_TYPE};
use super::{
build_generate_assistant_headers, build_mcp_headers, AWS_EVENTSTREAM_CONTENT_TYPE,
KIRO_PROFILE_ARN_HEADER, KIRO_TOKEN_TYPE_HEADER,
};
#[test]
fn builds_generate_assistant_headers_for_region() {
@@ -118,5 +166,96 @@ mod tests {
headers.get("x-amzn-kiro-agent-mode").map(String::as_str),
Some("vibe")
);
assert!(!headers.contains_key(KIRO_TOKEN_TYPE_HEADER));
}
#[test]
fn keeps_generate_assistant_headers_without_external_idp_token_type() {
let auth_config = KiroAuthConfig {
auth_method: Some("idc".to_string()),
refresh_token: None,
expires_at: None,
profile_arn: Some("arn:aws:codewhisperer:us-east-1:123456789012:profile/demo".into()),
region: None,
auth_region: None,
api_region: Some("us-east-1".to_string()),
client_id: Some("client-id".to_string()),
client_secret: Some("client-secret".to_string()),
machine_id: None,
kiro_version: None,
system_version: None,
node_version: None,
access_token: None,
};
let headers = build_generate_assistant_headers(&auth_config, "machine-123");
assert_eq!(
headers.get("accept").map(String::as_str),
Some(AWS_EVENTSTREAM_CONTENT_TYPE)
);
assert!(!headers.contains_key(KIRO_TOKEN_TYPE_HEADER));
}
#[test]
fn builds_mcp_headers_with_profile_arn_for_social_auth() {
let auth_config = KiroAuthConfig {
auth_method: Some("social".to_string()),
refresh_token: None,
expires_at: None,
profile_arn: Some("arn:aws:codewhisperer:us-east-1:123456789012:profile/demo".into()),
region: None,
auth_region: None,
api_region: Some("us-east-1".to_string()),
client_id: None,
client_secret: None,
machine_id: None,
kiro_version: Some("0.3.210".to_string()),
system_version: Some("darwin#24.6.0".to_string()),
node_version: Some("22.21.1".to_string()),
access_token: None,
};
let headers = build_mcp_headers(&auth_config, "machine-123");
assert_eq!(
headers.get("accept").map(String::as_str),
Some("application/json")
);
assert_eq!(
headers.get(KIRO_PROFILE_ARN_HEADER).map(String::as_str),
Some("arn:aws:codewhisperer:us-east-1:123456789012:profile/demo")
);
assert!(!headers.contains_key(KIRO_TOKEN_TYPE_HEADER));
}
#[test]
fn builds_mcp_headers_with_profile_arn_for_idc_auth() {
let auth_config = KiroAuthConfig {
auth_method: Some("idc".to_string()),
refresh_token: None,
expires_at: None,
profile_arn: Some("arn:aws:codewhisperer:us-east-1:123456789012:profile/demo".into()),
region: None,
auth_region: None,
api_region: Some("us-west-2".to_string()),
client_id: Some("client-id".to_string()),
client_secret: Some("client-secret".to_string()),
machine_id: None,
kiro_version: None,
system_version: None,
node_version: None,
access_token: None,
};
let headers = build_mcp_headers(&auth_config, "machine-123");
assert_eq!(
headers.get("host").map(String::as_str),
Some("q.us-west-2.amazonaws.com")
);
assert_eq!(
headers.get(KIRO_PROFILE_ARN_HEADER).map(String::as_str),
Some("arn:aws:codewhisperer:us-east-1:123456789012:profile/demo")
);
assert!(!headers.contains_key(KIRO_TOKEN_TYPE_HEADER));
}
}

View File

@@ -2,6 +2,8 @@ use super::super::url::build_passthrough_path_url;
use super::credentials::DEFAULT_REGION;
pub const GENERATE_ASSISTANT_RESPONSE_PATH: &str = "/generateAssistantResponse";
pub const MCP_PATH: &str = "/mcp";
pub const MCP_STREAM_PATH: &str = "/mcp/stream";
pub const KIRO_ENVELOPE_NAME: &str = "kiro:generateAssistantResponse";
pub fn resolve_kiro_base_url(upstream_base_url: &str, api_region: Option<&str>) -> String {
@@ -30,11 +32,24 @@ pub fn build_kiro_generate_assistant_response_url(
)
}
pub fn build_kiro_mcp_url(upstream_base_url: &str, api_region: Option<&str>) -> Option<String> {
let upstream_base_url = resolve_kiro_base_url(upstream_base_url, api_region);
build_passthrough_path_url(upstream_base_url.as_str(), MCP_PATH, None, &[])
}
pub fn build_kiro_mcp_url_from_resolved_url(resolved_url: &str) -> Option<String> {
let mut parsed = url::Url::parse(resolved_url).ok()?;
parsed.set_path(MCP_PATH);
parsed.set_query(None);
Some(parsed.to_string())
}
#[cfg(test)]
mod tests {
use super::{
build_kiro_generate_assistant_response_url, resolve_kiro_base_url,
GENERATE_ASSISTANT_RESPONSE_PATH, KIRO_ENVELOPE_NAME,
build_kiro_generate_assistant_response_url, build_kiro_mcp_url,
build_kiro_mcp_url_from_resolved_url, resolve_kiro_base_url,
GENERATE_ASSISTANT_RESPONSE_PATH, KIRO_ENVELOPE_NAME, MCP_PATH, MCP_STREAM_PATH,
};
#[test]
@@ -43,6 +58,8 @@ mod tests {
GENERATE_ASSISTANT_RESPONSE_PATH,
"/generateAssistantResponse"
);
assert_eq!(MCP_PATH, "/mcp");
assert_eq!(MCP_STREAM_PATH, "/mcp/stream");
assert_eq!(KIRO_ENVELOPE_NAME, "kiro:generateAssistantResponse");
}
@@ -68,4 +85,23 @@ mod tests {
"https://kiro.us-west-2.example"
);
}
#[test]
fn builds_mcp_url_for_latest_kiro_endpoint() {
assert_eq!(
build_kiro_mcp_url("https://q.{region}.amazonaws.com", Some("eu-west-1")).as_deref(),
Some("https://q.eu-west-1.amazonaws.com/mcp")
);
}
#[test]
fn rewrites_generate_assistant_url_to_mcp_url() {
assert_eq!(
build_kiro_mcp_url_from_resolved_url(
"https://q.us-east-1.amazonaws.com/generateAssistantResponse?beta=true"
)
.as_deref(),
Some("https://q.us-east-1.amazonaws.com/mcp")
);
}
}