yoi/crates/pod/src/feature.rs

1828 lines
59 KiB
Rust

//! Feature contribution registry for Pod-hosted builtin/plugin modules.
//!
//! This module defines the Pod-side feature boundary used to collect
//! descriptor metadata, authority requests, tool contributions, safe hook
//! contributions, background task declarations, and service declarations before
//! installing them into the existing Worker/HookRegistry host surfaces.
//!
//! The first implementation slice is intentionally host-mediated and
//! descriptor-first: tools are installed through the normal Worker tool path,
//! hooks are installed through [`crate::hook::HookRegistryBuilder`], while
//! service and background-task contributions are represented in descriptors and
//! install reports without starting an independent runtime lifecycle.
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::sync::Arc;
use llm_worker::Worker;
use llm_worker::llm_client::client::LlmClient;
use llm_worker::state::Mutable;
use llm_worker::tool::ToolDefinition;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::hook::{Hook, HookRegistryBuilder, OnTurnEnd, PostToolCall, PreLlmRequest, PreToolCall};
/// Stable source-qualified identifier for a feature module.
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct FeatureId(String);
impl FeatureId {
pub fn new(value: impl Into<String>) -> Result<Self, FeatureInstallError> {
let value = value.into();
if value.trim().is_empty() {
return Err(FeatureInstallError::InvalidDescriptor(
"feature id must not be empty".into(),
));
}
Ok(Self(value))
}
pub fn builtin(slug: impl AsRef<str>) -> Self {
Self(format!("builtin:{}", slug.as_ref()))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for FeatureId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl From<FeatureId> for String {
fn from(value: FeatureId) -> Self {
value.0
}
}
/// Runtime/source class for a feature module.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FeatureRuntimeKind {
Builtin,
LuaProfile,
ExternalPlugin,
}
/// Host authority requested by a feature for host-mediated operations that can
/// cross sandbox or model-context boundaries.
///
/// Contribution declarations such as tools, hooks, background tasks, and
/// services are descriptor/package-approved host-visible contributions, not
/// sandbox authorities. Grants are additive and do not replace manifest/tool
/// permission checks.
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HostAuthority {
Filesystem,
Network,
SecretRef { id: String },
ModelNotification,
PodManagement,
StateStore { name: String },
ServiceAccess { service: ServiceId },
}
/// A safe hook contribution point exposed to feature modules.
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FeatureHookPoint {
PreRequest,
PreToolCall,
ToolResult,
TurnEnd,
}
/// Authority request declared by a feature descriptor.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthorityRequest {
pub authority: HostAuthority,
pub required: bool,
pub reason: String,
}
impl AuthorityRequest {
pub fn required(authority: HostAuthority, reason: impl Into<String>) -> Self {
Self {
authority,
required: true,
reason: reason.into(),
}
}
pub fn optional(authority: HostAuthority, reason: impl Into<String>) -> Self {
Self {
authority,
required: false,
reason: reason.into(),
}
}
}
/// Authority grants resolved by the host for one feature installation.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthorityGrantSet {
granted: HashSet<HostAuthority>,
denied: Vec<AuthorityDenial>,
}
impl AuthorityGrantSet {
pub fn grant_all(requests: &[AuthorityRequest]) -> Self {
Self {
granted: requests
.iter()
.map(|request| request.authority.clone())
.collect(),
denied: Vec::new(),
}
}
pub fn empty() -> Self {
Self::default()
}
pub fn contains(&self, authority: &HostAuthority) -> bool {
self.granted.contains(authority)
}
pub fn denied(&self) -> &[AuthorityDenial] {
&self.denied
}
pub fn grant(&mut self, authority: HostAuthority) {
self.granted.insert(authority);
}
pub fn deny(&mut self, authority: HostAuthority, reason: impl Into<String>) {
self.granted.remove(&authority);
self.denied.push(AuthorityDenial {
authority,
reason: reason.into(),
});
}
}
/// Host-side denial of a requested feature authority.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthorityDenial {
pub authority: HostAuthority,
pub reason: String,
}
/// Serializable declaration of a tool contribution. The executable factory is
/// carried by [`ToolContribution`] during installation.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolDeclaration {
pub name: String,
pub description: String,
}
impl ToolDeclaration {
pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
Self {
name: name.into(),
description: description.into(),
}
}
}
/// Executable tool contribution wrapper.
pub struct ToolContribution {
name: String,
definition: ToolDefinition,
required_authorities: Vec<HostAuthority>,
}
impl ToolContribution {
pub fn new(name: impl Into<String>, definition: ToolDefinition) -> Self {
Self {
name: name.into(),
definition,
required_authorities: Vec::new(),
}
}
pub fn with_required_authorities(mut self, required_authorities: Vec<HostAuthority>) -> Self {
self.required_authorities = required_authorities;
self
}
pub fn name(&self) -> &str {
&self.name
}
}
/// Serializable declaration of a hook contribution.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct HookDeclaration {
pub name: String,
pub point: FeatureHookPoint,
}
impl HookDeclaration {
pub fn new(name: impl Into<String>, point: FeatureHookPoint) -> Self {
Self {
name: name.into(),
point,
}
}
}
/// Background task lifecycle phase represented by this registry slice.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BackgroundTaskLifecycle {
DescriptorOnly,
HostManaged,
}
/// Declaration for a feature-provided background task.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct BackgroundTaskDeclaration {
pub name: String,
pub description: String,
pub lifecycle: BackgroundTaskLifecycle,
}
impl BackgroundTaskDeclaration {
pub fn descriptor_only(name: impl Into<String>, description: impl Into<String>) -> Self {
Self {
name: name.into(),
description: description.into(),
lifecycle: BackgroundTaskLifecycle::DescriptorOnly,
}
}
}
/// Source-qualified service identifier.
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct ServiceId(String);
impl ServiceId {
pub fn new(value: impl Into<String>) -> Result<Self, FeatureInstallError> {
let value = value.into();
if value.trim().is_empty() {
return Err(FeatureInstallError::InvalidDescriptor(
"service id must not be empty".into(),
));
}
Ok(Self(value))
}
pub fn builtin(slug: impl AsRef<str>) -> Self {
Self(format!("builtin:{}", slug.as_ref()))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for ServiceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
/// Minimal version requirement placeholder for service resolution.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServiceVersionReq {
pub requirement: String,
}
impl ServiceVersionReq {
pub fn any() -> Self {
Self {
requirement: "*".into(),
}
}
}
/// Feature-provided service declaration. This first slice records provider
/// metadata and supports requirement matching; it does not expose concrete
/// provider objects across feature boundaries.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServiceDeclaration {
pub id: ServiceId,
pub version: String,
pub description: String,
}
impl ServiceDeclaration {
pub fn new(id: ServiceId, version: impl Into<String>, description: impl Into<String>) -> Self {
Self {
id,
version: version.into(),
description: description.into(),
}
}
}
/// Feature service requirement used for host-mediated dependency resolution.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct ServiceRequirement {
pub id: ServiceId,
pub version: ServiceVersionReq,
pub required: bool,
pub reason: String,
}
impl ServiceRequirement {
pub fn required(id: ServiceId, reason: impl Into<String>) -> Self {
Self {
id,
version: ServiceVersionReq::any(),
required: true,
reason: reason.into(),
}
}
pub fn optional(id: ServiceId, reason: impl Into<String>) -> Self {
Self {
id,
version: ServiceVersionReq::any(),
required: false,
reason: reason.into(),
}
}
}
/// Host-mediated service registry skeleton used during feature installation.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct FeatureServiceRegistry {
providers: HashMap<ServiceId, FeatureServiceProvider>,
}
impl FeatureServiceRegistry {
pub fn providers(&self) -> &HashMap<ServiceId, FeatureServiceProvider> {
&self.providers
}
pub fn provides(&self, id: &ServiceId) -> bool {
self.providers.contains_key(id)
}
fn register_provider(
&mut self,
feature_id: FeatureId,
declaration: ServiceDeclaration,
) -> Result<(), FeatureInstallError> {
if let Some(existing) = self.providers.get(&declaration.id) {
return Err(FeatureInstallError::DuplicateService {
service: declaration.id.to_string(),
first_feature: existing.feature_id.to_string(),
duplicate_feature: feature_id.to_string(),
});
}
self.providers.insert(
declaration.id.clone(),
FeatureServiceProvider {
feature_id,
declaration,
},
);
Ok(())
}
}
/// Provider metadata for one service declaration.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct FeatureServiceProvider {
pub feature_id: FeatureId,
pub declaration: ServiceDeclaration,
}
/// Feature descriptor advertised before installation.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct FeatureDescriptor {
pub id: FeatureId,
pub runtime: FeatureRuntimeKind,
pub display_name: String,
pub version: String,
pub description: String,
pub requested_authorities: Vec<AuthorityRequest>,
pub tools: Vec<ToolDeclaration>,
pub hooks: Vec<HookDeclaration>,
pub background_tasks: Vec<BackgroundTaskDeclaration>,
pub provides_services: Vec<ServiceDeclaration>,
pub requires_services: Vec<ServiceRequirement>,
}
impl FeatureDescriptor {
pub fn builtin(id: impl AsRef<str>, display_name: impl Into<String>) -> Self {
Self {
id: FeatureId::builtin(id),
runtime: FeatureRuntimeKind::Builtin,
display_name: display_name.into(),
version: env!("CARGO_PKG_VERSION").into(),
description: String::new(),
requested_authorities: Vec::new(),
tools: Vec::new(),
hooks: Vec::new(),
background_tasks: Vec::new(),
provides_services: Vec::new(),
requires_services: Vec::new(),
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = description.into();
self
}
pub fn with_authority(mut self, request: AuthorityRequest) -> Self {
self.requested_authorities.push(request);
self
}
pub fn with_tool(mut self, tool: ToolDeclaration) -> Self {
self.tools.push(tool);
self
}
pub fn with_hook(mut self, hook: HookDeclaration) -> Self {
self.hooks.push(hook);
self
}
pub fn with_background_task(mut self, task: BackgroundTaskDeclaration) -> Self {
self.background_tasks.push(task);
self
}
pub fn with_provided_service(mut self, service: ServiceDeclaration) -> Self {
self.provides_services.push(service);
self
}
pub fn with_service_requirement(mut self, requirement: ServiceRequirement) -> Self {
self.requires_services.push(requirement);
self
}
}
/// Feature module contribution boundary.
pub trait FeatureModule: Send + Sync {
fn descriptor(&self) -> FeatureDescriptor;
fn install(&self, context: &mut FeatureInstallContext<'_>) -> Result<(), FeatureInstallError>;
}
/// Severity for feature installation diagnostics.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FeatureDiagnosticSeverity {
Info,
Warning,
Error,
}
/// Installation diagnostic emitted by the feature host or feature module.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct FeatureDiagnostic {
pub severity: FeatureDiagnosticSeverity,
pub message: String,
}
impl FeatureDiagnostic {
pub fn info(message: impl Into<String>) -> Self {
Self {
severity: FeatureDiagnosticSeverity::Info,
message: message.into(),
}
}
pub fn warning(message: impl Into<String>) -> Self {
Self {
severity: FeatureDiagnosticSeverity::Warning,
message: message.into(),
}
}
pub fn error(message: impl Into<String>) -> Self {
Self {
severity: FeatureDiagnosticSeverity::Error,
message: message.into(),
}
}
}
/// Kind of contribution represented in install reports.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FeatureContributionKind {
Tool,
Hook,
BackgroundTask,
Service,
Notification,
Alert,
Diagnostic,
}
/// A contribution intentionally skipped by the host.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct SkippedContribution {
pub kind: FeatureContributionKind,
pub name: String,
pub reason: String,
}
/// Per-feature installation report.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct FeatureInstallReport {
pub feature_id: FeatureId,
pub runtime: FeatureRuntimeKind,
pub installed: bool,
pub granted_authorities: AuthorityGrantSet,
pub installed_tools: Vec<String>,
pub installed_hooks: Vec<HookDeclaration>,
pub declared_background_tasks: Vec<BackgroundTaskDeclaration>,
pub provided_services: Vec<ServiceDeclaration>,
pub resolved_service_requirements: Vec<ServiceRequirement>,
pub skipped: Vec<SkippedContribution>,
pub diagnostics: Vec<FeatureDiagnostic>,
}
impl FeatureInstallReport {
fn new(descriptor: &FeatureDescriptor, granted_authorities: AuthorityGrantSet) -> Self {
Self {
feature_id: descriptor.id.clone(),
runtime: descriptor.runtime.clone(),
installed: false,
granted_authorities,
installed_tools: Vec::new(),
installed_hooks: Vec::new(),
declared_background_tasks: Vec::new(),
provided_services: Vec::new(),
resolved_service_requirements: Vec::new(),
skipped: Vec::new(),
diagnostics: Vec::new(),
}
}
fn mark_skipped(
&mut self,
kind: FeatureContributionKind,
name: impl Into<String>,
reason: impl Into<String>,
) {
self.skipped.push(SkippedContribution {
kind,
name: name.into(),
reason: reason.into(),
});
}
}
#[derive(Clone, Debug)]
struct FeatureContributionDeclarations {
tools: HashSet<String>,
hooks: HashSet<(String, FeatureHookPoint)>,
background_tasks: HashSet<String>,
provided_services: HashSet<(ServiceId, String)>,
}
impl FeatureContributionDeclarations {
fn from_descriptor(descriptor: &FeatureDescriptor) -> Self {
Self {
tools: descriptor
.tools
.iter()
.map(|tool| tool.name.clone())
.collect(),
hooks: descriptor
.hooks
.iter()
.map(|hook| (hook.name.clone(), hook.point.clone()))
.collect(),
background_tasks: descriptor
.background_tasks
.iter()
.map(|task| task.name.clone())
.collect(),
provided_services: descriptor
.provides_services
.iter()
.map(|service| (service.id.clone(), service.version.clone()))
.collect(),
}
}
fn contains_tool(&self, name: &str) -> bool {
self.tools.contains(name)
}
fn contains_hook(&self, declaration: &HookDeclaration) -> bool {
self.hooks
.contains(&(declaration.name.clone(), declaration.point.clone()))
}
fn contains_background_task(&self, declaration: &BackgroundTaskDeclaration) -> bool {
self.background_tasks.contains(&declaration.name)
}
fn contains_provided_service(&self, declaration: &ServiceDeclaration) -> bool {
self.provided_services
.contains(&(declaration.id.clone(), declaration.version.clone()))
}
}
fn reject_undeclared_contribution(
feature_id: &FeatureId,
report: &mut FeatureInstallReport,
kind: FeatureContributionKind,
name: impl Into<String>,
) -> FeatureInstallError {
let name = name.into();
let error = FeatureInstallError::UndeclaredContribution {
kind: kind.clone(),
name: name.clone(),
feature: feature_id.to_string(),
};
report.mark_skipped(kind, name, error.to_string());
error
}
fn require_authority(
grants: &AuthorityGrantSet,
report: &mut FeatureInstallReport,
kind: FeatureContributionKind,
name: impl Into<String>,
authority: &HostAuthority,
) -> Result<(), FeatureInstallError> {
if grants.contains(authority) {
return Ok(());
}
let reason = format!("required authority was not granted: {authority:?}");
report.mark_skipped(kind, name, reason.clone());
Err(FeatureInstallError::AuthorityDenied(reason))
}
/// Model-visible durable notification sink skeleton. The first slice exposes
/// the boundary without implementing a new event channel.
pub struct FeatureNotificationSink<'a> {
grants: &'a AuthorityGrantSet,
report: &'a mut FeatureInstallReport,
}
impl FeatureNotificationSink<'_> {
pub fn notify_model(&mut self, message: impl Into<String>) -> Result<(), FeatureInstallError> {
require_authority(
self.grants,
self.report,
FeatureContributionKind::Notification,
"notify_model",
&HostAuthority::ModelNotification,
)?;
let message = message.into();
self.report.diagnostics.push(FeatureDiagnostic::warning(format!(
"model notification requested during feature installation but no durable Notify host is attached: {message}"
)));
self.report.mark_skipped(
FeatureContributionKind::Notification,
"notify_model",
"durable Notify/SystemItem host is not connected during feature installation",
);
Ok(())
}
}
/// Transient human-facing alert sink skeleton.
pub struct FeatureAlertSink<'a> {
report: &'a mut FeatureInstallReport,
}
impl FeatureAlertSink<'_> {
pub fn alert(&mut self, message: impl Into<String>) {
let message = message.into();
self.report
.diagnostics
.push(FeatureDiagnostic::info(format!("feature alert: {message}")));
self.report.mark_skipped(
FeatureContributionKind::Alert,
"alert",
"transient alert host is not connected during feature installation",
);
}
}
/// Diagnostic sink available to feature installers.
pub struct FeatureDiagnosticSink<'a> {
report: &'a mut FeatureInstallReport,
}
impl FeatureDiagnosticSink<'_> {
pub fn push(&mut self, diagnostic: FeatureDiagnostic) {
self.report.diagnostics.push(diagnostic);
}
pub fn info(&mut self, message: impl Into<String>) {
self.push(FeatureDiagnostic::info(message));
}
pub fn warning(&mut self, message: impl Into<String>) {
self.push(FeatureDiagnostic::warning(message));
}
pub fn error(&mut self, message: impl Into<String>) {
self.push(FeatureDiagnostic::error(message));
}
}
/// Tool contribution registrar exposed inside [`FeatureInstallContext`].
pub struct ToolContributionRegistrar<'a> {
feature_id: &'a FeatureId,
declarations: &'a FeatureContributionDeclarations,
grants: &'a AuthorityGrantSet,
pending_tools: &'a mut Vec<ToolDefinition>,
installed_tool_names: &'a mut HashMap<String, FeatureId>,
report: &'a mut FeatureInstallReport,
}
impl ToolContributionRegistrar<'_> {
pub fn register(&mut self, contribution: ToolContribution) -> Result<(), FeatureInstallError> {
let (tool_meta, tool) = (contribution.definition)();
let model_visible_name = tool_meta.name.clone();
if contribution.name != model_visible_name {
let error = FeatureInstallError::ToolNameMismatch {
declared: contribution.name,
model_visible: model_visible_name.clone(),
};
self.report.mark_skipped(
FeatureContributionKind::Tool,
model_visible_name,
error.to_string(),
);
return Err(error);
}
if !self.declarations.contains_tool(&model_visible_name) {
return Err(reject_undeclared_contribution(
self.feature_id,
self.report,
FeatureContributionKind::Tool,
model_visible_name,
));
}
for authority in &contribution.required_authorities {
require_authority(
self.grants,
self.report,
FeatureContributionKind::Tool,
model_visible_name.clone(),
authority,
)?;
}
if let Some(first) = self.installed_tool_names.get(&model_visible_name) {
let error = FeatureInstallError::DuplicateToolName {
tool: model_visible_name.clone(),
first_feature: first.to_string(),
duplicate_feature: self.feature_id.to_string(),
};
self.report.mark_skipped(
FeatureContributionKind::Tool,
model_visible_name,
error.to_string(),
);
return Err(error);
}
self.installed_tool_names
.insert(model_visible_name.clone(), self.feature_id.clone());
self.report.installed_tools.push(model_visible_name);
self.pending_tools
.push(Arc::new(move || (tool_meta.clone(), Arc::clone(&tool))));
Ok(())
}
}
/// Safe hook contribution registrar backed by [`HookRegistryBuilder`].
pub struct HookContributionRegistrar<'a> {
feature_id: &'a FeatureId,
declarations: &'a FeatureContributionDeclarations,
hook_builder: &'a mut HookRegistryBuilder,
report: &'a mut FeatureInstallReport,
}
impl HookContributionRegistrar<'_> {
fn require_declared(
&mut self,
declaration: &HookDeclaration,
) -> Result<(), FeatureInstallError> {
if self.declarations.contains_hook(declaration) {
return Ok(());
}
Err(reject_undeclared_contribution(
self.feature_id,
self.report,
FeatureContributionKind::Hook,
format!("{}:{:?}", declaration.name, declaration.point),
))
}
pub fn add_pre_request(
&mut self,
name: impl Into<String>,
hook: impl Hook<PreLlmRequest> + 'static,
) -> Result<(), FeatureInstallError> {
let declaration = HookDeclaration::new(name, FeatureHookPoint::PreRequest);
self.require_declared(&declaration)?;
self.hook_builder.add_pre_llm_request(hook);
self.report.installed_hooks.push(declaration);
Ok(())
}
pub fn add_pre_tool_call(
&mut self,
name: impl Into<String>,
hook: impl Hook<PreToolCall> + 'static,
) -> Result<(), FeatureInstallError> {
let declaration = HookDeclaration::new(name, FeatureHookPoint::PreToolCall);
self.require_declared(&declaration)?;
self.hook_builder.add_pre_tool_call(hook);
self.report.installed_hooks.push(declaration);
Ok(())
}
pub fn add_tool_result(
&mut self,
name: impl Into<String>,
hook: impl Hook<PostToolCall> + 'static,
) -> Result<(), FeatureInstallError> {
let declaration = HookDeclaration::new(name, FeatureHookPoint::ToolResult);
self.require_declared(&declaration)?;
self.hook_builder.add_post_tool_call(hook);
self.report.installed_hooks.push(declaration);
Ok(())
}
pub fn add_turn_end(
&mut self,
name: impl Into<String>,
hook: impl Hook<OnTurnEnd> + 'static,
) -> Result<(), FeatureInstallError> {
let declaration = HookDeclaration::new(name, FeatureHookPoint::TurnEnd);
self.require_declared(&declaration)?;
self.hook_builder.add_on_turn_end(hook);
self.report.installed_hooks.push(declaration);
Ok(())
}
}
/// Background task registrar for descriptor/report-only contributions.
pub struct BackgroundTaskRegistrar<'a> {
feature_id: &'a FeatureId,
declarations: &'a FeatureContributionDeclarations,
report: &'a mut FeatureInstallReport,
}
impl BackgroundTaskRegistrar<'_> {
pub fn declare(
&mut self,
declaration: BackgroundTaskDeclaration,
) -> Result<(), FeatureInstallError> {
if !self.declarations.contains_background_task(&declaration) {
return Err(reject_undeclared_contribution(
self.feature_id,
self.report,
FeatureContributionKind::BackgroundTask,
declaration.name,
));
}
if !self
.report
.declared_background_tasks
.iter()
.any(|task| task.name == declaration.name)
{
self.report.declared_background_tasks.push(declaration);
}
Ok(())
}
}
/// Service registrar for descriptor/report-only provider metadata.
pub struct FeatureServiceRegistrar<'a> {
feature_id: &'a FeatureId,
declarations: &'a FeatureContributionDeclarations,
service_registry: &'a mut FeatureServiceRegistry,
report: &'a mut FeatureInstallReport,
}
impl FeatureServiceRegistrar<'_> {
pub fn provide(&mut self, declaration: ServiceDeclaration) -> Result<(), FeatureInstallError> {
if !self.declarations.contains_provided_service(&declaration) {
return Err(reject_undeclared_contribution(
self.feature_id,
self.report,
FeatureContributionKind::Service,
declaration.id.to_string(),
));
}
if self
.report
.provided_services
.iter()
.any(|service| service.id == declaration.id && service.version == declaration.version)
{
return Ok(());
}
self.service_registry
.register_provider(self.feature_id.clone(), declaration.clone())?;
self.report.provided_services.push(declaration);
Ok(())
}
}
/// Install-time context provided to a feature module.
pub struct FeatureInstallContext<'a> {
feature_id: &'a FeatureId,
declarations: &'a FeatureContributionDeclarations,
grants: &'a AuthorityGrantSet,
pending_tools: &'a mut Vec<ToolDefinition>,
installed_tool_names: &'a mut HashMap<String, FeatureId>,
hook_builder: &'a mut HookRegistryBuilder,
service_registry: &'a mut FeatureServiceRegistry,
report: &'a mut FeatureInstallReport,
}
impl FeatureInstallContext<'_> {
pub fn feature_id(&self) -> &FeatureId {
self.feature_id
}
pub fn grants(&self) -> &AuthorityGrantSet {
self.grants
}
pub fn tools(&mut self) -> ToolContributionRegistrar<'_> {
ToolContributionRegistrar {
feature_id: self.feature_id,
declarations: self.declarations,
grants: self.grants,
pending_tools: self.pending_tools,
installed_tool_names: self.installed_tool_names,
report: self.report,
}
}
pub fn hooks(&mut self) -> HookContributionRegistrar<'_> {
HookContributionRegistrar {
feature_id: self.feature_id,
declarations: self.declarations,
hook_builder: self.hook_builder,
report: self.report,
}
}
pub fn background_tasks(&mut self) -> BackgroundTaskRegistrar<'_> {
BackgroundTaskRegistrar {
feature_id: self.feature_id,
declarations: self.declarations,
report: self.report,
}
}
pub fn services(&mut self) -> FeatureServiceRegistrar<'_> {
FeatureServiceRegistrar {
feature_id: self.feature_id,
declarations: self.declarations,
service_registry: self.service_registry,
report: self.report,
}
}
pub fn notifications(&mut self) -> FeatureNotificationSink<'_> {
FeatureNotificationSink {
grants: self.grants,
report: self.report,
}
}
pub fn alerts(&mut self) -> FeatureAlertSink<'_> {
FeatureAlertSink {
report: self.report,
}
}
pub fn diagnostics(&mut self) -> FeatureDiagnosticSink<'_> {
FeatureDiagnosticSink {
report: self.report,
}
}
}
/// Aggregate install output for a registry installation.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct FeatureRegistryInstallReport {
pub reports: Vec<FeatureInstallReport>,
pub services: FeatureServiceRegistry,
}
impl FeatureRegistryInstallReport {
pub fn installed_tool_names(&self) -> Vec<String> {
self.reports
.iter()
.flat_map(|report| report.installed_tools.iter().cloned())
.collect()
}
}
/// Builder/installer for enabled feature modules.
#[derive(Default)]
pub struct FeatureRegistryBuilder {
modules: Vec<Arc<dyn FeatureModule>>,
}
impl FeatureRegistryBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn add_module<M>(&mut self, module: M) -> &mut Self
where
M: FeatureModule + 'static,
{
self.modules.push(Arc::new(module));
self
}
pub fn with_module<M>(mut self, module: M) -> Self
where
M: FeatureModule + 'static,
{
self.add_module(module);
self
}
pub fn is_empty(&self) -> bool {
self.modules.is_empty()
}
pub fn descriptors(&self) -> Vec<FeatureDescriptor> {
self.modules
.iter()
.map(|module| module.descriptor())
.collect()
}
/// Install modules into the existing Worker tool path and hook builder.
pub(crate) fn install_into_worker<C: LlmClient>(
self,
worker: &mut Worker<C, Mutable>,
hook_builder: &mut HookRegistryBuilder,
) -> FeatureRegistryInstallReport {
let mut pending_tools = Vec::new();
let report = self.install_into_pending(&mut pending_tools, hook_builder);
worker.register_tools(pending_tools);
report
}
pub(crate) fn install_into_pending(
self,
pending_tools: &mut Vec<ToolDefinition>,
hook_builder: &mut HookRegistryBuilder,
) -> FeatureRegistryInstallReport {
let descriptors: Vec<_> = self
.modules
.iter()
.map(|module| module.descriptor())
.collect();
let mut service_registry = FeatureServiceRegistry::default();
let mut reports = Vec::with_capacity(self.modules.len());
let mut installed_tool_names = HashMap::new();
let mut seen_features = HashSet::new();
for (module, descriptor) in self.modules.into_iter().zip(descriptors.into_iter()) {
let grants = AuthorityGrantSet::grant_all(&descriptor.requested_authorities);
let declarations = FeatureContributionDeclarations::from_descriptor(&descriptor);
let mut report = FeatureInstallReport::new(&descriptor, grants.clone());
if !seen_features.insert(descriptor.id.clone()) {
report.diagnostics.push(FeatureDiagnostic::error(format!(
"duplicate feature id: {}",
descriptor.id
)));
report.mark_skipped(
FeatureContributionKind::Diagnostic,
descriptor.id.to_string(),
"duplicate feature id",
);
reports.push(report);
continue;
}
for authority in grants.denied() {
report.diagnostics.push(FeatureDiagnostic::warning(format!(
"authority denied: {:?}: {}",
authority.authority, authority.reason
)));
}
let mut required_service_failed = false;
for requirement in descriptor.requires_services.iter().cloned() {
if service_registry.provides(&requirement.id) {
report.resolved_service_requirements.push(requirement);
} else if requirement.required {
let reason = format!(
"required service requirement is not available: {}",
requirement.id
);
report
.diagnostics
.push(FeatureDiagnostic::error(reason.clone()));
report.mark_skipped(
FeatureContributionKind::Service,
requirement.id.to_string(),
reason,
);
required_service_failed = true;
} else {
report.diagnostics.push(FeatureDiagnostic::warning(format!(
"optional service requirement is not available: {}",
requirement.id
)));
report.mark_skipped(
FeatureContributionKind::Service,
requirement.id.to_string(),
"optional service requirement is not available",
);
}
}
if required_service_failed {
reports.push(report);
continue;
}
for background_task in descriptor.background_tasks.iter().cloned() {
report.declared_background_tasks.push(background_task);
}
for service in descriptor.provides_services.iter().cloned() {
match service_registry.register_provider(descriptor.id.clone(), service.clone()) {
Ok(()) => report.provided_services.push(service),
Err(error) => {
report
.diagnostics
.push(FeatureDiagnostic::error(error.to_string()));
report.mark_skipped(
FeatureContributionKind::Service,
service.id.to_string(),
error.to_string(),
);
}
}
}
let install_result = {
let mut context = FeatureInstallContext {
feature_id: &descriptor.id,
declarations: &declarations,
grants: &grants,
pending_tools,
installed_tool_names: &mut installed_tool_names,
hook_builder,
service_registry: &mut service_registry,
report: &mut report,
};
module.install(&mut context)
};
match install_result {
Ok(()) => report.installed = true,
Err(error) => {
report
.diagnostics
.push(FeatureDiagnostic::error(error.to_string()));
}
}
reports.push(report);
}
FeatureRegistryInstallReport {
reports,
services: service_registry,
}
}
}
/// Feature installation errors.
#[derive(Debug, Error)]
pub enum FeatureInstallError {
#[error("invalid feature descriptor: {0}")]
InvalidDescriptor(String),
#[error(
"duplicate tool contribution `{tool}` from feature `{duplicate_feature}`; first registered by `{first_feature}`"
)]
DuplicateToolName {
tool: String,
first_feature: String,
duplicate_feature: String,
},
#[error(
"tool contribution declared name `{declared}` does not match model-visible tool name `{model_visible}`"
)]
ToolNameMismatch {
declared: String,
model_visible: String,
},
#[error(
"undeclared {kind:?} contribution `{name}` from feature `{feature}` is not present in the approved feature descriptor"
)]
UndeclaredContribution {
kind: FeatureContributionKind,
name: String,
feature: String,
},
#[error(
"duplicate service declaration `{service}` from feature `{duplicate_feature}`; first provided by `{first_feature}`"
)]
DuplicateService {
service: String,
first_feature: String,
duplicate_feature: String,
},
#[error("feature authority denied: {0}")]
AuthorityDenied(String),
#[error("feature install failed: {0}")]
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(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use async_trait::async_trait;
use futures::stream;
use llm_worker::llm_client::{ClientError, Request, ResponseStream};
use llm_worker::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
use serde_json::json;
use std::sync::atomic::{AtomicUsize, Ordering};
#[derive(Clone)]
struct DummyClient;
#[async_trait]
impl LlmClient for DummyClient {
async fn stream(&self, _request: Request) -> Result<ResponseStream, ClientError> {
Ok(Box::pin(stream::empty()))
}
fn clone_boxed(&self) -> Box<dyn LlmClient> {
Box::new(self.clone())
}
}
struct DummyTool;
#[async_trait]
impl Tool for DummyTool {
async fn execute(&self, _input_json: &str) -> Result<ToolOutput, ToolError> {
Ok(ToolOutput::from("ok".to_string()))
}
}
fn dummy_tool(name: &'static str) -> ToolDefinition {
Arc::new(move || {
(
ToolMeta::new(name)
.description("dummy")
.input_schema(json!({})),
Arc::new(DummyTool) as Arc<dyn Tool>,
)
})
}
struct ToolFeature {
descriptor: FeatureDescriptor,
contribution_name: &'static str,
model_visible_name: &'static str,
}
impl FeatureModule for ToolFeature {
fn descriptor(&self) -> FeatureDescriptor {
self.descriptor.clone()
}
fn install(
&self,
context: &mut FeatureInstallContext<'_>,
) -> Result<(), FeatureInstallError> {
context.tools().register(ToolContribution::new(
self.contribution_name,
dummy_tool(self.model_visible_name),
))
}
}
#[test]
fn descriptor_authorities_and_install_report_are_recorded() {
let descriptor = FeatureDescriptor::builtin("dummy", "Dummy")
.with_tool(ToolDeclaration::new("Dummy", "dummy tool"))
.with_background_task(BackgroundTaskDeclaration::descriptor_only(
"daily",
"descriptor-only background task",
));
let mut hook_builder = HookRegistryBuilder::default();
let mut pending_tools = Vec::new();
let report = FeatureRegistryBuilder::new()
.with_module(ToolFeature {
descriptor,
contribution_name: "Dummy",
model_visible_name: "Dummy",
})
.install_into_pending(&mut pending_tools, &mut hook_builder);
assert_eq!(pending_tools.len(), 1);
assert_eq!(report.reports.len(), 1);
let feature_report = &report.reports[0];
assert!(feature_report.installed);
assert_eq!(feature_report.installed_tools, vec!["Dummy"]);
assert_eq!(feature_report.declared_background_tasks[0].name, "daily");
assert!(feature_report.granted_authorities.denied().is_empty());
}
#[test]
fn duplicate_tool_names_are_rejected() {
let descriptor_a = FeatureDescriptor::builtin("a", "A")
.with_tool(ToolDeclaration::new("Duplicate", "first tool"));
let descriptor_b = FeatureDescriptor::builtin("b", "B")
.with_tool(ToolDeclaration::new("Duplicate", "second tool"));
let mut hook_builder = HookRegistryBuilder::default();
let mut pending_tools = Vec::new();
let report = FeatureRegistryBuilder::new()
.with_module(ToolFeature {
descriptor: descriptor_a,
contribution_name: "Duplicate",
model_visible_name: "Duplicate",
})
.with_module(ToolFeature {
descriptor: descriptor_b,
contribution_name: "Duplicate",
model_visible_name: "Duplicate",
})
.install_into_pending(&mut pending_tools, &mut hook_builder);
assert_eq!(pending_tools.len(), 1);
assert!(report.reports[0].installed);
assert!(!report.reports[1].installed);
assert!(
report.reports[1]
.diagnostics
.iter()
.any(|diagnostic| diagnostic.message.contains("duplicate tool contribution"))
);
assert_eq!(
report.reports[1].skipped[0].kind,
FeatureContributionKind::Tool
);
}
#[test]
fn mismatched_tool_contribution_name_is_rejected_before_queueing() {
let descriptor = FeatureDescriptor::builtin("mismatch", "Mismatch")
.with_tool(ToolDeclaration::new("Actual", "actual model-visible tool"));
let mut hook_builder = HookRegistryBuilder::default();
let mut pending_tools = Vec::new();
let report = FeatureRegistryBuilder::new()
.with_module(ToolFeature {
descriptor,
contribution_name: "Declared",
model_visible_name: "Actual",
})
.install_into_pending(&mut pending_tools, &mut hook_builder);
assert!(pending_tools.is_empty());
assert!(!report.reports[0].installed);
assert!(report.reports[0].diagnostics.iter().any(|diagnostic| {
diagnostic
.message
.contains("does not match model-visible tool name")
}));
assert_eq!(report.reports[0].skipped[0].name, "Actual");
}
#[test]
fn stateful_tool_definition_is_materialized_once_for_report_and_worker() {
struct StatefulToolFeature {
calls: Arc<AtomicUsize>,
}
impl FeatureModule for StatefulToolFeature {
fn descriptor(&self) -> FeatureDescriptor {
FeatureDescriptor::builtin("stateful-tool", "Stateful tool")
.with_tool(ToolDeclaration::new("First", "stateful tool"))
}
fn install(
&self,
context: &mut FeatureInstallContext<'_>,
) -> Result<(), FeatureInstallError> {
let calls = Arc::clone(&self.calls);
let definition: ToolDefinition = Arc::new(move || {
let call_index = calls.fetch_add(1, Ordering::SeqCst);
let name = if call_index == 0 { "First" } else { "Second" };
(
ToolMeta::new(name)
.description("stateful")
.input_schema(json!({})),
Arc::new(DummyTool) as Arc<dyn Tool>,
)
});
context
.tools()
.register(ToolContribution::new("First", definition))
}
}
let calls = Arc::new(AtomicUsize::new(0));
let mut worker = Worker::new(DummyClient);
let mut hook_builder = HookRegistryBuilder::default();
let report = FeatureRegistryBuilder::new()
.with_module(StatefulToolFeature {
calls: Arc::clone(&calls),
})
.install_into_worker(&mut worker, &mut hook_builder);
worker.tool_server_handle().flush_pending();
let names: Vec<_> = worker
.tool_server_handle()
.tool_definitions_sorted()
.into_iter()
.map(|tool| tool.name)
.collect();
assert_eq!(report.installed_tool_names(), vec!["First"]);
assert_eq!(names, vec!["First"]);
assert_eq!(calls.load(Ordering::SeqCst), 1);
}
struct ServiceFeature {
descriptor: FeatureDescriptor,
}
impl FeatureModule for ServiceFeature {
fn descriptor(&self) -> FeatureDescriptor {
self.descriptor.clone()
}
fn install(
&self,
_context: &mut FeatureInstallContext<'_>,
) -> Result<(), FeatureInstallError> {
Ok(())
}
}
struct DummyPreToolHook;
#[async_trait]
impl Hook<PreToolCall> for DummyPreToolHook {
async fn call(
&self,
_input: &crate::hook::ToolCallSummary,
) -> crate::hook::HookPreToolAction {
crate::hook::HookPreToolAction::Continue
}
}
struct HookFeature {
descriptor: FeatureDescriptor,
hook_name: &'static str,
}
impl FeatureModule for HookFeature {
fn descriptor(&self) -> FeatureDescriptor {
self.descriptor.clone()
}
fn install(
&self,
context: &mut FeatureInstallContext<'_>,
) -> Result<(), FeatureInstallError> {
context
.hooks()
.add_pre_tool_call(self.hook_name, DummyPreToolHook)
}
}
struct BackgroundFeature {
descriptor: FeatureDescriptor,
task_name: &'static str,
}
impl FeatureModule for BackgroundFeature {
fn descriptor(&self) -> FeatureDescriptor {
self.descriptor.clone()
}
fn install(
&self,
context: &mut FeatureInstallContext<'_>,
) -> Result<(), FeatureInstallError> {
context
.background_tasks()
.declare(BackgroundTaskDeclaration::descriptor_only(
self.task_name,
"runtime background task",
))
}
}
struct ServiceProviderFeature {
descriptor: FeatureDescriptor,
service: ServiceId,
}
impl FeatureModule for ServiceProviderFeature {
fn descriptor(&self) -> FeatureDescriptor {
self.descriptor.clone()
}
fn install(
&self,
context: &mut FeatureInstallContext<'_>,
) -> Result<(), FeatureInstallError> {
context.services().provide(ServiceDeclaration::new(
self.service.clone(),
"1",
"runtime service provider",
))
}
}
#[test]
fn undeclared_tool_contribution_is_rejected() {
let descriptor = FeatureDescriptor::builtin("undeclared-tool", "Undeclared tool");
let mut hook_builder = HookRegistryBuilder::default();
let mut pending_tools = Vec::new();
let report = FeatureRegistryBuilder::new()
.with_module(ToolFeature {
descriptor,
contribution_name: "HiddenTool",
model_visible_name: "HiddenTool",
})
.install_into_pending(&mut pending_tools, &mut hook_builder);
assert!(pending_tools.is_empty());
assert!(!report.reports[0].installed);
assert_eq!(
report.reports[0].skipped[0].kind,
FeatureContributionKind::Tool
);
assert_eq!(report.reports[0].skipped[0].name, "HiddenTool");
assert!(report.reports[0].diagnostics.iter().any(|diagnostic| {
diagnostic
.message
.contains("is not present in the approved feature descriptor")
}));
}
#[test]
fn undeclared_hook_contribution_is_rejected() {
let descriptor = FeatureDescriptor::builtin("undeclared-hook", "Undeclared hook");
let mut hook_builder = HookRegistryBuilder::default();
let mut pending_tools = Vec::new();
let report = FeatureRegistryBuilder::new()
.with_module(HookFeature {
descriptor,
hook_name: "hidden-hook",
})
.install_into_pending(&mut pending_tools, &mut hook_builder);
assert!(!report.reports[0].installed);
assert!(report.reports[0].installed_hooks.is_empty());
assert_eq!(
report.reports[0].skipped[0].kind,
FeatureContributionKind::Hook
);
assert!(report.reports[0].skipped[0].name.contains("hidden-hook"));
}
#[test]
fn undeclared_background_task_contribution_is_rejected() {
let descriptor =
FeatureDescriptor::builtin("undeclared-background", "Undeclared background");
let mut hook_builder = HookRegistryBuilder::default();
let mut pending_tools = Vec::new();
let report = FeatureRegistryBuilder::new()
.with_module(BackgroundFeature {
descriptor,
task_name: "hidden-task",
})
.install_into_pending(&mut pending_tools, &mut hook_builder);
assert!(!report.reports[0].installed);
assert!(report.reports[0].declared_background_tasks.is_empty());
assert_eq!(
report.reports[0].skipped[0].kind,
FeatureContributionKind::BackgroundTask
);
assert_eq!(report.reports[0].skipped[0].name, "hidden-task");
}
#[test]
fn undeclared_service_provider_contribution_is_rejected() {
let service = ServiceId::builtin("hidden-service");
let descriptor = FeatureDescriptor::builtin("undeclared-service", "Undeclared service");
let mut hook_builder = HookRegistryBuilder::default();
let mut pending_tools = Vec::new();
let report = FeatureRegistryBuilder::new()
.with_module(ServiceProviderFeature {
descriptor,
service: service.clone(),
})
.install_into_pending(&mut pending_tools, &mut hook_builder);
assert!(!report.reports[0].installed);
assert!(!report.services.provides(&service));
assert!(report.reports[0].provided_services.is_empty());
assert_eq!(
report.reports[0].skipped[0].kind,
FeatureContributionKind::Service
);
assert_eq!(report.reports[0].skipped[0].name, service.to_string());
}
#[test]
fn service_requirements_resolve_against_prior_providers() {
let service = ServiceId::builtin("demo-service");
let provider = FeatureDescriptor::builtin("provider", "Provider").with_provided_service(
ServiceDeclaration::new(service.clone(), "1", "demo service"),
);
let consumer = FeatureDescriptor::builtin("consumer", "Consumer")
.with_service_requirement(ServiceRequirement::required(service.clone(), "needs demo"));
let missing_service = ServiceId::builtin("missing-service");
let missing = FeatureDescriptor::builtin("missing", "Missing").with_service_requirement(
ServiceRequirement::required(missing_service, "needs missing"),
);
let optional_service = ServiceId::builtin("optional-service");
let optional = FeatureDescriptor::builtin("optional", "Optional").with_service_requirement(
ServiceRequirement::optional(optional_service, "nice to have"),
);
let mut hook_builder = HookRegistryBuilder::default();
let mut pending_tools = Vec::new();
let report = FeatureRegistryBuilder::new()
.with_module(ServiceFeature {
descriptor: provider,
})
.with_module(ServiceFeature {
descriptor: consumer,
})
.with_module(ServiceFeature {
descriptor: missing,
})
.with_module(ServiceFeature {
descriptor: optional,
})
.install_into_pending(&mut pending_tools, &mut hook_builder);
assert!(report.services.provides(&service));
assert!(report.reports[1].installed);
assert_eq!(
report.reports[1].resolved_service_requirements[0].id,
service
);
assert!(!report.reports[2].installed);
assert!(
report.reports[2]
.diagnostics
.iter()
.any(|diagnostic| diagnostic.message.contains("required service requirement"))
);
assert!(report.reports[3].installed);
assert_eq!(
report.reports[3].skipped[0].kind,
FeatureContributionKind::Service
);
}
#[test]
fn background_task_declaration_is_not_sandbox_authority_gated() {
let descriptor = FeatureDescriptor::builtin("background", "Background")
.with_background_task(BackgroundTaskDeclaration::descriptor_only(
"declared-task",
"descriptor contribution",
));
let mut hook_builder = HookRegistryBuilder::default();
let mut pending_tools = Vec::new();
let report = FeatureRegistryBuilder::new()
.with_module(ServiceFeature { descriptor })
.install_into_pending(&mut pending_tools, &mut hook_builder);
assert!(report.reports[0].installed);
assert_eq!(
report.reports[0].declared_background_tasks[0].name,
"declared-task"
);
assert!(report.reports[0].skipped.is_empty());
}
#[test]
fn service_provider_declaration_is_not_sandbox_authority_gated() {
let service = ServiceId::builtin("declared-service");
let descriptor = FeatureDescriptor::builtin("service", "Service").with_provided_service(
ServiceDeclaration::new(service.clone(), "1", "descriptor contribution"),
);
let mut hook_builder = HookRegistryBuilder::default();
let mut pending_tools = Vec::new();
let report = FeatureRegistryBuilder::new()
.with_module(ServiceFeature { descriptor })
.install_into_pending(&mut pending_tools, &mut hook_builder);
assert!(report.reports[0].installed);
assert!(report.services.provides(&service));
assert_eq!(report.reports[0].provided_services[0].id, service);
assert!(report.reports[0].skipped.is_empty());
}
#[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))
.install_into_worker(&mut worker, &mut hook_builder);
worker.tool_server_handle().flush_pending();
let names: Vec<_> = worker
.tool_server_handle()
.tool_definitions_sorted()
.into_iter()
.map(|tool| tool.name)
.collect();
assert_eq!(
names,
vec!["TaskCreate", "TaskGet", "TaskList", "TaskUpdate"]
);
assert_eq!(
report.installed_tool_names(),
vec!["TaskCreate", "TaskList", "TaskGet", "TaskUpdate"]
);
assert!(report.reports[0].installed);
}
}