Skip to main content

starweaver_runtime/
dependency_assembly.rs

1//! Shared least-authority tool dependency assembly.
2
3use starweaver_context::{
4    AgentContext, AgentContextHandle, ContextMutationHandles, DependencyStore,
5};
6use starweaver_tools::{ToolDependencyProfile, ToolDependencyRequirements};
7
8/// Dependencies and mutation cells assembled for one tool invocation.
9#[derive(Clone, Debug)]
10pub struct ToolDependencyAssembly {
11    /// Typed dependencies exposed to the tool.
12    pub dependencies: DependencyStore,
13    /// Full compatibility context handle, present only for the Legacy profile.
14    pub legacy_context: Option<AgentContextHandle>,
15    /// Isolated mutable context capability cells.
16    pub context_mutations: ContextMutationHandles,
17    /// Context capability names authorized for this invocation.
18    pub authorized_context_capabilities: std::collections::BTreeSet<String>,
19}
20
21impl ToolDependencyAssembly {
22    /// Apply tool-owned context changes back to the runtime context.
23    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/// Assemble one named tool's dependencies from declared requirements and current host grants.
33#[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}