use std::{
fs,
io::{BufRead, BufReader, Write},
path::Path,
};
use serde::{Deserialize, Serialize};
use super::{index, layout::set_private_file_permissions, persistence::SessionEntry, Session};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum SessionAgentRole {
#[default]
Root,
Delegated,
}
impl SessionAgentRole {
pub(super) fn is_root(&self) -> bool {
matches!(self, Self::Root)
}
}
fn read_header(path: &Path) -> anyhow::Result<SessionEntry> {
let file = fs::File::open(path)?;
let line = BufReader::new(file)
.lines()
.next()
.transpose()?
.ok_or_else(|| anyhow::anyhow!("session file is empty"))?;
Ok(serde_json::from_str(&line)?)
}
pub(super) fn read_agent_identity(path: &Path) -> anyhow::Result<Option<(String, String)>> {
match read_header(path)? {
SessionEntry::Session {
agent_id: Some(id),
agent_fingerprint: Some(fingerprint),
..
} => Ok(Some((id, fingerprint))),
SessionEntry::Session { .. } => Ok(None),
_ => anyhow::bail!("session file does not start with session metadata"),
}
}
pub(super) fn read_delegated_agent_id(path: &Path) -> anyhow::Result<Option<String>> {
match read_header(path)? {
SessionEntry::Session {
agent_role: SessionAgentRole::Root,
..
} => Ok(None),
SessionEntry::Session {
agent_role: SessionAgentRole::Delegated,
agent_id: Some(id),
..
} => Ok(Some(id)),
SessionEntry::Session {
agent_role: SessionAgentRole::Delegated,
agent_id: None,
..
} => anyhow::bail!("delegated session header has no agent id"),
_ => anyhow::bail!("session file does not start with session metadata"),
}
}
impl Session {
pub(super) fn rewrite_fresh_delegated_identity(
&self,
agent_id: &str,
fingerprint: &str,
) -> anyhow::Result<()> {
let mut cursor = self
.write_lock
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let contents = fs::read_to_string(&self.path)?;
let mut lines = contents.lines();
let header = lines
.next()
.ok_or_else(|| anyhow::anyhow!("session file is empty"))?;
if lines.next().is_some() {
anyhow::bail!("cannot rebind agent: the session already has history");
}
let mut entry: SessionEntry = serde_json::from_str(header)?;
let SessionEntry::Session {
agent_id: stored_id,
agent_fingerprint,
agent_role,
..
} = &mut entry
else {
anyhow::bail!("session file does not start with session metadata");
};
*stored_id = Some(agent_id.to_owned());
*agent_fingerprint = Some(fingerprint.to_owned());
*agent_role = SessionAgentRole::Delegated;
let parent = self
.path
.parent()
.ok_or_else(|| anyhow::anyhow!("session file has no parent directory"))?;
let mut temporary = tempfile::NamedTempFile::new_in(parent)?;
set_private_file_permissions(temporary.as_file())?;
serde_json::to_writer(&mut temporary, &entry)?;
temporary.write_all(b"\n")?;
temporary.as_file().sync_all()?;
temporary.persist(&self.path)?;
cursor.valid_len = None;
cursor.invalidate_tree();
let _ = index::sync_session_file(&self.session_root, &self.cwd, &self.path);
Ok(())
}
}
#[cfg(test)]
#[path = "agent_identity_tests.rs"]
mod tests;