1use std::collections::HashSet;
15use std::sync::Arc;
16
17use serde_json::Value;
18
19use super::manifest::ExtensionManifest;
20use super::runtime::process::RegisteredExtensionToolSpec;
21use crate::tools::catalog::SchemaDigest;
22use crate::tools::{Tool, ToolContext, ToolOrigin};
23use agent_core::BoundedText;
24
25pub const DECLARED_MAX_TOOLS: usize = 256;
28pub const DECLARED_MAX_PROVIDERS: usize = 16;
29pub const DECLARED_NAME_MAX_BYTES: usize = 128;
30pub const DECLARED_DESCRIPTION_MAX_BYTES: usize = 1024;
31pub const DECLARED_SCHEMA_MAX_BYTES: usize = 64 * 1024;
32pub const DECLARED_MAX_CONTEXT_PROVIDERS: usize = 8;
36
37#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
39pub struct DeferredDeclarations {
40 #[serde(default)]
42 pub tools: Vec<DeclaredExtensionTool>,
43 #[serde(default)]
47 pub providers: Vec<DeclaredExtensionProvider>,
48 #[serde(default)]
50 pub lifecycle: Option<DeferredLifecycle>,
51 #[serde(default, skip_serializing_if = "Vec::is_empty")]
60 pub context_providers: Vec<DeclaredExtensionContextProvider>,
61}
62
63#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
67pub struct DeclaredExtensionProvider {
68 pub id: String,
69 pub display_name: String,
70 #[serde(default)]
71 pub description: String,
72 #[serde(default)]
73 pub models: Vec<DeclaredExtensionProviderModel>,
74 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub config_schema: Option<Value>,
76}
77
78#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
80pub struct DeclaredExtensionProviderModel {
81 pub id: String,
82 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub display_name: Option<String>,
84 #[serde(default)]
85 pub capabilities: Value,
86 #[serde(default, skip_serializing_if = "Option::is_none")]
87 pub context_window: Option<u64>,
88}
89
90pub const DECLARED_MAX_PROVIDER_MODELS: usize = 64;
92pub const DECLARED_DISPLAY_MAX_BYTES: usize = 256;
93
94#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
103pub struct DeclaredExtensionContextProvider {
104 pub id: String,
107 pub capability: String,
109 #[serde(default)]
111 pub description: String,
112 pub schema_version: u32,
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
117#[serde(rename_all = "lowercase")]
118pub enum DeferredLifecycle {
119 Hook,
121 User,
123}
124
125#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
129pub struct DeclaredExtensionTool {
130 pub name: String,
131 #[serde(default)]
132 pub description: String,
133 pub input_schema: Value,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum ExtensionClass {
139 LegacyEager,
141 ToolOnly,
142 Provider,
143 HookLifecycle,
144 UiSidecar,
145 ContextProvider,
149 Mixed,
150}
151
152pub fn has_context_provider(declared: &DeferredDeclarations) -> bool {
155 !declared.context_providers.is_empty()
156}
157
158pub fn classify(manifest: &ExtensionManifest) -> ExtensionClass {
160 let Some(deferred) = &manifest.deferred else {
161 return ExtensionClass::LegacyEager;
162 };
163 let tools = !deferred.tools.is_empty();
164 let providers = !deferred.providers.is_empty();
165 let context = has_context_provider(deferred);
166 let hooks = !manifest.hooks.is_empty() || deferred.lifecycle == Some(DeferredLifecycle::Hook);
167 let user = deferred.lifecycle == Some(DeferredLifecycle::User);
168 match (tools, providers, context, hooks, user) {
171 (false, false, false, false, false) => ExtensionClass::LegacyEager,
172 (true, false, false, false, false) => ExtensionClass::ToolOnly,
173 (false, true, false, false, false) => ExtensionClass::Provider,
174 (false, false, true, false, false) => ExtensionClass::ContextProvider,
175 (false, false, false, true, false) => ExtensionClass::HookLifecycle,
176 (false, false, false, false, true) => ExtensionClass::UiSidecar,
177 _ => ExtensionClass::Mixed,
178 }
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq)]
187pub enum ActivationTrigger {
188 Eager,
189 FirstAuthorizedHookEvent,
190 ProviderSelection,
191 ExactToolActivation,
192 UserAction,
193}
194
195pub fn earliest_trigger(manifest: &ExtensionManifest) -> ActivationTrigger {
196 match classify(manifest) {
197 ExtensionClass::LegacyEager => ActivationTrigger::Eager,
198 ExtensionClass::ToolOnly => ActivationTrigger::ExactToolActivation,
199 ExtensionClass::Provider => ActivationTrigger::ProviderSelection,
200 ExtensionClass::HookLifecycle => ActivationTrigger::FirstAuthorizedHookEvent,
201 ExtensionClass::UiSidecar => ActivationTrigger::UserAction,
202 ExtensionClass::ContextProvider => ActivationTrigger::UserAction,
207 ExtensionClass::Mixed => {
208 let deferred = manifest.deferred.as_ref();
209 let hooks = !manifest.hooks.is_empty()
210 || deferred.and_then(|d| d.lifecycle) == Some(DeferredLifecycle::Hook);
211 let providers = deferred.map(|d| !d.providers.is_empty()).unwrap_or(false);
212 if hooks {
213 ActivationTrigger::FirstAuthorizedHookEvent
214 } else if providers {
215 ActivationTrigger::ProviderSelection
216 } else {
217 ActivationTrigger::ExactToolActivation
218 }
219 }
220 }
221}
222
223fn valid_name(name: &str) -> bool {
224 !name.is_empty() && name.len() <= DECLARED_NAME_MAX_BYTES && !name.chars().any(char::is_control)
225}
226
227pub fn validate_deferred(deferred: &DeferredDeclarations) -> Result<(), &'static str> {
230 if deferred.tools.len() > DECLARED_MAX_TOOLS {
231 return Err("too_many_declared_tools");
232 }
233 let mut seen: HashSet<&str> = HashSet::new();
234 for tool in &deferred.tools {
235 validate_tool_shape(&tool.name, &tool.description, &tool.input_schema)?;
236 if !seen.insert(tool.name.as_str()) {
237 return Err("duplicate_declared_tool_name");
238 }
239 }
240 if deferred.providers.len() > DECLARED_MAX_PROVIDERS {
241 return Err("too_many_declared_providers");
242 }
243 let mut provider_ids: HashSet<&str> = HashSet::new();
244 for provider in &deferred.providers {
245 if !valid_name(&provider.id) || provider.id.contains(':') {
246 return Err("invalid_declared_provider_id");
247 }
248 if !provider_ids.insert(provider.id.as_str()) {
249 return Err("duplicate_declared_provider_id");
250 }
251 if provider.display_name.is_empty()
252 || provider.display_name.len() > DECLARED_DISPLAY_MAX_BYTES
253 || provider.display_name.chars().any(char::is_control)
254 {
255 return Err("invalid_declared_provider_display_name");
256 }
257 if provider.description.is_empty()
258 || provider.description.len() > DECLARED_DESCRIPTION_MAX_BYTES
259 {
260 return Err("invalid_declared_provider_description");
261 }
262 if provider.models.is_empty() || provider.models.len() > DECLARED_MAX_PROVIDER_MODELS {
263 return Err("invalid_declared_provider_model_count");
264 }
265 let mut model_ids: HashSet<&str> = HashSet::new();
266 for model in &provider.models {
267 if !valid_name(&model.id) {
268 return Err("invalid_declared_provider_model_id");
269 }
270 if !model_ids.insert(model.id.as_str()) {
271 return Err("duplicate_declared_provider_model_id");
272 }
273 if let Some(display) = &model.display_name {
274 if display.len() > DECLARED_DISPLAY_MAX_BYTES
275 || display.chars().any(char::is_control)
276 {
277 return Err("invalid_declared_provider_model_display_name");
278 }
279 }
280 match &model.capabilities {
282 Value::Null => {}
283 Value::Object(map) => {
284 for (key, value) in map {
285 if !valid_name(key) || !value.is_boolean() {
286 return Err("invalid_declared_provider_model_capabilities");
287 }
288 }
289 }
290 _ => return Err("invalid_declared_provider_model_capabilities"),
291 }
292 }
293 if let Some(config_schema) = &provider.config_schema {
294 let len = serde_json::to_vec(config_schema)
295 .map(|b| b.len())
296 .unwrap_or(usize::MAX);
297 if !config_schema.is_object() || len > DECLARED_SCHEMA_MAX_BYTES {
298 return Err("invalid_declared_provider_config_schema");
299 }
300 }
301 }
302 if deferred.context_providers.len() > DECLARED_MAX_CONTEXT_PROVIDERS {
306 return Err("too_many_declared_context_providers");
307 }
308 let mut context_provider_ids: HashSet<&str> = HashSet::new();
309 for provider in &deferred.context_providers {
310 if super::context_provider::ContextProviderId::parse(&provider.id).is_err() {
313 return Err("invalid_declared_context_provider_id");
314 }
315 if !context_provider_ids.insert(provider.id.as_str()) {
316 return Err("duplicate_declared_context_provider_id");
317 }
318 if !valid_name(&provider.capability) {
319 return Err("invalid_declared_context_provider_capability");
320 }
321 if provider.description.is_empty()
322 || provider.description.len() > DECLARED_DESCRIPTION_MAX_BYTES
323 {
324 return Err("invalid_declared_context_provider_description");
325 }
326 if provider.schema_version == 0 {
327 return Err("invalid_declared_context_provider_schema_version");
328 }
329 }
330 if deferred.lifecycle == Some(DeferredLifecycle::User)
333 && (!deferred.tools.is_empty() || !deferred.providers.is_empty())
334 {
335 return Err("user_lifecycle_conflicts_with_active_capabilities");
336 }
337 Ok(())
338}
339
340pub fn validate_manifest_deferred(manifest: &ExtensionManifest) -> Result<(), &'static str> {
353 let Some(deferred) = &manifest.deferred else {
354 return Ok(());
355 };
356 validate_deferred(deferred)?;
357 let has_permission = |wire: &str| manifest.permissions.iter().any(|p| p == wire);
358 if !deferred.tools.is_empty() && !has_permission("tools.register") {
359 return Err("deferred_tools_require_tools_register_permission");
360 }
361 if !deferred.providers.is_empty() && !has_permission("providers.register") {
362 return Err("deferred_providers_require_providers_register_permission");
363 }
364 if !deferred.context_providers.is_empty() && !has_permission("context_providers.register") {
369 return Err("deferred_context_providers_require_context_providers_register_permission");
370 }
371 if deferred.lifecycle == Some(DeferredLifecycle::Hook) && manifest.hooks.is_empty() {
372 return Err("hook_lifecycle_requires_hook_subscriptions");
373 }
374 Ok(())
375}
376
377fn validate_tool_shape(
381 name: &str,
382 description: &str,
383 input_schema: &Value,
384) -> Result<(), &'static str> {
385 if !valid_name(name) || name.contains(':') {
386 return Err("invalid_declared_tool_name");
387 }
388 if description.trim().is_empty() || description.len() > DECLARED_DESCRIPTION_MAX_BYTES {
392 return Err("invalid_declared_tool_description");
393 }
394 if !input_schema.is_object() {
395 return Err("declared_tool_schema_not_object");
396 }
397 let schema_len = serde_json::to_vec(input_schema)
398 .map(|b| b.len())
399 .unwrap_or(usize::MAX);
400 if schema_len > DECLARED_SCHEMA_MAX_BYTES {
401 return Err("oversized_declared_tool_schema");
402 }
403 Ok(())
404}
405
406pub fn validate_runtime_tool_declarations(
412 declared: &[DeclaredExtensionTool],
413 registered: &[RegisteredExtensionToolSpec],
414) -> Result<(), &'static str> {
415 if declared.len() != registered.len() {
416 return Err("declared_and_registered_tool_counts_differ");
417 }
418 for spec in registered {
419 validate_tool_shape(&spec.name, &spec.description, &spec.input_schema)?;
422 }
423 for decl in declared {
424 let Some(live) = registered.iter().find(|spec| spec.name == decl.name) else {
425 return Err("declared_tool_not_registered");
426 };
427 if SchemaDigest::of_schema(&live.input_schema)
428 != SchemaDigest::of_schema(&decl.input_schema)
429 {
430 return Err("registered_tool_schema_digest_mismatch");
431 }
432 if live.description != decl.description {
435 return Err("registered_tool_description_mismatch");
436 }
437 }
438 let mut live_names: HashSet<&str> = HashSet::new();
442 for spec in registered {
443 if !live_names.insert(spec.name.as_str()) {
444 return Err("registered_tool_names_duplicate");
445 }
446 }
447 Ok(())
448}
449
450pub fn validate_runtime_provider_declarations(
459 declared: &[DeclaredExtensionProvider],
460 registered: &[crate::extensions::runtime::process::RegisteredProviderSpec],
461) -> Result<(), &'static str> {
462 if declared.len() != registered.len() {
463 return Err("declared_and_registered_provider_counts_differ");
464 }
465 let mut live_ids: HashSet<&str> = HashSet::new();
466 for spec in registered {
467 if !live_ids.insert(spec.id.as_str()) {
468 return Err("registered_provider_ids_duplicate");
469 }
470 }
471 for decl in declared {
472 let Some(live) = registered.iter().find(|spec| spec.id == decl.id) else {
473 return Err("declared_provider_not_registered");
474 };
475 if live.display_name != decl.display_name {
476 return Err("registered_provider_display_name_mismatch");
477 }
478 if live.description != decl.description {
479 return Err("registered_provider_description_mismatch");
480 }
481 if live.config_schema != decl.config_schema {
482 return Err("registered_provider_config_schema_mismatch");
483 }
484 if live.models.len() != decl.models.len() {
485 return Err("registered_provider_model_counts_differ");
486 }
487 let mut live_model_ids: HashSet<&str> = HashSet::new();
488 for model in &live.models {
489 if !live_model_ids.insert(model.id.as_str()) {
490 return Err("registered_provider_model_ids_duplicate");
491 }
492 }
493 for declared_model in &decl.models {
494 let Some(live_model) = live.models.iter().find(|m| m.id == declared_model.id) else {
495 return Err("declared_provider_model_not_registered");
496 };
497 if live_model.display_name != declared_model.display_name {
498 return Err("registered_provider_model_display_name_mismatch");
499 }
500 if live_model.context_window != declared_model.context_window {
501 return Err("registered_provider_model_context_window_mismatch");
502 }
503 if live_model.capabilities != declared_model.capabilities {
504 return Err("registered_provider_model_capabilities_mismatch");
505 }
506 }
507 }
508 Ok(())
509}
510
511impl DeclaredExtensionProvider {
512 pub fn to_registered_spec(
517 &self,
518 ) -> crate::extensions::runtime::process::RegisteredProviderSpec {
519 crate::extensions::runtime::process::RegisteredProviderSpec {
520 id: self.id.clone(),
521 display_name: self.display_name.clone(),
522 description: self.description.clone(),
523 models: self
524 .models
525 .iter()
526 .map(
527 |model| crate::extensions::runtime::process::RegisteredProviderModelSpec {
528 id: model.id.clone(),
529 display_name: model.display_name.clone(),
530 capabilities: model.capabilities.clone(),
531 context_window: model.context_window,
532 },
533 )
534 .collect(),
535 config_schema: self.config_schema.clone(),
536 }
537 }
538}
539
540pub struct DeferredExtensionTool {
549 plugin_id: String,
550 runtime_name: String,
551 tool_name: String,
552 description: String,
553 input_schema: Value,
554 expected_digest: SchemaDigest,
555}
556
557impl DeferredExtensionTool {
558 fn new(plugin_id: &str, declared: &DeclaredExtensionTool) -> Self {
559 Self {
560 plugin_id: plugin_id.to_string(),
561 runtime_name: format!("{}:{}", plugin_id, declared.name),
563 tool_name: declared.name.clone(),
564 description: BoundedText::new(&declared.description, DECLARED_DESCRIPTION_MAX_BYTES)
565 .text,
566 input_schema: declared.input_schema.clone(),
567 expected_digest: SchemaDigest::of_schema(&declared.input_schema),
568 }
569 }
570
571 pub fn plugin_id(&self) -> &str {
572 &self.plugin_id
573 }
574
575 pub fn tool_name(&self) -> &str {
576 &self.tool_name
577 }
578
579 pub fn expected_digest(&self) -> &SchemaDigest {
582 &self.expected_digest
583 }
584}
585
586#[async_trait::async_trait]
587impl Tool for DeferredExtensionTool {
588 fn origin(&self) -> ToolOrigin {
589 ToolOrigin::Extension {
590 extension_id: self.plugin_id.clone(),
591 }
592 }
593
594 fn name(&self) -> &str {
595 &self.runtime_name
596 }
597
598 fn description(&self) -> &str {
599 &self.description
600 }
601
602 fn parameters(&self) -> Value {
603 self.input_schema.clone()
604 }
605
606 async fn execute(&self, params: Value, ctx: ToolContext) -> crate::Result<String> {
607 let Some(leases) = ctx.capabilities.extension_leases.clone() else {
611 return Err(crate::RuntimeError::Tool(format!(
612 "extension tool '{}' (plugin '{}') is activation-deferred and no extension \
613 runtime lease capability is available in this context; no process was started",
614 self.runtime_name, self.plugin_id
615 )));
616 };
617 match leases
618 .call_exact(
619 &self.plugin_id,
620 &self.tool_name,
621 &self.expected_digest,
622 params,
623 )
624 .await
625 {
626 Ok(value) => Ok(render_tool_result(&value)),
627 Err(err) => {
628 if err.revokes_exact_grant() {
636 if let Some(activation) = ctx.capabilities.tool_activation.as_ref() {
637 if activation.revoke_exact_extension_grant(
638 leases.session(),
639 &self.plugin_id,
640 &self.tool_name,
641 ) {
642 tracing::debug!(
643 "revoked exact extension activation after declaration invalidation"
644 );
645 }
646 }
647 }
648 Err(crate::RuntimeError::Tool(err.to_string()))
649 }
650 }
651 }
652}
653
654fn render_tool_result(value: &Value) -> String {
658 if let Some(text) = value.as_str() {
659 text.to_string()
660 } else if let Some(text) = value.get("content").and_then(Value::as_str) {
661 text.to_string()
662 } else {
663 value.to_string()
664 }
665}
666
667impl DeclaredExtensionTool {
668 pub fn schema_digest(&self) -> SchemaDigest {
672 SchemaDigest::of_schema(&self.input_schema)
673 }
674}
675
676pub fn dormant_extension_tools(
680 plugin_id: &str,
681 manifest: &ExtensionManifest,
682) -> Vec<Arc<dyn Tool>> {
683 if plugin_id.is_empty() || plugin_id.contains(':') || plugin_id.chars().any(char::is_control) {
685 return Vec::new();
686 }
687 let Some(deferred) = &manifest.deferred else {
688 return Vec::new();
689 };
690 if validate_manifest_deferred(manifest).is_err() {
694 return Vec::new();
695 }
696 deferred
697 .tools
698 .iter()
699 .map(|declared| Arc::new(DeferredExtensionTool::new(plugin_id, declared)) as Arc<dyn Tool>)
700 .collect()
701}
702
703#[cfg(test)]
704mod tests {
705 use super::*;
706 use serde_json::json;
707
708 fn base_manifest(permissions: &[&str]) -> ExtensionManifest {
709 serde_json::from_value(json!({
710 "runtime": "process",
711 "command": "/bin/false",
712 "permissions": permissions,
713 }))
714 .unwrap()
715 }
716
717 fn declared_tool(name: &str) -> DeclaredExtensionTool {
718 DeclaredExtensionTool {
719 name: name.to_string(),
720 description: format!("declared {name}"),
721 input_schema: json!({"type": "object"}),
722 }
723 }
724
725 fn declared_context_provider(id: &str) -> DeclaredExtensionContextProvider {
726 DeclaredExtensionContextProvider {
727 id: id.to_string(),
728 capability: "project-memory".to_string(),
729 description: "Project memory context contributions".to_string(),
730 schema_version: 1,
731 }
732 }
733
734 fn with_context_providers(
735 permissions: &[&str],
736 context_providers: Vec<DeclaredExtensionContextProvider>,
737 ) -> ExtensionManifest {
738 let mut m = base_manifest(permissions);
739 m.deferred = Some(DeferredDeclarations {
740 context_providers,
741 ..DeferredDeclarations::default()
742 });
743 m
744 }
745
746 #[test]
749 fn absent_or_empty_deferred_blocks_classify_exactly_as_before() {
750 let legacy = base_manifest(&["tools.register"]);
752 assert!(legacy.deferred.is_none());
753 assert_eq!(classify(&legacy), ExtensionClass::LegacyEager);
754 assert_eq!(earliest_trigger(&legacy), ActivationTrigger::Eager);
755 legacy.validate("legacy").unwrap();
756
757 let mut empty = base_manifest(&["tools.register"]);
759 empty.deferred = Some(DeferredDeclarations::default());
760 assert!(empty
761 .deferred
762 .as_ref()
763 .is_some_and(|d| d.context_providers.is_empty() && !has_context_provider(d)));
764 assert_eq!(classify(&empty), ExtensionClass::LegacyEager);
765 assert_eq!(earliest_trigger(&empty), ActivationTrigger::Eager);
766 empty.validate("empty").unwrap();
767
768 let d: DeferredDeclarations = serde_json::from_value(json!({
772 "tools": [], "providers": []
773 }))
774 .unwrap();
775 assert!(d.context_providers.is_empty());
776 let round = serde_json::to_value(&d).unwrap();
777 assert!(round.get("context_providers").is_none());
778
779 let mut tool_only = base_manifest(&["tools.register"]);
782 tool_only.deferred = Some(DeferredDeclarations {
783 tools: vec![declared_tool("t")],
784 ..DeferredDeclarations::default()
785 });
786 assert_eq!(classify(&tool_only), ExtensionClass::ToolOnly);
787 let mut sidecar = base_manifest(&["tools.register"]);
789 sidecar.deferred = Some(DeferredDeclarations {
790 lifecycle: Some(DeferredLifecycle::User),
791 ..DeferredDeclarations::default()
792 });
793 assert_eq!(classify(&sidecar), ExtensionClass::UiSidecar);
794 let mut hooky: ExtensionManifest = serde_json::from_value(json!({
796 "runtime": "process",
797 "command": "/bin/false",
798 "permissions": ["tools.intercept"],
799 "hooks": [{"hook": "before_tool_call"}]
800 }))
801 .unwrap();
802 hooky.deferred = Some(DeferredDeclarations::default());
803 assert_eq!(classify(&hooky), ExtensionClass::HookLifecycle);
804 }
805
806 #[test]
809 fn context_provider_only_manifest_classifies_distinctly_and_requires_permission() {
810 let m = with_context_providers(
811 &["context_providers.register"],
812 vec![declared_context_provider("project-memory")],
813 );
814 assert!(m.deferred.as_ref().is_some_and(has_context_provider));
815 assert_eq!(classify(&m), ExtensionClass::ContextProvider);
816 assert_eq!(earliest_trigger(&m), ActivationTrigger::UserAction);
819 m.validate("axel-memory-manager").unwrap();
820
821 let ungated = with_context_providers(
823 &["tools.register"],
824 vec![declared_context_provider("project-memory")],
825 );
826 let err = ungated.validate("axel-memory-manager").unwrap_err();
827 assert!(
828 err.contains(
829 "deferred_context_providers_require_context_providers_register_permission"
830 ),
831 "{err}"
832 );
833 let wrong_gate = with_context_providers(
835 &["providers.register"],
836 vec![declared_context_provider("project-memory")],
837 );
838 assert!(wrong_gate.validate("axel-memory-manager").is_err());
839 }
840
841 #[test]
842 fn mixed_combinations_with_context_providers_still_classify_mixed() {
843 let mut m = with_context_providers(
845 &["tools.register", "context_providers.register"],
846 vec![declared_context_provider("project-memory")],
847 );
848 m.deferred.as_mut().unwrap().tools = vec![declared_tool("t")];
849 assert_eq!(classify(&m), ExtensionClass::Mixed);
850 m.validate("plug").unwrap();
851 assert_eq!(earliest_trigger(&m), ActivationTrigger::ExactToolActivation);
854 }
855
856 #[test]
859 fn too_many_declared_context_providers_are_rejected() {
860 let at_cap: Vec<_> = (0..DECLARED_MAX_CONTEXT_PROVIDERS)
861 .map(|i| declared_context_provider(&format!("p{i}")))
862 .collect();
863 with_context_providers(&["context_providers.register"], at_cap)
864 .validate("plug")
865 .unwrap();
866
867 let over: Vec<_> = (0..=DECLARED_MAX_CONTEXT_PROVIDERS)
868 .map(|i| declared_context_provider(&format!("p{i}")))
869 .collect();
870 let m = with_context_providers(&["context_providers.register"], over);
871 assert_eq!(
872 validate_deferred(m.deferred.as_ref().unwrap()),
873 Err("too_many_declared_context_providers")
874 );
875 assert!(m.validate("plug").is_err());
876 }
877
878 #[test]
879 fn oversized_or_hostile_context_provider_fields_are_rejected() {
880 let check = |p: DeclaredExtensionContextProvider, reason: &'static str| {
881 let m = with_context_providers(&["context_providers.register"], vec![p]);
882 assert_eq!(validate_deferred(m.deferred.as_ref().unwrap()), Err(reason));
883 assert!(m.validate("plug").is_err());
884 };
885 check(
887 declared_context_provider(&"i".repeat(DECLARED_NAME_MAX_BYTES + 1)),
888 "invalid_declared_context_provider_id",
889 );
890 check(
892 declared_context_provider(""),
893 "invalid_declared_context_provider_id",
894 );
895 check(
896 declared_context_provider("has:colon"),
897 "invalid_declared_context_provider_id",
898 );
899 let mut p = declared_context_provider("p");
901 p.capability = "c".repeat(DECLARED_NAME_MAX_BYTES + 1);
902 check(p, "invalid_declared_context_provider_capability");
903 let mut p = declared_context_provider("p");
904 p.capability = String::new();
905 check(p, "invalid_declared_context_provider_capability");
906 let mut p = declared_context_provider("p");
907 p.capability = "ctrl\u{7}".to_string();
908 check(p, "invalid_declared_context_provider_capability");
909 let mut p = declared_context_provider("p");
911 p.description = "d".repeat(DECLARED_DESCRIPTION_MAX_BYTES + 1);
912 check(p, "invalid_declared_context_provider_description");
913 let mut p = declared_context_provider("p");
914 p.description = String::new();
915 check(p, "invalid_declared_context_provider_description");
916 let mut p = declared_context_provider("p");
918 p.schema_version = 0;
919 check(p, "invalid_declared_context_provider_schema_version");
920 let m = with_context_providers(
922 &["context_providers.register"],
923 vec![
924 declared_context_provider("dup"),
925 declared_context_provider("dup"),
926 ],
927 );
928 assert_eq!(
929 validate_deferred(m.deferred.as_ref().unwrap()),
930 Err("duplicate_declared_context_provider_id")
931 );
932 let mut p = declared_context_provider(&"i".repeat(DECLARED_NAME_MAX_BYTES));
934 p.capability = "c".repeat(DECLARED_NAME_MAX_BYTES);
935 p.description = "d".repeat(DECLARED_DESCRIPTION_MAX_BYTES);
936 with_context_providers(&["context_providers.register"], vec![p])
937 .validate("plug")
938 .unwrap();
939 }
940
941 #[test]
944 fn dormant_descriptors_are_minted_only_for_validated_gated_manifests() {
945 use super::super::context_provider::dormant_context_provider_descriptors;
946 let m = with_context_providers(
947 &["context_providers.register"],
948 vec![declared_context_provider("project-memory")],
949 );
950 let descriptors = dormant_context_provider_descriptors("axel-memory-manager", &m);
951 assert_eq!(descriptors.len(), 1);
952 assert_eq!(
953 descriptors[0].runtime_address(),
954 "extension:axel-memory-manager:project-memory"
955 );
956 assert_eq!(descriptors[0].schema_version(), 1);
957
958 let ungated = with_context_providers(
961 &["tools.register"],
962 vec![declared_context_provider("project-memory")],
963 );
964 assert!(dormant_context_provider_descriptors("axel-memory-manager", &ungated).is_empty());
965 assert!(dormant_context_provider_descriptors("bad:plugin", &m).is_empty());
967 assert!(
969 dormant_context_provider_descriptors("p", &base_manifest(&["tools.register"]))
970 .is_empty()
971 );
972 }
973}