1use serde_json::{Value, json};
4
5use super::types::{
6 AGENT_ENVELOPE_SCHEMA_VERSION, AgentEnvelope, CANONICAL_TOKEN_ENVELOPE_SCHEMA_VERSION,
7 CanonicalTokenEnvelopeV1, OCLA_API_VERSION, OclaError, OclaResult,
8};
9
10pub const OCLA_WIRE_SCHEMA_ID: &str =
11 "https://leanctx.com/schemas/ocla/v1/canonical-token-envelope.json";
12pub const OCLA_AGENT_ENVELOPE_WIRE_SCHEMA_ID: &str =
13 "https://leanctx.com/schemas/ocla/v1/agent-envelope.json";
14pub const MAX_OCLA_WIRE_BYTES: usize = 64 * 1024;
15
16fn validate_wire_size(json: &str) -> OclaResult<()> {
17 if json.len() > MAX_OCLA_WIRE_BYTES {
18 return Err(OclaError::InvalidRequest(format!(
19 "wire document exceeds {MAX_OCLA_WIRE_BYTES} bytes"
20 )));
21 }
22 Ok(())
23}
24
25pub fn encode_envelope(envelope: &CanonicalTokenEnvelopeV1) -> OclaResult<String> {
26 envelope.validate()?;
27 let json = serde_json::to_string(envelope)
28 .map_err(|error| OclaError::InvalidRequest(format!("cannot encode envelope: {error}")))?;
29 validate_wire_size(&json)?;
30 Ok(json)
31}
32
33pub fn decode_envelope(json: &str) -> OclaResult<CanonicalTokenEnvelopeV1> {
34 validate_wire_size(json)?;
35 let envelope: CanonicalTokenEnvelopeV1 = serde_json::from_str(json)
36 .map_err(|error| OclaError::InvalidRequest(format!("cannot decode envelope: {error}")))?;
37 envelope.validate()?;
38 Ok(envelope)
39}
40
41pub fn encode_agent_envelope(envelope: &AgentEnvelope) -> OclaResult<String> {
44 envelope.validate()?;
45 let json = serde_json::to_string(envelope).map_err(|error| {
46 OclaError::InvalidRequest(format!("cannot encode agent envelope: {error}"))
47 })?;
48 validate_wire_size(&json)?;
49 Ok(json)
50}
51
52pub fn decode_agent_envelope(json: &str) -> OclaResult<AgentEnvelope> {
55 validate_wire_size(json)?;
56 let envelope: AgentEnvelope = serde_json::from_str(json).map_err(|error| {
57 OclaError::InvalidRequest(format!("cannot decode agent envelope: {error}"))
58 })?;
59 envelope.validate()?;
60 Ok(envelope)
61}
62
63#[must_use]
67pub fn canonical_envelope_schema() -> Value {
68 json!({
69 "$schema": "https://json-schema.org/draft/2020-12/schema",
70 "$id": OCLA_WIRE_SCHEMA_ID,
71 "title": "LeanCTX CanonicalTokenEnvelopeV1",
72 "type": "object",
73 "additionalProperties": false,
74 "required": ["schema_version", "context", "surface", "direction", "provider", "model", "token_balance", "idempotency_key"],
75 "properties": {
76 "schema_version": {"const": CANONICAL_TOKEN_ENVELOPE_SCHEMA_VERSION},
77 "context": {
78 "type": "object",
79 "additionalProperties": false,
80 "required": ["request_id", "session_id", "agent_id", "content_ref", "tenant_id"],
81 "properties": {
82 "request_id": {"type": "string", "minLength": 1, "pattern": ".*\\S.*"},
83 "session_id": {"type": "string", "minLength": 1, "pattern": ".*\\S.*"},
84 "agent_id": {"type": "string", "minLength": 1, "pattern": ".*\\S.*"},
85 "content_ref": {"type": "string", "minLength": 1, "pattern": ".*\\S.*"},
86 "tenant_id": {"type": ["string", "null"]}
87 }
88 },
89 "surface": {"enum": ["mcp", "proxy", "shell", "agent"]},
90 "direction": {"enum": ["input", "output"]},
91 "provider": {"type": "string", "minLength": 1, "pattern": ".*\\S.*"},
92 "model": {"type": "string", "minLength": 1, "pattern": ".*\\S.*"},
93 "token_balance": {
94 "type": "object",
95 "additionalProperties": false,
96 "required": ["original_tokens", "materialized_tokens", "delivered_tokens", "provider_billed_tokens"],
97 "properties": {
98 "original_tokens": {"type": "integer", "minimum": 0, "maximum": u64::MAX},
99 "materialized_tokens": {"type": "integer", "minimum": 0, "maximum": u64::MAX},
100 "delivered_tokens": {"type": "integer", "minimum": 0, "maximum": u64::MAX},
101 "provider_billed_tokens": {"type": "integer", "minimum": 0, "maximum": u64::MAX}
102 }
103 },
104 "route_ref": {"type": ["string", "null"]},
105 "policy_ref": {"type": ["string", "null"]},
106 "idempotency_key": {"type": "string", "minLength": 1, "pattern": ".*\\S.*"}
107 },
108 "x-ocla-api-version": OCLA_API_VERSION
109 })
110}
111
112#[must_use]
116pub fn agent_envelope_schema() -> Value {
117 json!({
118 "$schema": "https://json-schema.org/draft/2020-12/schema",
119 "$id": OCLA_AGENT_ENVELOPE_WIRE_SCHEMA_ID,
120 "title": "LeanCTX AgentEnvelopeV1",
121 "type": "object",
122 "additionalProperties": false,
123 "required": ["schema_version", "relay_id", "context", "from_agent_id", "to_agent_id", "capsule_ref", "budget_tokens"],
124 "properties": {
125 "schema_version": {"const": AGENT_ENVELOPE_SCHEMA_VERSION},
126 "relay_id": {"type": "string", "pattern": "^agent-relay:[0-9a-f]{64}$"},
127 "context": {
128 "type": "object",
129 "additionalProperties": false,
130 "required": ["request_id", "session_id", "agent_id", "content_ref", "tenant_id"],
131 "properties": {
132 "request_id": {"type": "string", "minLength": 1, "pattern": ".*\\S.*"},
133 "session_id": {"type": "string", "minLength": 1, "pattern": ".*\\S.*"},
134 "agent_id": {"type": "string", "minLength": 1, "maxLength": 256, "pattern": "^[!-~]+$"},
135 "content_ref": {"type": "string", "minLength": 1, "pattern": ".*\\S.*"},
136 "tenant_id": {"type": ["string", "null"]}
137 }
138 },
139 "from_agent_id": {"type": "string", "minLength": 1, "maxLength": 256, "pattern": "^[!-~]+$"},
140 "to_agent_id": {"type": "string", "minLength": 1, "maxLength": 256, "pattern": "^[!-~]+$"},
141 "capsule_ref": {"type": "string", "pattern": "^capsule:[0-9a-f]{64}$"},
142 "budget_tokens": {"type": "integer", "minimum": 1, "maximum": u64::MAX}
143 },
144 "x-ocla-api-version": OCLA_API_VERSION,
145 "x-evidence-boundary": "admission_only"
146 })
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use crate::core::ocla::{
153 AGENT_ENVELOPE_SCHEMA_VERSION, OclaRequestContext, TokenBalanceV1, TokenEnvelopeSurface,
154 TokenFlowDirection,
155 };
156
157 fn envelope() -> CanonicalTokenEnvelopeV1 {
158 CanonicalTokenEnvelopeV1 {
159 schema_version: CANONICAL_TOKEN_ENVELOPE_SCHEMA_VERSION,
160 context: OclaRequestContext {
161 request_id: "request-1".into(),
162 session_id: "session-1".into(),
163 agent_id: "agent-1".into(),
164 content_ref: "blake3:content".into(),
165 tenant_id: None,
166 trace_id: "tr-unit".into(),
167 },
168 surface: TokenEnvelopeSurface::Proxy,
169 direction: TokenFlowDirection::Input,
170 provider: "openai".into(),
171 model: "gpt-5".into(),
172 token_balance: TokenBalanceV1 {
173 original_tokens: 100,
174 materialized_tokens: 80,
175 delivered_tokens: 60,
176 provider_billed_tokens: 60,
177 },
178 route_ref: Some("route-1".into()),
179 policy_ref: None,
180 idempotency_key: "request-1:input".into(),
181 }
182 }
183
184 fn agent_envelope() -> AgentEnvelope {
185 let mut envelope = AgentEnvelope {
186 schema_version: AGENT_ENVELOPE_SCHEMA_VERSION,
187 relay_id: "agent-relay:pending".to_string(),
188 context: OclaRequestContext {
189 request_id: "agent-request-1".into(),
190 session_id: "agent-session-1".into(),
191 agent_id: "owner-agent".into(),
192 content_ref: "blake3:agent-content".into(),
193 tenant_id: None,
194 trace_id: "tr-unit".into(),
195 },
196 from_agent_id: "owner-agent".into(),
197 to_agent_id: "reviewer-agent".into(),
198 capsule_ref: format!("capsule:{}", "a".repeat(64)),
199 budget_tokens: 900,
200 };
201 envelope.assign_relay_id().expect("relay ID assigns");
202 envelope
203 }
204
205 #[test]
206 fn canonical_envelope_roundtrips_without_payload() {
207 let original = envelope();
208 let wire = encode_envelope(&original).expect("valid envelope");
209 assert!(!wire.contains("payload"));
210 assert_eq!(decode_envelope(&wire).expect("decode"), original);
211 }
212
213 #[test]
214 fn invalid_token_order_and_unknown_fields_are_rejected() {
215 let mut invalid = envelope();
216 invalid.token_balance.delivered_tokens = 81;
217 assert!(encode_envelope(&invalid).is_err());
218 assert!(decode_envelope(r#"{"schema_version":1,"extra":true}"#).is_err());
219 }
220
221 #[test]
222 fn agent_envelope_roundtrips_without_payload_or_delivery_claim() {
223 let original = agent_envelope();
224 let wire = encode_agent_envelope(&original).expect("valid agent envelope");
225 assert!(!wire.contains("payload"));
226 assert!(!wire.contains("delivered_tokens"));
227 assert_eq!(decode_agent_envelope(&wire).expect("decode"), original);
228 assert!(decode_agent_envelope(r#"{"schema_version":1,"unexpected":true}"#).is_err());
229 }
230
231 #[test]
232 fn missing_tenant_id_is_rejected_for_both_public_envelopes() {
233 let token_wire = encode_envelope(&envelope()).expect("encode token envelope");
234 let mut token_value: Value = serde_json::from_str(&token_wire).expect("token JSON");
235 token_value["context"]
236 .as_object_mut()
237 .expect("token context object")
238 .remove("tenant_id");
239 assert!(decode_envelope(&token_value.to_string()).is_err());
240
241 let agent_wire = encode_agent_envelope(&agent_envelope()).expect("encode agent envelope");
242 let mut agent_value: Value = serde_json::from_str(&agent_wire).expect("agent JSON");
243 agent_value["context"]
244 .as_object_mut()
245 .expect("agent context object")
246 .remove("tenant_id");
247 assert!(decode_agent_envelope(&agent_value.to_string()).is_err());
248 }
249
250 #[test]
251 fn both_public_decoders_enforce_the_exact_wire_size_boundary() {
252 let token_wire = encode_envelope(&envelope()).expect("encode token envelope");
253 let token_at_limit = format!(
254 "{token_wire}{}",
255 " ".repeat(MAX_OCLA_WIRE_BYTES - token_wire.len())
256 );
257 assert_eq!(token_at_limit.len(), MAX_OCLA_WIRE_BYTES);
258 assert!(decode_envelope(&token_at_limit).is_ok());
259 assert!(decode_envelope(&(token_at_limit + " ")).is_err());
260
261 let agent_wire = encode_agent_envelope(&agent_envelope()).expect("encode agent envelope");
262 let agent_at_limit = format!(
263 "{agent_wire}{}",
264 " ".repeat(MAX_OCLA_WIRE_BYTES - agent_wire.len())
265 );
266 assert_eq!(agent_at_limit.len(), MAX_OCLA_WIRE_BYTES);
267 assert!(decode_agent_envelope(&agent_at_limit).is_ok());
268 assert!(decode_agent_envelope(&(agent_at_limit + " ")).is_err());
269 }
270
271 #[test]
272 fn both_public_encoders_reject_oversize_valid_envelopes() {
273 let mut oversized_token = envelope();
274 oversized_token.provider = "p".repeat(MAX_OCLA_WIRE_BYTES);
275 assert!(matches!(
276 encode_envelope(&oversized_token),
277 Err(OclaError::InvalidRequest(message))
278 if message == format!("wire document exceeds {MAX_OCLA_WIRE_BYTES} bytes")
279 ));
280
281 let mut oversized_agent = agent_envelope();
282 oversized_agent.context.content_ref = "c".repeat(MAX_OCLA_WIRE_BYTES);
283 oversized_agent
284 .assign_relay_id()
285 .expect("oversize relay identity assigns");
286 assert!(matches!(
287 encode_agent_envelope(&oversized_agent),
288 Err(OclaError::InvalidRequest(message))
289 if message == format!("wire document exceeds {MAX_OCLA_WIRE_BYTES} bytes")
290 ));
291 }
292
293 #[test]
294 fn schema_numeric_limits_match_engine_u64_boundaries() {
295 let token_schema = canonical_envelope_schema();
296 for field in [
297 "original_tokens",
298 "materialized_tokens",
299 "delivered_tokens",
300 "provider_billed_tokens",
301 ] {
302 assert_eq!(
303 token_schema["properties"]["token_balance"]["properties"][field]["maximum"],
304 Value::from(u64::MAX)
305 );
306 }
307 assert_eq!(
308 agent_envelope_schema()["properties"]["budget_tokens"]["maximum"],
309 Value::from(u64::MAX)
310 );
311
312 let mut max_token = envelope();
313 max_token.token_balance = TokenBalanceV1 {
314 original_tokens: u64::MAX,
315 materialized_tokens: u64::MAX,
316 delivered_tokens: u64::MAX,
317 provider_billed_tokens: u64::MAX,
318 };
319 let wire = encode_envelope(&max_token).expect("encode u64 maximum");
320 assert_eq!(
321 decode_envelope(&wire).expect("decode u64 maximum"),
322 max_token
323 );
324
325 let mut max_agent = agent_envelope();
326 max_agent.budget_tokens = u64::MAX;
327 max_agent
328 .assign_relay_id()
329 .expect("assign maximum-budget relay ID");
330 let wire = encode_agent_envelope(&max_agent).expect("encode u64 maximum");
331 assert_eq!(
332 decode_agent_envelope(&wire).expect("decode u64 maximum"),
333 max_agent
334 );
335 }
336
337 #[test]
338 fn committed_json_schema_cannot_drift_from_the_rust_projection() {
339 let committed: Value = serde_json::from_str(include_str!(
340 "../../../../docs/contracts/ocla-wire-v1.schema.json"
341 ))
342 .expect("valid committed OCLA schema");
343 assert_eq!(committed, canonical_envelope_schema());
344 }
345
346 #[test]
347 fn committed_agent_envelope_schema_cannot_drift_from_the_rust_projection() {
348 let committed: Value = serde_json::from_str(include_str!(
349 "../../../../docs/contracts/ocla-agent-envelope-v1.schema.json"
350 ))
351 .expect("valid committed agent-envelope schema");
352 assert_eq!(committed, agent_envelope_schema());
353 }
354}