use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AgentSession {
pub instance: String,
pub base_url: String,
pub node_id: String,
pub public_key: String,
pub secret_key_hex: String,
pub mnemonic: String,
pub token: String,
pub refresh_token: String,
pub onboarded_at: DateTime<Utc>,
pub last_login_at: DateTime<Utc>,
}
impl AgentSession {
pub fn file_path(dev_dir: &Path, instance: &str) -> PathBuf {
let name = if instance.is_empty() {
"default-agent-session.json".to_string()
} else {
format!("{instance}-agent-session.json")
};
dev_dir.join(name)
}
pub fn load(dev_dir: &Path, instance: &str) -> Result<Option<Self>> {
let path = Self::file_path(dev_dir, instance);
if !path.exists() {
return Ok(None);
}
let text = std::fs::read_to_string(&path)
.with_context(|| format!("read {}", path.display()))?;
let session: Self = serde_json::from_str(&text)
.with_context(|| format!("parse {}", path.display()))?;
Ok(Some(session))
}
pub fn read_token(path: &Path) -> Result<String> {
let text = std::fs::read_to_string(path)
.with_context(|| format!("read {}", path.display()))?;
let v: serde_json::Value = serde_json::from_str(&text)
.with_context(|| format!("parse {}", path.display()))?;
v.get("token")
.and_then(|t| t.as_str())
.map(str::to_owned)
.ok_or_else(|| anyhow::anyhow!("no 'token' field in {}", path.display()))
}
pub fn load_for_harness(monorepo_path: &Path, instance: &str) -> Result<Self> {
let dev_dir =
crate::commands::dev::host::monorepo::monorepo_dev_dir(monorepo_path, instance, None)?;
Self::load(&dev_dir, instance)?
.ok_or_else(|| anyhow::anyhow!(
"no agent session for '{instance}' — did `harness up` onboarding run? \
(expected at {})",
Self::file_path(&dev_dir, instance).display()
))
}
pub fn save(&self, dev_dir: &Path) -> Result<PathBuf> {
std::fs::create_dir_all(dev_dir)
.with_context(|| format!("mkdir -p {}", dev_dir.display()))?;
let final_path = Self::file_path(dev_dir, &self.instance);
let tmp_path = final_path.with_extension("json.tmp");
let text = serde_json::to_string_pretty(self).context("serialize agent session")?;
std::fs::write(&tmp_path, text)
.with_context(|| format!("write {}", tmp_path.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(0o600);
std::fs::set_permissions(&tmp_path, perms)
.with_context(|| format!("chmod 0600 {}", tmp_path.display()))?;
}
std::fs::rename(&tmp_path, &final_path).with_context(|| {
format!("rename {} → {}", tmp_path.display(), final_path.display())
})?;
Ok(final_path)
}
}
pub fn redact_token(token: &str) -> String {
let prefix: String = token.chars().take(12).collect();
format!("{prefix}…")
}