#![cfg(feature = "oauth")]
use std::io::{BufRead, BufReader, Read, Write};
use std::net::{TcpListener, TcpStream};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use agentd::intel::client::IntelClient;
use agentd::wire::intel::{Message, Request, ToolDef};
use serde_json::Value;
#[derive(Default)]
struct Captured {
method: String,
path: String,
authorization: String,
body: Vec<u8>,
}
fn read_request(s: &mut TcpStream) -> Captured {
s.set_read_timeout(Some(Duration::from_secs(5))).ok();
let mut r = BufReader::new(s.try_clone().unwrap());
let mut start = String::new();
r.read_line(&mut start).ok();
let mut parts = start.split_whitespace();
let method = parts.next().unwrap_or("").to_string();
let path = parts.next().unwrap_or("").to_string();
let mut authorization = String::new();
let mut clen = 0usize;
loop {
let mut h = String::new();
if r.read_line(&mut h).unwrap_or(0) == 0 || h.trim().is_empty() {
break;
}
if let Some((k, v)) = h.trim_end().split_once(':') {
let key = k.trim().to_ascii_lowercase();
if key == "content-length" {
clen = v.trim().parse().unwrap_or(0);
}
if key == "authorization" {
authorization = v.trim().to_string();
}
}
}
let mut body = vec![0u8; clen];
let _ = r.read_exact(&mut body);
Captured {
method,
path,
authorization,
body,
}
}
fn spawn_bedrock(captured: Arc<Mutex<Option<Captured>>>) -> String {
let l = TcpListener::bind("127.0.0.1:0").unwrap();
let port = l.local_addr().unwrap().port();
std::thread::spawn(move || {
for c in l.incoming().flatten() {
let mut s = c;
let req = read_request(&mut s);
{
let mut slot = captured.lock().unwrap();
if slot.is_none() {
*slot = Some(req);
}
}
let body = r#"{"output":{"message":{"role":"assistant","content":[{"text":"bedrock says hi"}]}},"stopReason":"end_turn","usage":{"inputTokens":11,"outputTokens":3,"totalTokens":14}}"#;
let head = format!(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
body.len()
);
let _ = s.write_all(head.as_bytes());
let _ = s.write_all(body.as_bytes());
}
});
format!("127.0.0.1:{port}")
}
#[test]
fn native_bedrock_dialect_signs_the_dynamic_path_and_round_trips() {
unsafe {
std::env::set_var("AWS_ACCESS_KEY_ID", "AKIDEXAMPLE");
std::env::set_var(
"AWS_SECRET_ACCESS_KEY",
"wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
);
}
let captured: Arc<Mutex<Option<Captured>>> = Arc::new(Mutex::new(None));
let host = spawn_bedrock(captured.clone());
let authspec = agentd::config::AuthSpec {
kind: "aws".into(),
region: Some("us-east-1".into()),
service: Some("bedrock".into()),
source: Some("env".into()),
..Default::default()
};
let signer = agentd::auth::aws::SigV4Signer::from_spec(&authspec, "intelligence").unwrap();
let intel = IntelClient::from_parts(&format!("http://{host}"), None)
.unwrap()
.with_dialect(Some("bedrock"))
.with_signer(Some(signer as Arc<dyn agentd::mcp::http::RequestSigner>));
let model = "anthropic.claude-3-5-sonnet-20241022-v2:0";
let req = Request {
model: model.into(),
messages: vec![Message::system("be terse"), Message::user("hi")],
tools: vec![ToolDef {
name: "read_file".into(),
description: "read".into(),
input_schema: serde_json::json!({"type": "object"}),
}],
max_tokens: 32,
temperature: Some(0.0),
};
let resp = intel.complete(&req).expect("completion succeeds");
assert_eq!(resp.text.as_deref(), Some("bedrock says hi"));
assert_eq!(resp.usage.total(), 14);
let cap = captured
.lock()
.unwrap()
.take()
.expect("mock served a request");
assert_eq!(cap.method, "POST");
assert_eq!(
cap.path, "/model/anthropic.claude-3-5-sonnet-20241022-v2%3A0/converse",
"the model id rides the encoded path"
);
assert!(
cap.authorization
.starts_with("AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/"),
"Bedrock is SigV4-signed: {}",
cap.authorization
);
assert!(
cap.authorization
.contains("/us-east-1/bedrock/aws4_request"),
"region+service scope: {}",
cap.authorization
);
let body: Value = serde_json::from_slice(&cap.body).expect("body is JSON");
assert!(body.get("model").is_none(), "no top-level model: {body}");
assert_eq!(body["inferenceConfig"]["maxTokens"], 32);
assert_eq!(body["system"][0]["text"], "be terse");
assert_eq!(body["messages"][0]["content"][0]["text"], "hi");
assert_eq!(
body["toolConfig"]["tools"][0]["toolSpec"]["name"],
"read_file"
);
unsafe {
std::env::remove_var("AWS_ACCESS_KEY_ID");
std::env::remove_var("AWS_SECRET_ACCESS_KEY");
}
}