1use std::collections::HashMap;
14use std::sync::Arc;
15
16use serde::{Deserialize, Serialize};
17
18use super::agent::AgentExtension;
19use super::completion::CompletionExtension;
20use super::delegation::DelegationExtension;
21use super::framework::FrameworkExtension;
22use super::guarded::{Guarded, WriteToken};
23use super::http::HttpExtension;
24use super::llm::LLMExtension;
25use super::mcp::MCPExtension;
26use super::meta::MetaExtension;
27use super::provenance::ProvenanceExtension;
28use super::raw_credentials::RawCredentialsExtension;
29use super::request::RequestExtension;
30use super::security::SecurityExtension;
31
32#[derive(Debug, Default, Serialize, Deserialize)]
49pub struct Extensions {
50 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub request: Option<Arc<RequestExtension>>,
53
54 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub agent: Option<Arc<AgentExtension>>,
57
58 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub http: Option<Arc<HttpExtension>>,
61
62 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub security: Option<Arc<SecurityExtension>>,
65
66 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub delegation: Option<Arc<DelegationExtension>>,
69
70 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub raw_credentials: Option<Arc<RawCredentialsExtension>>,
82
83 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub mcp: Option<Arc<MCPExtension>>,
86
87 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub completion: Option<Arc<CompletionExtension>>,
90
91 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub provenance: Option<Arc<ProvenanceExtension>>,
94
95 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub llm: Option<Arc<LLMExtension>>,
98
99 #[serde(default, skip_serializing_if = "Option::is_none")]
101 pub framework: Option<Arc<FrameworkExtension>>,
102
103 #[serde(default)]
105 pub meta: Option<Arc<MetaExtension>>,
106
107 #[serde(default, skip_serializing_if = "Option::is_none")]
109 pub custom: Option<Arc<HashMap<String, serde_json::Value>>>,
110
111 #[serde(skip)]
114 pub http_write_token: Option<WriteToken>,
115 #[serde(skip)]
116 pub labels_write_token: Option<WriteToken>,
117 #[serde(skip)]
118 pub delegation_write_token: Option<WriteToken>,
119}
120
121impl Clone for Extensions {
122 fn clone(&self) -> Self {
124 Self {
125 request: self.request.clone(),
126 agent: self.agent.clone(),
127 http: self.http.clone(),
128 security: self.security.clone(),
129 delegation: self.delegation.clone(),
130 raw_credentials: self.raw_credentials.clone(),
131 mcp: self.mcp.clone(),
132 completion: self.completion.clone(),
133 provenance: self.provenance.clone(),
134 llm: self.llm.clone(),
135 framework: self.framework.clone(),
136 meta: self.meta.clone(),
137 custom: self.custom.clone(),
138 http_write_token: None,
139 labels_write_token: None,
140 delegation_write_token: None,
141 }
142 }
143}
144
145impl Extensions {
146 pub fn cow_copy(&self) -> OwnedExtensions {
166 OwnedExtensions {
167 request: self.request.clone(),
169 agent: self.agent.clone(),
170 mcp: self.mcp.clone(),
171 completion: self.completion.clone(),
172 provenance: self.provenance.clone(),
173 llm: self.llm.clone(),
174 framework: self.framework.clone(),
175 meta: self.meta.clone(),
176 raw_credentials: self.raw_credentials.clone(),
177
178 http: self.http.as_ref().map(|arc| Guarded::new((**arc).clone())),
180 security: self.security.as_ref().map(|arc| (**arc).clone()),
181 delegation: self.delegation.as_ref().map(|arc| (**arc).clone()),
182 custom: self.custom.as_ref().map(|arc| (**arc).clone()),
183
184 http_write_token: if self.http_write_token.is_some() {
186 Some(WriteToken::new())
187 } else {
188 None
189 },
190 labels_write_token: if self.labels_write_token.is_some() {
191 Some(WriteToken::new())
192 } else {
193 None
194 },
195 delegation_write_token: if self.delegation_write_token.is_some() {
196 Some(WriteToken::new())
197 } else {
198 None
199 },
200 }
201 }
202
203 pub fn validate_immutable(&self, modified: &OwnedExtensions) -> bool {
211 fn ptr_eq_opt<T>(a: &Option<Arc<T>>, b: &Option<Arc<T>>) -> bool {
212 match (a, b) {
213 (Some(a), Some(b)) => Arc::ptr_eq(a, b),
214 (None, None) => true,
215 (_, None) => true, (None, Some(_)) => false, }
218 }
219
220 ptr_eq_opt(&self.request, &modified.request)
221 && ptr_eq_opt(&self.agent, &modified.agent)
222 && ptr_eq_opt(&self.mcp, &modified.mcp)
223 && ptr_eq_opt(&self.completion, &modified.completion)
224 && ptr_eq_opt(&self.provenance, &modified.provenance)
225 && ptr_eq_opt(&self.llm, &modified.llm)
226 && ptr_eq_opt(&self.framework, &modified.framework)
227 && ptr_eq_opt(&self.meta, &modified.meta)
228 }
239
240 pub fn merge_owned(&mut self, owned: OwnedExtensions) {
242 self.http = owned.http.map(|g| Arc::new(g.into_inner()));
243 self.security = owned.security.map(Arc::new);
244 self.delegation = owned.delegation.map(Arc::new);
245 self.custom = owned.custom.map(Arc::new);
246 if owned.raw_credentials.is_some() {
256 self.raw_credentials = owned.raw_credentials;
257 }
258 }
259}
260
261#[derive(Debug)]
279pub struct OwnedExtensions {
280 pub request: Option<Arc<RequestExtension>>,
282 pub agent: Option<Arc<AgentExtension>>,
283 pub mcp: Option<Arc<MCPExtension>>,
284 pub completion: Option<Arc<CompletionExtension>>,
285 pub provenance: Option<Arc<ProvenanceExtension>>,
286 pub llm: Option<Arc<LLMExtension>>,
287 pub framework: Option<Arc<FrameworkExtension>>,
288 pub meta: Option<Arc<MetaExtension>>,
289 pub raw_credentials: Option<Arc<RawCredentialsExtension>>,
294
295 pub http: Option<Guarded<HttpExtension>>,
297 pub security: Option<SecurityExtension>,
298 pub delegation: Option<DelegationExtension>,
299 pub custom: Option<HashMap<String, serde_json::Value>>,
300
301 pub http_write_token: Option<WriteToken>,
303 pub labels_write_token: Option<WriteToken>,
304 pub delegation_write_token: Option<WriteToken>,
305}
306#[cfg(test)]
307mod tests {
308 use super::*;
309 use crate::extensions::{
310 DelegationExtension, HttpExtension, RequestExtension, SecurityExtension,
311 };
312
313 fn make_extensions() -> Extensions {
314 let mut security = SecurityExtension::default();
315 security.add_label("PII");
316
317 let mut http = HttpExtension::default();
318 http.set_header("Authorization", "Bearer token");
319
320 Extensions {
321 request: Some(Arc::new(RequestExtension {
322 request_id: Some("req-001".into()),
323 ..Default::default()
324 })),
325 security: Some(Arc::new(security)),
326 http: Some(Arc::new(http)),
327 delegation: Some(Arc::new(DelegationExtension::default())),
328 meta: Some(Arc::new(MetaExtension {
329 entity_type: Some("tool".into()),
330 ..Default::default()
331 })),
332 ..Default::default()
333 }
334 }
335
336 #[test]
337 fn test_cow_copy_shares_immutable_arcs() {
338 let ext = make_extensions();
339 let cow = ext.cow_copy();
340
341 assert!(Arc::ptr_eq(
343 ext.request.as_ref().unwrap(),
344 cow.request.as_ref().unwrap()
345 ));
346 assert!(Arc::ptr_eq(
347 ext.meta.as_ref().unwrap(),
348 cow.meta.as_ref().unwrap()
349 ));
350 }
351
352 #[test]
353 fn test_cow_copy_deep_clones_mutable_slots() {
354 let ext = make_extensions();
355 let cow = ext.cow_copy();
356
357 assert!(cow.security.is_some());
359 assert!(cow.http.is_some());
360 assert!(cow.delegation.is_some());
361
362 cow.security.as_ref().unwrap().has_label("PII");
364 }
365
366 #[test]
367 fn test_cow_copy_propagates_write_tokens() {
368 let mut ext = make_extensions();
369
370 let cow_no_tokens = ext.cow_copy();
372 assert!(cow_no_tokens.http_write_token.is_none());
373 assert!(cow_no_tokens.labels_write_token.is_none());
374 assert!(cow_no_tokens.delegation_write_token.is_none());
375
376 ext.http_write_token = Some(WriteToken::new());
378 ext.labels_write_token = Some(WriteToken::new());
379
380 let cow_with_tokens = ext.cow_copy();
382 assert!(cow_with_tokens.http_write_token.is_some());
383 assert!(cow_with_tokens.labels_write_token.is_some());
384 assert!(cow_with_tokens.delegation_write_token.is_none()); }
386
387 #[test]
388 fn test_cow_copy_write_token_enables_guarded_write() {
389 let mut ext = make_extensions();
390 ext.http_write_token = Some(WriteToken::new());
391
392 let mut cow = ext.cow_copy();
393
394 assert_eq!(
396 cow.http
397 .as_ref()
398 .unwrap()
399 .read()
400 .get_header("Authorization"),
401 Some("Bearer token")
402 );
403
404 let token = cow.http_write_token.as_ref().unwrap();
406 cow.http
407 .as_mut()
408 .unwrap()
409 .write(token)
410 .set_header("X-Custom", "value");
411
412 assert_eq!(
413 cow.http.as_ref().unwrap().read().get_header("X-Custom"),
414 Some("value")
415 );
416
417 assert!(ext.http.as_ref().unwrap().get_header("X-Custom").is_none());
419 }
420
421 #[test]
422 fn test_cow_copy_monotonic_label_insert() {
423 let mut ext = make_extensions();
424 ext.labels_write_token = Some(WriteToken::new());
425
426 let mut cow = ext.cow_copy();
427
428 cow.security.as_mut().unwrap().add_label("HIPAA");
430 assert!(cow.security.as_ref().unwrap().has_label("HIPAA"));
431
432 assert!(!ext.security.as_ref().unwrap().has_label("HIPAA"));
434 }
435
436 #[test]
437 fn test_validate_immutable_passes_for_cow() {
438 let ext = make_extensions();
439 let cow = ext.cow_copy();
440
441 assert!(ext.validate_immutable(&cow));
443 }
444
445 #[test]
446 fn test_validate_immutable_fails_when_tampered() {
447 let ext = make_extensions();
448 let mut cow = ext.cow_copy();
449
450 cow.request = Some(Arc::new(RequestExtension {
452 request_id: Some("TAMPERED".into()),
453 ..Default::default()
454 }));
455
456 assert!(!ext.validate_immutable(&cow));
458 }
459
460 #[test]
461 fn test_validate_immutable_both_none_passes() {
462 let ext = Extensions::default();
463 let cow = ext.cow_copy();
464 assert!(ext.validate_immutable(&cow));
465 }
466
467 #[test]
468 fn test_clone_drops_write_tokens() {
469 let mut ext = make_extensions();
470 ext.http_write_token = Some(WriteToken::new());
471 ext.labels_write_token = Some(WriteToken::new());
472 ext.delegation_write_token = Some(WriteToken::new());
473
474 let cloned = ext.clone();
476 assert!(cloned.http_write_token.is_none());
477 assert!(cloned.labels_write_token.is_none());
478 assert!(cloned.delegation_write_token.is_none());
479
480 let cow = ext.cow_copy();
482 assert!(cow.http_write_token.is_some());
483 assert!(cow.labels_write_token.is_some());
484 assert!(cow.delegation_write_token.is_some());
485 }
486
487 #[test]
488 fn test_cow_copy_modify_multiple_fields() {
489 use crate::extensions::delegation::DelegationHop;
490 use crate::extensions::DelegationExtension;
491
492 let mut security = SecurityExtension::default();
494 security.add_label("PII");
495
496 let mut http = HttpExtension::default();
497 http.set_header("Authorization", "Bearer token");
498
499 let mut ext = Extensions {
500 security: Some(Arc::new(security)),
501 http: Some(Arc::new(http)),
502 delegation: Some(Arc::new(DelegationExtension::default())),
503 custom: Some(Arc::new(
504 [("existing".to_string(), serde_json::json!("value"))].into(),
505 )),
506 meta: Some(Arc::new(MetaExtension {
507 entity_type: Some("tool".into()),
508 ..Default::default()
509 })),
510 ..Default::default()
511 };
512
513 ext.http_write_token = Some(WriteToken::new());
515 ext.labels_write_token = Some(WriteToken::new());
516 ext.delegation_write_token = Some(WriteToken::new());
517
518 let mut cow = ext.cow_copy();
520
521 cow.security.as_mut().unwrap().add_label("CHECKED");
523 cow.security.as_mut().unwrap().add_label("COMPLIANT");
524
525 let token = cow.http_write_token.as_ref().unwrap();
527 cow.http
528 .as_mut()
529 .unwrap()
530 .write(token)
531 .set_header("X-Checked", "true");
532 cow.http
533 .as_mut()
534 .unwrap()
535 .write(token)
536 .set_header("X-Policy", "v2");
537
538 cow.delegation.as_mut().unwrap().append_hop(DelegationHop {
540 subject_id: "service-a".into(),
541 scopes_granted: vec!["read_hr".into()],
542 ..Default::default()
543 });
544
545 cow.custom
547 .as_mut()
548 .unwrap()
549 .insert("audit.timestamp".into(), serde_json::json!("2026-04-29"));
550
551 let sec = cow.security.as_ref().unwrap();
553 assert!(sec.has_label("PII")); assert!(sec.has_label("CHECKED")); assert!(sec.has_label("COMPLIANT")); let http = cow.http.as_ref().unwrap().read();
558 assert_eq!(http.get_header("Authorization"), Some("Bearer token")); assert_eq!(http.get_header("X-Checked"), Some("true")); assert_eq!(http.get_header("X-Policy"), Some("v2")); assert_eq!(cow.delegation.as_ref().unwrap().chain.len(), 1);
563 assert_eq!(
564 cow.delegation.as_ref().unwrap().chain[0].subject_id,
565 "service-a"
566 );
567
568 assert_eq!(
569 cow.custom.as_ref().unwrap().get("existing").unwrap(),
570 "value"
571 );
572 assert_eq!(
573 cow.custom.as_ref().unwrap().get("audit.timestamp").unwrap(),
574 "2026-04-29"
575 );
576
577 assert!(!ext.security.as_ref().unwrap().has_label("CHECKED"));
579 assert!(ext.http.as_ref().unwrap().get_header("X-Checked").is_none());
580 assert!(ext.delegation.as_ref().unwrap().chain.is_empty());
581 assert!(!ext.custom.as_ref().unwrap().contains_key("audit.timestamp"));
582
583 assert!(ext.validate_immutable(&cow));
585 }
586
587 #[test]
588 fn test_validate_immutable_passes_when_slot_filtered_out() {
589 let ext = make_extensions();
594 let mut cow = ext.cow_copy();
595
596 cow.agent = None;
598
599 assert!(ext.validate_immutable(&cow));
601 }
602
603 #[test]
604 fn test_validate_immutable_fails_when_slot_fabricated() {
605 let ext = Extensions::default(); let mut cow = ext.cow_copy();
609
610 cow.agent = Some(Arc::new(AgentExtension {
611 agent_id: Some("fabricated".into()),
612 ..Default::default()
613 }));
614
615 assert!(!ext.validate_immutable(&cow));
616 }
617
618 #[test]
619 fn test_validate_immutable_passes_multiple_slots_filtered() {
620 let ext = make_extensions();
622 let mut cow = ext.cow_copy();
623
624 cow.agent = None;
625 cow.mcp = None;
626 cow.completion = None;
627 cow.framework = None;
628
629 assert!(ext.validate_immutable(&cow));
630 }
631
632 #[test]
633 fn test_merge_owned_preserves_http_response_headers() {
634 let mut http = HttpExtension::default();
637 http.set_request_header("Authorization", "Bearer tok");
638
639 let mut ext = Extensions {
640 http: Some(Arc::new(http)),
641 ..Default::default()
642 };
643 ext.http_write_token = Some(WriteToken::new());
644
645 let mut cow = ext.cow_copy();
646
647 let token = cow.http_write_token.as_ref().unwrap();
649 let h = cow.http.as_mut().unwrap().write(token);
650 h.set_response_header("X-Tool-Name", "get_compensation");
651 h.set_response_header("X-Status", "success");
652
653 ext.merge_owned(cow);
655
656 let merged_http = ext.http.as_ref().unwrap();
658 assert_eq!(
659 merged_http.get_response_header("X-Tool-Name"),
660 Some("get_compensation")
661 );
662 assert_eq!(merged_http.get_response_header("X-Status"), Some("success"));
663 assert_eq!(
665 merged_http.get_request_header("Authorization"),
666 Some("Bearer tok")
667 );
668 }
669
670 #[test]
671 fn test_merge_owned_with_filtered_security() {
672 let mut security = SecurityExtension::default();
676 security.add_label("PII");
677 security.add_label("HR");
678
679 let ext = Extensions {
680 security: Some(Arc::new(security)),
681 ..Default::default()
682 };
683
684 let mut cow = ext.cow_copy();
687
688 cow.security.as_mut().unwrap().labels = crate::extensions::MonotonicSet::new();
692
693 let mut ext_mut = ext.clone();
695 ext_mut.merge_owned(cow);
696
697 let merged_sec = ext_mut.security.as_ref().unwrap();
701 assert!(!merged_sec.has_label("PII")); }
703
704 #[test]
705 fn test_merge_owned_none_http_preserves_pipeline() {
706 let mut http = HttpExtension::default();
710 http.set_request_header("X-Original", "value");
711
712 let mut ext = Extensions {
713 http: Some(Arc::new(http)),
714 ..Default::default()
715 };
716
717 let mut cow = ext.cow_copy();
718 cow.http = None; ext.merge_owned(cow);
721
722 assert!(ext.http.is_none());
726 }
727
728 #[test]
729 fn test_read_only_plugin_zero_cost() {
730 let ext = make_extensions();
732
733 let has_pii = ext
735 .security
736 .as_ref()
737 .map(|s| s.has_label("PII"))
738 .unwrap_or(false);
739 assert!(has_pii);
740
741 let auth = ext
743 .http
744 .as_ref()
745 .and_then(|h| h.get_header("Authorization"));
746 assert_eq!(auth, Some("Bearer token"));
747
748 let entity = ext.meta.as_ref().and_then(|m| m.entity_type.as_deref());
750 assert_eq!(entity, Some("tool"));
751
752 }
754}