fix: align feature authority boundaries
This commit is contained in:
parent
40701760eb
commit
98bbd6f185
|
|
@ -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]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user