use super::Options;
use crate::mcp::lys_pass::{LysAgent, PassSource};
use serde_json::json;
use std::io::{BufRead as _, BufReader, Read as _, Write as _};
use std::net::TcpListener;
use std::os::unix::fs::{PermissionsExt as _, symlink};
use std::path::Path;
fn proof(path: &Path, generation: usize) -> anyhow::Result<()> {
std::fs::write(
path,
serde_json::to_vec(&json!({"headers":{
"lys-agent-pass":format!("synthetic-run-{generation}"),
"lys-seat":format!("synthetic seat {generation}")
}}))?,
)?;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
Ok(())
}
fn agent(path: &Path, issuer: &str, helper: bool) -> anyhow::Result<LysAgent> {
let arguments = vec![
"--lys-agent".to_owned(),
issuer.to_owned(),
"fixture".to_owned(),
"--launch-proof-file".to_owned(),
path.to_string_lossy().into_owned(),
];
if helper {
return crate::mcp::pass_header::parse(arguments.into_iter()).map_err(anyhow::Error::msg);
}
let mut arguments = arguments;
arguments.extend(["--config".to_owned(), "/unused.json".to_owned()]);
let options = Options::parse(arguments.into_iter())?;
match options.pass {
Some(PassSource::Lys(agent)) => Ok(agent),
_ => anyhow::bail!("launch proof did not select the Lys pass source"),
}
}
#[test]
fn assist004_r1_mcp_and_helper_send_only_the_launch_headers() -> anyhow::Result<()> {
for helper in [false, true] {
let directory = tempfile::tempdir()?;
let path = directory.path().join("synthetic.json");
proof(&path, 1)?;
let listener = TcpListener::bind("127.0.0.1:0")?;
let mut source = agent(&path, &format!("http://{}", listener.local_addr()?), helper)?;
let server = std::thread::spawn(move || receive(&listener, 1));
let answer = source
.ask_now()
.map_err(|error| anyhow::anyhow!("{}", error.code));
let received = server
.join()
.map_err(|_| anyhow::anyhow!("issuer panicked"))??;
answer?;
assert!(
received,
"issuer did not receive the exact synthetic headers and audience"
);
}
Ok(())
}
fn receive(listener: &TcpListener, generation: usize) -> anyhow::Result<bool> {
let (mut stream, _) = listener.accept()?;
let mut reader = BufReader::new(stream.try_clone()?);
let mut headers = Vec::new();
let mut line = String::new();
reader.read_line(&mut line)?;
let route = line == "POST /api/agents/fixture/pass HTTP/1.1\r\n";
loop {
line.clear();
anyhow::ensure!(
reader.read_line(&mut line)? != 0,
"request ended before headers"
);
if line == "\r\n" {
break;
}
headers.push(line.trim().to_owned());
}
let length: usize = headers
.iter()
.find_map(|line| line.strip_prefix("content-length: "))
.ok_or_else(|| anyhow::anyhow!("request length absent"))?
.parse()?;
let mut body = vec![0; length];
reader.read_exact(&mut body)?;
let valid = route
&& headers.contains(&format!("lys-agent-pass: synthetic-run-{generation}"))
&& headers.contains(&format!("lys-seat: synthetic seat {generation}"))
&& !headers.iter().any(|line| {
line.starts_with("lys-agent-signature:") || line.starts_with("lys-grant-token:")
})
&& serde_json::from_slice::<serde_json::Value>(&body)? == json!({"audience":"haematite"});
let answer = br#"{"pass":"synthetic-answer","expires_at":4102444800,"audience":"haematite"}"#;
write!(
stream,
"HTTP/1.1 200 OK\r\ncontent-length: {}\r\nconnection: close\r\n\r\n",
answer.len()
)?;
stream.write_all(answer)?;
stream.flush()?;
Ok(valid)
}
fn refused(change: impl FnOnce(&Path) -> anyhow::Result<()>, name: &str) -> anyhow::Result<()> {
let directory = tempfile::tempdir()?;
let path = directory.path().join("synthetic.json");
proof(&path, 1)?;
change(&path)?;
let mut source = agent(&path, "http://127.0.0.1:1", false)?;
let error = source
.ask_now()
.err()
.ok_or_else(|| anyhow::anyhow!("invalid proof accepted"))?;
assert!(
error.message.contains(name),
"expected named {name} refusal"
);
assert!(
!error.message.contains("synthetic"),
"proof contents entered a refusal"
);
Ok(())
}
#[test]
fn assist004_r1_shared_proof_is_refused_without_contents() -> anyhow::Result<()> {
refused(
|path| {
Ok(std::fs::set_permissions(
path,
std::fs::Permissions::from_mode(0o644),
)?)
},
"proof_file_not_private",
)
}
#[test]
fn assist004_r1_symlink_proof_is_refused_without_contents() -> anyhow::Result<()> {
refused(
|path| {
let target = path.with_extension("target");
std::fs::rename(path, &target)?;
Ok(symlink(target, path)?)
},
"proof_file_unreadable",
)
}
#[test]
fn assist004_r1_oversize_proof_is_refused_without_contents() -> anyhow::Result<()> {
refused(
|path| Ok(std::fs::write(path, "synthetic".repeat(9000))?),
"proof_file_exceeds_bound",
)
}
#[test]
fn assist004_r1_missing_pass_is_refused_without_contents() -> anyhow::Result<()> {
refused(
|path| {
Ok(std::fs::write(
path,
br#"{"headers":{"lys-seat":"synthetic"}}"#,
)?)
},
"proof_headers_invalid",
)
}
#[test]
fn assist004_r1_replacement_is_read_at_each_ask() -> anyhow::Result<()> {
let directory = tempfile::tempdir()?;
let path = directory.path().join("synthetic.json");
proof(&path, 1)?;
let listener = TcpListener::bind("127.0.0.1:0")?;
let mut source = agent(&path, &format!("http://{}", listener.local_addr()?), false)?;
let server = std::thread::spawn(move || {
Ok::<_, anyhow::Error>(receive(&listener, 1)? && receive(&listener, 2)?)
});
source
.ask_now()
.map_err(|error| anyhow::anyhow!("{}", error.code))?;
let replacement = path.with_extension("replacement");
proof(&replacement, 2)?;
std::fs::rename(replacement, &path)?;
source
.ask_now()
.map_err(|error| anyhow::anyhow!("{}", error.code))?;
assert!(
server
.join()
.map_err(|_| anyhow::anyhow!("issuer panicked"))??,
"a pass ask used headers from the previous proof"
);
Ok(())
}