agora_agentkit/govlog/
record.rs1use serde::{Deserialize, Serialize};
18
19use crate::ids::GovernanceLogId;
20
21pub const STEWARD_RECORD_VERSION: u32 = 1;
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
31#[cfg_attr(feature = "schemars", schemars(inline))]
32pub struct StewardRecord {
33 pub agora_steward_record: u32,
35 pub kind: String,
39 pub title: String,
40 pub body: String,
42 #[serde(default, skip_serializing_if = "Vec::is_empty")]
44 pub concerns: Vec<GovernanceLogId>,
45 #[serde(default, skip_serializing_if = "Vec::is_empty")]
47 pub participants: Vec<RecordParticipant>,
48 #[serde(default, skip_serializing_if = "Vec::is_empty")]
52 pub attachments: Vec<RecordAttachment>,
53}
54
55impl StewardRecord {
56 pub fn new(
58 kind: impl Into<String>,
59 title: impl Into<String>,
60 body: impl Into<String>,
61 ) -> Self {
62 Self {
63 agora_steward_record: STEWARD_RECORD_VERSION,
64 kind: kind.into(),
65 title: title.into(),
66 body: body.into(),
67 concerns: Vec::new(),
68 participants: Vec::new(),
69 attachments: Vec::new(),
70 }
71 }
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
76#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
77#[cfg_attr(feature = "schemars", schemars(inline))]
78pub struct RecordParticipant {
79 pub name: String,
80 pub role: String,
82 pub attests: String,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
90#[cfg_attr(feature = "schemars", schemars(inline))]
91pub struct RecordAttachment {
92 pub name: String,
93 pub note: String,
95 pub content: String,
96}
97
98#[cfg(test)]
99mod tests {
100 use super::super::{Blind, blind_data, non_integer_number, redact_data};
101 use super::*;
102
103 fn ceremony() -> StewardRecord {
104 let mut record = StewardRecord::new(
105 "key_ceremony",
106 "The first key ceremony",
107 "The root certified a fresh online key.",
108 );
109 record.concerns = vec!["KEY-2026-0001".parse().unwrap()];
110 record.participants = vec![RecordParticipant {
111 name: "A. Steward".into(),
112 role: "Steward".into(),
113 attests: "held the key".into(),
114 }];
115 record.attachments = vec![RecordAttachment {
116 name: "attestation.pem".into(),
117 note: "chains to the vendor's root".into(),
118 content: "-----BEGIN CERTIFICATE-----\n…".into(),
119 }];
120 record
121 }
122
123 #[test]
124 fn a_record_round_trips_and_holds_no_fractions() {
125 let record = ceremony();
126 let value = serde_json::to_value(&record).unwrap();
127 assert_eq!(value["agora_steward_record"], 1);
128 assert_eq!(non_integer_number(&value), None);
129 let back: StewardRecord = serde_json::from_value(value).unwrap();
130 assert_eq!(back, record);
131 }
132
133 #[test]
134 fn an_empty_list_is_not_written() {
135 let value =
136 serde_json::to_value(StewardRecord::new("restore", "t", "b"))
137 .unwrap();
138 let keys: Vec<&str> = value
139 .as_object()
140 .unwrap()
141 .keys()
142 .map(String::as_str)
143 .collect();
144 assert_eq!(keys, ["agora_steward_record", "body", "kind", "title"]);
145 }
146
147 #[test]
150 fn a_stored_and_redacted_record_still_reads() {
151 let stored = blind_data(
152 &serde_json::to_value(ceremony()).unwrap(),
153 Blind::from([7; 32]),
154 )
155 .unwrap();
156 let read: StewardRecord =
157 serde_json::from_value(stored.clone()).unwrap();
158 assert_eq!(read, ceremony());
159
160 let amendment: GovernanceLogId = "AMD-2026-0009".parse().unwrap();
161 let redacted = redact_data(
162 &stored,
163 &["/participants/0/name".to_string()],
164 &amendment,
165 Blind::from([8; 32]),
166 )
167 .unwrap();
168 let read: StewardRecord = serde_json::from_value(redacted).unwrap();
169 assert_eq!(read.participants[0].name, "[redacted by AMD-2026-0009]");
170 assert_eq!(read.participants[0].role, "Steward");
171 }
172}