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 task_id: None,
168 parent_task_id: None,
169 },
170 surface: TokenEnvelopeSurface::Proxy,
171 direction: TokenFlowDirection::Input,
172 provider: "openai".into(),
173 model: "gpt-5".into(),
174 token_balance: TokenBalanceV1 {
175 original_tokens: 100,
176 materialized_tokens: 80,
177 delivered_tokens: 60,
178 provider_billed_tokens: 60,
179 },
180 route_ref: Some("route-1".into()),
181 policy_ref: None,
182 idempotency_key: "request-1:input".into(),
183 }
184 }
185
186 fn agent_envelope() -> AgentEnvelope {
187 let mut envelope = AgentEnvelope {
188 schema_version: AGENT_ENVELOPE_SCHEMA_VERSION,
189 relay_id: "agent-relay:pending".to_string(),
190 context: OclaRequestContext {
191 request_id: "agent-request-1".into(),
192 session_id: "agent-session-1".into(),
193 agent_id: "owner-agent".into(),
194 content_ref: "blake3:agent-content".into(),
195 tenant_id: None,
196 trace_id: "tr-unit".into(),
197 task_id: None,
198 parent_task_id: None,
199 },
200 from_agent_id: "owner-agent".into(),
201 to_agent_id: "reviewer-agent".into(),
202 capsule_ref: format!("capsule:{}", "a".repeat(64)),
203 budget_tokens: 900,
204 };
205 envelope.assign_relay_id().expect("relay ID assigns");
206 envelope
207 }
208
209 #[test]
210 fn canonical_envelope_roundtrips_without_payload() {
211 let original = envelope();
212 let wire = encode_envelope(&original).expect("valid envelope");
213 assert!(!wire.contains("payload"));
214 assert_eq!(decode_envelope(&wire).expect("decode"), original);
215 }
216
217 #[test]
218 fn invalid_token_order_and_unknown_fields_are_rejected() {
219 let mut invalid = envelope();
220 invalid.token_balance.delivered_tokens = 81;
221 assert!(encode_envelope(&invalid).is_err());
222 assert!(decode_envelope(r#"{"schema_version":1,"extra":true}"#).is_err());
223 }
224
225 #[test]
226 fn agent_envelope_roundtrips_without_payload_or_delivery_claim() {
227 let original = agent_envelope();
228 let wire = encode_agent_envelope(&original).expect("valid agent envelope");
229 assert!(!wire.contains("payload"));
230 assert!(!wire.contains("delivered_tokens"));
231 assert_eq!(decode_agent_envelope(&wire).expect("decode"), original);
232 assert!(decode_agent_envelope(r#"{"schema_version":1,"unexpected":true}"#).is_err());
233 }
234
235 #[test]
236 fn missing_tenant_id_is_rejected_for_both_public_envelopes() {
237 let token_wire = encode_envelope(&envelope()).expect("encode token envelope");
238 let mut token_value: Value = serde_json::from_str(&token_wire).expect("token JSON");
239 token_value["context"]
240 .as_object_mut()
241 .expect("token context object")
242 .remove("tenant_id");
243 assert!(decode_envelope(&token_value.to_string()).is_err());
244
245 let agent_wire = encode_agent_envelope(&agent_envelope()).expect("encode agent envelope");
246 let mut agent_value: Value = serde_json::from_str(&agent_wire).expect("agent JSON");
247 agent_value["context"]
248 .as_object_mut()
249 .expect("agent context object")
250 .remove("tenant_id");
251 assert!(decode_agent_envelope(&agent_value.to_string()).is_err());
252 }
253
254 #[test]
255 fn both_public_decoders_enforce_the_exact_wire_size_boundary() {
256 let token_wire = encode_envelope(&envelope()).expect("encode token envelope");
257 let token_at_limit = format!(
258 "{token_wire}{}",
259 " ".repeat(MAX_OCLA_WIRE_BYTES - token_wire.len())
260 );
261 assert_eq!(token_at_limit.len(), MAX_OCLA_WIRE_BYTES);
262 assert!(decode_envelope(&token_at_limit).is_ok());
263 assert!(decode_envelope(&(token_at_limit + " ")).is_err());
264
265 let agent_wire = encode_agent_envelope(&agent_envelope()).expect("encode agent envelope");
266 let agent_at_limit = format!(
267 "{agent_wire}{}",
268 " ".repeat(MAX_OCLA_WIRE_BYTES - agent_wire.len())
269 );
270 assert_eq!(agent_at_limit.len(), MAX_OCLA_WIRE_BYTES);
271 assert!(decode_agent_envelope(&agent_at_limit).is_ok());
272 assert!(decode_agent_envelope(&(agent_at_limit + " ")).is_err());
273 }
274
275 #[test]
276 fn both_public_encoders_reject_oversize_valid_envelopes() {
277 let mut oversized_token = envelope();
278 oversized_token.provider = "p".repeat(MAX_OCLA_WIRE_BYTES);
279 assert!(matches!(
280 encode_envelope(&oversized_token),
281 Err(OclaError::InvalidRequest(message))
282 if message == format!("wire document exceeds {MAX_OCLA_WIRE_BYTES} bytes")
283 ));
284
285 let mut oversized_agent = agent_envelope();
286 oversized_agent.context.content_ref = "c".repeat(MAX_OCLA_WIRE_BYTES);
287 oversized_agent
288 .assign_relay_id()
289 .expect("oversize relay identity assigns");
290 assert!(matches!(
291 encode_agent_envelope(&oversized_agent),
292 Err(OclaError::InvalidRequest(message))
293 if message == format!("wire document exceeds {MAX_OCLA_WIRE_BYTES} bytes")
294 ));
295 }
296
297 #[test]
298 fn schema_numeric_limits_match_engine_u64_boundaries() {
299 let token_schema = canonical_envelope_schema();
300 for field in [
301 "original_tokens",
302 "materialized_tokens",
303 "delivered_tokens",
304 "provider_billed_tokens",
305 ] {
306 assert_eq!(
307 token_schema["properties"]["token_balance"]["properties"][field]["maximum"],
308 Value::from(u64::MAX)
309 );
310 }
311 assert_eq!(
312 agent_envelope_schema()["properties"]["budget_tokens"]["maximum"],
313 Value::from(u64::MAX)
314 );
315
316 let mut max_token = envelope();
317 max_token.token_balance = TokenBalanceV1 {
318 original_tokens: u64::MAX,
319 materialized_tokens: u64::MAX,
320 delivered_tokens: u64::MAX,
321 provider_billed_tokens: u64::MAX,
322 };
323 let wire = encode_envelope(&max_token).expect("encode u64 maximum");
324 assert_eq!(
325 decode_envelope(&wire).expect("decode u64 maximum"),
326 max_token
327 );
328
329 let mut max_agent = agent_envelope();
330 max_agent.budget_tokens = u64::MAX;
331 max_agent
332 .assign_relay_id()
333 .expect("assign maximum-budget relay ID");
334 let wire = encode_agent_envelope(&max_agent).expect("encode u64 maximum");
335 assert_eq!(
336 decode_agent_envelope(&wire).expect("decode u64 maximum"),
337 max_agent
338 );
339 }
340
341 #[test]
342 fn committed_json_schema_cannot_drift_from_the_rust_projection() {
343 let committed: Value = serde_json::from_str(include_str!(
344 "../../../../docs/contracts/ocla-wire-v1.schema.json"
345 ))
346 .expect("valid committed OCLA schema");
347 assert_eq!(committed, canonical_envelope_schema());
348 }
349
350 #[test]
351 fn committed_agent_envelope_schema_cannot_drift_from_the_rust_projection() {
352 let committed: Value = serde_json::from_str(include_str!(
353 "../../../../docs/contracts/ocla-agent-envelope-v1.schema.json"
354 ))
355 .expect("valid committed agent-envelope schema");
356 assert_eq!(committed, agent_envelope_schema());
357 }
358}