feat: remove feature host authority API

This commit is contained in:
Keisuke Hirata 2026-06-14 03:52:26 +09:00
parent c4465a04d8
commit 5549c50d86
No known key found for this signature in database
3 changed files with 16 additions and 287 deletions

View File

@ -1,8 +1,8 @@
//! Feature contribution registry for Pod-hosted builtin/plugin modules.
//!
//! This module defines the Pod-side feature boundary used to collect
//! descriptor metadata, host authority requests, tool contributions, safe hook
//! contributions, background task declarations, and service declarations before
//! descriptor metadata, tool contributions, safe hook contributions, background
//! task declarations, and service declarations before
//! installing them into the existing Worker/HookRegistry host surfaces.
//!
//! The first implementation slice is intentionally host-mediated and
@ -69,26 +69,6 @@ pub enum FeatureRuntimeKind {
ExternalPlugin,
}
/// Host authority requested by a feature for host-mediated operations that can
/// cross sandbox or model-context boundaries.
///
/// Contribution declarations such as tools, hooks, background tasks, and
/// services are descriptor/package-approved host-visible contributions, not
/// host authorities. Host authority grants are additive and do not replace
/// manifest/tool permission checks.
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HostAuthority {
Filesystem,
Network,
SecretRef { id: String },
ModelNotification,
PodManagement,
StateStore { name: String },
TicketBackend { root: String },
ServiceAccess { service: ServiceId },
}
/// A safe hook contribution point exposed to feature modules.
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
@ -99,82 +79,6 @@ pub enum FeatureHookPoint {
TurnEnd,
}
/// Host authority request declared by a feature descriptor.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct HostAuthorityRequest {
pub authority: HostAuthority,
pub required: bool,
pub reason: String,
}
impl HostAuthorityRequest {
pub fn required(authority: HostAuthority, reason: impl Into<String>) -> Self {
Self {
authority,
required: true,
reason: reason.into(),
}
}
pub fn optional(authority: HostAuthority, reason: impl Into<String>) -> Self {
Self {
authority,
required: false,
reason: reason.into(),
}
}
}
/// Host authority grants resolved by the host for one feature installation.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct HostAuthorityGrantSet {
granted: HashSet<HostAuthority>,
denied: Vec<HostAuthorityDenial>,
}
impl HostAuthorityGrantSet {
pub fn grant_all(requests: &[HostAuthorityRequest]) -> Self {
Self {
granted: requests
.iter()
.map(|request| request.authority.clone())
.collect(),
denied: Vec::new(),
}
}
pub fn empty() -> Self {
Self::default()
}
pub fn contains(&self, authority: &HostAuthority) -> bool {
self.granted.contains(authority)
}
pub fn denied(&self) -> &[HostAuthorityDenial] {
&self.denied
}
pub fn grant(&mut self, authority: HostAuthority) {
self.granted.insert(authority);
}
pub fn deny(&mut self, authority: HostAuthority, reason: impl Into<String>) {
self.granted.remove(&authority);
self.denied.push(HostAuthorityDenial {
authority,
reason: reason.into(),
});
}
}
/// Host-side denial of a requested feature host authority.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct HostAuthorityDenial {
pub authority: HostAuthority,
pub reason: String,
}
/// Serializable declaration of a tool contribution. The executable factory is
/// carried by [`ToolContribution`] during installation.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
@ -192,12 +96,10 @@ impl ToolDeclaration {
}
}
/// Executable tool contribution wrapper. Host-authority requirements are optional
/// per-tool gates for privileged host APIs, not permission to contribute a tool.
/// Executable tool contribution wrapper.
pub struct ToolContribution {
name: String,
definition: ToolDefinition,
required_host_authorities: Vec<HostAuthority>,
}
impl ToolContribution {
@ -205,18 +107,9 @@ impl ToolContribution {
Self {
name: name.into(),
definition,
required_host_authorities: Vec::new(),
}
}
pub fn with_required_host_authorities(
mut self,
required_host_authorities: Vec<HostAuthority>,
) -> Self {
self.required_host_authorities = required_host_authorities;
self
}
pub fn name(&self) -> &str {
&self.name
}
@ -410,7 +303,6 @@ pub struct FeatureDescriptor {
pub display_name: String,
pub version: String,
pub description: String,
pub requested_host_authorities: Vec<HostAuthorityRequest>,
pub tools: Vec<ToolDeclaration>,
pub hooks: Vec<HookDeclaration>,
pub background_tasks: Vec<BackgroundTaskDeclaration>,
@ -426,7 +318,6 @@ impl FeatureDescriptor {
display_name: display_name.into(),
version: env!("CARGO_PKG_VERSION").into(),
description: String::new(),
requested_host_authorities: Vec::new(),
tools: Vec::new(),
hooks: Vec::new(),
background_tasks: Vec::new(),
@ -440,11 +331,6 @@ impl FeatureDescriptor {
self
}
pub fn with_host_authority(mut self, request: HostAuthorityRequest) -> Self {
self.requested_host_authorities.push(request);
self
}
pub fn with_tool(mut self, tool: ToolDeclaration) -> Self {
self.tools.push(tool);
self
@ -543,7 +429,6 @@ pub struct FeatureInstallReport {
pub feature_id: FeatureId,
pub runtime: FeatureRuntimeKind,
pub installed: bool,
pub host_authority_grants: HostAuthorityGrantSet,
pub installed_tools: Vec<String>,
pub installed_hooks: Vec<HookDeclaration>,
pub declared_background_tasks: Vec<BackgroundTaskDeclaration>,
@ -554,12 +439,11 @@ pub struct FeatureInstallReport {
}
impl FeatureInstallReport {
fn new(descriptor: &FeatureDescriptor, host_authority_grants: HostAuthorityGrantSet) -> Self {
fn new(descriptor: &FeatureDescriptor) -> Self {
Self {
feature_id: descriptor.id.clone(),
runtime: descriptor.runtime.clone(),
installed: false,
host_authority_grants,
installed_tools: Vec::new(),
installed_hooks: Vec::new(),
declared_background_tasks: Vec::new(),
@ -653,38 +537,14 @@ fn reject_undeclared_contribution(
error
}
fn require_host_authority(
host_authority_grants: &HostAuthorityGrantSet,
report: &mut FeatureInstallReport,
kind: FeatureContributionKind,
name: impl Into<String>,
authority: &HostAuthority,
) -> Result<(), FeatureInstallError> {
if host_authority_grants.contains(authority) {
return Ok(());
}
let reason = format!("required host authority was not granted: {authority:?}");
report.mark_skipped(kind, name, reason.clone());
Err(FeatureInstallError::HostAuthorityDenied(reason))
}
/// Model-visible durable notification sink skeleton. The first slice exposes
/// the boundary without implementing a new event channel.
pub struct FeatureNotificationSink<'a> {
host_authority_grants: &'a HostAuthorityGrantSet,
report: &'a mut FeatureInstallReport,
}
impl FeatureNotificationSink<'_> {
pub fn notify_model(&mut self, message: impl Into<String>) -> Result<(), FeatureInstallError> {
require_host_authority(
self.host_authority_grants,
self.report,
FeatureContributionKind::Notification,
"notify_model",
&HostAuthority::ModelNotification,
)?;
let message = message.into();
self.report.diagnostics.push(FeatureDiagnostic::warning(format!(
"model notification requested during feature installation but no durable Notify host is attached: {message}"
@ -744,7 +604,6 @@ impl FeatureDiagnosticSink<'_> {
pub struct ToolContributionRegistrar<'a> {
feature_id: &'a FeatureId,
declarations: &'a FeatureContributionDeclarations,
host_authority_grants: &'a HostAuthorityGrantSet,
pending_tools: &'a mut Vec<ToolDefinition>,
installed_tool_names: &'a mut HashMap<String, FeatureId>,
report: &'a mut FeatureInstallReport,
@ -776,16 +635,6 @@ impl ToolContributionRegistrar<'_> {
));
}
for authority in &contribution.required_host_authorities {
require_host_authority(
self.host_authority_grants,
self.report,
FeatureContributionKind::Tool,
model_visible_name.clone(),
authority,
)?;
}
if let Some(first) = self.installed_tool_names.get(&model_visible_name) {
let error = FeatureInstallError::DuplicateToolName {
tool: model_visible_name.clone(),
@ -951,7 +800,6 @@ impl FeatureServiceRegistrar<'_> {
pub struct FeatureInstallContext<'a> {
feature_id: &'a FeatureId,
declarations: &'a FeatureContributionDeclarations,
host_authority_grants: &'a HostAuthorityGrantSet,
pending_tools: &'a mut Vec<ToolDefinition>,
installed_tool_names: &'a mut HashMap<String, FeatureId>,
hook_builder: &'a mut HookRegistryBuilder,
@ -964,15 +812,10 @@ impl FeatureInstallContext<'_> {
self.feature_id
}
pub fn host_authority_grants(&self) -> &HostAuthorityGrantSet {
self.host_authority_grants
}
pub fn tools(&mut self) -> ToolContributionRegistrar<'_> {
ToolContributionRegistrar {
feature_id: self.feature_id,
declarations: self.declarations,
host_authority_grants: self.host_authority_grants,
pending_tools: self.pending_tools,
installed_tool_names: self.installed_tool_names,
report: self.report,
@ -1007,7 +850,6 @@ impl FeatureInstallContext<'_> {
pub fn notifications(&mut self) -> FeatureNotificationSink<'_> {
FeatureNotificationSink {
host_authority_grants: self.host_authority_grants,
report: self.report,
}
}
@ -1107,10 +949,8 @@ impl FeatureRegistryBuilder {
let mut seen_features = HashSet::new();
for (module, descriptor) in self.modules.into_iter().zip(descriptors.into_iter()) {
let host_authority_grants =
HostAuthorityGrantSet::grant_all(&descriptor.requested_host_authorities);
let declarations = FeatureContributionDeclarations::from_descriptor(&descriptor);
let mut report = FeatureInstallReport::new(&descriptor, host_authority_grants.clone());
let mut report = FeatureInstallReport::new(&descriptor);
if !seen_features.insert(descriptor.id.clone()) {
report.diagnostics.push(FeatureDiagnostic::error(format!(
@ -1126,13 +966,6 @@ impl FeatureRegistryBuilder {
continue;
}
for authority in host_authority_grants.denied() {
report.diagnostics.push(FeatureDiagnostic::warning(format!(
"host authority denied: {:?}: {}",
authority.authority, authority.reason
)));
}
let mut required_service_failed = false;
for requirement in descriptor.requires_services.iter().cloned() {
if service_registry.provides(&requirement.id) {
@ -1192,7 +1025,6 @@ impl FeatureRegistryBuilder {
let mut context = FeatureInstallContext {
feature_id: &descriptor.id,
declarations: &declarations,
host_authority_grants: &host_authority_grants,
pending_tools,
installed_tool_names: &mut installed_tool_names,
hook_builder,
@ -1256,8 +1088,6 @@ pub enum FeatureInstallError {
first_feature: String,
duplicate_feature: String,
},
#[error("feature host authority denied: {0}")]
HostAuthorityDenied(String),
#[error("feature install failed: {0}")]
Install(String),
}
@ -1335,7 +1165,7 @@ mod tests {
}
#[test]
fn descriptor_contributions_and_empty_host_authority_grants_are_recorded() {
fn descriptor_contributions_are_recorded() {
let descriptor = FeatureDescriptor::builtin("dummy", "Dummy")
.with_tool(ToolDeclaration::new("Dummy", "dummy tool"))
.with_background_task(BackgroundTaskDeclaration::descriptor_only(
@ -1358,7 +1188,6 @@ mod tests {
assert!(feature_report.installed);
assert_eq!(feature_report.installed_tools, vec!["Dummy"]);
assert_eq!(feature_report.declared_background_tasks[0].name, "daily");
assert!(feature_report.host_authority_grants.denied().is_empty());
}
#[test]
@ -1421,79 +1250,6 @@ mod tests {
assert_eq!(report.reports[0].skipped[0].name, "Actual");
}
#[test]
fn tool_host_authority_requirements_use_host_authority_grants_not_contribution_declarations() {
struct HostAuthorityToolFeature {
descriptor: FeatureDescriptor,
required_host_authorities: Vec<HostAuthority>,
}
impl FeatureModule for HostAuthorityToolFeature {
fn descriptor(&self) -> FeatureDescriptor {
self.descriptor.clone()
}
fn install(
&self,
context: &mut FeatureInstallContext<'_>,
) -> Result<(), FeatureInstallError> {
context.tools().register(
ToolContribution::new("NetworkTool", dummy_tool("NetworkTool"))
.with_required_host_authorities(self.required_host_authorities.clone()),
)
}
}
let mut hook_builder = HookRegistryBuilder::default();
let mut pending_tools = Vec::new();
let missing_grant = FeatureDescriptor::builtin("missing-host-authority", "Missing")
.with_tool(ToolDeclaration::new("NetworkTool", "network host API tool"));
let missing_report = FeatureRegistryBuilder::new()
.with_module(HostAuthorityToolFeature {
descriptor: missing_grant,
required_host_authorities: vec![HostAuthority::Network],
})
.install_into_pending(&mut pending_tools, &mut hook_builder);
assert!(pending_tools.is_empty());
assert!(!missing_report.reports[0].installed);
assert!(
missing_report.reports[0]
.diagnostics
.iter()
.any(|diagnostic| {
diagnostic
.message
.contains("required host authority was not granted")
})
);
assert_eq!(
missing_report.reports[0].skipped[0].kind,
FeatureContributionKind::Tool
);
let granted = FeatureDescriptor::builtin("granted-host-authority", "Granted")
.with_host_authority(HostAuthorityRequest::required(
HostAuthority::Network,
"uses a host network API",
))
.with_tool(ToolDeclaration::new("NetworkTool", "network host API tool"));
let granted_report = FeatureRegistryBuilder::new()
.with_module(HostAuthorityToolFeature {
descriptor: granted,
required_host_authorities: vec![HostAuthority::Network],
})
.install_into_pending(&mut pending_tools, &mut hook_builder);
assert!(granted_report.reports[0].installed);
assert!(
granted_report.reports[0]
.host_authority_grants
.contains(&HostAuthority::Network)
);
assert_eq!(pending_tools.len(), 1);
}
#[test]
fn stateful_tool_definition_is_materialized_once_for_report_and_worker() {
struct StatefulToolFeature {
@ -1790,7 +1546,7 @@ mod tests {
}
#[test]
fn background_task_declaration_is_not_host_authority_gated() {
fn background_task_declaration_is_descriptor_contribution() {
let descriptor = FeatureDescriptor::builtin("background", "Background")
.with_background_task(BackgroundTaskDeclaration::descriptor_only(
"declared-task",
@ -1811,7 +1567,7 @@ mod tests {
}
#[test]
fn service_provider_declaration_is_not_host_authority_gated() {
fn service_provider_declaration_is_descriptor_contribution() {
let service = ServiceId::builtin("declared-service");
let descriptor = FeatureDescriptor::builtin("service", "Service").with_provided_service(
ServiceDeclaration::new(service.clone(), "1", "descriptor contribution"),
@ -1829,7 +1585,7 @@ mod tests {
}
#[test]
fn builtin_internal_task_feature_descriptor_has_exact_tools_hooks_and_no_host_authorities() {
fn builtin_internal_task_feature_descriptor_has_exact_tools_hooks() {
let descriptor = builtin::task_tools_feature().descriptor();
let tool_names: Vec<_> = descriptor
.tools
@ -1845,7 +1601,6 @@ mod tests {
assert_eq!(descriptor.id.as_str(), "builtin:task-tools");
assert_eq!(descriptor.runtime, FeatureRuntimeKind::Builtin);
assert!(descriptor.requested_host_authorities.is_empty());
assert_eq!(
hook_points,
vec![FeatureHookPoint::PreRequest, FeatureHookPoint::PreToolCall]
@ -1860,7 +1615,7 @@ mod tests {
}
#[test]
fn builtin_internal_task_feature_installs_declared_tools_without_host_authorities() {
fn builtin_internal_task_feature_installs_declared_tools() {
let mut hook_builder = HookRegistryBuilder::default();
let mut pending_tools = Vec::new();
let mut builder = FeatureRegistryBuilder::new();
@ -1882,10 +1637,6 @@ mod tests {
assert_eq!(report.reports.len(), 1);
assert!(report.reports[0].installed);
assert_eq!(
report.reports[0].host_authority_grants,
HostAuthorityGrantSet::empty()
);
assert!(report.reports[0].skipped.is_empty());
assert!(report.reports[0].diagnostics.is_empty());
assert_eq!(report.reports[0].installed_hooks.len(), 2);

View File

@ -35,9 +35,8 @@ const TASK_MANAGEMENT_TOOL_NAMES: [&str; 2] = ["TaskCreate", "TaskUpdate"];
///
/// The returned module contributes `TaskCreate`, `TaskUpdate`, `TaskGet`, and
/// `TaskList` through descriptor-approved tool registration, plus built-in hooks
/// that maintain Task-reminder state. It does not request sandbox/external-plugin
/// host authorities; normal ToolRegistry and PreToolCall permission policy still
/// applies at call time.
/// that maintain Task-reminder state. Normal ToolRegistry and PreToolCall
/// permission policy still applies at call time.
pub fn task_tools_feature() -> TaskFeature {
TaskFeature::new()
}

View File

@ -18,14 +18,13 @@ use ticket::{
use crate::feature::{
FeatureDescriptor, FeatureDiagnostic, FeatureInstallContext, FeatureInstallError,
FeatureModule, HostAuthority, HostAuthorityRequest, ToolContribution, ToolDeclaration,
FeatureModule, ToolContribution, ToolDeclaration,
};
const FEATURE_ID: &str = "ticket";
const FEATURE_NAME: &str = "Ticket tools";
const FEATURE_DESCRIPTION: &str = "Typed local Ticket work-item operations over a bounded backend root. \
The tools operate through the ticket crate backend and do not grant generic filesystem write scope.";
const AUTHORITY_REASON: &str = "Use a configured local Ticket backend root for typed work-item operations without generic filesystem write authority.";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TicketFeatureAccess {
@ -150,12 +149,6 @@ impl TicketFeature {
names
}
fn authority(&self) -> HostAuthority {
HostAuthority::TicketBackend {
root: self.backend_root.display().to_string(),
}
}
fn usable_backend_root(&self) -> Result<PathBuf, String> {
let root = self
.backend_root
@ -171,11 +164,7 @@ impl TicketFeature {
impl FeatureModule for TicketFeature {
fn descriptor(&self) -> FeatureDescriptor {
let mut descriptor = FeatureDescriptor::builtin(FEATURE_ID, FEATURE_NAME)
.with_description(FEATURE_DESCRIPTION)
.with_host_authority(HostAuthorityRequest::required(
self.authority(),
AUTHORITY_REASON,
));
.with_description(FEATURE_DESCRIPTION);
let enabled_tool_names = self.enabled_tool_names();
for name in &enabled_tool_names {
descriptor = descriptor.with_tool(ToolDeclaration::new(
@ -207,7 +196,6 @@ impl FeatureModule for TicketFeature {
return Ok(());
}
};
let authority = self.authority();
let backend = LocalTicketBackend::new(usable_root)
.with_record_language(self.record_language.as_deref());
let allowed_tool_names = self.enabled_tool_names();
@ -221,10 +209,7 @@ impl FeatureModule for TicketFeature {
{
continue;
}
tools.register(
ToolContribution::new(name, definition)
.with_required_host_authorities(vec![authority.clone()]),
)?;
tools.register(ToolContribution::new(name, definition))?;
}
Ok(())
}
@ -284,7 +269,7 @@ mod tests {
}
#[test]
fn descriptor_declares_ticket_tools_and_backend_authority() {
fn descriptor_declares_ticket_tools() {
let temp = TempDir::new().unwrap();
let feature = ticket_tools_feature(temp.path());
let descriptor = feature.descriptor();
@ -299,11 +284,6 @@ mod tests {
.collect::<Vec<_>>(),
TICKET_TOOL_NAMES
);
assert_eq!(descriptor.requested_host_authorities.len(), 1);
assert!(matches!(
descriptor.requested_host_authorities[0].authority,
HostAuthority::TicketBackend { .. }
));
}
#[test]
@ -321,7 +301,6 @@ mod tests {
.collect::<Vec<_>>(),
TICKET_READ_ONLY_TOOL_NAMES
);
assert_eq!(descriptor.requested_host_authorities.len(), 1);
}
#[test]