use std::io::Write;
use std::path::{Path, PathBuf};
use agent_frame::{
verify_unit, Packet, PacketAdmitError, PacketId, RawPacket, RawUnit, RootEvent, SourceResolver,
Unit, UnitId, VerifyError,
};
use anyhow::{bail, Context, Result};
use clap::Subcommand;
use content_addressable::{ContentAddressable, ContentId, RawContentId};
use serde::Serialize;
#[derive(clap::Args, Debug, Clone, Copy)]
pub struct InspectionArgs {
#[arg(long, default_value_t = agent_harness::forensics::InspectionLimits::default().max_bytes)]
max_bytes: usize,
#[arg(long, default_value_t = agent_harness::forensics::InspectionLimits::default().max_references)]
max_references: usize,
}
impl From<InspectionArgs> for agent_harness::forensics::InspectionLimits {
fn from(value: InspectionArgs) -> Self {
Self {
max_bytes: value.max_bytes,
max_references: value.max_references,
}
}
}
#[derive(Subcommand, Debug)]
pub enum FrameCmd {
Verify {
cid: String,
#[arg(long)]
frame: Option<PathBuf>,
#[arg(long)]
source: Option<PathBuf>,
#[arg(long)]
json: bool,
},
Explain {
cid: String,
#[arg(long)]
frame: Option<PathBuf>,
#[arg(long)]
json: bool,
#[command(flatten)]
limits: InspectionArgs,
},
Parents {
cid: String,
#[arg(long)]
frame: Option<PathBuf>,
#[arg(long)]
json: bool,
#[command(flatten)]
limits: InspectionArgs,
},
Replay {
cid: String,
#[arg(long)]
frame: Option<PathBuf>,
#[arg(long)]
json: bool,
},
}
pub struct FrameStore {
dir: PathBuf,
}
impl FrameStore {
pub fn open(dir: Option<PathBuf>) -> Result<Self> {
let dir = match dir {
Some(d) => d,
None => newt_core::Config::user_config_dir()
.context("no --frame given and no home directory to default to")?
.join("frame"),
};
Ok(Self { dir })
}
#[must_use]
pub fn dir(&self) -> &Path {
&self.dir
}
fn read_raw(&self, key: &str) -> Result<Vec<u8>> {
let path = self.dir.join(key);
std::fs::read(&path).with_context(|| format!("reading {}", path.display()))
}
fn canonical(&self) -> Result<agent_harness::store::FrameStore> {
if !self.dir.is_dir() {
bail!("frame store is not a directory: {}", self.dir.display());
}
Ok(agent_harness::store::FrameStore::open(&self.dir)?)
}
fn has_canonical(&self, id: &ContentId) -> Result<bool> {
match std::fs::symlink_metadata(self.dir.join(format!("{id}.cbor"))) {
Ok(_) => Ok(true),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(e) => Err(e.into()),
}
}
fn read_legacy_node<T, F>(&self, id: &ContentId, decode: F) -> Result<T>
where
F: FnOnce(&[u8]) -> Result<T>,
T: ContentAddressable,
{
let bytes = self.read_raw(&format!("{id}.json"))?;
let value = decode(&bytes)?;
let actual = value.content_id().context("recomputing the node's id")?;
if actual != *id {
bail!(
"store corruption: {id}.json decodes to a value whose id is {actual}. \
The file is not what it is filed as; nothing here can be trusted."
);
}
Ok(value)
}
fn unit(&self, id: &UnitId) -> Result<Unit> {
if self.has_canonical(id.as_content_id())? {
return Ok(self.canonical()?.unit(*id.as_content_id())?);
}
self.read_legacy_node(id.as_content_id(), |bytes| {
let raw: RawUnit = serde_json::from_slice(bytes).context("decoding the stored unit")?;
Unit::try_from(raw).map_err(|e| {
anyhow::anyhow!("the stored unit is not admissible under the v0 contract: {e}")
})
})
}
fn root(&self, id: &ContentId) -> Result<RootEvent> {
if self.has_canonical(id)? {
return Ok(self.canonical()?.get(id)?);
}
self.read_legacy_node(id, |bytes| {
serde_json::from_slice(bytes).context("decoding the stored root event")
})
}
fn packet(&self, id: &PacketId) -> Result<Packet> {
if self.has_canonical(id.as_content_id())? {
let raw: RawPacket = self.canonical()?.get(id.as_content_id())?;
return Packet::try_from(raw)
.context("the stored packet is not admissible under the v0 contract");
}
self.read_legacy_node(id.as_content_id(), |bytes| {
let raw: RawPacket =
serde_json::from_slice(bytes).context("decoding the stored packet")?;
Packet::try_from(raw)
.context("the stored packet is not admissible under the v0 contract")
})
}
}
impl SourceResolver for FrameStore {
fn resolve_unverified(&self, id: &RawContentId) -> Result<Vec<u8>, String> {
self.read_raw(&id.to_string()).map_err(|e| e.to_string())
}
}
#[derive(Serialize)]
pub struct SpanOut {
start: u64,
end: u64,
len: u64,
}
#[derive(Serialize)]
pub struct DerivationOut {
unit: String,
op: String,
check: String,
depth: u32,
life: String,
source: String,
span: SpanOut,
elided: String,
root: String,
#[serde(skip_serializing_if = "Option::is_none")]
root_kind: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
root_seq: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
root_content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
root_unresolved: Option<String>,
}
fn derivation_out(store: &FrameStore, id: &UnitId, u: &Unit) -> DerivationOut {
let d = u.derivation();
let (kind, seq, content, unresolved) = match store.root(&d.root) {
Ok(ev) => (
Some(format!("{:?}", ev.kind)),
Some(ev.seq),
Some(ev.content.to_string()),
None,
),
Err(e) => (None, None, None, Some(e.to_string())),
};
DerivationOut {
unit: id.to_string(),
op: format!("{:?}", d.op),
check: format!("{:?}", d.op.check()),
depth: u.depth(),
life: format!("{:?}", u.life()),
source: d.source.to_string(),
span: SpanOut {
start: d.span.start,
end: d.span.end,
len: d.span.len(),
},
elided: d.elided.to_string(),
root: d.root.to_string(),
root_kind: kind,
root_seq: seq,
root_content: content,
root_unresolved: unresolved,
}
}
#[derive(Serialize)]
pub struct VerifyReport {
command: &'static str,
store: String,
verified: bool,
#[serde(flatten)]
derivation: DerivationOut,
source_from: String,
source_profile: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
source_len: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
failure: Option<FailureOut>,
}
#[derive(Serialize)]
pub struct FailureOut {
kind: &'static str,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
declared: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
actual: Option<String>,
}
impl From<&VerifyError> for FailureOut {
fn from(e: &VerifyError) -> Self {
let (kind, declared, actual) = match e {
VerifyError::SourceMismatch { at, .. } => (
"source_mismatch",
Some(at.declared.to_string()),
Some(at.actual.to_string()),
),
VerifyError::SpanOutOfBounds { .. } => ("span_out_of_bounds", None, None),
VerifyError::ElisionMismatch { at, .. } => (
"elision_mismatch",
Some(at.declared.to_string()),
Some(at.actual.to_string()),
),
VerifyError::SourceUnavailable { .. } => ("source_unavailable", None, None),
};
Self {
kind,
message: e.to_string(),
declared,
actual,
}
}
}
#[derive(Serialize)]
pub struct ExplainReport {
command: &'static str,
store: String,
#[serde(flatten)]
derivation: DerivationOut,
asserts: bool,
well_formed: bool,
evictable: bool,
}
#[derive(Serialize)]
pub struct ParentsReport {
command: &'static str,
store: String,
subject: String,
kind: &'static str,
outcome: &'static str,
meaning: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
parent: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
units: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
antecedents: Vec<Antecedent>,
}
#[derive(Serialize)]
pub struct Antecedent {
relation: &'static str,
id: String,
profile: &'static str,
}
fn render_derivation(d: &DerivationOut) {
println!(" unit : {}", d.unit);
println!(" op : {} (check: {})", d.op, d.check);
println!(" depth : {}", d.depth);
println!(" life : {}", d.life);
println!(" source : {}", d.source);
println!(
" span : [{}, {}) ({} bytes)",
d.span.start, d.span.end, d.span.len
);
println!(" elided : {}", d.elided);
print!(" root : {}", d.root);
match (&d.root_kind, d.root_seq) {
(Some(k), Some(s)) => println!(" ({k}, seq {s})"),
_ => println!(),
}
if let Some(c) = &d.root_content {
println!(" root material : {c}");
}
if let Some(why) = &d.root_unresolved {
println!(" root UNRESOLVED : {why}");
}
}
pub fn run(cmd: &FrameCmd) -> Result<i32> {
match cmd {
FrameCmd::Verify {
cid,
frame,
source,
json,
} => run_verify(cid, frame.clone(), source.as_deref(), *json),
FrameCmd::Explain {
cid,
frame,
json,
limits,
} => run_explain(cid, frame.clone(), *json, (*limits).into()),
FrameCmd::Parents {
cid,
frame,
json,
limits,
} => run_parents(cid, frame.clone(), *json, (*limits).into()),
FrameCmd::Replay { cid, frame, json } => run_replay(cid, frame.clone(), *json),
}
}
fn emit<T: Serialize>(report: &T, json: bool, render: impl FnOnce(&T)) -> Result<()> {
if json {
println!("{}", serde_json::to_string_pretty(report)?);
} else {
render(report);
}
Ok(())
}
fn parse_unit(cid: &str) -> Result<UnitId> {
cid.parse::<UnitId>()
.map_err(|e| anyhow::anyhow!("`{cid}` is not a content id: {e}"))
}
fn run_replay(cid: &str, frame: Option<PathBuf>, json: bool) -> Result<i32> {
let store = FrameStore::open(frame)?;
let request = cid
.parse::<ContentId>()
.map_err(|e| anyhow::anyhow!("`{cid}` is not a content id: {e}"))?;
let bytes = agent_harness::replay_from_store(&store.canonical()?, request)?;
if json {
let report = serde_json::json!({
"command": "frame.replay",
"store": store.dir().display().to_string(),
"request": cid,
"verified": true,
"byte_count": bytes.len(),
"body": String::from_utf8(bytes)?,
});
println!("{}", serde_json::to_string_pretty(&report)?);
} else {
std::io::stdout().lock().write_all(&bytes)?;
}
Ok(0)
}
fn run_verify(cid: &str, frame: Option<PathBuf>, source: Option<&Path>, json: bool) -> Result<i32> {
let store = FrameStore::open(frame)?;
let id = parse_unit(cid)?;
let unit = store.unit(&id)?;
let (outcome, source_from) = match source {
Some(path) => {
let bytes = std::fs::read(path)
.with_context(|| format!("reading --source {}", path.display()))?;
(verify_unit(&unit, &bytes), path.display().to_string())
}
None => (
agent_frame::verify_unit_with(&unit, &store),
format!("store:{}", unit.derivation().source),
),
};
let report = VerifyReport {
command: "frame.verify",
store: store.dir().display().to_string(),
verified: outcome.is_ok(),
derivation: derivation_out(&store, &id, &unit),
source_from,
source_profile: "raw",
source_len: outcome.as_ref().ok().map(|v| v.source_len),
failure: outcome.as_ref().err().map(FailureOut::from),
};
emit(&report, json, |r| {
println!(
"frame verify — {}\n",
if r.verified {
"VERIFIED"
} else {
"NOT VERIFIED"
}
);
render_derivation(&r.derivation);
println!(" source from : {}", r.source_from);
if let Some(n) = r.source_len {
println!(" source bytes : {n}");
}
println!(
" source profile: {} (opaque bytes, not a unit id)",
r.source_profile
);
if let Some(f) = &r.failure {
println!("\n FAILURE [{}]\n {}", f.kind, f.message);
}
})?;
Ok(i32::from(!report.verified))
}
fn emit_inspection(
store: &FrameStore,
cid: &str,
json: bool,
command: &str,
limits: agent_harness::forensics::InspectionLimits,
) -> Result<bool> {
let id: ContentId = cid.parse().context("parsing frame content id")?;
if !store.has_canonical(&id)? {
return Ok(false);
}
let Some(inspection) =
agent_harness::forensics::inspect_from_store(&store.canonical()?, id, limits)?
else {
return Ok(false);
};
if json {
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"command": command, "store": store.dir().display().to_string(), "inspection": inspection,
}))?
);
} else {
println!("{command} — {} {}\n", inspection.kind, inspection.id);
println!("{}", serde_json::to_string_pretty(&inspection.record)?);
for reference in &inspection.references {
println!(
" {}: {} [{}]",
reference.relation, reference.cid, reference.profile
);
}
println!("\n{}", inspection.validation);
}
Ok(true)
}
fn run_explain(
cid: &str,
frame: Option<PathBuf>,
json: bool,
limits: agent_harness::forensics::InspectionLimits,
) -> Result<i32> {
let store = FrameStore::open(frame)?;
if emit_inspection(&store, cid, json, "frame.explain", limits)? {
return Ok(0);
}
let id = parse_unit(cid)?;
let unit = store.unit(&id)?;
let report = ExplainReport {
command: "frame.explain",
store: store.dir().display().to_string(),
derivation: derivation_out(&store, &id, &unit),
asserts: unit.op().asserts(),
well_formed: unit.is_well_formed(),
evictable: unit.evictable(),
};
emit(&report, json, |r| {
println!("frame explain\n");
render_derivation(&r.derivation);
println!(" asserts : {}", r.asserts);
println!(" wf : {} (depth <= 1)", r.well_formed);
println!(" evictable : {}", r.evictable);
})?;
Ok(0)
}
fn run_parents(
cid: &str,
frame: Option<PathBuf>,
json: bool,
limits: agent_harness::forensics::InspectionLimits,
) -> Result<i32> {
let store = FrameStore::open(frame)?;
if emit_inspection(&store, cid, json, "frame.parents", limits)? {
return Ok(0);
}
let pid = cid
.parse::<PacketId>()
.map_err(|e| anyhow::anyhow!("`{cid}` is not a content id: {e}"))?;
let report = match store.packet(&pid) {
Err(e) if e.downcast_ref::<PacketAdmitError>().is_some() => return Err(e),
Ok(p) => {
let units: Vec<String> = p.units().iter().map(ToString::to_string).collect();
match p.prior() {
None => ParentsReport {
command: "frame.parents",
store: store.dir().display().to_string(),
subject: cid.to_string(),
kind: "packet",
outcome: "genesis",
meaning: "no parents: this frame is an origin, and the chain ends here",
parent: None,
units,
antecedents: Vec::new(),
},
Some(prev) => ParentsReport {
command: "frame.parents",
store: store.dir().display().to_string(),
subject: cid.to_string(),
kind: "packet",
outcome: "parent",
meaning: "one parent link, named but NOT followed: run this command on it \
to go one link deeper",
parent: Some(prev.to_string()),
units,
antecedents: Vec::new(),
},
}
}
Err(_) => {
let uid = parse_unit(cid)?;
let unit = store.unit(&uid)?;
let d = unit.derivation();
ParentsReport {
command: "frame.parents",
store: store.dir().display().to_string(),
subject: cid.to_string(),
kind: "unit",
outcome: "antecedents",
meaning: "a unit is a leaf: it names the source it was derived from and the \
root event that caused it, and neither is followed here",
parent: None,
units: Vec::new(),
antecedents: vec![
Antecedent {
relation: "source",
id: d.source.to_string(),
profile: "raw",
},
Antecedent {
relation: "root",
id: d.root.to_string(),
profile: "dag-cbor",
},
],
}
}
};
emit(&report, json, |r| {
println!("frame parents — {} {}\n", r.kind, r.subject);
println!(" outcome : {}", r.outcome);
println!(" {}", r.meaning);
if let Some(p) = &r.parent {
println!(" parent : {p}");
}
for u in &r.units {
println!(" unit : {u}");
}
for a in &r.antecedents {
println!(" {:<7} : {} [{}]", a.relation, a.id, a.profile);
}
})?;
Ok(0)
}