Skip to main content

harn_vm/
trust_graph.rs

1use std::collections::{BTreeMap, HashMap, HashSet};
2use std::sync::Arc;
3
4use serde::{Deserialize, Serialize};
5use sha2::{Digest, Sha256};
6use time::{Duration, OffsetDateTime};
7use uuid::Uuid;
8
9use crate::actor_chain::ActorChain;
10use crate::event_log::{
11    active_event_log, sanitize_topic_component, AnyEventLog, EventId, EventLog, LogError, LogEvent,
12    Topic,
13};
14use crate::orchestration::{CapabilityPolicy, EffectRecord};
15
16pub const OPENTRUSTGRAPH_SCHEMA_V0: &str = "opentrustgraph/v0";
17/// OpenTrustGraph v0.1: additive metadata schema. Reserves lineage keys under
18/// `TrustRecord.metadata` so chain validators can prove that child-agent
19/// effects, actors, and actor-chain policy alerts stay inside the parent chain.
20///
21/// Backwards compatible: v0 records are still accepted (the new keys are
22/// optional). One patch release window after this bump, v0 will be
23/// dropped per `opentrustgraph-spec/CONFORMANCE.md` §5.
24pub const OPENTRUSTGRAPH_SCHEMA_V0_1: &str = "opentrustgraph/v0.1";
25/// Set of schema discriminators accepted by the v0.1 validator.
26pub const OPENTRUSTGRAPH_ACCEPTED_SCHEMAS: &[&str] =
27    &[OPENTRUSTGRAPH_SCHEMA_V0_1, OPENTRUSTGRAPH_SCHEMA_V0];
28pub const OPENTRUSTGRAPH_CHAIN_SCHEMA_V0: &str = "opentrustgraph-chain/v0";
29
30/// Reserved metadata key for the effect grant attached to a record by its
31/// spawning parent.
32pub const METADATA_KEY_EFFECTS_GRANT: &str = "effects_grant";
33/// Reserved metadata key for the effects the recorded action actually
34/// exercised. Must be a subset of the parent's `effects_grant`.
35pub const METADATA_KEY_EFFECTS_USED: &str = "effects_used";
36/// Reserved metadata key pointing at the parent record's `record_id`.
37/// Lets verifiers reconstruct the agent chain without scanning the whole
38/// stream.
39pub const METADATA_KEY_PARENT_RECORD_ID: &str = "parent_record_id";
40/// Reserved metadata key carrying the RFC 8693 actor chain for the record.
41/// When paired with `parent_record_id`, the nested `act` chain must extend
42/// the parent's actor chain by exactly one hop.
43pub const METADATA_KEY_ACTOR_CHAIN: &str = "actor_chain";
44/// Reserved metadata key for actor-chain policy alerts.
45pub const METADATA_KEY_ACTOR_CHAIN_ALERT: &str = "actor_chain_alert";
46pub const TRUST_GRAPH_RECORDS_TOPIC: &str = "trust_graph.records";
47pub const TRUST_GRAPH_GLOBAL_TOPIC: &str = "trust_graph";
48pub const TRUST_GRAPH_LEGACY_GLOBAL_TOPIC: &str = "trust.graph";
49pub const TRUST_GRAPH_TOPIC_PREFIX: &str = "trust_graph.";
50pub const TRUST_GRAPH_LEGACY_TOPIC_PREFIX: &str = "trust.graph.";
51pub const TRUST_GRAPH_EVENT_KIND: &str = "trust_recorded";
52pub const TRUST_ACTION_RELEASE: &str = "release";
53
54#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
55#[serde(rename_all = "snake_case")]
56pub enum AutonomyTier {
57    Shadow,
58    Suggest,
59    ActWithApproval,
60    #[default]
61    ActAuto,
62}
63
64impl AutonomyTier {
65    pub fn as_str(self) -> &'static str {
66        match self {
67            Self::Shadow => "shadow",
68            Self::Suggest => "suggest",
69            Self::ActWithApproval => "act_with_approval",
70            Self::ActAuto => "act_auto",
71        }
72    }
73}
74
75#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
76#[serde(rename_all = "snake_case")]
77pub enum TrustOutcome {
78    Success,
79    Failure,
80    Denied,
81    Timeout,
82}
83
84impl TrustOutcome {
85    pub fn as_str(self) -> &'static str {
86        match self {
87            Self::Success => "success",
88            Self::Failure => "failure",
89            Self::Denied => "denied",
90            Self::Timeout => "timeout",
91        }
92    }
93}
94
95#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
96pub struct TrustRecord {
97    pub schema: String,
98    pub record_id: String,
99    pub agent: String,
100    pub action: String,
101    pub approver: Option<String>,
102    pub outcome: TrustOutcome,
103    pub trace_id: String,
104    pub autonomy_tier: AutonomyTier,
105    #[serde(with = "time::serde::rfc3339")]
106    pub timestamp: OffsetDateTime,
107    pub cost_usd: Option<f64>,
108    #[serde(default)]
109    pub chain_index: u64,
110    #[serde(default)]
111    pub previous_hash: Option<String>,
112    #[serde(default)]
113    pub entry_hash: String,
114    #[serde(default)]
115    pub metadata: BTreeMap<String, serde_json::Value>,
116}
117
118#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
119#[serde(tag = "kind", rename_all = "snake_case")]
120pub enum TrustRecordActionKind {
121    Release {
122        bundle_hash: String,
123        harn_version: String,
124        parent_trust_record_id: Option<String>,
125    },
126}
127
128impl TrustRecord {
129    pub fn new(
130        agent: impl Into<String>,
131        action: impl Into<String>,
132        approver: Option<String>,
133        outcome: TrustOutcome,
134        trace_id: impl Into<String>,
135        autonomy_tier: AutonomyTier,
136    ) -> Self {
137        Self {
138            schema: OPENTRUSTGRAPH_SCHEMA_V0_1.to_string(),
139            record_id: Uuid::now_v7().to_string(),
140            agent: agent.into(),
141            action: action.into(),
142            approver,
143            outcome,
144            trace_id: trace_id.into(),
145            autonomy_tier,
146            timestamp: OffsetDateTime::now_utc(),
147            cost_usd: None,
148            chain_index: 0,
149            previous_hash: None,
150            entry_hash: String::new(),
151            metadata: BTreeMap::new(),
152        }
153    }
154
155    pub fn release(
156        agent: impl Into<String>,
157        bundle_hash: impl Into<String>,
158        harn_version: impl Into<String>,
159        parent_trust_record_id: Option<String>,
160        trace_id: impl Into<String>,
161        autonomy_tier: AutonomyTier,
162    ) -> Self {
163        let bundle_hash = bundle_hash.into();
164        let harn_version = harn_version.into();
165        let action_kind = TrustRecordActionKind::Release {
166            bundle_hash: bundle_hash.clone(),
167            harn_version: harn_version.clone(),
168            parent_trust_record_id: parent_trust_record_id.clone(),
169        };
170        let mut record = Self::new(
171            agent,
172            TRUST_ACTION_RELEASE,
173            None,
174            TrustOutcome::Success,
175            trace_id,
176            autonomy_tier,
177        );
178        record
179            .metadata
180            .insert("action_kind".to_string(), serde_json::json!(action_kind));
181        record
182            .metadata
183            .insert("bundle_hash".to_string(), serde_json::json!(bundle_hash));
184        record
185            .metadata
186            .insert("harn_version".to_string(), serde_json::json!(harn_version));
187        record.metadata.insert(
188            "parent_trust_record_id".to_string(),
189            parent_trust_record_id
190                .map(serde_json::Value::String)
191                .unwrap_or(serde_json::Value::Null),
192        );
193        record
194    }
195
196    /// Attach the typed effect grant a parent extended to this record.
197    /// Empty grants are skipped so records stay compact when there is
198    /// nothing to prove.
199    pub fn with_effects_grant(mut self, effects: Vec<EffectRecord>) -> Self {
200        self.set_effects_grant(effects);
201        self
202    }
203
204    pub fn set_effects_grant(&mut self, effects: Vec<EffectRecord>) {
205        if effects.is_empty() {
206            self.metadata.remove(METADATA_KEY_EFFECTS_GRANT);
207            return;
208        }
209        self.metadata.insert(
210            METADATA_KEY_EFFECTS_GRANT.to_string(),
211            serde_json::to_value(effects).expect("EffectRecord is serializable"),
212        );
213    }
214
215    pub fn effects_grant(&self) -> Vec<EffectRecord> {
216        decode_effect_list(self.metadata.get(METADATA_KEY_EFFECTS_GRANT))
217    }
218
219    /// Attach the typed effect set the action actually exercised.
220    /// Verifiers must check `effects_used ⊆ effects_grant` through the
221    /// parent chain.
222    pub fn with_effects_used(mut self, effects: Vec<EffectRecord>) -> Self {
223        self.set_effects_used(effects);
224        self
225    }
226
227    pub fn set_effects_used(&mut self, effects: Vec<EffectRecord>) {
228        if effects.is_empty() {
229            self.metadata.remove(METADATA_KEY_EFFECTS_USED);
230            return;
231        }
232        self.metadata.insert(
233            METADATA_KEY_EFFECTS_USED.to_string(),
234            serde_json::to_value(effects).expect("EffectRecord is serializable"),
235        );
236    }
237
238    pub fn effects_used(&self) -> Vec<EffectRecord> {
239        decode_effect_list(self.metadata.get(METADATA_KEY_EFFECTS_USED))
240    }
241
242    /// Point this record at its parent's `record_id`. The existing
243    /// release-record key (`parent_trust_record_id`) is retained for the
244    /// release flow; this is the generic spawn-lineage pointer.
245    pub fn with_parent_record_id(mut self, parent_record_id: impl Into<String>) -> Self {
246        self.set_parent_record_id(Some(parent_record_id.into()));
247        self
248    }
249
250    pub fn set_parent_record_id(&mut self, parent_record_id: Option<String>) {
251        match parent_record_id {
252            Some(id) if !id.is_empty() => {
253                self.metadata.insert(
254                    METADATA_KEY_PARENT_RECORD_ID.to_string(),
255                    serde_json::Value::String(id),
256                );
257            }
258            _ => {
259                self.metadata.remove(METADATA_KEY_PARENT_RECORD_ID);
260            }
261        }
262    }
263
264    pub fn parent_record_id(&self) -> Option<String> {
265        self.metadata
266            .get(METADATA_KEY_PARENT_RECORD_ID)
267            .and_then(|value| value.as_str())
268            .map(str::to_string)
269    }
270
271    /// Attach the RFC 8693 actor chain for the principal that caused this
272    /// record.
273    pub fn with_actor_chain(mut self, actor_chain: ActorChain) -> Self {
274        self.set_actor_chain(Some(actor_chain));
275        self
276    }
277
278    /// Set or clear the reserved `actor_chain` metadata entry.
279    pub fn set_actor_chain(&mut self, actor_chain: Option<ActorChain>) {
280        match actor_chain {
281            Some(actor_chain) => {
282                self.metadata.insert(
283                    METADATA_KEY_ACTOR_CHAIN.to_string(),
284                    actor_chain.to_json_value(),
285                );
286            }
287            None => {
288                self.metadata.remove(METADATA_KEY_ACTOR_CHAIN);
289            }
290        }
291    }
292
293    /// Decode the reserved actor-chain metadata entry, dropping malformed
294    /// values for callers that only need best-effort display data.
295    pub fn actor_chain(&self) -> Option<ActorChain> {
296        self.try_actor_chain().ok().flatten()
297    }
298
299    /// Decode the reserved actor-chain metadata entry and report malformed
300    /// RFC 8693 claim shapes to strict validators.
301    pub fn try_actor_chain(&self) -> Result<Option<ActorChain>, crate::ActorChainError> {
302        self.metadata
303            .get(METADATA_KEY_ACTOR_CHAIN)
304            .map(ActorChain::from_json_value)
305            .transpose()
306    }
307
308    pub fn with_actor_chain_alert(mut self, alert: serde_json::Value) -> Self {
309        self.set_actor_chain_alert(Some(alert));
310        self
311    }
312
313    pub fn set_actor_chain_alert(&mut self, alert: Option<serde_json::Value>) {
314        match alert {
315            Some(alert) => {
316                self.metadata
317                    .insert(METADATA_KEY_ACTOR_CHAIN_ALERT.to_string(), alert);
318            }
319            None => {
320                self.metadata.remove(METADATA_KEY_ACTOR_CHAIN_ALERT);
321            }
322        }
323    }
324
325    pub fn actor_chain_alert(&self) -> Option<&serde_json::Value> {
326        self.metadata.get(METADATA_KEY_ACTOR_CHAIN_ALERT)
327    }
328}
329
330fn decode_effect_list(value: Option<&serde_json::Value>) -> Vec<EffectRecord> {
331    value
332        .and_then(|value| serde_json::from_value::<Vec<EffectRecord>>(value.clone()).ok())
333        .unwrap_or_default()
334}
335
336#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
337pub struct TrustGraphRecord {
338    pub actor_id: String,
339    pub action: String,
340    pub approver: Option<String>,
341    pub outcome: TrustOutcome,
342    #[serde(default)]
343    pub evidence_refs: Vec<serde_json::Value>,
344    pub trace_id: String,
345    #[serde(with = "time::serde::rfc3339")]
346    pub timestamp: OffsetDateTime,
347    pub autonomy_tier_at_time: AutonomyTier,
348}
349
350impl TrustGraphRecord {
351    pub fn from_trust_record(record: &TrustRecord) -> Self {
352        Self {
353            actor_id: record.agent.clone(),
354            action: record.action.clone(),
355            approver: record.approver.clone(),
356            outcome: record.outcome,
357            evidence_refs: evidence_refs_from_metadata(&record.metadata),
358            trace_id: record.trace_id.clone(),
359            timestamp: record.timestamp,
360            autonomy_tier_at_time: record.autonomy_tier,
361        }
362    }
363}
364
365#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
366#[serde(default)]
367pub struct TrustQueryFilters {
368    pub agent: Option<String>,
369    pub action: Option<String>,
370    #[serde(with = "time::serde::rfc3339::option")]
371    pub since: Option<OffsetDateTime>,
372    #[serde(with = "time::serde::rfc3339::option")]
373    pub until: Option<OffsetDateTime>,
374    pub tier: Option<AutonomyTier>,
375    pub outcome: Option<TrustOutcome>,
376    pub limit: Option<usize>,
377    pub grouped_by_trace: bool,
378}
379
380#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
381#[serde(default)]
382pub struct TrustTraceGroup {
383    pub trace_id: String,
384    pub records: Vec<TrustRecord>,
385}
386
387#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
388#[serde(default)]
389pub struct TrustAgentSummary {
390    pub agent: String,
391    pub total: u64,
392    pub success_rate: f64,
393    pub mean_cost_usd: Option<f64>,
394    pub tier_distribution: BTreeMap<String, u64>,
395    pub outcome_distribution: BTreeMap<String, u64>,
396}
397
398#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
399#[serde(default)]
400pub struct TrustScore {
401    pub agent: String,
402    pub action: Option<String>,
403    pub total: u64,
404    pub successes: u64,
405    pub failures: u64,
406    pub denied: u64,
407    pub timeouts: u64,
408    pub success_rate: f64,
409    pub latest_outcome: Option<TrustOutcome>,
410    #[serde(with = "time::serde::rfc3339::option")]
411    pub latest_timestamp: Option<OffsetDateTime>,
412    pub effective_tier: AutonomyTier,
413    pub policy: CapabilityPolicy,
414}
415
416#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
417#[serde(default)]
418pub struct TrustChainReport {
419    pub topic: String,
420    pub total: u64,
421    pub verified: bool,
422    pub root_hash: Option<String>,
423    pub broken_at_event_id: Option<EventId>,
424    pub errors: Vec<String>,
425}
426
427#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
428pub struct TrustChainExportProducer {
429    pub name: String,
430    pub version: String,
431}
432
433impl Default for TrustChainExportProducer {
434    fn default() -> Self {
435        Self {
436            name: "harn".to_string(),
437            version: env!("CARGO_PKG_VERSION").to_string(),
438        }
439    }
440}
441
442#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
443pub struct TrustChainExportMetadata {
444    pub topic: String,
445    pub total: u64,
446    pub root_hash: Option<String>,
447    pub verified: bool,
448    #[serde(with = "time::serde::rfc3339")]
449    pub generated_at: OffsetDateTime,
450    pub producer: TrustChainExportProducer,
451}
452
453#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
454pub struct TrustChainExport {
455    pub schema: String,
456    pub chain: TrustChainExportMetadata,
457    pub records: Vec<TrustRecord>,
458}
459
460fn global_topic() -> Result<Topic, LogError> {
461    Topic::new(TRUST_GRAPH_GLOBAL_TOPIC)
462}
463
464fn legacy_global_topic() -> Result<Topic, LogError> {
465    Topic::new(TRUST_GRAPH_LEGACY_GLOBAL_TOPIC)
466}
467
468fn records_topic() -> Result<Topic, LogError> {
469    Topic::new(TRUST_GRAPH_RECORDS_TOPIC)
470}
471
472pub fn topic_for_agent(agent: &str) -> Result<Topic, LogError> {
473    Topic::new(format!(
474        "{TRUST_GRAPH_TOPIC_PREFIX}{}",
475        sanitize_topic_component(agent)
476    ))
477}
478
479pub fn legacy_topic_for_agent(agent: &str) -> Result<Topic, LogError> {
480    Topic::new(format!(
481        "{TRUST_GRAPH_LEGACY_TOPIC_PREFIX}{}",
482        sanitize_topic_component(agent)
483    ))
484}
485
486pub async fn append_trust_record(
487    log: &Arc<AnyEventLog>,
488    record: &TrustRecord,
489) -> Result<TrustRecord, LogError> {
490    let finalized = finalize_trust_record(log, record.clone()).await?;
491    let payload = serde_json::to_value(&finalized)
492        .map_err(|error| LogError::Serde(format!("trust record encode error: {error}")))?;
493    let mut headers = BTreeMap::new();
494    headers.insert("trace_id".to_string(), finalized.trace_id.clone());
495    headers.insert("agent".to_string(), finalized.agent.clone());
496    headers.insert(
497        "autonomy_tier".to_string(),
498        finalized.autonomy_tier.as_str().to_string(),
499    );
500    headers.insert(
501        "outcome".to_string(),
502        finalized.outcome.as_str().to_string(),
503    );
504    headers.insert("entry_hash".to_string(), finalized.entry_hash.clone());
505    let event = LogEvent::new(TRUST_GRAPH_EVENT_KIND, payload).with_headers(headers);
506    for topic in append_topics_for_record(&finalized)? {
507        log.append(&topic, event.clone()).await?;
508    }
509    append_trust_graph_record_projection(log, &finalized).await?;
510    Ok(finalized)
511}
512
513pub async fn append_active_trust_record(record: &TrustRecord) -> Result<TrustRecord, LogError> {
514    let log = active_event_log()
515        .ok_or_else(|| LogError::Config("trust graph requires an active event log".to_string()))?;
516    append_trust_record(&log, record).await
517}
518
519pub async fn append_scope_attenuation_alert(
520    log: &Arc<AnyEventLog>,
521    actor_chain: &crate::ActorChain,
522    violation: &crate::ScopeAttenuationViolation,
523    trace_id: impl Into<String>,
524) -> Result<TrustRecord, LogError> {
525    let record = TrustRecord::new(
526        violation.child_subject(),
527        "identity.scope_attenuation",
528        None,
529        TrustOutcome::Denied,
530        trace_id,
531        AutonomyTier::ActAuto,
532    )
533    .with_actor_chain(actor_chain.clone())
534    .with_actor_chain_alert(violation.to_json_value());
535    append_trust_record(log, &record).await
536}
537
538pub async fn append_active_scope_attenuation_alert(
539    actor_chain: &crate::ActorChain,
540    violation: &crate::ScopeAttenuationViolation,
541    trace_id: impl Into<String>,
542) -> Result<TrustRecord, LogError> {
543    let log = active_event_log()
544        .ok_or_else(|| LogError::Config("trust graph requires an active event log".to_string()))?;
545    append_scope_attenuation_alert(&log, actor_chain, violation, trace_id).await
546}
547
548pub async fn query_trust_records(
549    log: &Arc<AnyEventLog>,
550    filters: &TrustQueryFilters,
551) -> Result<Vec<TrustRecord>, LogError> {
552    let topics = query_topics(filters)?;
553    let mut records = Vec::new();
554    let mut seen = HashSet::new();
555    for topic in topics {
556        for (_, event) in log.read_range(&topic, None, usize::MAX).await? {
557            if event.kind != TRUST_GRAPH_EVENT_KIND {
558                continue;
559            }
560            let Ok(record) = serde_json::from_value::<TrustRecord>(event.payload) else {
561                continue;
562            };
563            if !matches_filters(&record, filters) {
564                continue;
565            }
566            let dedupe_key = trust_record_dedupe_key(&record);
567            if seen.insert(dedupe_key) {
568                records.push(record);
569            }
570        }
571    }
572    records.sort_by(|left, right| {
573        left.timestamp
574            .cmp(&right.timestamp)
575            .then(left.chain_index.cmp(&right.chain_index))
576            .then(left.agent.cmp(&right.agent))
577            .then(left.record_id.cmp(&right.record_id))
578    });
579    apply_record_limit(&mut records, filters.limit);
580    Ok(records)
581}
582
583pub async fn query_trust_graph_records(
584    log: &Arc<AnyEventLog>,
585    filters: &TrustQueryFilters,
586) -> Result<Vec<TrustGraphRecord>, LogError> {
587    let mut graph_records = Vec::new();
588    let mut seen = HashSet::new();
589
590    for record in query_trust_records(log, filters).await? {
591        let graph_record = TrustGraphRecord::from_trust_record(&record);
592        let dedupe_key = trust_graph_record_dedupe_key(&graph_record);
593        if seen.insert(dedupe_key) {
594            graph_records.push(graph_record);
595        }
596    }
597
598    for (_, event) in log.read_range(&records_topic()?, None, usize::MAX).await? {
599        if event.kind != TRUST_GRAPH_EVENT_KIND {
600            continue;
601        }
602        let Ok(record) = serde_json::from_value::<TrustGraphRecord>(event.payload) else {
603            continue;
604        };
605        if !matches_graph_filters(&record, filters) {
606            continue;
607        }
608        let dedupe_key = trust_graph_record_dedupe_key(&record);
609        if seen.insert(dedupe_key) {
610            graph_records.push(record);
611        }
612    }
613
614    graph_records.sort_by(|left, right| {
615        left.timestamp
616            .cmp(&right.timestamp)
617            .then(left.actor_id.cmp(&right.actor_id))
618            .then(left.action.cmp(&right.action))
619            .then(left.trace_id.cmp(&right.trace_id))
620    });
621    apply_graph_record_limit(&mut graph_records, filters.limit);
622    Ok(graph_records)
623}
624
625pub async fn trust_score_for(
626    log: &Arc<AnyEventLog>,
627    agent: &str,
628    action: Option<&str>,
629) -> Result<TrustScore, LogError> {
630    let records = query_trust_records(
631        log,
632        &TrustQueryFilters {
633            agent: Some(agent.to_string()),
634            action: action.map(ToString::to_string),
635            ..TrustQueryFilters::default()
636        },
637    )
638    .await?;
639    let effective_tier = resolve_agent_autonomy_tier(log, agent, AutonomyTier::ActAuto).await?;
640    let mut score = score_from_records(agent, action, effective_tier, &records);
641    score.policy =
642        crate::corrections::apply_corrections_to_policy(log, agent, score.policy).await?;
643    Ok(score)
644}
645
646pub async fn policy_for_agent(
647    log: &Arc<AnyEventLog>,
648    agent: &str,
649) -> Result<CapabilityPolicy, LogError> {
650    Ok(trust_score_for(log, agent, None).await?.policy)
651}
652
653pub async fn verify_trust_chain(log: &Arc<AnyEventLog>) -> Result<TrustChainReport, LogError> {
654    let (topic, records) = preferred_chain_records(log).await?;
655    let mut previous_hash: Option<String> = None;
656    let mut errors = Vec::new();
657    let mut broken_at_event_id = None;
658
659    for (position, (event_id, record)) in records.iter().enumerate() {
660        let expected_index = (position as u64) + 1;
661        if record.chain_index != expected_index {
662            errors.push(format!(
663                "event {event_id}: expected chain_index {expected_index}, found {}",
664                record.chain_index
665            ));
666        }
667        if record.previous_hash != previous_hash {
668            errors.push(format!(
669                "event {event_id}: previous_hash mismatch; expected {:?}, found {:?}",
670                previous_hash, record.previous_hash
671            ));
672        }
673        match compute_trust_record_hash(record) {
674            Ok(expected_hash) if expected_hash == record.entry_hash => {}
675            Ok(expected_hash) => errors.push(format!(
676                "event {event_id}: entry_hash mismatch; expected {expected_hash}, found {}",
677                record.entry_hash
678            )),
679            Err(error) => errors.push(format!("event {event_id}: {error}")),
680        }
681        if !errors.is_empty() && broken_at_event_id.is_none() {
682            broken_at_event_id = Some(*event_id);
683        }
684        previous_hash = Some(record.entry_hash.clone());
685    }
686    let lineage_errors = validate_lineage_invariants(
687        records
688            .iter()
689            .map(|(event_id, record)| (format!("event {event_id}"), Some(*event_id), record)),
690    );
691    if broken_at_event_id.is_none() {
692        broken_at_event_id = lineage_errors.iter().find_map(|error| error.event_id);
693    }
694    errors.extend(lineage_errors.into_iter().map(|error| error.message));
695
696    Ok(TrustChainReport {
697        topic: topic.as_str().to_string(),
698        total: records.len() as u64,
699        verified: errors.is_empty(),
700        root_hash: records.last().map(|(_, record)| record.entry_hash.clone()),
701        broken_at_event_id,
702        errors,
703    })
704}
705
706pub async fn export_trust_chain(log: &Arc<AnyEventLog>) -> Result<TrustChainExport, LogError> {
707    let (topic, records_with_ids) = preferred_chain_records(log).await?;
708    let report = verify_trust_chain(log).await?;
709    let records: Vec<TrustRecord> = records_with_ids.into_iter().map(|(_, r)| r).collect();
710    Ok(TrustChainExport {
711        schema: OPENTRUSTGRAPH_CHAIN_SCHEMA_V0.to_string(),
712        chain: TrustChainExportMetadata {
713            topic: topic.as_str().to_string(),
714            total: records.len() as u64,
715            root_hash: records.last().map(|record| record.entry_hash.clone()),
716            verified: report.verified,
717            generated_at: OffsetDateTime::now_utc(),
718            producer: TrustChainExportProducer::default(),
719        },
720        records,
721    })
722}
723
724pub fn compute_trust_record_hash(record: &TrustRecord) -> Result<String, LogError> {
725    let mut value = serde_json::to_value(record)
726        .map_err(|error| LogError::Serde(format!("trust record hash encode error: {error}")))?;
727    if let Some(object) = value.as_object_mut() {
728        object.remove("entry_hash");
729    }
730    let canonical = serde_json::to_string(&value)
731        .map_err(|error| LogError::Serde(format!("trust record canonicalize error: {error}")))?;
732    let digest = Sha256::digest(canonical.as_bytes());
733    Ok(format!("sha256:{}", hex::encode(digest)))
734}
735
736struct LineageInvariantError {
737    event_id: Option<EventId>,
738    message: String,
739}
740
741impl LineageInvariantError {
742    fn new(event_id: Option<EventId>, message: String) -> Self {
743        Self { event_id, message }
744    }
745}
746
747fn validate_lineage_invariants<'a, I>(records: I) -> Vec<LineageInvariantError>
748where
749    I: IntoIterator<Item = (String, Option<EventId>, &'a TrustRecord)>,
750{
751    let mut errors = Vec::new();
752    let mut by_id: HashMap<&'a str, &'a TrustRecord> = HashMap::new();
753
754    for (label, event_id, record) in records {
755        let actor_chain = match record.try_actor_chain() {
756            Ok(actor_chain) => actor_chain,
757            Err(error) => {
758                errors.push(LineageInvariantError::new(
759                    event_id,
760                    format!("{label}: actor_chain invalid: {error}"),
761                ));
762                None
763            }
764        };
765        let effects_used = record.effects_used();
766        if let Some(parent_id) = record.parent_record_id() {
767            let parent = by_id.get(parent_id.as_str()).copied();
768            if parent.is_none() && (!effects_used.is_empty() || actor_chain.is_some()) {
769                errors.push(LineageInvariantError::new(
770                    event_id,
771                    format!("{label}: parent_record_id {parent_id:?} not found in chain"),
772                ));
773            }
774            if let Some(parent) = parent {
775                validate_effect_lineage(
776                    &mut errors,
777                    &label,
778                    event_id,
779                    &parent_id,
780                    parent,
781                    &effects_used,
782                );
783                validate_actor_lineage(
784                    &mut errors,
785                    &label,
786                    event_id,
787                    &parent_id,
788                    parent,
789                    actor_chain,
790                );
791            }
792        }
793
794        if !record.record_id.is_empty() {
795            by_id.insert(record.record_id.as_str(), record);
796        }
797    }
798
799    errors
800}
801
802fn validate_effect_lineage(
803    errors: &mut Vec<LineageInvariantError>,
804    label: &str,
805    event_id: Option<EventId>,
806    parent_id: &str,
807    parent: &TrustRecord,
808    effects_used: &[EffectRecord],
809) {
810    if effects_used.is_empty() {
811        return;
812    }
813    let parent_grant = parent.effects_grant();
814    for effect in effects_used {
815        if !parent_grant.contains(effect) {
816            errors.push(LineageInvariantError::new(
817                event_id,
818                format!(
819                    "{label}: effects_used escaped grant from parent {parent_id:?}: {effect:?}"
820                ),
821            ));
822        }
823    }
824}
825
826fn validate_actor_lineage(
827    errors: &mut Vec<LineageInvariantError>,
828    label: &str,
829    event_id: Option<EventId>,
830    parent_id: &str,
831    parent: &TrustRecord,
832    actor_chain: Option<ActorChain>,
833) {
834    let Some(actor_chain) = actor_chain else {
835        return;
836    };
837    let parent_actor_chain = match parent.try_actor_chain() {
838        Ok(Some(parent_actor_chain)) => parent_actor_chain,
839        Ok(None) => {
840            errors.push(LineageInvariantError::new(
841                event_id,
842                format!("{label}: actor_chain parent {parent_id:?} missing actor_chain"),
843            ));
844            return;
845        }
846        Err(error) => {
847            errors.push(LineageInvariantError::new(
848                event_id,
849                format!("{label}: parent actor_chain invalid: {error}"),
850            ));
851            return;
852        }
853    };
854    if !actor_chain_extends_parent(&actor_chain, &parent_actor_chain) {
855        errors.push(LineageInvariantError::new(
856            event_id,
857            format!("{label}: actor_chain escaped parentage from parent {parent_id:?}"),
858        ));
859    }
860}
861
862fn actor_chain_extends_parent(child: &ActorChain, parent: &ActorChain) -> bool {
863    if child.origin() != parent.origin() {
864        return false;
865    }
866    let child_actors: Vec<&str> = child.actors().collect();
867    let parent_actors: Vec<&str> = parent.actors().collect();
868    child_actors.len() == parent_actors.len() + 1 && child_actors[1..] == parent_actors[..]
869}
870
871pub fn group_trust_records_by_trace(records: &[TrustRecord]) -> Vec<TrustTraceGroup> {
872    let mut groups: Vec<TrustTraceGroup> = Vec::new();
873    let mut positions: HashMap<String, usize> = HashMap::new();
874    for record in records {
875        if let Some(index) = positions.get(record.trace_id.as_str()).copied() {
876            groups[index].records.push(record.clone());
877            continue;
878        }
879        positions.insert(record.trace_id.clone(), groups.len());
880        groups.push(TrustTraceGroup {
881            trace_id: record.trace_id.clone(),
882            records: vec![record.clone()],
883        });
884    }
885    groups
886}
887
888pub fn summarize_trust_records(records: &[TrustRecord]) -> Vec<TrustAgentSummary> {
889    #[derive(Default)]
890    struct RunningSummary {
891        total: u64,
892        successes: u64,
893        cost_sum: f64,
894        cost_count: u64,
895        tier_distribution: BTreeMap<String, u64>,
896        outcome_distribution: BTreeMap<String, u64>,
897    }
898
899    let mut by_agent: BTreeMap<String, RunningSummary> = BTreeMap::new();
900    for record in records {
901        let entry = by_agent.entry(record.agent.clone()).or_default();
902        entry.total += 1;
903        if record.outcome == TrustOutcome::Success {
904            entry.successes += 1;
905        }
906        if let Some(cost_usd) = record.cost_usd {
907            entry.cost_sum += cost_usd;
908            entry.cost_count += 1;
909        }
910        *entry
911            .tier_distribution
912            .entry(record.autonomy_tier.as_str().to_string())
913            .or_default() += 1;
914        *entry
915            .outcome_distribution
916            .entry(record.outcome.as_str().to_string())
917            .or_default() += 1;
918    }
919
920    by_agent
921        .into_iter()
922        .map(|(agent, summary)| TrustAgentSummary {
923            agent,
924            total: summary.total,
925            success_rate: if summary.total == 0 {
926                0.0
927            } else {
928                summary.successes as f64 / summary.total as f64
929            },
930            mean_cost_usd: (summary.cost_count > 0)
931                .then_some(summary.cost_sum / summary.cost_count as f64),
932            tier_distribution: summary.tier_distribution,
933            outcome_distribution: summary.outcome_distribution,
934        })
935        .collect()
936}
937
938pub async fn resolve_agent_autonomy_tier(
939    log: &Arc<AnyEventLog>,
940    agent: &str,
941    default: AutonomyTier,
942) -> Result<AutonomyTier, LogError> {
943    let records = query_trust_records(
944        log,
945        &TrustQueryFilters {
946            agent: Some(agent.to_string()),
947            ..TrustQueryFilters::default()
948        },
949    )
950    .await?;
951    let mut current = default;
952    for record in records {
953        if matches!(record.action.as_str(), "trust.promote" | "trust.demote")
954            && record.outcome == TrustOutcome::Success
955        {
956            current = record.autonomy_tier;
957        }
958    }
959    Ok(current)
960}
961
962fn matches_filters(record: &TrustRecord, filters: &TrustQueryFilters) -> bool {
963    if let Some(agent) = filters.agent.as_deref() {
964        if record.agent != agent {
965            return false;
966        }
967    }
968    if let Some(action) = filters.action.as_deref() {
969        if record.action != action {
970            return false;
971        }
972    }
973    if let Some(since) = filters.since {
974        if record.timestamp < since {
975            return false;
976        }
977    }
978    if let Some(until) = filters.until {
979        if record.timestamp > until {
980            return false;
981        }
982    }
983    if let Some(tier) = filters.tier {
984        if record.autonomy_tier != tier {
985            return false;
986        }
987    }
988    if let Some(outcome) = filters.outcome {
989        if record.outcome != outcome {
990            return false;
991        }
992    }
993    true
994}
995
996fn matches_graph_filters(record: &TrustGraphRecord, filters: &TrustQueryFilters) -> bool {
997    if let Some(agent) = filters.agent.as_deref() {
998        if record.actor_id != agent {
999            return false;
1000        }
1001    }
1002    if let Some(action) = filters.action.as_deref() {
1003        if record.action != action {
1004            return false;
1005        }
1006    }
1007    if let Some(since) = filters.since {
1008        if record.timestamp < since {
1009            return false;
1010        }
1011    }
1012    if let Some(until) = filters.until {
1013        if record.timestamp > until {
1014            return false;
1015        }
1016    }
1017    if let Some(tier) = filters.tier {
1018        if record.autonomy_tier_at_time != tier {
1019            return false;
1020        }
1021    }
1022    if let Some(outcome) = filters.outcome {
1023        if record.outcome != outcome {
1024            return false;
1025        }
1026    }
1027    true
1028}
1029
1030fn query_topics(filters: &TrustQueryFilters) -> Result<Vec<Topic>, LogError> {
1031    match filters.agent.as_deref() {
1032        Some(agent) => unique_topics(vec![
1033            topic_for_agent(agent)?,
1034            legacy_topic_for_agent(agent)?,
1035        ]),
1036        None => unique_topics(vec![global_topic()?, legacy_global_topic()?]),
1037    }
1038}
1039
1040fn append_topics_for_record(record: &TrustRecord) -> Result<Vec<Topic>, LogError> {
1041    unique_topics(vec![
1042        global_topic()?,
1043        legacy_global_topic()?,
1044        topic_for_agent(&record.agent)?,
1045        legacy_topic_for_agent(&record.agent)?,
1046    ])
1047}
1048
1049fn unique_topics(topics: Vec<Topic>) -> Result<Vec<Topic>, LogError> {
1050    let mut seen = HashSet::new();
1051    Ok(topics
1052        .into_iter()
1053        .filter(|topic| seen.insert(topic.as_str().to_string()))
1054        .collect())
1055}
1056
1057async fn append_trust_graph_record_projection(
1058    log: &Arc<AnyEventLog>,
1059    record: &TrustRecord,
1060) -> Result<(), LogError> {
1061    let payload = serde_json::to_value(TrustGraphRecord::from_trust_record(record))
1062        .map_err(|error| LogError::Serde(format!("trust graph record encode error: {error}")))?;
1063    let mut headers = BTreeMap::new();
1064    headers.insert("trace_id".to_string(), record.trace_id.clone());
1065    headers.insert("actor_id".to_string(), record.agent.clone());
1066    headers.insert("action".to_string(), record.action.clone());
1067    headers.insert(
1068        "autonomy_tier_at_time".to_string(),
1069        record.autonomy_tier.as_str().to_string(),
1070    );
1071    headers.insert("outcome".to_string(), record.outcome.as_str().to_string());
1072    log.append(
1073        &records_topic()?,
1074        LogEvent::new(TRUST_GRAPH_EVENT_KIND, payload).with_headers(headers),
1075    )
1076    .await?;
1077    Ok(())
1078}
1079
1080async fn finalize_trust_record(
1081    log: &Arc<AnyEventLog>,
1082    mut record: TrustRecord,
1083) -> Result<TrustRecord, LogError> {
1084    attach_current_actor_chain(&mut record);
1085    let latest = latest_chain_record(log).await?;
1086    record.chain_index = latest
1087        .as_ref()
1088        .map(|(_, record)| record.chain_index.saturating_add(1).max(1))
1089        .unwrap_or(1);
1090    record.previous_hash = latest.and_then(|(_, record)| {
1091        if record.entry_hash.is_empty() {
1092            compute_trust_record_hash(&record).ok()
1093        } else {
1094            Some(record.entry_hash)
1095        }
1096    });
1097    record.entry_hash.clear();
1098    record.entry_hash = compute_trust_record_hash(&record)?;
1099    Ok(record)
1100}
1101
1102fn attach_current_actor_chain(record: &mut TrustRecord) {
1103    if record.metadata.contains_key(METADATA_KEY_ACTOR_CHAIN) {
1104        return;
1105    }
1106    if let Some(actor_chain) = crate::agent_sessions::current_actor_chain() {
1107        record.set_actor_chain(Some(actor_chain));
1108    }
1109}
1110
1111async fn latest_chain_record(
1112    log: &Arc<AnyEventLog>,
1113) -> Result<Option<(EventId, TrustRecord)>, LogError> {
1114    let (_, records) = preferred_chain_records(log).await?;
1115    Ok(records.into_iter().last())
1116}
1117
1118async fn preferred_chain_records(
1119    log: &Arc<AnyEventLog>,
1120) -> Result<(Topic, Vec<(EventId, TrustRecord)>), LogError> {
1121    let canonical = global_topic()?;
1122    let canonical_records = read_trust_records_from_topic(log, &canonical).await?;
1123    if !canonical_records.is_empty() {
1124        return Ok((canonical, canonical_records));
1125    }
1126    let legacy = legacy_global_topic()?;
1127    let legacy_records = read_trust_records_from_topic(log, &legacy).await?;
1128    if legacy_records.is_empty() {
1129        Ok((canonical, Vec::new()))
1130    } else {
1131        Ok((legacy, legacy_records))
1132    }
1133}
1134
1135async fn read_trust_records_from_topic(
1136    log: &Arc<AnyEventLog>,
1137    topic: &Topic,
1138) -> Result<Vec<(EventId, TrustRecord)>, LogError> {
1139    let events = log.read_range(topic, None, usize::MAX).await?;
1140    let mut records = Vec::new();
1141    let mut seen = HashSet::new();
1142    for (event_id, event) in events {
1143        if event.kind != TRUST_GRAPH_EVENT_KIND {
1144            continue;
1145        }
1146        let Ok(record) = serde_json::from_value::<TrustRecord>(event.payload) else {
1147            continue;
1148        };
1149        if seen.insert(trust_record_dedupe_key(&record)) {
1150            records.push((event_id, record));
1151        }
1152    }
1153    Ok(records)
1154}
1155
1156fn trust_record_dedupe_key(record: &TrustRecord) -> String {
1157    if !record.entry_hash.is_empty() {
1158        return record.entry_hash.clone();
1159    }
1160    record.record_id.clone()
1161}
1162
1163fn trust_graph_record_dedupe_key(record: &TrustGraphRecord) -> String {
1164    format!(
1165        "{}\u{1f}{}\u{1f}{}\u{1f}{}\u{1f}{}",
1166        record.actor_id,
1167        record.action,
1168        record.trace_id,
1169        record.timestamp,
1170        record.outcome.as_str()
1171    )
1172}
1173
1174fn evidence_refs_from_metadata(
1175    metadata: &BTreeMap<String, serde_json::Value>,
1176) -> Vec<serde_json::Value> {
1177    metadata
1178        .get("evidence_refs")
1179        .or_else(|| metadata.get("evidenceRefs"))
1180        .or_else(|| {
1181            metadata
1182                .get("approval")
1183                .and_then(|approval| approval.get("evidence_refs"))
1184        })
1185        .and_then(|value| value.as_array())
1186        .cloned()
1187        .unwrap_or_default()
1188}
1189
1190fn score_from_records(
1191    agent: &str,
1192    action: Option<&str>,
1193    effective_tier: AutonomyTier,
1194    records: &[TrustRecord],
1195) -> TrustScore {
1196    let mut score = TrustScore {
1197        agent: agent.to_string(),
1198        action: action.map(ToString::to_string),
1199        effective_tier,
1200        ..TrustScore::default()
1201    };
1202    let recent_cutoff = OffsetDateTime::now_utc() - Duration::days(30);
1203    let mut recent_successes = 0;
1204    let mut recent_bad_or_rollback = false;
1205    for record in records {
1206        score.total += 1;
1207        match record.outcome {
1208            TrustOutcome::Success => score.successes += 1,
1209            TrustOutcome::Failure => score.failures += 1,
1210            TrustOutcome::Denied => score.denied += 1,
1211            TrustOutcome::Timeout => score.timeouts += 1,
1212        }
1213        if record.timestamp >= recent_cutoff {
1214            if record.outcome == TrustOutcome::Success && !is_control_plane_action(&record.action) {
1215                recent_successes += 1;
1216            } else if record.outcome != TrustOutcome::Success {
1217                recent_bad_or_rollback = true;
1218            }
1219            if record.action.contains("rollback") {
1220                recent_bad_or_rollback = true;
1221            }
1222        }
1223        score.latest_outcome = Some(record.outcome);
1224        score.latest_timestamp = Some(record.timestamp);
1225    }
1226    score.success_rate = if score.total == 0 {
1227        0.0
1228    } else {
1229        score.successes as f64 / score.total as f64
1230    };
1231    score.policy = policy_from_score(&score, recent_successes, recent_bad_or_rollback);
1232    score
1233}
1234
1235fn policy_from_score(
1236    score: &TrustScore,
1237    recent_successes: u64,
1238    recent_bad_or_rollback: bool,
1239) -> CapabilityPolicy {
1240    let mut policy = policy_for_autonomy_tier(score.effective_tier);
1241    let latest_bad = matches!(
1242        score.latest_outcome,
1243        Some(TrustOutcome::Denied | TrustOutcome::Failure | TrustOutcome::Timeout)
1244    );
1245    let trusted_recent_track_record = score.effective_tier == AutonomyTier::ActWithApproval
1246        && recent_successes >= 10
1247        && !recent_bad_or_rollback;
1248    if latest_bad || (!trusted_recent_track_record && score.total >= 3 && score.success_rate < 0.5)
1249    {
1250        policy.side_effect_level = Some("read_only".to_string());
1251    } else if trusted_recent_track_record {
1252        policy.side_effect_level = Some("network".to_string());
1253    }
1254    policy
1255}
1256
1257pub fn policy_for_autonomy_tier(tier: AutonomyTier) -> CapabilityPolicy {
1258    use crate::tool_annotations::SideEffectLevel;
1259    let level = match tier {
1260        AutonomyTier::Shadow => SideEffectLevel::None,
1261        AutonomyTier::Suggest => SideEffectLevel::ReadOnly,
1262        AutonomyTier::ActWithApproval => SideEffectLevel::ReadOnly,
1263        // Full autonomy carries the outermost ceiling — the TOP of the ladder,
1264        // not a hardcoded level. This must track the ladder so a newly-added
1265        // most-invasive level (e.g. `desktop_control`, added above `network`) is
1266        // not silently capped out of the fully-autonomous tier.
1267        AutonomyTier::ActAuto => SideEffectLevel::MAX,
1268    };
1269    // An autonomy tier bounds how much a handler may *do*, not where it may
1270    // read and write, so this is an overlay on whatever confinement the run
1271    // already has. Built on `default()` it would carry `SandboxProfile::
1272    // Worktree` and confine handlers dispatched from an unsandboxed run.
1273    CapabilityPolicy {
1274        side_effect_level: Some(level.as_str().to_string()),
1275        recursion_limit: matches!(tier, AutonomyTier::Shadow).then_some(0),
1276        ..CapabilityPolicy::neutral()
1277    }
1278}
1279
1280fn apply_record_limit(records: &mut Vec<TrustRecord>, limit: Option<usize>) {
1281    let Some(limit) = limit else {
1282        return;
1283    };
1284    if records.len() <= limit {
1285        return;
1286    }
1287    let keep_from = records.len() - limit;
1288    records.drain(0..keep_from);
1289}
1290
1291fn apply_graph_record_limit(records: &mut Vec<TrustGraphRecord>, limit: Option<usize>) {
1292    let Some(limit) = limit else {
1293        return;
1294    };
1295    if records.len() <= limit {
1296        return;
1297    }
1298    let keep_from = records.len() - limit;
1299    records.drain(0..keep_from);
1300}
1301
1302fn is_control_plane_action(action: &str) -> bool {
1303    matches!(
1304        action,
1305        "trust.promote" | "trust.demote" | "autonomy.tier_transition"
1306    )
1307}
1308
1309#[cfg(test)]
1310mod tests;