#![cfg(feature = "aauth")]
use agentd::aauth::{AAuthClient, AgentKey, ApdConfig, RequestSigner};
use agentd::mcp::client::McpClient;
use std::io::{BufRead, BufReader, Read, Write};
use std::net::{TcpListener, TcpStream};
use std::sync::Arc;
use std::time::Duration;
fn read_http(s: &mut TcpStream) -> (String, std::collections::HashMap<String, String>, Vec<u8>) {
s.set_read_timeout(Some(Duration::from_secs(5))).ok();
let mut r = BufReader::new(s.try_clone().unwrap());
let mut line = String::new();
r.read_line(&mut line).ok();
let request_line = line.trim().to_string();
let mut headers = std::collections::HashMap::new();
let mut clen = 0usize;
loop {
let mut h = String::new();
if r.read_line(&mut h).unwrap_or(0) == 0 {
break;
}
let t = h.trim_end();
if t.is_empty() {
break;
}
if let Some((k, v)) = t.split_once(':') {
let key = k.trim().to_ascii_lowercase();
let val = v.trim().to_string();
if key == "content-length" {
clen = val.parse().unwrap_or(0);
}
headers.insert(key, val);
}
}
let mut body = vec![0u8; clen];
let _ = r.read_exact(&mut body);
(request_line, headers, body)
}
fn write_http(s: &mut TcpStream, status: &str, extra: &[(&str, &str)], body: &str) {
let mut head = format!(
"HTTP/1.1 {status}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n",
body.len()
);
for (k, v) in extra {
head.push_str(&format!("{k}: {v}\r\n"));
}
head.push_str("\r\n");
let _ = s.write_all(head.as_bytes());
let _ = s.write_all(body.as_bytes());
}
fn spawn_apd() -> String {
let l = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = l.local_addr().unwrap();
std::thread::spawn(move || {
for c in l.incoming().flatten() {
let mut s = c;
let (line, _h, _b) = read_http(&mut s);
let body = if line.contains("enroll") {
r#"{"agent":"aauth:agent7@apd.mock"}"#
} else {
r#"{"agent_token":"eyJagent.tok","expires_in":3600,"agent":"aauth:agent7@apd.mock"}"#
};
write_http(&mut s, "200 OK", &[], body);
}
});
format!("http://{addr}")
}
fn spawn_ps() -> String {
let l = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = l.local_addr().unwrap();
std::thread::spawn(move || {
for c in l.incoming().flatten() {
let mut s = c;
let (_line, _h, body) = read_http(&mut s);
let got: serde_json::Value = serde_json::from_slice(&body).unwrap_or_default();
assert_eq!(got["resource_token"], "RT-xyz", "PS got the resource token");
assert!(got["justification"].is_string(), "PS got a justification");
write_http(&mut s, "200 OK", &[], r#"{"auth_token":"eyJuser.auth"}"#);
}
});
format!("http://{addr}")
}
fn spawn_aauth_mcp() -> String {
let l = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = l.local_addr().unwrap();
std::thread::spawn(move || {
for c in l.incoming().flatten() {
let mut s = c;
let (line, h, body) = read_http(&mut s);
if line.starts_with("GET ") {
write_http(&mut s, "404 Not Found", &[], "");
continue;
}
let key = h.get("signature-key").cloned().unwrap_or_default();
assert!(h.contains_key("signature"), "request is signed");
let req: serde_json::Value = serde_json::from_slice(&body).unwrap_or_default();
let method = req["method"].as_str().unwrap_or("");
let id = req["id"].clone();
if method == "server/discover" {
let r = serde_json::json!({"jsonrpc":"2.0","id":id,
"error":{"code":-32601,"message":"method not found"}});
write_http(&mut s, "200 OK", &[], &r.to_string());
} else if method == "initialize" {
let r = serde_json::json!({"jsonrpc":"2.0","id":id,"result":{
"protocolVersion":"2025-11-25",
"capabilities":{"tools":{}},
"serverInfo":{"name":"aauth-mock","version":"0"}}});
write_http(
&mut s,
"200 OK",
&[("Mcp-Session-Id", "sess-1")],
&r.to_string(),
);
} else if method == "notifications/initialized" {
write_http(&mut s, "202 Accepted", &[], "");
} else if method == "tools/call" {
if key.contains("eyJuser.auth") {
let r = serde_json::json!({"jsonrpc":"2.0","id":id,"result":{
"content":[{"type":"text","text":"secret data for the user"}],
"isError":false}});
write_http(&mut s, "200 OK", &[], &r.to_string());
} else {
write_http(
&mut s,
"401 Unauthorized",
&[(
"AAuth-Requirement",
r#"auth-token; resource-token="RT-xyz""#,
)],
"{}",
);
}
} else {
let r = serde_json::json!({"jsonrpc":"2.0","id":id,"result":{}});
write_http(&mut s, "200 OK", &[], &r.to_string());
}
}
});
format!("127.0.0.1:{}", addr.port())
}
#[test]
fn case_c_person_server_exchange_over_the_transport() {
let apd = spawn_apd();
let ps = spawn_ps();
let mcp_addr = spawn_aauth_mcp();
let dir = tempfile::tempdir().unwrap();
let key = AgentKey::load_or_create(&dir.path().join("agent.key")).unwrap();
let client = AAuthClient::new(
key,
ApdConfig {
base_url: apd,
enrollment_token: None,
enroll_assertion_file: None,
person_server: Some(ps),
platform: "workload".into(),
},
Duration::from_secs(5),
);
client.prime().expect("enroll + token");
let signer: Arc<dyn RequestSigner> = Arc::new(client);
let mut mcp = McpClient::connect_signed(
"secure",
&format!("http://{mcp_addr}/mcp"),
vec![],
Duration::from_secs(5),
Some(signer),
)
.expect("connect");
mcp.initialize().expect("initialize (signed)");
let result = mcp
.call_tool("read_secret", Some(serde_json::json!({})))
.expect("tool call succeeds after the Case-C exchange");
let text = result.text();
assert!(
text.contains("secret data for the user"),
"the user-scoped call returned the protected result: {text:?}"
);
}