Skip to repository content212 lines · 7.0 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T03:10:58.021Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
public_ref.rs
1//! Public-safe reference validation and redaction for the receipt inspector.
2//!
3//! Only bounded, opaque reference strings may reach the interface or a log.
4//! Raw tokens, credentials, private filesystem paths, and unbounded prose are
5//! rejected and never echoed.
6
7use serde::{Deserialize, Serialize};
8
9/// Maximum length for any public-safe reference shown in the inspector.
10pub const PUBLIC_REF_MAX_LEN: usize = 256;
11
12/// A reference that passed public-safety checks.
13#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[serde(transparent)]
15pub struct PublicRef(String);
16
17impl PublicRef {
18 /// Validate and wrap a candidate reference. Returns `None` when unsafe.
19 pub fn new(raw: impl AsRef<str>) -> Option<Self> {
20 sanitize_public_ref(raw.as_ref())
21 }
22
23 pub fn as_str(&self) -> &str {
24 &self.0
25 }
26
27 pub fn into_string(self) -> String {
28 self.0
29 }
30}
31
32impl std::fmt::Display for PublicRef {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 f.write_str(&self.0)
35 }
36}
37
38impl AsRef<str> for PublicRef {
39 fn as_ref(&self) -> &str {
40 &self.0
41 }
42}
43
44/// Return true when `raw` is a bounded public-safe reference.
45pub fn is_public_safe_ref(raw: &str) -> bool {
46 sanitize_public_ref(raw).is_some()
47}
48
49/// Sanitize a candidate reference.
50///
51/// Accepts the same character class as OpenAgents public ref segments:
52/// ASCII letters, digits, `.`, `_`, `:`, and `-`, length 1..=256.
53/// Rejects tokens, private paths, whitespace, and other unsafe material.
54pub fn sanitize_public_ref(raw: &str) -> Option<PublicRef> {
55 let trimmed = raw.trim();
56 if trimmed.is_empty() || trimmed.len() > PUBLIC_REF_MAX_LEN {
57 return None;
58 }
59 if looks_like_private_path(trimmed) {
60 return None;
61 }
62 if looks_like_secret_or_token(trimmed) {
63 return None;
64 }
65 if trimmed.chars().any(|c| c.is_whitespace() || c == '\0') {
66 return None;
67 }
68 if !trimmed
69 .chars()
70 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | ':' | '-'))
71 {
72 return None;
73 }
74 // Unbounded prose often lacks separators; public refs use dotted segments.
75 // Allow short identifiers without dots (tool names, reason classes) up to
76 // a modest length; longer undotted strings are treated as opaque dumps.
77 if !trimmed.contains('.') && !trimmed.contains(':') && !trimmed.contains('_') && trimmed.len() > 64
78 {
79 return None;
80 }
81 Some(PublicRef(trimmed.to_string()))
82}
83
84fn looks_like_private_path(value: &str) -> bool {
85 let lower = value.to_ascii_lowercase();
86 value.starts_with('/')
87 || value.starts_with("~/")
88 || value.starts_with("\\\\")
89 || (value.len() > 2
90 && value.as_bytes()[1] == b':'
91 && (value.as_bytes()[2] == b'\\' || value.as_bytes()[2] == b'/'))
92 || lower.contains("/users/")
93 || lower.contains("/home/")
94 || lower.contains("/private/")
95 || lower.contains("\\users\\")
96 || lower.ends_with(".pem")
97 || lower.ends_with(".key")
98 || lower.contains("id_rsa")
99 || lower.contains(".ssh/")
100}
101
102fn looks_like_secret_or_token(value: &str) -> bool {
103 let lower = value.to_ascii_lowercase();
104 if lower.starts_with("npub1")
105 && lower.len() == 63
106 && lower[5..]
107 .chars()
108 .all(|character| "023456789acdefghjklmnpqrstuvwxyz".contains(character))
109 {
110 return false;
111 }
112 if lower.contains("bearer ")
113 || lower.contains("authorization:")
114 || lower.starts_with("sk-")
115 || lower.starts_with("sk_")
116 || lower.starts_with("ghp_")
117 || lower.starts_with("gho_")
118 || lower.starts_with("github_pat_")
119 || lower.starts_with("xox")
120 || lower.starts_with("nsec1")
121 || lower.starts_with("ncryptsec1")
122 || lower.contains("api_key")
123 || lower.contains("apikey")
124 || lower.contains("access_token")
125 || lower.contains("refresh_token")
126 || lower.contains("openagents_agent_token")
127 || lower.contains("client_secret")
128 || lower.contains("private_key")
129 {
130 return true;
131 }
132 // Long base64-ish blobs without ref structure are tokens, not refs.
133 if value.len() >= 40
134 && !value.contains('.')
135 && value
136 .chars()
137 .all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '=' || c == '-' || c == '_')
138 {
139 // Still allow dotted public refs; this branch is undotted/opaque.
140 if !value.contains(':') {
141 return true;
142 }
143 }
144 false
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn accepts_dotted_public_refs() {
153 assert!(is_public_safe_ref("tool.sarah.list_full_auto_runs"));
154 assert!(is_public_safe_ref(
155 "receipt.authority.sarah.tool.abcdef.list_full_auto_runs.turn.1"
156 ));
157 assert!(is_public_safe_ref("decision.sarah.tool.call-1"));
158 assert!(is_public_safe_ref("openagents.sarah-owner-orchestrator"));
159 assert!(is_public_safe_ref("blocker.sarah.authority_refused"));
160 assert!(is_public_safe_ref("reserved_action"));
161 }
162
163 #[test]
164 fn rejects_private_paths() {
165 assert!(!is_public_safe_ref("/Users/christopherdavid/.codex/auth.json"));
166 assert!(!is_public_safe_ref("~/work/openagents/secrets"));
167 assert!(!is_public_safe_ref("C:\\Users\\owner\\token.txt"));
168 assert!(!is_public_safe_ref("/home/owner/.ssh/id_rsa"));
169 }
170
171 #[test]
172 fn rejects_tokens_and_secrets() {
173 assert!(!is_public_safe_ref("sk-ant-api03-WOOOOSECRETVALUEHERE"));
174 assert!(!is_public_safe_ref(
175 "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.payload.sig"
176 ));
177 assert!(!is_public_safe_ref("OPENAGENTS_AGENT_TOKEN=abc123"));
178 assert!(!is_public_safe_ref(
179 "ghp_abcdefghijklmnopqrstuvwxyz0123456789"
180 ));
181 assert!(!is_public_safe_ref(
182 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
183 ));
184 assert!(!is_public_safe_ref(
185 "nsec1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq"
186 ));
187 assert!(!is_public_safe_ref(
188 "ncryptsec1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq"
189 ));
190 assert!(is_public_safe_ref(
191 "npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq"
192 ));
193 }
194
195 #[test]
196 fn rejects_unbounded_output() {
197 assert!(!is_public_safe_ref(
198 "tool said: here is a long answer with spaces and prose"
199 ));
200 assert!(!is_public_safe_ref("line1\nline2\nline3"));
201 assert!(!is_public_safe_ref(""));
202 let too_long = "a".repeat(PUBLIC_REF_MAX_LEN + 1);
203 assert!(!is_public_safe_ref(&too_long));
204 }
205
206 #[test]
207 fn never_echoes_unsafe_input() {
208 let raw = "sk-secret-token-value-that-must-not-leak";
209 assert!(sanitize_public_ref(raw).is_none());
210 }
211}
212