Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:01:03.827Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

receipt.rs

410 lines · 15.3 KB · rust
1//! Authority receipt detail for one activity row.
2//!
3//! An allowed decision is never proof that the target broker acted. The
4//! inspector surfaces the authority block only: tool, allowed flag, receipt,
5//! decision, bounded result refs, and refusal reason class when refused.
6
7use serde::{Deserialize, Serialize};
8
9use crate::public_ref::{is_public_safe_ref, sanitize_public_ref, PublicRef};
10
11/// Cap on bounded result references kept on one inspector row.
12pub const MAX_BOUNDED_RESULT_REFS: usize = 8;
13
14/// Allowed vs refused authority decision.
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum AuthorityStatus {
18    Allowed,
19    Refused,
20}
21
22impl AuthorityStatus {
23    pub fn from_allowed_flag(allowed: bool) -> Self {
24        if allowed {
25            Self::Allowed
26        } else {
27            Self::Refused
28        }
29    }
30
31    pub fn is_allowed(self) -> bool {
32        matches!(self, Self::Allowed)
33    }
34
35    pub fn as_label(self) -> &'static str {
36        match self {
37            Self::Allowed => "allowed",
38            Self::Refused => "refused",
39        }
40    }
41}
42
43/// Reason class for a refused authority decision.
44///
45/// Matches the `@openagentsinc/authority` denial reasons plus a reserved
46/// category when the refusal is a reserved-action deny.
47#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "snake_case", tag = "class")]
49pub enum RefusalReasonClass {
50    ProfileInactive,
51    ReservedAction,
52    GrantNotFound,
53    ConditionMissing,
54    ConditionFailed,
55    ProfileInvalid,
56    AuthorityRefused,
57    /// Public-safe unknown class string after sanitization.
58    Other { reason_class: PublicRef },
59}
60
61impl RefusalReasonClass {
62    /// Parse a public-safe reason class token from service/runtime output.
63    pub fn parse(raw: &str) -> Option<Self> {
64        let trimmed = raw.trim();
65        let normalized = trimmed.to_ascii_lowercase().replace('-', "_");
66        match normalized.as_str() {
67            "profile_inactive" => Some(Self::ProfileInactive),
68            "reserved_action" | "reserved" | "needs_owner_reserved_action" => {
69                Some(Self::ReservedAction)
70            }
71            "grant_not_found" => Some(Self::GrantNotFound),
72            "condition_missing" => Some(Self::ConditionMissing),
73            "condition_failed" => Some(Self::ConditionFailed),
74            "profile_invalid" => Some(Self::ProfileInvalid),
75            "authority_refused" | "denied" | "refused" => Some(Self::AuthorityRefused),
76            other => sanitize_public_ref(other).map(|reason_class| Self::Other { reason_class }),
77        }
78    }
79
80    pub fn as_label(&self) -> &str {
81        match self {
82            Self::ProfileInactive => "profile_inactive",
83            Self::ReservedAction => "reserved_action",
84            Self::GrantNotFound => "grant_not_found",
85            Self::ConditionMissing => "condition_missing",
86            Self::ConditionFailed => "condition_failed",
87            Self::ProfileInvalid => "profile_invalid",
88            Self::AuthorityRefused => "authority_refused",
89            Self::Other { reason_class } => reason_class.as_str(),
90        }
91    }
92
93    pub fn is_reserved(&self) -> bool {
94        matches!(self, Self::ReservedAction)
95    }
96}
97
98/// Refusal detail shown when `allowed` is false.
99#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
100pub struct RefusalDetail {
101    pub reason_class: RefusalReasonClass,
102    /// Present when the refusal maps to a reserved category id.
103    #[serde(default, skip_serializing_if = "Option::is_none")]
104    pub reserved_category: Option<PublicRef>,
105}
106
107impl RefusalDetail {
108    pub fn new(reason_class: RefusalReasonClass, reserved_category: Option<PublicRef>) -> Self {
109        // Only surface reserved_category when the reason class is reserved.
110        let reserved_category = if reason_class.is_reserved() {
111            reserved_category
112        } else {
113            None
114        };
115        Self {
116            reason_class,
117            reserved_category,
118        }
119    }
120}
121
122/// Public-safe details for one activity/receipt row.
123#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
124pub struct AuthorityReceiptDetail {
125    pub tool_ref: PublicRef,
126    pub allowed: bool,
127    pub authority_receipt_ref: PublicRef,
128    pub decision_ref: PublicRef,
129    /// Primary bounded result reference (first safe result ref, if any).
130    #[serde(default, skip_serializing_if = "Option::is_none")]
131    pub bounded_result_ref: Option<PublicRef>,
132    /// Additional bounded result references (already capped and redacted).
133    #[serde(default, skip_serializing_if = "Vec::is_empty")]
134    pub bounded_result_refs: Vec<PublicRef>,
135    #[serde(default, skip_serializing_if = "Option::is_none")]
136    pub refusal: Option<RefusalDetail>,
137}
138
139impl AuthorityReceiptDetail {
140    pub fn status(&self) -> AuthorityStatus {
141        AuthorityStatus::from_allowed_flag(self.allowed)
142    }
143
144    /// Build a detail view from raw service fields, redacting unsafe values.
145    ///
146    /// Returns `None` when the required public refs cannot be sanitized.
147    pub fn from_raw(input: RawAuthorityBlock<'_>) -> Option<Self> {
148        let tool_ref = sanitize_public_ref(input.tool_ref)?;
149        let authority_receipt_ref = sanitize_public_ref(input.authority_receipt_ref)?;
150        let decision_ref = sanitize_public_ref(input.decision_ref)?;
151
152        let mut bounded_result_refs: Vec<PublicRef> = input
153            .result_refs
154            .iter()
155            .filter_map(|r| sanitize_public_ref(r))
156            .take(MAX_BOUNDED_RESULT_REFS)
157            .collect();
158        // Prefer an explicit primary when provided and safe.
159        if let Some(primary) = input.bounded_result_ref.and_then(sanitize_public_ref) {
160            bounded_result_refs.retain(|r| r != &primary);
161            bounded_result_refs.insert(0, primary);
162            if bounded_result_refs.len() > MAX_BOUNDED_RESULT_REFS {
163                bounded_result_refs.truncate(MAX_BOUNDED_RESULT_REFS);
164            }
165        }
166        let bounded_result_ref = bounded_result_refs.first().cloned();
167
168        let refusal = if input.allowed {
169            None
170        } else {
171            let reason_class = input
172                .refusal_reason
173                .and_then(RefusalReasonClass::parse)
174                .or_else(|| {
175                    // Derive reserved from blocker / category hints.
176                    if input
177                        .reserved_category
178                        .is_some_and(|c| is_public_safe_ref(c) || c.starts_with("reserved."))
179                    {
180                        Some(RefusalReasonClass::ReservedAction)
181                    } else {
182                        Some(RefusalReasonClass::AuthorityRefused)
183                    }
184                })
185                .unwrap_or(RefusalReasonClass::AuthorityRefused);
186            let reserved_category = input
187                .reserved_category
188                .and_then(sanitize_public_ref)
189                .or_else(|| {
190                    input
191                        .result_refs
192                        .iter()
193                        .find_map(|r| {
194                            let s = r.trim();
195                            if s.starts_with("reserved.") {
196                                sanitize_public_ref(s)
197                            } else {
198                                None
199                            }
200                        })
201                });
202            Some(RefusalDetail::new(reason_class, reserved_category))
203        };
204
205        Some(Self {
206            tool_ref,
207            allowed: input.allowed,
208            authority_receipt_ref,
209            decision_ref,
210            bounded_result_ref,
211            bounded_result_refs,
212            refusal,
213        })
214    }
215}
216
217/// Raw authority block fields as they arrive from the service projection.
218///
219/// Callers pass references only; this type never owns secrets and drops any
220/// field that fails public-safety checks during projection.
221#[derive(Clone, Debug)]
222pub struct RawAuthorityBlock<'a> {
223    pub tool_ref: &'a str,
224    pub allowed: bool,
225    pub authority_receipt_ref: &'a str,
226    pub decision_ref: &'a str,
227    pub result_refs: &'a [&'a str],
228    pub bounded_result_ref: Option<&'a str>,
229    pub refusal_reason: Option<&'a str>,
230    pub reserved_category: Option<&'a str>,
231}
232
233/// JSON-facing authority block (service event payload subset).
234#[derive(Clone, Debug, Default, Serialize, Deserialize)]
235#[serde(rename_all = "camelCase")]
236pub struct AuthorityBlockJson {
237    #[serde(default, alias = "tool_ref")]
238    pub tool_ref: Option<String>,
239    #[serde(default)]
240    pub allowed: Option<bool>,
241    #[serde(
242        default,
243        alias = "authority_receipt_ref",
244        alias = "authorityRef",
245        alias = "authority_ref"
246    )]
247    pub authority_receipt_ref: Option<String>,
248    #[serde(default, alias = "decision_ref")]
249    pub decision_ref: Option<String>,
250    #[serde(default, alias = "result_refs", alias = "resultRefs")]
251    pub result_refs: Vec<String>,
252    #[serde(
253        default,
254        alias = "bounded_result_ref",
255        alias = "boundedResultRef"
256    )]
257    pub bounded_result_ref: Option<String>,
258    #[serde(
259        default,
260        alias = "refusal_reason",
261        alias = "refusalReason",
262        alias = "reason"
263    )]
264    pub refusal_reason: Option<String>,
265    #[serde(
266        default,
267        alias = "reserved_category",
268        alias = "reservedCategory"
269    )]
270    pub reserved_category: Option<String>,
271    // Unknown keys (content, token, rawOutput, paths, etc.) are ignored by
272    // serde's default and never become inspector fields.
273}
274
275impl AuthorityBlockJson {
276    /// Project into a public-safe receipt detail, dropping unsafe fields.
277    pub fn project(&self) -> Option<AuthorityReceiptDetail> {
278        let tool_ref = self.tool_ref.as_deref()?;
279        let authority_receipt_ref = self.authority_receipt_ref.as_deref()?;
280        let decision_ref = self.decision_ref.as_deref()?;
281        let result_ref_stores: Vec<&str> = self.result_refs.iter().map(String::as_str).collect();
282        AuthorityReceiptDetail::from_raw(RawAuthorityBlock {
283            tool_ref,
284            allowed: self.allowed.unwrap_or(true),
285            authority_receipt_ref,
286            decision_ref,
287            result_refs: &result_ref_stores,
288            bounded_result_ref: self.bounded_result_ref.as_deref(),
289            refusal_reason: self.refusal_reason.as_deref(),
290            reserved_category: self.reserved_category.as_deref(),
291        })
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298
299    #[test]
300    fn allowed_receipt_has_no_refusal() {
301        let detail = AuthorityReceiptDetail::from_raw(RawAuthorityBlock {
302            tool_ref: "tool.sarah.list_full_auto_runs",
303            allowed: true,
304            authority_receipt_ref: "receipt.authority.sarah.tool.abc.list",
305            decision_ref: "decision.sarah.tool.call-1",
306            result_refs: &["result.full_auto.run.r1"],
307            bounded_result_ref: None,
308            refusal_reason: Some("should_be_ignored"),
309            reserved_category: Some("reserved.secret_export"),
310        })
311        .expect("valid allowed receipt");
312        assert!(detail.allowed);
313        assert!(detail.refusal.is_none());
314        assert_eq!(
315            detail.bounded_result_ref.as_ref().map(PublicRef::as_str),
316            Some("result.full_auto.run.r1")
317        );
318    }
319
320    #[test]
321    fn refused_receipt_exposes_reason_class_and_reserved_category() {
322        let detail = AuthorityReceiptDetail::from_raw(RawAuthorityBlock {
323            tool_ref: "tool.sarah.move_financial_value",
324            allowed: false,
325            authority_receipt_ref: "receipt.authority.sarah.tool.abc.refuse",
326            decision_ref: "decision.sarah.tool.call-2",
327            result_refs: &["blocker.sarah.authority_refused"],
328            bounded_result_ref: None,
329            refusal_reason: Some("reserved_action"),
330            reserved_category: Some("reserved.financial_custody"),
331        })
332        .expect("valid refused receipt");
333        assert!(!detail.allowed);
334        let refusal = detail.refusal.expect("refusal present");
335        assert_eq!(refusal.reason_class.as_label(), "reserved_action");
336        assert_eq!(
337            refusal.reserved_category.as_ref().map(PublicRef::as_str),
338            Some("reserved.financial_custody")
339        );
340    }
341
342    #[test]
343    fn redacts_unsafe_result_refs_and_required_fields() {
344        let detail = AuthorityReceiptDetail::from_raw(RawAuthorityBlock {
345            tool_ref: "tool.sarah.read_release_state",
346            allowed: true,
347            authority_receipt_ref: "receipt.authority.sarah.tool.safe",
348            decision_ref: "decision.sarah.tool.call-3",
349            result_refs: &[
350                "result.release.state.ok",
351                "/Users/owner/.codex/auth.json",
352                "sk-ant-api03-SECRETVALUE0000000000",
353                "result.with spaces is bad",
354            ],
355            bounded_result_ref: Some("Bearer eyJhbGciOiJIUzI1NiJ9"),
356            refusal_reason: None,
357            reserved_category: None,
358        })
359        .expect("required refs valid");
360        assert_eq!(detail.bounded_result_refs.len(), 1);
361        assert_eq!(
362            detail.bounded_result_ref.as_ref().map(PublicRef::as_str),
363            Some("result.release.state.ok")
364        );
365    }
366
367    #[test]
368    fn rejects_when_required_ref_is_unsafe() {
369        assert!(
370            AuthorityReceiptDetail::from_raw(RawAuthorityBlock {
371                tool_ref: "/tmp/evil",
372                allowed: true,
373                authority_receipt_ref: "receipt.authority.sarah.tool.safe",
374                decision_ref: "decision.sarah.tool.call-4",
375                result_refs: &[],
376                bounded_result_ref: None,
377                refusal_reason: None,
378                reserved_category: None,
379            })
380            .is_none()
381        );
382    }
383
384    #[test]
385    fn json_projection_drops_unsafe_payload_fields() {
386        let json = serde_json::json!({
387            "toolRef": "tool.sarah.control_full_auto",
388            "allowed": false,
389            "authorityReceiptRef": "receipt.authority.sarah.tool.ctrl",
390            "decisionRef": "decision.sarah.tool.call-5",
391            "resultRefs": ["blocker.sarah.authority_refused"],
392            "refusalReason": "reserved_action",
393            "reservedCategory": "reserved.stable_release_without_direction",
394            "content": "full tool dump with secrets sk-abc",
395            "token": "OPENAGENTS_AGENT_TOKEN=should-not-appear",
396            "rawOutput": {"path": "/Users/owner/private"}
397        });
398        let block: AuthorityBlockJson = serde_json::from_value(json).expect("parse");
399        let detail = block.project().expect("project");
400        let encoded = serde_json::to_string(&detail).expect("encode");
401        assert!(!encoded.contains("sk-abc"));
402        assert!(!encoded.contains("OPENAGENTS_AGENT_TOKEN"));
403        assert!(!encoded.contains("/Users/owner"));
404        assert!(!encoded.contains("full tool dump"));
405        assert!(encoded.contains("reserved_action"));
406        assert!(encoded.contains("reserved.stable_release_without_direction"));
407        assert!(!detail.allowed);
408    }
409}
410
Served at tenant.openagents/omega Member data and write actions are omitted.