use crate::{append_locked_row, data_base, now_utc, Durability};
use serde_json::Value;
use std::path::{Path, PathBuf};
pub const REWRITE: &str = "rewrite";
pub const SWITCH: &str = "rewrite";
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Swap {
pub line: String,
pub note: String,
}
#[must_use]
pub fn rewritten(verdict: &str) -> Option<String> {
crate::rewrite::from_policyd(verdict)
}
#[must_use]
pub fn note(asked: &str, ran: &str) -> String {
format!(
"ljos ran `{}` in place of `{}`. The guard asked for the swap. \
Set `{SWITCH} = false` in this repository's ljos.toml to run lines as written.",
one_line(ran),
one_line(asked)
)
}
#[must_use]
pub fn log_path() -> Option<PathBuf> {
Some(data_base()?.join("ljos").join("rewrite.jsonl"))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Row {
pub at: String,
pub asked: String,
pub ran: String,
pub applied: bool,
pub seat: Option<String>,
}
impl Row {
fn of(value: &Value) -> Option<Self> {
Some(Self {
at: value["ts"].as_str()?.to_string(),
asked: value["asked"].as_str()?.to_string(),
ran: value["ran"].as_str()?.to_string(),
applied: value["applied"].as_bool()?,
seat: value["seat"].as_str().map(str::to_string),
})
}
fn value(&self) -> Value {
serde_json::json!({
"ts": self.at,
"asked": self.asked,
"ran": self.ran,
"applied": self.applied,
"seat": self.seat,
})
}
}
#[must_use]
pub fn rows_in(path: &Path) -> Vec<Row> {
let Ok(text) = std::fs::read_to_string(path) else {
return Vec::new();
};
text.lines()
.filter_map(|line| serde_json::from_str::<Value>(line).ok())
.filter_map(|value| Row::of(&value))
.collect()
}
#[must_use]
pub fn rows() -> Vec<Row> {
log_path().map(|path| rows_in(&path)).unwrap_or_default()
}
pub fn log_swap(asked: &str, ran: &str, applied: bool) {
let Some(path) = log_path() else {
return;
};
let row = Row {
at: now_utc(),
asked: one_line(asked),
ran: one_line(ran),
applied,
seat: std::env::var("LJOS_SEAT").ok(),
};
let Ok(mut line) = serde_json::to_string(&row.value()) else {
return;
};
line.push('\n');
let _ = path.parent().map(std::fs::create_dir_all);
let _ = append_locked_row(&path, line.as_bytes(), Durability::BestEffort);
}
#[must_use]
pub fn allowed_in(cwd: Option<&Path>) -> bool {
let mut on = true;
for path in crate::gates::rewrite_layers(cwd) {
if !path.is_file() {
continue;
}
match crate::gates::rewrite_key(&path) {
Ok(Some(set)) => on = set,
Ok(None) => {}
Err(_) => return false,
}
}
on
}
thread_local! {
static HELD: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
}
pub fn clear() {
HELD.with(|slot| *slot.borrow_mut() = None);
}
#[must_use]
pub fn take_line() -> Option<String> {
HELD.with(|slot| slot.borrow_mut().take())
}
pub fn apply_rewrite(asked: &str, verdict: &str, cwd: Option<&Path>) -> Option<Swap> {
let ran = rewritten(verdict)?;
if one_line(&ran) == one_line(asked) {
return None;
}
if !allowed_in(cwd) {
log_swap(asked, &ran, false);
return None;
}
log_swap(asked, &ran, true);
let swap = Swap {
note: note(asked, &ran),
line: ran,
};
HELD.with(|slot| *slot.borrow_mut() = Some(swap.line.clone()));
Some(swap)
}
fn one_line(text: &str) -> String {
text.replace("\\\r\n", " ")
.replace("\\\n", " ")
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_rewrite_verdict_names_the_call_to_run() {
assert_eq!(
rewritten("allow\trewrite\tuv run pytest -q").as_deref(),
Some("uv run pytest -q")
);
assert_eq!(rewritten("allow"), None);
assert_eq!(rewritten("allow\tthe ljos guard is unhealthy"), None);
assert_eq!(rewritten("allow\trewrite\t "), None);
assert_eq!(rewritten("deny\trewrite\tuv run pytest"), None);
}
#[test]
fn the_note_is_one_line_and_names_both_calls() {
let line = note("pytest \\\n -q", "uv run pytest \\\n -q");
assert!(!line.contains('\n'), "{line}");
assert!(line.contains("`uv run pytest -q`"), "{line}");
assert!(line.contains("`pytest -q`"), "{line}");
assert!(line.contains("rewrite = false"), "{line}");
}
#[test]
fn a_kept_row_reads_back_as_it_was_written() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("rewrite.jsonl");
let row = Row {
at: "2026-10-28T10:00:00Z".into(),
asked: "pytest".into(),
ran: "uv run pytest".into(),
applied: true,
seat: Some("claude".into()),
};
let mut text = serde_json::to_string(&row.value()).unwrap();
text.push('\n');
text.push_str("{not json}\n");
std::fs::write(&path, text).unwrap();
assert_eq!(rows_in(&path), vec![row]);
}
#[test]
fn a_verdict_that_hands_back_the_same_call_is_no_swap() {
assert_eq!(
apply_rewrite("uv run pytest", "allow\trewrite\tuv run pytest", None),
None
);
assert!(take_line().is_none());
}
fn fake_policyd(dir: &Path, verdict: &str) -> PathBuf {
use std::os::unix::fs::PermissionsExt as _;
let path = dir.join("policyd");
std::fs::write(&path, format!("#!/bin/sh\nprintf '{verdict}\\n'\n")).unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
path
}
fn git_repo(path: &Path) {
std::fs::create_dir_all(path).unwrap();
let out = crate::git_cmd()
.args(["init", "-q"])
.current_dir(path)
.output()
.unwrap();
assert!(out.status.success(), "{out:?}");
}
#[test]
fn a_swapped_call_is_said_once_and_kept_until_the_repo_turns_it_off() {
let _lock = crate::test_env_lock();
let home = tempfile::TempDir::new().unwrap();
let repo = home.path().join("proj");
git_repo(&repo);
let bin = fake_policyd(home.path(), "allow\\trewrite\\tuv run pytest -q");
unsafe {
std::env::set_var("HOME", home.path());
std::env::set_var("XDG_DATA_HOME", home.path().join("data"));
std::env::set_var("XDG_CONFIG_HOME", home.path().join("config"));
std::env::set_var("POLICYD_BIN", &bin);
std::env::set_var("PACKSET_URL", "off");
std::env::set_var("LJOS_GUARD", "on");
std::env::set_var("LJOS_OFF", "0");
std::env::set_var("LJOS_SEAT", "claude");
for key in ["LJOS_GUARD_FAIL_OPEN", "POLICYD_REQUIRED", "LJOS_CONFIG"] {
std::env::remove_var(key);
}
}
let _ = crate::take_guard_note();
assert!(
crate::tcb_verdict_in("pytest -q", Some(&repo)).is_none(),
"a rewrite is an allow"
);
let note = crate::take_guard_note().expect("the swap is said");
assert!(note.contains("`uv run pytest -q`"), "{note}");
assert!(note.contains("`pytest -q`"), "{note}");
assert_eq!(take_line().as_deref(), Some("uv run pytest -q"));
let kept = rows();
assert_eq!(kept.len(), 1, "{kept:?}");
assert!(kept[0].applied, "{kept:?}");
assert_eq!(kept[0].seat.as_deref(), Some("claude"));
std::fs::write(repo.join("ljos.toml"), "rewrite = false\n").unwrap();
assert!(
crate::tcb_verdict_in("pytest -q", Some(&repo)).is_none(),
"the line still runs, as it was written"
);
assert!(crate::take_guard_note().is_none(), "nothing was swapped");
assert!(take_line().is_none());
let kept = rows();
assert_eq!(kept.len(), 2, "{kept:?}");
assert!(!kept[1].applied, "the switch declined it: {kept:?}");
}
}