use std::path::{Component, Path, PathBuf};
use argonauts_core::events::approval::ApprovalKind;
use argonauts_core::names::ApprovalId;
use argonauts_core::policy::{
EVIDENCE_INLINE_MAX, EvidenceDocument, EvidenceFacts, EvidencePacket,
};
use argonauts_core::sha256_hex;
use crate::engine::host::Host;
use crate::error::{EngineError, EngineResult};
pub const DEFAULT_RULES: &str = argonauts_core::template!("prompts/approvals/default-rules.md");
pub fn rules_label(model_rules: &str) -> String {
format!(".argonauts/{}", in_argonauts(model_rules))
}
fn in_argonauts(model_rules: &str) -> &str {
let name = model_rules.trim();
name.strip_prefix(".argonauts/").unwrap_or(name)
}
pub fn packet(
host: &Host,
kind: ApprovalKind,
facts: EvidenceFacts,
agent_outputs: Vec<String>,
documents: Vec<EvidenceDocument>,
) -> EngineResult<EvidencePacket> {
let goal = host.read(|state| {
state
.goal
.approved
.clone()
.or_else(|| state.goal.drafted.clone())
});
let mut documents = documents;
documents.push(rules_document(
&host.repo().root,
&host.settings().approvals.judge.model_rules,
)?);
let dir = evidence_dir(&host.repo().run_dir);
let documents = documents
.into_iter()
.map(|document| store_if_large(&dir, document))
.collect::<EngineResult<Vec<_>>>()?;
let mut facts = facts;
facts.irreversible_actions.get_or_insert_with(Vec::new);
Ok(EvidencePacket {
kind,
goal,
facts,
agent_outputs,
documents,
})
}
pub fn write(host: &Host, id: &ApprovalId, packet: &EvidencePacket) -> EngineResult<PathBuf> {
let dir = evidence_dir(&host.repo().run_dir);
std::fs::create_dir_all(&dir)?;
let path = dir.join(format!("{id}.json"));
std::fs::write(&path, serde_json::to_vec_pretty(packet)?)?;
Ok(path)
}
pub fn read(host: &Host, id: &ApprovalId) -> EngineResult<Option<EvidencePacket>> {
let path = evidence_dir(&host.repo().run_dir).join(format!("{id}.json"));
match std::fs::read(&path) {
Ok(bytes) => Ok(Some(serde_json::from_slice(&bytes)?)),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(err) => Err(err.into()),
}
}
fn evidence_dir(run_dir: &Path) -> PathBuf {
run_dir.join("evidence")
}
fn rules_document(root: &Path, model_rules: &str) -> EngineResult<EvidenceDocument> {
let name = in_argonauts(model_rules);
let relative = Path::new(name);
if name.is_empty()
|| !relative
.components()
.all(|component| matches!(component, Component::Normal(_)))
{
return Err(EngineError::Refused(format!(
"approvals.judge.model_rules must name a file inside .argonauts/, not `{name}`"
)));
}
let path = root.join(".argonauts").join(relative);
let text = match std::fs::read_to_string(&path) {
Ok(text) => text,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => DEFAULT_RULES.to_owned(),
Err(err) => return Err(err.into()),
};
Ok(EvidenceDocument {
label: rules_label(model_rules),
text,
path: None,
sha256: None,
})
}
fn store_if_large(dir: &Path, document: EvidenceDocument) -> EngineResult<EvidenceDocument> {
if document.text.len() <= EVIDENCE_INLINE_MAX {
return Ok(document);
}
let sha256 = sha256_hex(document.text.as_bytes());
std::fs::create_dir_all(dir)?;
let path = dir.join(format!("doc-{sha256}.txt"));
if !path.exists() {
std::fs::write(&path, &document.text)?;
}
Ok(EvidenceDocument {
label: document.label,
text: String::new(),
path: Some(path),
sha256: Some(sha256),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_large_document_moves_beside_the_packet_with_its_hash() {
let dir = tempfile::tempdir().unwrap_or_else(|err| panic!("{err}"));
let text = "x".repeat(EVIDENCE_INLINE_MAX + 1);
let stored = store_if_large(
dir.path(),
EvidenceDocument {
label: "combined diff".to_owned(),
text: text.clone(),
path: None,
sha256: None,
},
)
.unwrap_or_else(|err| panic!("{err}"));
assert!(stored.text.is_empty());
assert_eq!(
stored.sha256.as_deref(),
Some(sha256_hex(text.as_bytes()).as_str())
);
let path = stored.path.unwrap_or_default();
assert_eq!(std::fs::read_to_string(path).unwrap_or_default(), text);
let small = EvidenceDocument {
label: "plan".to_owned(),
text: "short".to_owned(),
path: None,
sha256: None,
};
assert_eq!(store_if_large(dir.path(), small.clone()).ok(), Some(small));
}
#[test]
fn the_rules_file_is_read_from_the_argonauts_folder_only() {
let dir = tempfile::tempdir().unwrap_or_else(|err| panic!("{err}"));
let read = |name: &str| rules_document(dir.path(), name).ok();
let missing = read("rules.md").map(|document| (document.label, document.text));
assert_eq!(
missing,
Some((".argonauts/rules.md".to_owned(), DEFAULT_RULES.to_owned()))
);
std::fs::create_dir_all(dir.path().join(".argonauts")).unwrap_or_default();
std::fs::write(dir.path().join(".argonauts/rules.md"), "merge small diffs")
.unwrap_or_default();
for name in ["rules.md", ".argonauts/rules.md"] {
let found = read(name).map(|document| (document.label, document.text));
assert_eq!(
found,
Some((
".argonauts/rules.md".to_owned(),
"merge small diffs".to_owned()
)),
"{name}"
);
}
assert!(rules_document(dir.path(), "../secrets").is_err());
assert!(rules_document(dir.path(), "/etc/passwd").is_err());
}
}