use std::{
io::{BufRead as _, Seek as _},
path::{Path, PathBuf},
};
use serde_json::Value;
use crate::args::bounded;
pub(crate) const CLAUDE_FULL_MODE: &str = "bypassPermissions";
pub(crate) const CODEX_FULL_MODE: &str = "approval_policy never, sandbox danger-full-access";
const MAX_ROLLOUT_LINE_BYTES: usize = 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Expected {
pub(crate) agent: String,
pub(crate) policy: String,
pub(crate) full_mode: String,
pub(crate) full: bool,
}
impl Expected {
pub(crate) fn accepts(&self, reported: &str) -> bool {
(reported == self.full_mode) == self.full
}
fn expects(&self) -> String {
if self.full {
self.full_mode.clone()
} else {
format!("a mode other than {}", self.full_mode)
}
}
pub(crate) fn mismatch(&self, reported: &str) -> String {
format!(
"{} reported permission mode {}, but policy mode {} expects {}; the run was stopped",
self.agent,
bounded(reported, 80),
self.policy,
self.expects()
)
}
pub(crate) fn missing(&self) -> String {
format!(
"{} did not report its permission mode, and policy mode {} expects {}; the run was \
stopped",
self.agent,
self.policy,
self.expects()
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Check {
BeforeTools,
AfterStart,
BeforePrompt,
Unverified,
NotApplicable,
}
impl Check {
fn clause(self) -> &'static str {
match self {
Self::BeforeTools => "; SCV verifies the mode it reports before any tool runs",
Self::AfterStart => "; SCV verifies the mode it reports shortly after its turn starts",
Self::BeforePrompt => "; SCV verifies the mode it reports before sending the prompt",
Self::Unverified => "; mode unverified: it reports none SCV can read",
Self::NotApplicable => "",
}
}
}
pub(crate) fn summary(policy: &str, full: bool, check: Check, relays: bool) -> String {
let full_clause = check.clause();
if full {
format!(
" FULL PERMISSIONS (policy mode {policy}{full_clause}): the agent's own approval \
prompts and sandbox are off, so it edits files, runs commands, and uses the \
network without asking."
)
} else if relays {
format!(
" Policy mode {policy}{full_clause}. Its own permission requests come back here for \
approval."
)
} else if check == Check::NotApplicable {
format!(
" Policy mode {policy}. It has no full-autonomy switch, so its own configuration \
decides what it asks."
)
} else {
format!(
" Policy mode {policy}{full_clause}. It starts without its full-autonomy switches, \
so its own configuration decides what it asks."
)
}
}
pub(crate) fn unverified(check: Check, reported: Option<&str>) -> Option<String> {
match (check, reported) {
(Check::NotApplicable, _) => None,
(_, Some(reported)) => Some(format!("unverified (it reports {})", bounded(reported, 80))),
(_, None) => Some("unverified".into()),
}
}
pub(crate) fn verified(reported: &str) -> String {
format!("verified {}", bounded(reported, 80))
}
pub(crate) fn codex_rollout(home: &Path, thread: &str) -> Option<PathBuf> {
fn find(dir: &Path, name: &str, depth: usize) -> Option<PathBuf> {
let entries = std::fs::read_dir(dir).ok()?;
let mut dirs = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
let Ok(metadata) = std::fs::symlink_metadata(&path) else {
continue;
};
if metadata.is_file()
&& path
.file_name()
.and_then(|file| file.to_str())
.is_some_and(|file| file.ends_with(name))
{
return Some(path);
}
if metadata.is_dir() && depth > 0 {
dirs.push(path);
}
}
dirs.sort();
dirs.into_iter()
.rev()
.find_map(|dir| find(&dir, name, depth - 1))
}
find(&home.join("sessions"), &format!("{thread}.jsonl"), 3)
}
pub(crate) fn codex_turn_mode(file: &Path, offset: u64) -> Option<String> {
let mut file = std::fs::File::open(file).ok()?;
file.seek(std::io::SeekFrom::Start(offset)).ok()?;
let mut reader = std::io::BufReader::new(file);
let mut mode = None;
let mut line = Vec::new();
loop {
line.clear();
match reader.read_until(b'\n', &mut line) {
Ok(0) | Err(_) => break,
Ok(_) => {}
}
if line.len() > MAX_ROLLOUT_LINE_BYTES || line.last() != Some(&b'\n') {
continue;
}
let Ok(event) = serde_json::from_slice::<Value>(&line) else {
continue;
};
if event.get("type").and_then(Value::as_str) != Some("turn_context") {
continue;
}
let context = event.get("payload").unwrap_or(&event);
let approval = context.get("approval_policy").and_then(Value::as_str);
let sandbox = context
.pointer("/sandbox_policy/type")
.or_else(|| context.get("sandbox_policy"))
.and_then(Value::as_str);
if let (Some(approval), Some(sandbox)) = (approval, sandbox) {
mode = Some(format!("approval_policy {approval}, sandbox {sandbox}"));
}
}
mode
}
#[cfg(test)]
mod tests;