diff --git a/crates/pod/src/controller.rs b/crates/pod/src/controller.rs index 79046c6e..1b1e9c83 100644 --- a/crates/pod/src/controller.rs +++ b/crates/pod/src/controller.rs @@ -9,7 +9,7 @@ use session_store::Store; use tokio::sync::{broadcast, mpsc, oneshot}; use crate::discovery::{PodDiscovery, list_pods_tool, restore_pod_tool, send_to_peer_pod_tool}; -use crate::feature::{FeatureRegistryBuilder, builtin::task_feature}; +use crate::feature::{FeatureRegistryBuilder, builtin::task_tools_feature}; use crate::ipc::alerter::Alerter; use crate::ipc::notify_buffer::NotifyBuffer; use crate::ipc::server::SocketServer; @@ -522,7 +522,7 @@ where )); let mut feature_registry = FeatureRegistryBuilder::new(); - feature_registry.add_module(task_feature(task_store)); + feature_registry.add_module(task_tools_feature(task_store)); let _feature_install_report = pod.install_features(feature_registry); let worker = pod.worker_mut(); diff --git a/crates/pod/src/feature.rs b/crates/pod/src/feature.rs index 377ac7d9..24ea6d50 100644 --- a/crates/pod/src/feature.rs +++ b/crates/pod/src/feature.rs @@ -1256,59 +1256,7 @@ pub enum FeatureInstallError { Install(String), } -/// Builtin task tools feature used to prove existing builtin tool registration -/// through the feature registry without changing tool names, schemas, or -/// permission behavior. -pub mod builtin { - use super::*; - - pub fn task_feature(task_store: tools::TaskStore) -> impl FeatureModule { - TaskFeature { task_store } - } - - struct TaskFeature { - task_store: tools::TaskStore, - } - - impl FeatureModule for TaskFeature { - fn descriptor(&self) -> FeatureDescriptor { - FeatureDescriptor::builtin("task-tools", "Task tools") - .with_description("Session-lifetime task tracking builtin tools") - .with_tool(ToolDeclaration::new( - "TaskCreate", - "Create a session-lifetime user-visible task", - )) - .with_tool(ToolDeclaration::new( - "TaskUpdate", - "Update a session-lifetime user-visible task", - )) - .with_tool(ToolDeclaration::new( - "TaskGet", - "Get one session-lifetime user-visible task", - )) - .with_tool(ToolDeclaration::new( - "TaskList", - "List session-lifetime user-visible tasks", - )) - } - - fn install( - &self, - context: &mut FeatureInstallContext<'_>, - ) -> Result<(), FeatureInstallError> { - let names = ["TaskCreate", "TaskList", "TaskGet", "TaskUpdate"]; - for (name, definition) in names - .into_iter() - .zip(tools::task_tools(self.task_store.clone())) - { - context - .tools() - .register(ToolContribution::new(name, definition))?; - } - Ok(()) - } - } -} +pub mod builtin; #[cfg(test)] mod tests { @@ -1797,13 +1745,76 @@ mod tests { assert!(report.reports[0].skipped.is_empty()); } + #[test] + fn builtin_internal_task_feature_descriptor_has_exact_tools_and_no_authorities() { + let descriptor = builtin::task_tools_feature(tools::TaskStore::new()).descriptor(); + let tool_names: Vec<_> = descriptor + .tools + .iter() + .map(|tool| tool.name.as_str()) + .collect(); + + assert_eq!(descriptor.id.as_str(), "builtin:task-tools"); + assert_eq!(descriptor.runtime, FeatureRuntimeKind::Builtin); + assert!(descriptor.requested_authorities.is_empty()); + assert!(descriptor.hooks.is_empty()); + assert!(descriptor.background_tasks.is_empty()); + assert!(descriptor.provides_services.is_empty()); + assert!(descriptor.requires_services.is_empty()); + assert_eq!( + tool_names, + vec!["TaskCreate", "TaskUpdate", "TaskGet", "TaskList"] + ); + } + + #[test] + fn builtin_internal_task_feature_installs_declared_tools_without_host_authorities() { + let task_store = tools::TaskStore::new(); + let mut hook_builder = HookRegistryBuilder::default(); + let mut pending_tools = Vec::new(); + let mut builder = FeatureRegistryBuilder::new(); + builder.add_module(builtin::task_tools_feature(task_store)); + let mut declared_names: Vec<_> = builder.descriptors()[0] + .tools + .iter() + .map(|tool| tool.name.clone()) + .collect(); + let report = builder.install_into_pending(&mut pending_tools, &mut hook_builder); + let pending_names: Vec<_> = pending_tools + .iter() + .map(|definition| definition().0.name) + .collect(); + let installed_names = report.installed_tool_names(); + let mut sorted_installed_names = installed_names.clone(); + declared_names.sort(); + sorted_installed_names.sort(); + + assert_eq!(report.reports.len(), 1); + assert!(report.reports[0].installed); + assert_eq!( + report.reports[0].granted_authorities, + AuthorityGrantSet::empty() + ); + assert!(report.reports[0].skipped.is_empty()); + assert!(report.reports[0].diagnostics.is_empty()); + assert_eq!(declared_names, sorted_installed_names); + assert_eq!( + installed_names, + vec!["TaskCreate", "TaskList", "TaskGet", "TaskUpdate"] + ); + assert_eq!( + pending_names, + vec!["TaskCreate", "TaskList", "TaskGet", "TaskUpdate"] + ); + } + #[test] fn builtin_task_feature_installs_through_worker_tool_path() { let task_store = tools::TaskStore::new(); let mut worker = Worker::new(DummyClient); let mut hook_builder = HookRegistryBuilder::default(); let report = FeatureRegistryBuilder::new() - .with_module(builtin::task_feature(task_store)) + .with_module(builtin::task_tools_feature(task_store)) .install_into_worker(&mut worker, &mut hook_builder); worker.tool_server_handle().flush_pending(); diff --git a/crates/pod/src/feature/builtin.rs b/crates/pod/src/feature/builtin.rs new file mode 100644 index 00000000..55591b04 --- /dev/null +++ b/crates/pod/src/feature/builtin.rs @@ -0,0 +1,9 @@ +//! Built-in internal feature modules. +//! +//! These modules are compiled into the Pod host and contribute through the +//! same descriptor-approved registry path used by feature modules. They are not +//! an external plugin-loading surface. + +pub mod task; + +pub use task::task_tools_feature; diff --git a/crates/pod/src/feature/builtin/task.rs b/crates/pod/src/feature/builtin/task.rs new file mode 100644 index 00000000..b476ebfb --- /dev/null +++ b/crates/pod/src/feature/builtin/task.rs @@ -0,0 +1,61 @@ +//! Task tools built-in feature module. +//! +//! This is the reference path for extracting an internal built-in module into +//! the feature contribution boundary. The Pod host still owns the Pod-lifetime +//! [`tools::TaskStore`] and passes the shared handle in at construction time; +//! the module requests no sandbox/external-plugin host authorities. + +use crate::feature::{ + FeatureDescriptor, FeatureInstallContext, FeatureInstallError, FeatureModule, ToolContribution, + ToolDeclaration, +}; + +/// Construct the built-in Task tool feature module. +/// +/// The returned module contributes only `TaskCreate`, `TaskUpdate`, `TaskGet`, +/// and `TaskList` through descriptor-approved tool registration. It does not +/// request host authorities; normal ToolRegistry and PreToolCall permission +/// policy still applies at call time. +pub fn task_tools_feature(task_store: tools::TaskStore) -> impl FeatureModule { + TaskToolsFeature { task_store } +} + +struct TaskToolsFeature { + task_store: tools::TaskStore, +} + +impl FeatureModule for TaskToolsFeature { + fn descriptor(&self) -> FeatureDescriptor { + FeatureDescriptor::builtin("task-tools", "Task tools") + .with_description("Session-lifetime task tracking builtin tools") + .with_tool(ToolDeclaration::new( + "TaskCreate", + "Create a session-lifetime user-visible task", + )) + .with_tool(ToolDeclaration::new( + "TaskUpdate", + "Update a session-lifetime user-visible task", + )) + .with_tool(ToolDeclaration::new( + "TaskGet", + "Get one session-lifetime user-visible task", + )) + .with_tool(ToolDeclaration::new( + "TaskList", + "List session-lifetime user-visible tasks", + )) + } + + fn install(&self, context: &mut FeatureInstallContext<'_>) -> Result<(), FeatureInstallError> { + let names = ["TaskCreate", "TaskList", "TaskGet", "TaskUpdate"]; + for (name, definition) in names + .into_iter() + .zip(tools::task_tools(self.task_store.clone())) + { + context + .tools() + .register(ToolContribution::new(name, definition))?; + } + Ok(()) + } +}