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 = 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 AutonomyTier::ActAuto => SideEffectLevel::MAX,
1268 };
1269 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;