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";
17pub const OPENTRUSTGRAPH_SCHEMA_V0_1: &str = "opentrustgraph/v0.1";
25pub const OPENTRUSTGRAPH_ACCEPTED_SCHEMAS: &[&str] =
27 &[OPENTRUSTGRAPH_SCHEMA_V0_1, OPENTRUSTGRAPH_SCHEMA_V0];
28pub const OPENTRUSTGRAPH_CHAIN_SCHEMA_V0: &str = "opentrustgraph-chain/v0";
29
30pub const METADATA_KEY_EFFECTS_GRANT: &str = "effects_grant";
33pub const METADATA_KEY_EFFECTS_USED: &str = "effects_used";
36pub const METADATA_KEY_PARENT_RECORD_ID: &str = "parent_record_id";
40pub const METADATA_KEY_ACTOR_CHAIN: &str = "actor_chain";
44pub 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 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 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 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 pub fn with_actor_chain(mut self, actor_chain: ActorChain) -> Self {
274 self.set_actor_chain(Some(actor_chain));
275 self
276 }
277
278 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 pub fn actor_chain(&self) -> Option<ActorChain> {
296 self.try_actor_chain().ok().flatten()
297 }
298
299 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 = crate::canonical_json::to_string(&value);
731 let digest = Sha256::digest(canonical.as_bytes());
732 Ok(format!("sha256:{}", hex::encode(digest)))
733}
734
735struct LineageInvariantError {
736 event_id: Option<EventId>,
737 message: String,
738}
739
740impl LineageInvariantError {
741 fn new(event_id: Option<EventId>, message: String) -> Self {
742 Self { event_id, message }
743 }
744}
745
746fn validate_lineage_invariants<'a, I>(records: I) -> Vec<LineageInvariantError>
747where
748 I: IntoIterator<Item = (String, Option<EventId>, &'a TrustRecord)>,
749{
750 let mut errors = Vec::new();
751 let mut by_id: HashMap<&'a str, &'a TrustRecord> = HashMap::new();
752
753 for (label, event_id, record) in records {
754 let actor_chain = match record.try_actor_chain() {
755 Ok(actor_chain) => actor_chain,
756 Err(error) => {
757 errors.push(LineageInvariantError::new(
758 event_id,
759 format!("{label}: actor_chain invalid: {error}"),
760 ));
761 None
762 }
763 };
764 let effects_used = record.effects_used();
765 if let Some(parent_id) = record.parent_record_id() {
766 let parent = by_id.get(parent_id.as_str()).copied();
767 if parent.is_none() && (!effects_used.is_empty() || actor_chain.is_some()) {
768 errors.push(LineageInvariantError::new(
769 event_id,
770 format!("{label}: parent_record_id {parent_id:?} not found in chain"),
771 ));
772 }
773 if let Some(parent) = parent {
774 validate_effect_lineage(
775 &mut errors,
776 &label,
777 event_id,
778 &parent_id,
779 parent,
780 &effects_used,
781 );
782 validate_actor_lineage(
783 &mut errors,
784 &label,
785 event_id,
786 &parent_id,
787 parent,
788 actor_chain,
789 );
790 }
791 }
792
793 if !record.record_id.is_empty() {
794 by_id.insert(record.record_id.as_str(), record);
795 }
796 }
797
798 errors
799}
800
801fn validate_effect_lineage(
802 errors: &mut Vec<LineageInvariantError>,
803 label: &str,
804 event_id: Option<EventId>,
805 parent_id: &str,
806 parent: &TrustRecord,
807 effects_used: &[EffectRecord],
808) {
809 if effects_used.is_empty() {
810 return;
811 }
812 let parent_grant = parent.effects_grant();
813 for effect in effects_used {
814 if !parent_grant.contains(effect) {
815 errors.push(LineageInvariantError::new(
816 event_id,
817 format!(
818 "{label}: effects_used escaped grant from parent {parent_id:?}: {effect:?}"
819 ),
820 ));
821 }
822 }
823}
824
825fn validate_actor_lineage(
826 errors: &mut Vec<LineageInvariantError>,
827 label: &str,
828 event_id: Option<EventId>,
829 parent_id: &str,
830 parent: &TrustRecord,
831 actor_chain: Option<ActorChain>,
832) {
833 let Some(actor_chain) = actor_chain else {
834 return;
835 };
836 let parent_actor_chain = match parent.try_actor_chain() {
837 Ok(Some(parent_actor_chain)) => parent_actor_chain,
838 Ok(None) => {
839 errors.push(LineageInvariantError::new(
840 event_id,
841 format!("{label}: actor_chain parent {parent_id:?} missing actor_chain"),
842 ));
843 return;
844 }
845 Err(error) => {
846 errors.push(LineageInvariantError::new(
847 event_id,
848 format!("{label}: parent actor_chain invalid: {error}"),
849 ));
850 return;
851 }
852 };
853 if !actor_chain_extends_parent(&actor_chain, &parent_actor_chain) {
854 errors.push(LineageInvariantError::new(
855 event_id,
856 format!("{label}: actor_chain escaped parentage from parent {parent_id:?}"),
857 ));
858 }
859}
860
861fn actor_chain_extends_parent(child: &ActorChain, parent: &ActorChain) -> bool {
862 if child.origin() != parent.origin() {
863 return false;
864 }
865 let child_actors: Vec<&str> = child.actors().collect();
866 let parent_actors: Vec<&str> = parent.actors().collect();
867 child_actors.len() == parent_actors.len() + 1 && child_actors[1..] == parent_actors[..]
868}
869
870pub fn group_trust_records_by_trace(records: &[TrustRecord]) -> Vec<TrustTraceGroup> {
871 let mut groups: Vec<TrustTraceGroup> = Vec::new();
872 let mut positions: HashMap<String, usize> = HashMap::new();
873 for record in records {
874 if let Some(index) = positions.get(record.trace_id.as_str()).copied() {
875 groups[index].records.push(record.clone());
876 continue;
877 }
878 positions.insert(record.trace_id.clone(), groups.len());
879 groups.push(TrustTraceGroup {
880 trace_id: record.trace_id.clone(),
881 records: vec![record.clone()],
882 });
883 }
884 groups
885}
886
887pub fn summarize_trust_records(records: &[TrustRecord]) -> Vec<TrustAgentSummary> {
888 #[derive(Default)]
889 struct RunningSummary {
890 total: u64,
891 successes: u64,
892 cost_sum: f64,
893 cost_count: u64,
894 tier_distribution: BTreeMap<String, u64>,
895 outcome_distribution: BTreeMap<String, u64>,
896 }
897
898 let mut by_agent: BTreeMap<String, RunningSummary> = BTreeMap::new();
899 for record in records {
900 let entry = by_agent.entry(record.agent.clone()).or_default();
901 entry.total += 1;
902 if record.outcome == TrustOutcome::Success {
903 entry.successes += 1;
904 }
905 if let Some(cost_usd) = record.cost_usd {
906 entry.cost_sum += cost_usd;
907 entry.cost_count += 1;
908 }
909 *entry
910 .tier_distribution
911 .entry(record.autonomy_tier.as_str().to_string())
912 .or_default() += 1;
913 *entry
914 .outcome_distribution
915 .entry(record.outcome.as_str().to_string())
916 .or_default() += 1;
917 }
918
919 by_agent
920 .into_iter()
921 .map(|(agent, summary)| TrustAgentSummary {
922 agent,
923 total: summary.total,
924 success_rate: if summary.total == 0 {
925 0.0
926 } else {
927 summary.successes as f64 / summary.total as f64
928 },
929 mean_cost_usd: (summary.cost_count > 0)
930 .then_some(summary.cost_sum / summary.cost_count as f64),
931 tier_distribution: summary.tier_distribution,
932 outcome_distribution: summary.outcome_distribution,
933 })
934 .collect()
935}
936
937pub async fn resolve_agent_autonomy_tier(
938 log: &Arc<AnyEventLog>,
939 agent: &str,
940 default: AutonomyTier,
941) -> Result<AutonomyTier, LogError> {
942 let records = query_trust_records(
943 log,
944 &TrustQueryFilters {
945 agent: Some(agent.to_string()),
946 ..TrustQueryFilters::default()
947 },
948 )
949 .await?;
950 let mut current = default;
951 for record in records {
952 if matches!(record.action.as_str(), "trust.promote" | "trust.demote")
953 && record.outcome == TrustOutcome::Success
954 {
955 current = record.autonomy_tier;
956 }
957 }
958 Ok(current)
959}
960
961fn matches_filters(record: &TrustRecord, filters: &TrustQueryFilters) -> bool {
962 if let Some(agent) = filters.agent.as_deref() {
963 if record.agent != agent {
964 return false;
965 }
966 }
967 if let Some(action) = filters.action.as_deref() {
968 if record.action != action {
969 return false;
970 }
971 }
972 if let Some(since) = filters.since {
973 if record.timestamp < since {
974 return false;
975 }
976 }
977 if let Some(until) = filters.until {
978 if record.timestamp > until {
979 return false;
980 }
981 }
982 if let Some(tier) = filters.tier {
983 if record.autonomy_tier != tier {
984 return false;
985 }
986 }
987 if let Some(outcome) = filters.outcome {
988 if record.outcome != outcome {
989 return false;
990 }
991 }
992 true
993}
994
995fn matches_graph_filters(record: &TrustGraphRecord, filters: &TrustQueryFilters) -> bool {
996 if let Some(agent) = filters.agent.as_deref() {
997 if record.actor_id != agent {
998 return false;
999 }
1000 }
1001 if let Some(action) = filters.action.as_deref() {
1002 if record.action != action {
1003 return false;
1004 }
1005 }
1006 if let Some(since) = filters.since {
1007 if record.timestamp < since {
1008 return false;
1009 }
1010 }
1011 if let Some(until) = filters.until {
1012 if record.timestamp > until {
1013 return false;
1014 }
1015 }
1016 if let Some(tier) = filters.tier {
1017 if record.autonomy_tier_at_time != tier {
1018 return false;
1019 }
1020 }
1021 if let Some(outcome) = filters.outcome {
1022 if record.outcome != outcome {
1023 return false;
1024 }
1025 }
1026 true
1027}
1028
1029fn query_topics(filters: &TrustQueryFilters) -> Result<Vec<Topic>, LogError> {
1030 match filters.agent.as_deref() {
1031 Some(agent) => unique_topics(vec![
1032 topic_for_agent(agent)?,
1033 legacy_topic_for_agent(agent)?,
1034 ]),
1035 None => unique_topics(vec![global_topic()?, legacy_global_topic()?]),
1036 }
1037}
1038
1039fn append_topics_for_record(record: &TrustRecord) -> Result<Vec<Topic>, LogError> {
1040 unique_topics(vec![
1041 global_topic()?,
1042 legacy_global_topic()?,
1043 topic_for_agent(&record.agent)?,
1044 legacy_topic_for_agent(&record.agent)?,
1045 ])
1046}
1047
1048fn unique_topics(topics: Vec<Topic>) -> Result<Vec<Topic>, LogError> {
1049 let mut seen = HashSet::new();
1050 Ok(topics
1051 .into_iter()
1052 .filter(|topic| seen.insert(topic.as_str().to_string()))
1053 .collect())
1054}
1055
1056async fn append_trust_graph_record_projection(
1057 log: &Arc<AnyEventLog>,
1058 record: &TrustRecord,
1059) -> Result<(), LogError> {
1060 let payload = serde_json::to_value(TrustGraphRecord::from_trust_record(record))
1061 .map_err(|error| LogError::Serde(format!("trust graph record encode error: {error}")))?;
1062 let mut headers = BTreeMap::new();
1063 headers.insert("trace_id".to_string(), record.trace_id.clone());
1064 headers.insert("actor_id".to_string(), record.agent.clone());
1065 headers.insert("action".to_string(), record.action.clone());
1066 headers.insert(
1067 "autonomy_tier_at_time".to_string(),
1068 record.autonomy_tier.as_str().to_string(),
1069 );
1070 headers.insert("outcome".to_string(), record.outcome.as_str().to_string());
1071 log.append(
1072 &records_topic()?,
1073 LogEvent::new(TRUST_GRAPH_EVENT_KIND, payload).with_headers(headers),
1074 )
1075 .await?;
1076 Ok(())
1077}
1078
1079async fn finalize_trust_record(
1080 log: &Arc<AnyEventLog>,
1081 mut record: TrustRecord,
1082) -> Result<TrustRecord, LogError> {
1083 attach_current_actor_chain(&mut record);
1084 let latest = latest_chain_record(log).await?;
1085 record.chain_index = latest
1086 .as_ref()
1087 .map(|(_, record)| record.chain_index.saturating_add(1).max(1))
1088 .unwrap_or(1);
1089 record.previous_hash = latest.and_then(|(_, record)| {
1090 if record.entry_hash.is_empty() {
1091 compute_trust_record_hash(&record).ok()
1092 } else {
1093 Some(record.entry_hash)
1094 }
1095 });
1096 record.entry_hash.clear();
1097 record.entry_hash = compute_trust_record_hash(&record)?;
1098 Ok(record)
1099}
1100
1101fn attach_current_actor_chain(record: &mut TrustRecord) {
1102 if record.metadata.contains_key(METADATA_KEY_ACTOR_CHAIN) {
1103 return;
1104 }
1105 if let Some(actor_chain) = crate::agent_sessions::current_actor_chain() {
1106 record.set_actor_chain(Some(actor_chain));
1107 }
1108}
1109
1110async fn latest_chain_record(
1111 log: &Arc<AnyEventLog>,
1112) -> Result<Option<(EventId, TrustRecord)>, LogError> {
1113 let (_, records) = preferred_chain_records(log).await?;
1114 Ok(records.into_iter().last())
1115}
1116
1117async fn preferred_chain_records(
1118 log: &Arc<AnyEventLog>,
1119) -> Result<(Topic, Vec<(EventId, TrustRecord)>), LogError> {
1120 let canonical = global_topic()?;
1121 let canonical_records = read_trust_records_from_topic(log, &canonical).await?;
1122 if !canonical_records.is_empty() {
1123 return Ok((canonical, canonical_records));
1124 }
1125 let legacy = legacy_global_topic()?;
1126 let legacy_records = read_trust_records_from_topic(log, &legacy).await?;
1127 if legacy_records.is_empty() {
1128 Ok((canonical, Vec::new()))
1129 } else {
1130 Ok((legacy, legacy_records))
1131 }
1132}
1133
1134async fn read_trust_records_from_topic(
1135 log: &Arc<AnyEventLog>,
1136 topic: &Topic,
1137) -> Result<Vec<(EventId, TrustRecord)>, LogError> {
1138 let events = log.read_range(topic, None, usize::MAX).await?;
1139 let mut records = Vec::new();
1140 let mut seen = HashSet::new();
1141 for (event_id, event) in events {
1142 if event.kind != TRUST_GRAPH_EVENT_KIND {
1143 continue;
1144 }
1145 let Ok(record) = serde_json::from_value::<TrustRecord>(event.payload) else {
1146 continue;
1147 };
1148 if seen.insert(trust_record_dedupe_key(&record)) {
1149 records.push((event_id, record));
1150 }
1151 }
1152 Ok(records)
1153}
1154
1155fn trust_record_dedupe_key(record: &TrustRecord) -> String {
1156 if !record.entry_hash.is_empty() {
1157 return record.entry_hash.clone();
1158 }
1159 record.record_id.clone()
1160}
1161
1162fn trust_graph_record_dedupe_key(record: &TrustGraphRecord) -> String {
1163 format!(
1164 "{}\u{1f}{}\u{1f}{}\u{1f}{}\u{1f}{}",
1165 record.actor_id,
1166 record.action,
1167 record.trace_id,
1168 record.timestamp,
1169 record.outcome.as_str()
1170 )
1171}
1172
1173fn evidence_refs_from_metadata(
1174 metadata: &BTreeMap<String, serde_json::Value>,
1175) -> Vec<serde_json::Value> {
1176 metadata
1177 .get("evidence_refs")
1178 .or_else(|| metadata.get("evidenceRefs"))
1179 .or_else(|| {
1180 metadata
1181 .get("approval")
1182 .and_then(|approval| approval.get("evidence_refs"))
1183 })
1184 .and_then(|value| value.as_array())
1185 .cloned()
1186 .unwrap_or_default()
1187}
1188
1189fn score_from_records(
1190 agent: &str,
1191 action: Option<&str>,
1192 effective_tier: AutonomyTier,
1193 records: &[TrustRecord],
1194) -> TrustScore {
1195 let mut score = TrustScore {
1196 agent: agent.to_string(),
1197 action: action.map(ToString::to_string),
1198 effective_tier,
1199 ..TrustScore::default()
1200 };
1201 let recent_cutoff = OffsetDateTime::now_utc() - Duration::days(30);
1202 let mut recent_successes = 0;
1203 let mut recent_bad_or_rollback = false;
1204 for record in records {
1205 score.total += 1;
1206 match record.outcome {
1207 TrustOutcome::Success => score.successes += 1,
1208 TrustOutcome::Failure => score.failures += 1,
1209 TrustOutcome::Denied => score.denied += 1,
1210 TrustOutcome::Timeout => score.timeouts += 1,
1211 }
1212 if record.timestamp >= recent_cutoff {
1213 if record.outcome == TrustOutcome::Success && !is_control_plane_action(&record.action) {
1214 recent_successes += 1;
1215 } else if record.outcome != TrustOutcome::Success {
1216 recent_bad_or_rollback = true;
1217 }
1218 if record.action.contains("rollback") {
1219 recent_bad_or_rollback = true;
1220 }
1221 }
1222 score.latest_outcome = Some(record.outcome);
1223 score.latest_timestamp = Some(record.timestamp);
1224 }
1225 score.success_rate = if score.total == 0 {
1226 0.0
1227 } else {
1228 score.successes as f64 / score.total as f64
1229 };
1230 score.policy = policy_from_score(&score, recent_successes, recent_bad_or_rollback);
1231 score
1232}
1233
1234fn policy_from_score(
1235 score: &TrustScore,
1236 recent_successes: u64,
1237 recent_bad_or_rollback: bool,
1238) -> CapabilityPolicy {
1239 let mut policy = policy_for_autonomy_tier(score.effective_tier);
1240 let latest_bad = matches!(
1241 score.latest_outcome,
1242 Some(TrustOutcome::Denied | TrustOutcome::Failure | TrustOutcome::Timeout)
1243 );
1244 let trusted_recent_track_record = score.effective_tier == AutonomyTier::ActWithApproval
1245 && recent_successes >= 10
1246 && !recent_bad_or_rollback;
1247 if latest_bad || (!trusted_recent_track_record && score.total >= 3 && score.success_rate < 0.5)
1248 {
1249 policy.side_effect_level = Some("read_only".to_string());
1250 } else if trusted_recent_track_record {
1251 policy.side_effect_level = Some("network".to_string());
1252 }
1253 policy
1254}
1255
1256pub fn policy_for_autonomy_tier(tier: AutonomyTier) -> CapabilityPolicy {
1257 use crate::tool_annotations::SideEffectLevel;
1258 let level = match tier {
1259 AutonomyTier::Shadow => SideEffectLevel::ReadOnly,
1264 AutonomyTier::Suggest => SideEffectLevel::ReadOnly,
1265 AutonomyTier::ActWithApproval => SideEffectLevel::ReadOnly,
1266 AutonomyTier::ActAuto => SideEffectLevel::MAX,
1271 };
1272 CapabilityPolicy {
1277 side_effect_level: Some(level.as_str().to_string()),
1278 recursion_limit: matches!(tier, AutonomyTier::Shadow).then_some(0),
1279 ..CapabilityPolicy::neutral()
1280 }
1281}
1282
1283fn apply_record_limit(records: &mut Vec<TrustRecord>, limit: Option<usize>) {
1284 let Some(limit) = limit else {
1285 return;
1286 };
1287 if records.len() <= limit {
1288 return;
1289 }
1290 let keep_from = records.len() - limit;
1291 records.drain(0..keep_from);
1292}
1293
1294fn apply_graph_record_limit(records: &mut Vec<TrustGraphRecord>, limit: Option<usize>) {
1295 let Some(limit) = limit else {
1296 return;
1297 };
1298 if records.len() <= limit {
1299 return;
1300 }
1301 let keep_from = records.len() - limit;
1302 records.drain(0..keep_from);
1303}
1304
1305fn is_control_plane_action(action: &str) -> bool {
1306 matches!(
1307 action,
1308 "trust.promote" | "trust.demote" | "autonomy.tier_transition"
1309 )
1310}
1311
1312#[cfg(test)]
1313mod tests;