starweaver_runtime/
dependency_assembly.rs1use starweaver_context::{
4 AgentContext, AgentContextHandle, ContextMutationHandles, DependencyStore,
5};
6use starweaver_tools::{ToolDependencyProfile, ToolDependencyRequirements};
7
8#[derive(Clone, Debug)]
10pub struct ToolDependencyAssembly {
11 pub dependencies: DependencyStore,
13 pub legacy_context: Option<AgentContextHandle>,
15 pub context_mutations: ContextMutationHandles,
17 pub authorized_context_capabilities: std::collections::BTreeSet<String>,
19}
20
21impl ToolDependencyAssembly {
22 pub fn apply_to(&self, context: &mut AgentContext) {
24 if let Some(handle) = &self.legacy_context {
25 absorb_legacy_context(context, &handle.snapshot());
26 }
27 self.context_mutations
28 .apply_to(context, &self.authorized_context_capabilities);
29 }
30}
31
32#[must_use]
34pub fn assemble_tool_dependencies_for_name(
35 context: &AgentContext,
36 tool_name: &str,
37 requirements: &ToolDependencyRequirements,
38 strict_grant: &starweaver_context::ToolCapabilityGrant,
39) -> ToolDependencyAssembly {
40 let strict_grant = if tool_name.is_empty() {
41 strict_grant.clone()
42 } else {
43 context.tool_capability_grant(tool_name)
44 };
45 let grant_intersected = requirements.profile == ToolDependencyProfile::Strict;
46 let authorized_host_capabilities = if grant_intersected {
47 requirements
48 .host_capabilities
49 .intersection(&strict_grant.host_capabilities)
50 .cloned()
51 .collect()
52 } else {
53 requirements.host_capabilities.clone()
54 };
55 let authorized_context_capabilities = if grant_intersected {
56 requirements
57 .context_capabilities
58 .intersection(&strict_grant.context_capabilities)
59 .cloned()
60 .collect()
61 } else {
62 requirements.context_capabilities.clone()
63 };
64 let shell_environment_authorized =
65 requirements.shell_environment && (!grant_intersected || strict_grant.shell_environment);
66 let context_mutations = ContextMutationHandles::from_context(context);
67 let legacy_context = (requirements.profile == ToolDependencyProfile::Legacy)
68 .then(|| AgentContextHandle::new(context.clone()));
69 let mut dependencies = match requirements.profile {
70 ToolDependencyProfile::Legacy => context.tool_dependency_store(),
71 ToolDependencyProfile::Filtered => context.filtered_tool_dependency_store(
72 &authorized_host_capabilities,
73 shell_environment_authorized,
74 ),
75 ToolDependencyProfile::Strict => context.strict_tool_dependency_store(
76 &authorized_host_capabilities,
77 shell_environment_authorized,
78 ),
79 };
80 if let Some(handle) = &legacy_context {
81 dependencies.insert(handle.clone());
82 }
83 context_mutations.insert_grants(&mut dependencies, &authorized_context_capabilities);
84 ToolDependencyAssembly {
85 dependencies,
86 legacy_context,
87 context_mutations,
88 authorized_context_capabilities,
89 }
90}
91
92fn absorb_legacy_context(context: &mut AgentContext, snapshot: &AgentContext) {
93 context.usage.clone_from(&snapshot.usage);
94 context.notes.clone_from(&snapshot.notes);
95 context.state.clone_from(&snapshot.state);
96 context.tools.clone_from(&snapshot.tools);
97 context.events.clone_from(&snapshot.events);
98 context.messages.clone_from(&snapshot.messages);
99 context.metadata.clone_from(&snapshot.metadata);
100 context.agent_registry.clone_from(&snapshot.agent_registry);
101 context
102 .subagent_history
103 .clone_from(&snapshot.subagent_history);
104 context
105 .handoff_message
106 .clone_from(&snapshot.handoff_message);
107 context
108 .runtime
109 .context_manage_tool_names
110 .clone_from(&snapshot.runtime.context_manage_tool_names);
111 context
112 .runtime
113 .tool_tags
114 .clone_from(&snapshot.runtime.tool_tags);
115 context
116 .runtime
117 .wrapper_metadata
118 .clone_from(&snapshot.runtime.wrapper_metadata);
119}
120
121#[cfg(test)]
122mod tests {
123 use starweaver_context::{AgentContext, HostCapabilities, ToolCapabilityGrant};
124 use starweaver_core::AgentId;
125 use starweaver_tools::ToolDependencyRequirements;
126
127 use super::assemble_tool_dependencies_for_name;
128
129 #[derive(Debug)]
130 struct AllowedCapability;
131
132 #[derive(Debug)]
133 struct AmbientProductDependency;
134
135 #[test]
136 fn granted_filtered_intersects_named_grants_and_excludes_ambient_dependencies() {
137 let mut context = AgentContext::new(AgentId::from_string("test"));
138 context.insert_named_dependency("allowed", AllowedCapability);
139 context.insert_named_dependency("ambient", AmbientProductDependency);
140 context.grant_tool_capabilities(
141 "safe_tool",
142 ToolCapabilityGrant::new().with_host_capabilities(["allowed"]),
143 );
144 let requirements =
145 ToolDependencyRequirements::granted_filtered(["allowed", "ambient"], false);
146
147 let assembly = assemble_tool_dependencies_for_name(
148 &context,
149 "safe_tool",
150 &requirements,
151 &ToolCapabilityGrant::new(),
152 );
153 let Some(host) = assembly.dependencies.get::<HostCapabilities>() else {
154 panic!("generated host capabilities should exist");
155 };
156
157 assert!(host.get_named::<AllowedCapability>("allowed").is_some());
158 assert!(
159 host.get_named::<AmbientProductDependency>("ambient")
160 .is_none()
161 );
162 assert!(assembly.dependencies.get::<AllowedCapability>().is_none());
163 assert!(
164 assembly
165 .dependencies
166 .get::<AmbientProductDependency>()
167 .is_none()
168 );
169 }
170}