use std::time::Instant;
use anyhow::Result;
use openmoat_audit::{EventId, KnownSecrets, NewEvent, NewTicket, Store};
use openmoat_core::{Action, Decision, Verdict};
use openmoat_hosts::{HookRequest, Host};
use super::drift;
use crate::hangs::{self, Phase};
use crate::home::Home;
use crate::integrity::Drift;
use crate::taint;
pub(super) struct Recorded<'a> {
pub(super) request: Option<&'a HookRequest>,
pub(super) decision: &'a Decision,
pub(super) drifted: &'a [Drift],
pub(super) started: Instant,
pub(super) known: KnownSecrets,
pub(super) cache: Option<taint::CacheWrite>,
pub(super) provenance: taint::provenance::Seen,
}
pub(super) fn record(host: Host, recorded: Recorded<'_>) -> Result<()> {
let Recorded {
request,
decision,
drifted,
started,
known,
cache,
provenance,
} = recorded;
hangs::enter(Phase::Record);
let home = Home::locate()?;
home.require_exists()?;
let store = Store::open_existing(&home.audit_path())?;
if let Some(cache) = cache {
hangs::enter(Phase::Cache);
cache.write(&store);
hangs::enter(Phase::Record);
}
let store = store.with_secrets(known);
let noted = match request {
Some(r) => drift::noted(&store, &r.session_id, decision, drifted)?,
None => None,
};
let decision = noted.as_ref().unwrap_or(decision);
let latency_us = u64::try_from(started.elapsed().as_micros()).unwrap_or(u64::MAX);
let event = NewEvent {
host: host.id(),
session_id: request.map_or("unknown", |r| r.session_id.as_str()),
call_id: request.and_then(|r| r.call_id.as_deref()),
cwd: request.and_then(|r| r.cwd.as_deref()),
tool: request.map_or("unknown", |r| r.tool.as_str()),
action: request.and_then(|r| r.action.as_ref()),
decision,
latency_us,
};
let id = store.record(&event)?;
if let Some(r) = request
&& taint::ran(host, decision.verdict)
{
provenance.write(&store, id, host.id(), &r.session_id)?;
}
if let Some(r) = request {
mcp_ticket(&store, id, host, r, decision)?;
}
let run = std::env::var(taint::run::RUN_SESSION_ENV).ok();
if let (Some(run), Some(r)) = (taint::run::run_session(run.as_deref()), request) {
taint::run::link(&store, run, host, &r.session_id)?;
}
if !matches!(decision.verdict, Verdict::Allow | Verdict::Defer) {
eprintln!("moat: trace {id} (openmoat show {id})");
}
Ok(())
}
fn mcp_ticket(
store: &Store,
event: EventId,
host: Host,
request: &HookRequest,
decision: &Decision,
) -> Result<()> {
let (Some(Action::McpTool { name, .. }), Some(arguments)) =
(&request.action, &request.mcp_arguments)
else {
return Ok(());
};
if host.answer(&request.event, decision).verdict == Verdict::Deny {
return Ok(());
}
store.add_mcp_ticket(&NewTicket {
event,
host: host.id(),
session_id: &request.session_id,
call_id: request.call_id.as_deref(),
tool: name,
arguments_sha256: &crate::integrity::sha256_hex(arguments.as_bytes()),
verdict: decision.verdict.as_str(),
})?;
Ok(())
}