use std::io::{Read, Write};
use anyhow::{Context as _, Result, bail, ensure};
use zeroize::Zeroizing;
use super::MAX_PAYLOAD_BYTES;
pub const REQUEST_MAGIC: &[u8] = b"MOAT-GUARD/1\n";
pub const REPLY_MAGIC: &[u8] = b"MOAT-GUARD-REPLY/1\n";
pub const NONCE_LEN: usize = 32;
pub const MAX_OUTPUT: usize = 4 * 1024 * 1024;
const MAX_TOKEN: usize = 256;
const MAX_HOST: usize = 64;
const MAX_CWD: usize = 4096;
const FLAG_CONTINUE: u8 = 1;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Request {
pub token: Zeroizing<Vec<u8>>,
pub nonce: String,
pub host: String,
pub cwd: String,
pub continue_cli: bool,
pub payload: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Reply {
pub nonce: String,
pub code: u8,
pub stdout: Vec<u8>,
pub stderr: Vec<u8>,
}
impl Request {
pub fn write_to(&self, out: &mut impl Write) -> Result<()> {
let flags = if self.continue_cli { FLAG_CONTINUE } else { 0 };
let mut frame = Zeroizing::new(REQUEST_MAGIC.to_vec());
for field in [
&self.token[..],
self.nonce.as_bytes(),
self.host.as_bytes(),
self.cwd.as_bytes(),
&[flags],
self.payload.as_bytes(),
] {
push_field(&mut frame, field)?;
}
out.write_all(&frame)?;
Ok(out.flush()?)
}
pub fn read_from(input: &mut impl Read) -> Result<Self> {
expect_magic(input, REQUEST_MAGIC).context("not a guard request")?;
let token = Zeroizing::new(field(input, MAX_TOKEN, "token")?);
let nonce = nonce(field(input, NONCE_LEN, "nonce")?)?;
let host = text(field(input, MAX_HOST, "host")?, "host")?;
let cwd = text(
field(input, MAX_CWD, "working directory")?,
"working directory",
)?;
let flags = field(input, 1, "flags")?;
ensure!(flags.len() == 1, "the flags field is not one byte");
let payload = field(input, payload_limit(), "payload").context(
"the tool call is larger than OpenMoat reads, so it is refused; ask the agent to \
write the file in smaller parts",
)?;
Ok(Self {
token,
nonce,
host,
cwd,
continue_cli: flags[0] & FLAG_CONTINUE != 0,
payload: text(payload, "payload")?,
})
}
}
impl Reply {
pub fn write_to(&self, out: &mut impl Write) -> Result<()> {
let mut frame = REPLY_MAGIC.to_vec();
for field in [
self.nonce.as_bytes(),
&[self.code],
&self.stdout[..],
&self.stderr[..],
] {
push_field(&mut frame, field)?;
}
out.write_all(&frame)?;
Ok(out.flush()?)
}
pub fn read_from(input: &mut impl Read, expected_nonce: &str) -> Result<Self> {
expect_magic(input, REPLY_MAGIC).context("not a guard reply")?;
let nonce = nonce(field(input, NONCE_LEN, "nonce")?)?;
ensure!(nonce == expected_nonce, "the reply answers another request");
let code = field(input, 1, "exit code")?;
let [code @ (0 | 2)] = code[..] else {
bail!("the reply's exit code is neither 0 nor 2");
};
let stdout = field(input, MAX_OUTPUT, "standard output")?;
let stderr = field(input, MAX_OUTPUT, "standard error")?;
Ok(Self {
nonce,
code,
stdout,
stderr,
})
}
}
fn payload_limit() -> usize {
usize::try_from(MAX_PAYLOAD_BYTES).unwrap_or(usize::MAX)
}
fn push_field(frame: &mut Vec<u8>, bytes: &[u8]) -> Result<()> {
let len = u32::try_from(bytes.len()).context("a field longer than 4 GiB")?;
frame.extend_from_slice(&len.to_be_bytes());
frame.extend_from_slice(bytes);
Ok(())
}
fn expect_magic(input: &mut impl Read, magic: &[u8]) -> Result<()> {
let mut seen = vec![0u8; magic.len()];
input.read_exact(&mut seen)?;
ensure!(seen == magic, "wrong start");
Ok(())
}
fn field(input: &mut impl Read, max: usize, what: &str) -> Result<Vec<u8>> {
let mut len = [0u8; 4];
input
.read_exact(&mut len)
.with_context(|| format!("reading the {what} field"))?;
let len = usize::try_from(u32::from_be_bytes(len)).unwrap_or(usize::MAX);
if len > max {
bail!("the {what} field holds {len} bytes, more than the {max} allowed");
}
let mut bytes = vec![0u8; len];
input
.read_exact(&mut bytes)
.with_context(|| format!("reading the {what} field"))?;
Ok(bytes)
}
fn nonce(bytes: Vec<u8>) -> Result<String> {
ensure!(
bytes.len() == NONCE_LEN && bytes.iter().all(u8::is_ascii_hexdigit),
"the nonce is not {NONCE_LEN} hex characters"
);
text(bytes, "nonce")
}
fn text(bytes: Vec<u8>, what: &str) -> Result<String> {
String::from_utf8(bytes).with_context(|| format!("the {what} field is not UTF-8"))
}