fix: align feature authority boundaries

This commit is contained in:
Keisuke Hirata 2026-06-05 07:03:21 +09:00
parent 40701760eb
commit 98bbd6f185
No known key found for this signature in database

View File

@ -1,7 +1,7 @@
//! Feature contribution registry for Pod-hosted builtin/plugin modules. //! Feature contribution registry for Pod-hosted builtin/plugin modules.
//! //!
//! This module defines the Pod-side feature boundary used to collect //! This module defines the Pod-side feature boundary used to collect
//! descriptor metadata, capability requests, tool contributions, safe hook //! descriptor metadata, authority requests, tool contributions, safe hook
//! contributions, background task declarations, and service declarations before //! contributions, background task declarations, and service declarations before
//! installing them into the existing Worker/HookRegistry host surfaces. //! installing them into the existing Worker/HookRegistry host surfaces.
//! //!
@ -69,20 +69,23 @@ pub enum FeatureRuntimeKind {
ExternalPlugin, ExternalPlugin,
} }
/// Host capability requested by a feature before it contributes host-visible /// Host authority requested by a feature for host-mediated operations that can
/// behavior. Grants are additive and do not replace manifest/tool permission /// cross sandbox or model-context boundaries.
/// checks. ///
/// 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)] #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum HostCapability { pub enum HostAuthority {
ContributeTool { name: String }, Filesystem,
ContributeHook { point: FeatureHookPoint }, Network,
DeclareBackgroundTask { name: String }, SecretRef { id: String },
ProvideService { service: ServiceId }, ModelNotification,
RequireService { service: ServiceId }, PodManagement,
EmitNotification, StateStore { name: String },
EmitAlert, ServiceAccess { service: ServiceId },
EmitDiagnostic,
} }
/// A safe hook contribution point exposed to feature modules. /// A safe hook contribution point exposed to feature modules.
@ -95,45 +98,45 @@ pub enum FeatureHookPoint {
TurnEnd, TurnEnd,
} }
/// Capability request declared by a feature descriptor. /// Authority request declared by a feature descriptor.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapabilityRequest { pub struct AuthorityRequest {
pub capability: HostCapability, pub authority: HostAuthority,
pub required: bool, pub required: bool,
pub reason: String, pub reason: String,
} }
impl CapabilityRequest { impl AuthorityRequest {
pub fn required(capability: HostCapability, reason: impl Into<String>) -> Self { pub fn required(authority: HostAuthority, reason: impl Into<String>) -> Self {
Self { Self {
capability, authority,
required: true, required: true,
reason: reason.into(), reason: reason.into(),
} }
} }
pub fn optional(capability: HostCapability, reason: impl Into<String>) -> Self { pub fn optional(authority: HostAuthority, reason: impl Into<String>) -> Self {
Self { Self {
capability, authority,
required: false, required: false,
reason: reason.into(), reason: reason.into(),
} }
} }
} }
/// Capability grants resolved by the host for one feature installation. /// Authority grants resolved by the host for one feature installation.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapabilityGrantSet { pub struct AuthorityGrantSet {
granted: HashSet<HostCapability>, granted: HashSet<HostAuthority>,
denied: Vec<CapabilityDenial>, denied: Vec<AuthorityDenial>,
} }
impl CapabilityGrantSet { impl AuthorityGrantSet {
pub fn grant_all(requests: &[CapabilityRequest]) -> Self { pub fn grant_all(requests: &[AuthorityRequest]) -> Self {
Self { Self {
granted: requests granted: requests
.iter() .iter()
.map(|request| request.capability.clone()) .map(|request| request.authority.clone())
.collect(), .collect(),
denied: Vec::new(), denied: Vec::new(),
} }
@ -143,31 +146,31 @@ impl CapabilityGrantSet {
Self::default() Self::default()
} }
pub fn contains(&self, capability: &HostCapability) -> bool { pub fn contains(&self, authority: &HostAuthority) -> bool {
self.granted.contains(capability) self.granted.contains(authority)
} }
pub fn denied(&self) -> &[CapabilityDenial] { pub fn denied(&self) -> &[AuthorityDenial] {
&self.denied &self.denied
} }
pub fn grant(&mut self, capability: HostCapability) { pub fn grant(&mut self, authority: HostAuthority) {
self.granted.insert(capability); self.granted.insert(authority);
} }
pub fn deny(&mut self, capability: HostCapability, reason: impl Into<String>) { pub fn deny(&mut self, authority: HostAuthority, reason: impl Into<String>) {
self.granted.remove(&capability); self.granted.remove(&authority);
self.denied.push(CapabilityDenial { self.denied.push(AuthorityDenial {
capability, authority,
reason: reason.into(), reason: reason.into(),
}); });
} }
} }
/// Host-side denial of a requested feature capability. /// Host-side denial of a requested feature authority.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapabilityDenial { pub struct AuthorityDenial {
pub capability: HostCapability, pub authority: HostAuthority,
pub reason: String, pub reason: String,
} }
@ -177,15 +180,12 @@ pub struct CapabilityDenial {
pub struct ToolDeclaration { pub struct ToolDeclaration {
pub name: String, pub name: String,
pub description: String, pub description: String,
pub required_capabilities: Vec<HostCapability>,
} }
impl ToolDeclaration { impl ToolDeclaration {
pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self { pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
let name = name.into();
Self { Self {
required_capabilities: vec![HostCapability::ContributeTool { name: name.clone() }], name: name.into(),
name,
description: description.into(), description: description.into(),
} }
} }
@ -195,24 +195,20 @@ impl ToolDeclaration {
pub struct ToolContribution { pub struct ToolContribution {
name: String, name: String,
definition: ToolDefinition, definition: ToolDefinition,
required_capabilities: Vec<HostCapability>, required_authorities: Vec<HostAuthority>,
} }
impl ToolContribution { impl ToolContribution {
pub fn new(name: impl Into<String>, definition: ToolDefinition) -> Self { pub fn new(name: impl Into<String>, definition: ToolDefinition) -> Self {
let name = name.into();
Self { Self {
required_capabilities: vec![HostCapability::ContributeTool { name: name.clone() }], name: name.into(),
name,
definition, definition,
required_authorities: Vec::new(),
} }
} }
pub fn with_required_capabilities( pub fn with_required_authorities(mut self, required_authorities: Vec<HostAuthority>) -> Self {
mut self, self.required_authorities = required_authorities;
required_capabilities: Vec<HostCapability>,
) -> Self {
self.required_capabilities = required_capabilities;
self self
} }
@ -226,16 +222,12 @@ impl ToolContribution {
pub struct HookDeclaration { pub struct HookDeclaration {
pub name: String, pub name: String,
pub point: FeatureHookPoint, pub point: FeatureHookPoint,
pub required_capabilities: Vec<HostCapability>,
} }
impl HookDeclaration { impl HookDeclaration {
pub fn new(name: impl Into<String>, point: FeatureHookPoint) -> Self { pub fn new(name: impl Into<String>, point: FeatureHookPoint) -> Self {
Self { Self {
name: name.into(), name: name.into(),
required_capabilities: vec![HostCapability::ContributeHook {
point: point.clone(),
}],
point, point,
} }
} }
@ -255,17 +247,12 @@ pub struct BackgroundTaskDeclaration {
pub name: String, pub name: String,
pub description: String, pub description: String,
pub lifecycle: BackgroundTaskLifecycle, pub lifecycle: BackgroundTaskLifecycle,
pub required_capabilities: Vec<HostCapability>,
} }
impl BackgroundTaskDeclaration { impl BackgroundTaskDeclaration {
pub fn descriptor_only(name: impl Into<String>, description: impl Into<String>) -> Self { pub fn descriptor_only(name: impl Into<String>, description: impl Into<String>) -> Self {
let name = name.into();
Self { Self {
required_capabilities: vec![HostCapability::DeclareBackgroundTask { name: name.into(),
name: name.clone(),
}],
name,
description: description.into(), description: description.into(),
lifecycle: BackgroundTaskLifecycle::DescriptorOnly, lifecycle: BackgroundTaskLifecycle::DescriptorOnly,
} }
@ -418,7 +405,7 @@ pub struct FeatureDescriptor {
pub display_name: String, pub display_name: String,
pub version: String, pub version: String,
pub description: String, pub description: String,
pub requested_capabilities: Vec<CapabilityRequest>, pub requested_authorities: Vec<AuthorityRequest>,
pub tools: Vec<ToolDeclaration>, pub tools: Vec<ToolDeclaration>,
pub hooks: Vec<HookDeclaration>, pub hooks: Vec<HookDeclaration>,
pub background_tasks: Vec<BackgroundTaskDeclaration>, pub background_tasks: Vec<BackgroundTaskDeclaration>,
@ -434,7 +421,7 @@ impl FeatureDescriptor {
display_name: display_name.into(), display_name: display_name.into(),
version: env!("CARGO_PKG_VERSION").into(), version: env!("CARGO_PKG_VERSION").into(),
description: String::new(), description: String::new(),
requested_capabilities: Vec::new(), requested_authorities: Vec::new(),
tools: Vec::new(), tools: Vec::new(),
hooks: Vec::new(), hooks: Vec::new(),
background_tasks: Vec::new(), background_tasks: Vec::new(),
@ -448,8 +435,8 @@ impl FeatureDescriptor {
self self
} }
pub fn with_capability(mut self, request: CapabilityRequest) -> Self { pub fn with_authority(mut self, request: AuthorityRequest) -> Self {
self.requested_capabilities.push(request); self.requested_authorities.push(request);
self self
} }
@ -551,7 +538,7 @@ pub struct FeatureInstallReport {
pub feature_id: FeatureId, pub feature_id: FeatureId,
pub runtime: FeatureRuntimeKind, pub runtime: FeatureRuntimeKind,
pub installed: bool, pub installed: bool,
pub granted_capabilities: CapabilityGrantSet, pub granted_authorities: AuthorityGrantSet,
pub installed_tools: Vec<String>, pub installed_tools: Vec<String>,
pub installed_hooks: Vec<HookDeclaration>, pub installed_hooks: Vec<HookDeclaration>,
pub declared_background_tasks: Vec<BackgroundTaskDeclaration>, pub declared_background_tasks: Vec<BackgroundTaskDeclaration>,
@ -562,12 +549,12 @@ pub struct FeatureInstallReport {
} }
impl FeatureInstallReport { impl FeatureInstallReport {
fn new(descriptor: &FeatureDescriptor, granted_capabilities: CapabilityGrantSet) -> Self { fn new(descriptor: &FeatureDescriptor, granted_authorities: AuthorityGrantSet) -> Self {
Self { Self {
feature_id: descriptor.id.clone(), feature_id: descriptor.id.clone(),
runtime: descriptor.runtime.clone(), runtime: descriptor.runtime.clone(),
installed: false, installed: false,
granted_capabilities, granted_authorities,
installed_tools: Vec::new(), installed_tools: Vec::new(),
installed_hooks: Vec::new(), installed_hooks: Vec::new(),
declared_background_tasks: Vec::new(), declared_background_tasks: Vec::new(),
@ -592,81 +579,37 @@ impl FeatureInstallReport {
} }
} }
fn require_capability( fn require_authority(
grants: &CapabilityGrantSet, grants: &AuthorityGrantSet,
report: &mut FeatureInstallReport, report: &mut FeatureInstallReport,
kind: FeatureContributionKind, kind: FeatureContributionKind,
name: impl Into<String>, name: impl Into<String>,
capability: &HostCapability, authority: &HostAuthority,
) -> Result<(), FeatureInstallError> { ) -> Result<(), FeatureInstallError> {
if grants.contains(capability) { if grants.contains(authority) {
return Ok(()); return Ok(());
} }
let reason = format!("required capability was not granted: {capability:?}"); let reason = format!("required authority was not granted: {authority:?}");
report.mark_skipped(kind, name, reason.clone()); report.mark_skipped(kind, name, reason.clone());
Err(FeatureInstallError::CapabilityDenied(reason)) Err(FeatureInstallError::AuthorityDenied(reason))
}
fn require_background_task_capability(
grants: &CapabilityGrantSet,
report: &mut FeatureInstallReport,
declaration: &BackgroundTaskDeclaration,
) -> Result<(), FeatureInstallError> {
let default_capability = HostCapability::DeclareBackgroundTask {
name: declaration.name.clone(),
};
require_capability(
grants,
report,
FeatureContributionKind::BackgroundTask,
declaration.name.clone(),
&default_capability,
)?;
for capability in &declaration.required_capabilities {
require_capability(
grants,
report,
FeatureContributionKind::BackgroundTask,
declaration.name.clone(),
capability,
)?;
}
Ok(())
}
fn require_service_provider_capability(
grants: &CapabilityGrantSet,
report: &mut FeatureInstallReport,
declaration: &ServiceDeclaration,
) -> Result<(), FeatureInstallError> {
let capability = HostCapability::ProvideService {
service: declaration.id.clone(),
};
require_capability(
grants,
report,
FeatureContributionKind::Service,
declaration.id.to_string(),
&capability,
)
} }
/// Model-visible durable notification sink skeleton. The first slice exposes /// Model-visible durable notification sink skeleton. The first slice exposes
/// the boundary without implementing a new event channel. /// the boundary without implementing a new event channel.
pub struct FeatureNotificationSink<'a> { pub struct FeatureNotificationSink<'a> {
grants: &'a CapabilityGrantSet, grants: &'a AuthorityGrantSet,
report: &'a mut FeatureInstallReport, report: &'a mut FeatureInstallReport,
} }
impl FeatureNotificationSink<'_> { impl FeatureNotificationSink<'_> {
pub fn notify_model(&mut self, message: impl Into<String>) -> Result<(), FeatureInstallError> { pub fn notify_model(&mut self, message: impl Into<String>) -> Result<(), FeatureInstallError> {
require_capability( require_authority(
self.grants, self.grants,
self.report, self.report,
FeatureContributionKind::Notification, FeatureContributionKind::Notification,
"notify_model", "notify_model",
&HostCapability::EmitNotification, &HostAuthority::ModelNotification,
)?; )?;
let message = message.into(); let message = message.into();
self.report.diagnostics.push(FeatureDiagnostic::warning(format!( self.report.diagnostics.push(FeatureDiagnostic::warning(format!(
@ -683,19 +626,11 @@ impl FeatureNotificationSink<'_> {
/// Transient human-facing alert sink skeleton. /// Transient human-facing alert sink skeleton.
pub struct FeatureAlertSink<'a> { pub struct FeatureAlertSink<'a> {
grants: &'a CapabilityGrantSet,
report: &'a mut FeatureInstallReport, report: &'a mut FeatureInstallReport,
} }
impl FeatureAlertSink<'_> { impl FeatureAlertSink<'_> {
pub fn alert(&mut self, message: impl Into<String>) -> Result<(), FeatureInstallError> { pub fn alert(&mut self, message: impl Into<String>) {
require_capability(
self.grants,
self.report,
FeatureContributionKind::Alert,
"alert",
&HostCapability::EmitAlert,
)?;
let message = message.into(); let message = message.into();
self.report self.report
.diagnostics .diagnostics
@ -705,46 +640,36 @@ impl FeatureAlertSink<'_> {
"alert", "alert",
"transient alert host is not connected during feature installation", "transient alert host is not connected during feature installation",
); );
Ok(())
} }
} }
/// Diagnostic sink available to feature installers. /// Diagnostic sink available to feature installers.
pub struct FeatureDiagnosticSink<'a> { pub struct FeatureDiagnosticSink<'a> {
grants: &'a CapabilityGrantSet,
report: &'a mut FeatureInstallReport, report: &'a mut FeatureInstallReport,
} }
impl FeatureDiagnosticSink<'_> { impl FeatureDiagnosticSink<'_> {
pub fn push(&mut self, diagnostic: FeatureDiagnostic) -> Result<(), FeatureInstallError> { pub fn push(&mut self, diagnostic: FeatureDiagnostic) {
require_capability(
self.grants,
self.report,
FeatureContributionKind::Diagnostic,
"diagnostic",
&HostCapability::EmitDiagnostic,
)?;
self.report.diagnostics.push(diagnostic); self.report.diagnostics.push(diagnostic);
Ok(())
} }
pub fn info(&mut self, message: impl Into<String>) -> Result<(), FeatureInstallError> { pub fn info(&mut self, message: impl Into<String>) {
self.push(FeatureDiagnostic::info(message)) self.push(FeatureDiagnostic::info(message));
} }
pub fn warning(&mut self, message: impl Into<String>) -> Result<(), FeatureInstallError> { pub fn warning(&mut self, message: impl Into<String>) {
self.push(FeatureDiagnostic::warning(message)) self.push(FeatureDiagnostic::warning(message));
} }
pub fn error(&mut self, message: impl Into<String>) -> Result<(), FeatureInstallError> { pub fn error(&mut self, message: impl Into<String>) {
self.push(FeatureDiagnostic::error(message)) self.push(FeatureDiagnostic::error(message));
} }
} }
/// Tool contribution registrar exposed inside [`FeatureInstallContext`]. /// Tool contribution registrar exposed inside [`FeatureInstallContext`].
pub struct ToolContributionRegistrar<'a> { pub struct ToolContributionRegistrar<'a> {
feature_id: &'a FeatureId, feature_id: &'a FeatureId,
grants: &'a CapabilityGrantSet, grants: &'a AuthorityGrantSet,
pending_tools: &'a mut Vec<ToolDefinition>, pending_tools: &'a mut Vec<ToolDefinition>,
installed_tool_names: &'a mut HashMap<String, FeatureId>, installed_tool_names: &'a mut HashMap<String, FeatureId>,
report: &'a mut FeatureInstallReport, report: &'a mut FeatureInstallReport,
@ -752,7 +677,8 @@ pub struct ToolContributionRegistrar<'a> {
impl ToolContributionRegistrar<'_> { impl ToolContributionRegistrar<'_> {
pub fn register(&mut self, contribution: ToolContribution) -> Result<(), FeatureInstallError> { pub fn register(&mut self, contribution: ToolContribution) -> Result<(), FeatureInstallError> {
let model_visible_name = (contribution.definition)().0.name; let (tool_meta, tool) = (contribution.definition)();
let model_visible_name = tool_meta.name.clone();
if contribution.name != model_visible_name { if contribution.name != model_visible_name {
let error = FeatureInstallError::ToolNameMismatch { let error = FeatureInstallError::ToolNameMismatch {
declared: contribution.name, declared: contribution.name,
@ -766,23 +692,13 @@ impl ToolContributionRegistrar<'_> {
return Err(error); return Err(error);
} }
let tool_capability = HostCapability::ContributeTool { for authority in &contribution.required_authorities {
name: model_visible_name.clone(), require_authority(
};
require_capability(
self.grants, self.grants,
self.report, self.report,
FeatureContributionKind::Tool, FeatureContributionKind::Tool,
model_visible_name.clone(), model_visible_name.clone(),
&tool_capability, authority,
)?;
for capability in &contribution.required_capabilities {
require_capability(
self.grants,
self.report,
FeatureContributionKind::Tool,
model_visible_name.clone(),
capability,
)?; )?;
} }
@ -803,14 +719,14 @@ impl ToolContributionRegistrar<'_> {
self.installed_tool_names self.installed_tool_names
.insert(model_visible_name.clone(), self.feature_id.clone()); .insert(model_visible_name.clone(), self.feature_id.clone());
self.report.installed_tools.push(model_visible_name); self.report.installed_tools.push(model_visible_name);
self.pending_tools.push(contribution.definition); self.pending_tools
.push(Arc::new(move || (tool_meta.clone(), Arc::clone(&tool))));
Ok(()) Ok(())
} }
} }
/// Safe hook contribution registrar backed by [`HookRegistryBuilder`]. /// Safe hook contribution registrar backed by [`HookRegistryBuilder`].
pub struct HookContributionRegistrar<'a> { pub struct HookContributionRegistrar<'a> {
grants: &'a CapabilityGrantSet,
hook_builder: &'a mut HookRegistryBuilder, hook_builder: &'a mut HookRegistryBuilder,
report: &'a mut FeatureInstallReport, report: &'a mut FeatureInstallReport,
} }
@ -822,7 +738,6 @@ impl HookContributionRegistrar<'_> {
hook: impl Hook<PreLlmRequest> + 'static, hook: impl Hook<PreLlmRequest> + 'static,
) -> Result<(), FeatureInstallError> { ) -> Result<(), FeatureInstallError> {
let declaration = HookDeclaration::new(name, FeatureHookPoint::PreRequest); let declaration = HookDeclaration::new(name, FeatureHookPoint::PreRequest);
self.require_hook_capability(&declaration)?;
self.hook_builder.add_pre_llm_request(hook); self.hook_builder.add_pre_llm_request(hook);
self.report.installed_hooks.push(declaration); self.report.installed_hooks.push(declaration);
Ok(()) Ok(())
@ -834,7 +749,6 @@ impl HookContributionRegistrar<'_> {
hook: impl Hook<PreToolCall> + 'static, hook: impl Hook<PreToolCall> + 'static,
) -> Result<(), FeatureInstallError> { ) -> Result<(), FeatureInstallError> {
let declaration = HookDeclaration::new(name, FeatureHookPoint::PreToolCall); let declaration = HookDeclaration::new(name, FeatureHookPoint::PreToolCall);
self.require_hook_capability(&declaration)?;
self.hook_builder.add_pre_tool_call(hook); self.hook_builder.add_pre_tool_call(hook);
self.report.installed_hooks.push(declaration); self.report.installed_hooks.push(declaration);
Ok(()) Ok(())
@ -846,7 +760,6 @@ impl HookContributionRegistrar<'_> {
hook: impl Hook<PostToolCall> + 'static, hook: impl Hook<PostToolCall> + 'static,
) -> Result<(), FeatureInstallError> { ) -> Result<(), FeatureInstallError> {
let declaration = HookDeclaration::new(name, FeatureHookPoint::ToolResult); let declaration = HookDeclaration::new(name, FeatureHookPoint::ToolResult);
self.require_hook_capability(&declaration)?;
self.hook_builder.add_post_tool_call(hook); self.hook_builder.add_post_tool_call(hook);
self.report.installed_hooks.push(declaration); self.report.installed_hooks.push(declaration);
Ok(()) Ok(())
@ -858,59 +771,32 @@ impl HookContributionRegistrar<'_> {
hook: impl Hook<OnTurnEnd> + 'static, hook: impl Hook<OnTurnEnd> + 'static,
) -> Result<(), FeatureInstallError> { ) -> Result<(), FeatureInstallError> {
let declaration = HookDeclaration::new(name, FeatureHookPoint::TurnEnd); let declaration = HookDeclaration::new(name, FeatureHookPoint::TurnEnd);
self.require_hook_capability(&declaration)?;
self.hook_builder.add_on_turn_end(hook); self.hook_builder.add_on_turn_end(hook);
self.report.installed_hooks.push(declaration); self.report.installed_hooks.push(declaration);
Ok(()) Ok(())
} }
fn require_hook_capability(
&mut self,
declaration: &HookDeclaration,
) -> Result<(), FeatureInstallError> {
for capability in &declaration.required_capabilities {
if !self.grants.contains(capability) {
let reason = format!("required capability was not granted: {capability:?}");
self.report.mark_skipped(
FeatureContributionKind::Hook,
declaration.name.clone(),
reason.clone(),
);
return Err(FeatureInstallError::CapabilityDenied(reason));
}
}
Ok(())
}
} }
/// Background task registrar for descriptor/report-only contributions. /// Background task registrar for descriptor/report-only contributions.
pub struct BackgroundTaskRegistrar<'a> { pub struct BackgroundTaskRegistrar<'a> {
grants: &'a CapabilityGrantSet,
report: &'a mut FeatureInstallReport, report: &'a mut FeatureInstallReport,
} }
impl BackgroundTaskRegistrar<'_> { impl BackgroundTaskRegistrar<'_> {
pub fn declare( pub fn declare(&mut self, declaration: BackgroundTaskDeclaration) {
&mut self,
declaration: BackgroundTaskDeclaration,
) -> Result<(), FeatureInstallError> {
require_background_task_capability(self.grants, self.report, &declaration)?;
self.report.declared_background_tasks.push(declaration); self.report.declared_background_tasks.push(declaration);
Ok(())
} }
} }
/// Service registrar for descriptor/report-only provider metadata. /// Service registrar for descriptor/report-only provider metadata.
pub struct FeatureServiceRegistrar<'a> { pub struct FeatureServiceRegistrar<'a> {
feature_id: &'a FeatureId, feature_id: &'a FeatureId,
grants: &'a CapabilityGrantSet,
service_registry: &'a mut FeatureServiceRegistry, service_registry: &'a mut FeatureServiceRegistry,
report: &'a mut FeatureInstallReport, report: &'a mut FeatureInstallReport,
} }
impl FeatureServiceRegistrar<'_> { impl FeatureServiceRegistrar<'_> {
pub fn provide(&mut self, declaration: ServiceDeclaration) -> Result<(), FeatureInstallError> { pub fn provide(&mut self, declaration: ServiceDeclaration) -> Result<(), FeatureInstallError> {
require_service_provider_capability(self.grants, self.report, &declaration)?;
self.service_registry self.service_registry
.register_provider(self.feature_id.clone(), declaration.clone())?; .register_provider(self.feature_id.clone(), declaration.clone())?;
self.report.provided_services.push(declaration); self.report.provided_services.push(declaration);
@ -921,7 +807,7 @@ impl FeatureServiceRegistrar<'_> {
/// Install-time context provided to a feature module. /// Install-time context provided to a feature module.
pub struct FeatureInstallContext<'a> { pub struct FeatureInstallContext<'a> {
feature_id: &'a FeatureId, feature_id: &'a FeatureId,
grants: &'a CapabilityGrantSet, grants: &'a AuthorityGrantSet,
pending_tools: &'a mut Vec<ToolDefinition>, pending_tools: &'a mut Vec<ToolDefinition>,
installed_tool_names: &'a mut HashMap<String, FeatureId>, installed_tool_names: &'a mut HashMap<String, FeatureId>,
hook_builder: &'a mut HookRegistryBuilder, hook_builder: &'a mut HookRegistryBuilder,
@ -934,7 +820,7 @@ impl FeatureInstallContext<'_> {
self.feature_id self.feature_id
} }
pub fn grants(&self) -> &CapabilityGrantSet { pub fn grants(&self) -> &AuthorityGrantSet {
self.grants self.grants
} }
@ -950,7 +836,6 @@ impl FeatureInstallContext<'_> {
pub fn hooks(&mut self) -> HookContributionRegistrar<'_> { pub fn hooks(&mut self) -> HookContributionRegistrar<'_> {
HookContributionRegistrar { HookContributionRegistrar {
grants: self.grants,
hook_builder: self.hook_builder, hook_builder: self.hook_builder,
report: self.report, report: self.report,
} }
@ -958,7 +843,6 @@ impl FeatureInstallContext<'_> {
pub fn background_tasks(&mut self) -> BackgroundTaskRegistrar<'_> { pub fn background_tasks(&mut self) -> BackgroundTaskRegistrar<'_> {
BackgroundTaskRegistrar { BackgroundTaskRegistrar {
grants: self.grants,
report: self.report, report: self.report,
} }
} }
@ -966,7 +850,6 @@ impl FeatureInstallContext<'_> {
pub fn services(&mut self) -> FeatureServiceRegistrar<'_> { pub fn services(&mut self) -> FeatureServiceRegistrar<'_> {
FeatureServiceRegistrar { FeatureServiceRegistrar {
feature_id: self.feature_id, feature_id: self.feature_id,
grants: self.grants,
service_registry: self.service_registry, service_registry: self.service_registry,
report: self.report, report: self.report,
} }
@ -981,14 +864,12 @@ impl FeatureInstallContext<'_> {
pub fn alerts(&mut self) -> FeatureAlertSink<'_> { pub fn alerts(&mut self) -> FeatureAlertSink<'_> {
FeatureAlertSink { FeatureAlertSink {
grants: self.grants,
report: self.report, report: self.report,
} }
} }
pub fn diagnostics(&mut self) -> FeatureDiagnosticSink<'_> { pub fn diagnostics(&mut self) -> FeatureDiagnosticSink<'_> {
FeatureDiagnosticSink { FeatureDiagnosticSink {
grants: self.grants,
report: self.report, report: self.report,
} }
} }
@ -1076,7 +957,7 @@ impl FeatureRegistryBuilder {
let mut seen_features = HashSet::new(); let mut seen_features = HashSet::new();
for (module, descriptor) in self.modules.into_iter().zip(descriptors.into_iter()) { for (module, descriptor) in self.modules.into_iter().zip(descriptors.into_iter()) {
let grants = CapabilityGrantSet::grant_all(&descriptor.requested_capabilities); let grants = AuthorityGrantSet::grant_all(&descriptor.requested_authorities);
let mut report = FeatureInstallReport::new(&descriptor, grants.clone()); let mut report = FeatureInstallReport::new(&descriptor, grants.clone());
if !seen_features.insert(descriptor.id.clone()) { if !seen_features.insert(descriptor.id.clone()) {
@ -1093,38 +974,15 @@ impl FeatureRegistryBuilder {
continue; continue;
} }
for capability in grants.denied() { for authority in grants.denied() {
report.diagnostics.push(FeatureDiagnostic::warning(format!( report.diagnostics.push(FeatureDiagnostic::warning(format!(
"capability denied: {:?}: {}", "authority denied: {:?}: {}",
capability.capability, capability.reason authority.authority, authority.reason
))); )));
} }
let mut required_service_failed = false; let mut required_service_failed = false;
for requirement in descriptor.requires_services.iter().cloned() { for requirement in descriptor.requires_services.iter().cloned() {
let capability = HostCapability::RequireService {
service: requirement.id.clone(),
};
if let Err(error) = require_capability(
&grants,
&mut report,
FeatureContributionKind::Service,
requirement.id.to_string(),
&capability,
) {
if requirement.required {
report
.diagnostics
.push(FeatureDiagnostic::error(error.to_string()));
required_service_failed = true;
} else {
report
.diagnostics
.push(FeatureDiagnostic::warning(error.to_string()));
}
continue;
}
if service_registry.provides(&requirement.id) { if service_registry.provides(&requirement.id) {
report.resolved_service_requirements.push(requirement); report.resolved_service_requirements.push(requirement);
} else if requirement.required { } else if requirement.required {
@ -1159,19 +1017,11 @@ impl FeatureRegistryBuilder {
} }
for background_task in descriptor.background_tasks.iter().cloned() { for background_task in descriptor.background_tasks.iter().cloned() {
match require_background_task_capability(&grants, &mut report, &background_task) { report.declared_background_tasks.push(background_task);
Ok(()) => report.declared_background_tasks.push(background_task),
Err(error) => report
.diagnostics
.push(FeatureDiagnostic::warning(error.to_string())),
}
} }
for service in descriptor.provides_services.iter().cloned() { for service in descriptor.provides_services.iter().cloned() {
match require_service_provider_capability(&grants, &mut report, &service) { match service_registry.register_provider(descriptor.id.clone(), service.clone()) {
Ok(()) => match service_registry
.register_provider(descriptor.id.clone(), service.clone())
{
Ok(()) => report.provided_services.push(service), Ok(()) => report.provided_services.push(service),
Err(error) => { Err(error) => {
report report
@ -1183,10 +1033,6 @@ impl FeatureRegistryBuilder {
error.to_string(), error.to_string(),
); );
} }
},
Err(error) => report
.diagnostics
.push(FeatureDiagnostic::warning(error.to_string())),
} }
} }
@ -1249,8 +1095,8 @@ pub enum FeatureInstallError {
first_feature: String, first_feature: String,
duplicate_feature: String, duplicate_feature: String,
}, },
#[error("feature capability denied: {0}")] #[error("feature authority denied: {0}")]
CapabilityDenied(String), AuthorityDenied(String),
#[error("feature install failed: {0}")] #[error("feature install failed: {0}")]
Install(String), Install(String),
} }
@ -1273,30 +1119,6 @@ pub mod builtin {
fn descriptor(&self) -> FeatureDescriptor { fn descriptor(&self) -> FeatureDescriptor {
FeatureDescriptor::builtin("task-tools", "Task tools") FeatureDescriptor::builtin("task-tools", "Task tools")
.with_description("Session-lifetime task tracking builtin tools") .with_description("Session-lifetime task tracking builtin tools")
.with_capability(CapabilityRequest::required(
HostCapability::ContributeTool {
name: "TaskCreate".into(),
},
"register TaskCreate builtin tool",
))
.with_capability(CapabilityRequest::required(
HostCapability::ContributeTool {
name: "TaskUpdate".into(),
},
"register TaskUpdate builtin tool",
))
.with_capability(CapabilityRequest::required(
HostCapability::ContributeTool {
name: "TaskGet".into(),
},
"register TaskGet builtin tool",
))
.with_capability(CapabilityRequest::required(
HostCapability::ContributeTool {
name: "TaskList".into(),
},
"register TaskList builtin tool",
))
.with_tool(ToolDeclaration::new( .with_tool(ToolDeclaration::new(
"TaskCreate", "TaskCreate",
"Create a session-lifetime user-visible task", "Create a session-lifetime user-visible task",
@ -1338,6 +1160,7 @@ mod tests {
use llm_worker::llm_client::{ClientError, Request, ResponseStream}; use llm_worker::llm_client::{ClientError, Request, ResponseStream};
use llm_worker::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput}; use llm_worker::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
use serde_json::json; use serde_json::json;
use std::sync::atomic::{AtomicUsize, Ordering};
#[derive(Clone)] #[derive(Clone)]
struct DummyClient; struct DummyClient;
@ -1396,20 +1219,8 @@ mod tests {
} }
#[test] #[test]
fn descriptor_capabilities_and_install_report_are_recorded() { fn descriptor_authorities_and_install_report_are_recorded() {
let descriptor = FeatureDescriptor::builtin("dummy", "Dummy") let descriptor = FeatureDescriptor::builtin("dummy", "Dummy")
.with_capability(CapabilityRequest::required(
HostCapability::ContributeTool {
name: "Dummy".into(),
},
"test",
))
.with_capability(CapabilityRequest::required(
HostCapability::DeclareBackgroundTask {
name: "daily".into(),
},
"test background task declaration",
))
.with_tool(ToolDeclaration::new("Dummy", "dummy tool")) .with_tool(ToolDeclaration::new("Dummy", "dummy tool"))
.with_background_task(BackgroundTaskDeclaration::descriptor_only( .with_background_task(BackgroundTaskDeclaration::descriptor_only(
"daily", "daily",
@ -1431,32 +1242,14 @@ mod tests {
assert!(feature_report.installed); assert!(feature_report.installed);
assert_eq!(feature_report.installed_tools, vec!["Dummy"]); assert_eq!(feature_report.installed_tools, vec!["Dummy"]);
assert_eq!(feature_report.declared_background_tasks[0].name, "daily"); assert_eq!(feature_report.declared_background_tasks[0].name, "daily");
assert!( assert!(feature_report.granted_authorities.denied().is_empty());
feature_report
.granted_capabilities
.contains(&HostCapability::ContributeTool {
name: "Dummy".into()
})
);
} }
#[test] #[test]
fn duplicate_tool_names_are_rejected() { fn duplicate_tool_names_are_rejected() {
let descriptor_a = FeatureDescriptor::builtin("a", "A") let descriptor_a = FeatureDescriptor::builtin("a", "A")
.with_capability(CapabilityRequest::required(
HostCapability::ContributeTool {
name: "Duplicate".into(),
},
"test duplicate handling",
))
.with_tool(ToolDeclaration::new("Duplicate", "first tool")); .with_tool(ToolDeclaration::new("Duplicate", "first tool"));
let descriptor_b = FeatureDescriptor::builtin("b", "B") let descriptor_b = FeatureDescriptor::builtin("b", "B")
.with_capability(CapabilityRequest::required(
HostCapability::ContributeTool {
name: "Duplicate".into(),
},
"test duplicate handling",
))
.with_tool(ToolDeclaration::new("Duplicate", "second tool")); .with_tool(ToolDeclaration::new("Duplicate", "second tool"));
let mut hook_builder = HookRegistryBuilder::default(); let mut hook_builder = HookRegistryBuilder::default();
let mut pending_tools = Vec::new(); let mut pending_tools = Vec::new();
@ -1491,12 +1284,6 @@ mod tests {
#[test] #[test]
fn mismatched_tool_contribution_name_is_rejected_before_queueing() { fn mismatched_tool_contribution_name_is_rejected_before_queueing() {
let descriptor = FeatureDescriptor::builtin("mismatch", "Mismatch") let descriptor = FeatureDescriptor::builtin("mismatch", "Mismatch")
.with_capability(CapabilityRequest::required(
HostCapability::ContributeTool {
name: "Actual".into(),
},
"test mismatch handling",
))
.with_tool(ToolDeclaration::new("Actual", "actual model-visible tool")); .with_tool(ToolDeclaration::new("Actual", "actual model-visible tool"));
let mut hook_builder = HookRegistryBuilder::default(); let mut hook_builder = HookRegistryBuilder::default();
let mut pending_tools = Vec::new(); let mut pending_tools = Vec::new();
@ -1518,6 +1305,61 @@ mod tests {
assert_eq!(report.reports[0].skipped[0].name, "Actual"); 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 { struct ServiceFeature {
descriptor: FeatureDescriptor, descriptor: FeatureDescriptor,
} }
@ -1538,50 +1380,19 @@ mod tests {
#[test] #[test]
fn service_requirements_resolve_against_prior_providers() { fn service_requirements_resolve_against_prior_providers() {
let service = ServiceId::builtin("demo-service"); let service = ServiceId::builtin("demo-service");
let provider = FeatureDescriptor::builtin("provider", "Provider") let provider = FeatureDescriptor::builtin("provider", "Provider").with_provided_service(
.with_capability(CapabilityRequest::required( ServiceDeclaration::new(service.clone(), "1", "demo service"),
HostCapability::ProvideService { );
service: service.clone(),
},
"provide demo service",
))
.with_provided_service(ServiceDeclaration::new(
service.clone(),
"1",
"demo service",
));
let consumer = FeatureDescriptor::builtin("consumer", "Consumer") let consumer = FeatureDescriptor::builtin("consumer", "Consumer")
.with_capability(CapabilityRequest::required(
HostCapability::RequireService {
service: service.clone(),
},
"require demo service",
))
.with_service_requirement(ServiceRequirement::required(service.clone(), "needs demo")); .with_service_requirement(ServiceRequirement::required(service.clone(), "needs demo"));
let missing_service = ServiceId::builtin("missing-service"); let missing_service = ServiceId::builtin("missing-service");
let missing = FeatureDescriptor::builtin("missing", "Missing") let missing = FeatureDescriptor::builtin("missing", "Missing").with_service_requirement(
.with_capability(CapabilityRequest::required( ServiceRequirement::required(missing_service, "needs missing"),
HostCapability::RequireService { );
service: missing_service.clone(),
},
"require missing service",
))
.with_service_requirement(ServiceRequirement::required(
missing_service,
"needs missing",
));
let optional_service = ServiceId::builtin("optional-service"); let optional_service = ServiceId::builtin("optional-service");
let optional = FeatureDescriptor::builtin("optional", "Optional") let optional = FeatureDescriptor::builtin("optional", "Optional").with_service_requirement(
.with_capability(CapabilityRequest::required( ServiceRequirement::optional(optional_service, "nice to have"),
HostCapability::RequireService { );
service: optional_service.clone(),
},
"optionally require service",
))
.with_service_requirement(ServiceRequirement::optional(
optional_service,
"nice to have",
));
let mut hook_builder = HookRegistryBuilder::default(); let mut hook_builder = HookRegistryBuilder::default();
let mut pending_tools = Vec::new(); let mut pending_tools = Vec::new();
let report = FeatureRegistryBuilder::new() let report = FeatureRegistryBuilder::new()
@ -1620,11 +1431,11 @@ mod tests {
} }
#[test] #[test]
fn background_task_declaration_without_capability_is_skipped() { fn background_task_declaration_is_not_sandbox_authority_gated() {
let descriptor = FeatureDescriptor::builtin("background-denied", "Background denied") let descriptor = FeatureDescriptor::builtin("background", "Background")
.with_background_task(BackgroundTaskDeclaration::descriptor_only( .with_background_task(BackgroundTaskDeclaration::descriptor_only(
"denied-task", "declared-task",
"should be skipped", "descriptor contribution",
)); ));
let mut hook_builder = HookRegistryBuilder::default(); let mut hook_builder = HookRegistryBuilder::default();
let mut pending_tools = Vec::new(); let mut pending_tools = Vec::new();
@ -1633,24 +1444,18 @@ mod tests {
.install_into_pending(&mut pending_tools, &mut hook_builder); .install_into_pending(&mut pending_tools, &mut hook_builder);
assert!(report.reports[0].installed); assert!(report.reports[0].installed);
assert!(report.reports[0].declared_background_tasks.is_empty());
assert_eq!( assert_eq!(
report.reports[0].skipped[0].kind, report.reports[0].declared_background_tasks[0].name,
FeatureContributionKind::BackgroundTask "declared-task"
); );
assert!(report.reports[0].diagnostics.iter().any(|diagnostic| { assert!(report.reports[0].skipped.is_empty());
diagnostic
.message
.contains("required capability was not granted")
}));
} }
#[test] #[test]
fn service_provider_without_capability_is_skipped() { fn service_provider_declaration_is_not_sandbox_authority_gated() {
let service = ServiceId::builtin("denied-service"); let service = ServiceId::builtin("declared-service");
let descriptor = let descriptor = FeatureDescriptor::builtin("service", "Service").with_provided_service(
FeatureDescriptor::builtin("service-denied", "Service denied").with_provided_service( ServiceDeclaration::new(service.clone(), "1", "descriptor contribution"),
ServiceDeclaration::new(service.clone(), "1", "should be skipped"),
); );
let mut hook_builder = HookRegistryBuilder::default(); let mut hook_builder = HookRegistryBuilder::default();
let mut pending_tools = Vec::new(); let mut pending_tools = Vec::new();
@ -1659,17 +1464,9 @@ mod tests {
.install_into_pending(&mut pending_tools, &mut hook_builder); .install_into_pending(&mut pending_tools, &mut hook_builder);
assert!(report.reports[0].installed); assert!(report.reports[0].installed);
assert!(!report.services.provides(&service)); assert!(report.services.provides(&service));
assert!(report.reports[0].provided_services.is_empty()); assert_eq!(report.reports[0].provided_services[0].id, service);
assert_eq!( assert!(report.reports[0].skipped.is_empty());
report.reports[0].skipped[0].kind,
FeatureContributionKind::Service
);
assert!(report.reports[0].diagnostics.iter().any(|diagnostic| {
diagnostic
.message
.contains("required capability was not granted")
}));
} }
#[test] #[test]