plugin: implement fs host api

This commit is contained in:
Keisuke Hirata 2026-06-20 00:58:44 +09:00
parent 6711bcf300
commit 717c0999a5
No known key found for this signature in database
3 changed files with 1068 additions and 17 deletions

View File

@ -76,11 +76,13 @@ pub struct PluginGrantConfig {
pub permissions: Vec<PluginPermission>, pub permissions: Vec<PluginPermission>,
/// Bounded outbound HTTPS allowlist entries for `host_api.https`. /// Bounded outbound HTTPS allowlist entries for `host_api.https`.
pub https: Vec<PluginHttpsGrant>, pub https: Vec<PluginHttpsGrant>,
/// Scoped filesystem allowlist entries for `host_api.fs`.
pub fs: Vec<PluginFsGrant>,
} }
impl PluginGrantConfig { impl PluginGrantConfig {
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.permissions.is_empty() && self.https.is_empty() self.permissions.is_empty() && self.https.is_empty() && self.fs.is_empty()
} }
pub fn binding_error( pub fn binding_error(
@ -157,6 +159,48 @@ impl PluginHttpsGrant {
} }
} }
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct PluginFsGrant {
/// Absolute host path that bounds every relative `host_api.fs` request.
pub root: String,
/// Explicit operation kinds allowed below `root`; write does not imply read/list.
pub operations: Vec<PluginFsOperation>,
}
impl PluginFsGrant {
pub fn label(&self) -> String {
let operations = if self.operations.is_empty() {
"<no-operations>".to_string()
} else {
self.operations
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(",")
};
format!("{} {}", self.root, operations)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PluginFsOperation {
Read,
List,
Write,
}
impl fmt::Display for PluginFsOperation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Read => f.write_str("read"),
Self::List => f.write_str("list"),
Self::Write => f.write_str("write"),
}
}
}
impl PluginPermission { impl PluginPermission {
pub fn label(&self) -> String { pub fn label(&self) -> String {
match self { match self {
@ -2082,6 +2126,7 @@ input_schema = { type = "object", properties = { query = { type = "string" } },
digest: Some(digest.clone()), digest: Some(digest.clone()),
permissions: vec![PluginPermission::surface(PluginSurface::Hook)], permissions: vec![PluginPermission::surface(PluginSurface::Hook)],
https: Vec::new(), https: Vec::new(),
fs: Vec::new(),
}; };
let resolution = resolve_enabled_plugins( let resolution = resolve_enabled_plugins(
&PluginConfig { &PluginConfig {
@ -2108,6 +2153,7 @@ input_schema = { type = "object", properties = { query = { type = "string" } },
digest: Some(digest.clone()), digest: Some(digest.clone()),
permissions: vec![PluginPermission::surface(PluginSurface::Hook)], permissions: vec![PluginPermission::surface(PluginSurface::Hook)],
https: Vec::new(), https: Vec::new(),
fs: Vec::new(),
}, },
PluginGrantConfig { PluginGrantConfig {
id: Some("project:example".to_string()), id: Some("project:example".to_string()),
@ -2115,6 +2161,7 @@ input_schema = { type = "object", properties = { query = { type = "string" } },
digest: Some(digest.clone()), digest: Some(digest.clone()),
permissions: vec![PluginPermission::surface(PluginSurface::Hook)], permissions: vec![PluginPermission::surface(PluginSurface::Hook)],
https: Vec::new(), https: Vec::new(),
fs: Vec::new(),
}, },
PluginGrantConfig { PluginGrantConfig {
id: Some("project:example".to_string()), id: Some("project:example".to_string()),
@ -2122,6 +2169,7 @@ input_schema = { type = "object", properties = { query = { type = "string" } },
digest: Some("sha256:unrelated".to_string()), digest: Some("sha256:unrelated".to_string()),
permissions: vec![PluginPermission::surface(PluginSurface::Hook)], permissions: vec![PluginPermission::surface(PluginSurface::Hook)],
https: Vec::new(), https: Vec::new(),
fs: Vec::new(),
}, },
] { ] {
let resolution = resolve_enabled_plugins( let resolution = resolve_enabled_plugins(

File diff suppressed because it is too large Load Diff

View File

@ -190,6 +190,11 @@ fn render_item_human(item: &PluginInspectionItem) -> Result<String> {
" configured_https_grants: {}", " configured_https_grants: {}",
join_or_none(&item.configured_https_grants) join_or_none(&item.configured_https_grants)
)?; )?;
writeln!(
out,
" configured_fs_grants: {}",
join_or_none(&item.configured_fs_grants)
)?;
if let Some(runtime) = &item.static_runtime { if let Some(runtime) = &item.static_runtime {
writeln!( writeln!(
@ -360,6 +365,7 @@ fn snapshot_from_resolution(
builder.enabled_surfaces = surface_strings(enablement.surfaces.iter().copied()); builder.enabled_surfaces = surface_strings(enablement.surfaces.iter().copied());
builder.configured_grants = permission_strings(&enablement.grants.permissions); builder.configured_grants = permission_strings(&enablement.grants.permissions);
builder.configured_https_grants = https_grant_strings(&enablement.grants.https); builder.configured_https_grants = https_grant_strings(&enablement.grants.https);
builder.configured_fs_grants = fs_grant_strings(&enablement.grants.fs);
if let Ok(identity) = SourceQualifiedPluginId::parse(&enablement.id) { if let Ok(identity) = SourceQualifiedPluginId::parse(&enablement.id) {
builder builder
.source .source
@ -452,6 +458,7 @@ fn fill_resolved(builder: &mut ItemBuilder, resolved: &ResolvedPlugin) {
builder.requested_permissions = permission_strings(&resolved.manifest.permissions); builder.requested_permissions = permission_strings(&resolved.manifest.permissions);
builder.configured_grants = permission_strings(&resolved.grants.permissions); builder.configured_grants = permission_strings(&resolved.grants.permissions);
builder.configured_https_grants = https_grant_strings(&resolved.grants.https); builder.configured_https_grants = https_grant_strings(&resolved.grants.https);
builder.configured_fs_grants = fs_grant_strings(&resolved.grants.fs);
let record = ResolvedPluginRecord::from_resolved(resolved); let record = ResolvedPluginRecord::from_resolved(resolved);
let static_runtime = inspect_resolved_plugin_static(&record); let static_runtime = inspect_resolved_plugin_static(&record);
@ -554,6 +561,13 @@ fn https_grant_strings(grants: &[manifest::plugin::PluginHttpsGrant]) -> Vec<Str
values values
} }
fn fs_grant_strings(grants: &[manifest::plugin::PluginFsGrant]) -> Vec<String> {
let mut values: Vec<_> = grants.iter().map(|grant| grant.label()).collect();
values.sort();
values.dedup();
values
}
fn permission_requested(manifest: &PluginPackageManifest, permission: &PluginPermission) -> bool { fn permission_requested(manifest: &PluginPackageManifest, permission: &PluginPermission) -> bool {
manifest manifest
.permissions .permissions
@ -625,6 +639,7 @@ struct PluginInspectionItem {
requested_permissions: Vec<String>, requested_permissions: Vec<String>,
configured_grants: Vec<String>, configured_grants: Vec<String>,
configured_https_grants: Vec<String>, configured_https_grants: Vec<String>,
configured_fs_grants: Vec<String>,
tools: Vec<ToolSummary>, tools: Vec<ToolSummary>,
static_runtime: Option<PluginStaticInspection>, static_runtime: Option<PluginStaticInspection>,
diagnostics: Vec<DiagnosticSummary>, diagnostics: Vec<DiagnosticSummary>,
@ -693,6 +708,7 @@ struct ItemBuilder {
requested_permissions: Vec<String>, requested_permissions: Vec<String>,
configured_grants: Vec<String>, configured_grants: Vec<String>,
configured_https_grants: Vec<String>, configured_https_grants: Vec<String>,
configured_fs_grants: Vec<String>,
tools: Vec<ToolSummary>, tools: Vec<ToolSummary>,
static_runtime: Option<PluginStaticInspection>, static_runtime: Option<PluginStaticInspection>,
diagnostics: Vec<DiagnosticSummary>, diagnostics: Vec<DiagnosticSummary>,
@ -719,6 +735,7 @@ impl ItemBuilder {
requested_permissions: Vec::new(), requested_permissions: Vec::new(),
configured_grants: Vec::new(), configured_grants: Vec::new(),
configured_https_grants: Vec::new(), configured_https_grants: Vec::new(),
configured_fs_grants: Vec::new(),
tools: Vec::new(), tools: Vec::new(),
static_runtime: None, static_runtime: None,
diagnostics: Vec::new(), diagnostics: Vec::new(),
@ -790,6 +807,7 @@ impl ItemBuilder {
requested_permissions: self.requested_permissions, requested_permissions: self.requested_permissions,
configured_grants: self.configured_grants, configured_grants: self.configured_grants,
configured_https_grants: self.configured_https_grants, configured_https_grants: self.configured_https_grants,
configured_fs_grants: self.configured_fs_grants,
tools: self.tools, tools: self.tools,
static_runtime: self.static_runtime, static_runtime: self.static_runtime,
diagnostics: self.diagnostics, diagnostics: self.diagnostics,
@ -883,6 +901,7 @@ mod tests {
PluginPermission::tool("Echo"), PluginPermission::tool("Echo"),
], ],
https: Vec::new(), https: Vec::new(),
fs: Vec::new(),
}, },
config: None, config: None,
}); });
@ -900,6 +919,7 @@ mod tests {
PluginPermission::tool("Echo"), PluginPermission::tool("Echo"),
], ],
https: Vec::new(), https: Vec::new(),
fs: Vec::new(),
}, },
config: None, config: None,
}); });
@ -1017,6 +1037,7 @@ mod tests {
PluginPermission::tool("Echo"), PluginPermission::tool("Echo"),
], ],
https: Vec::new(), https: Vec::new(),
fs: Vec::new(),
}, },
config: None, config: None,
}); });
@ -1280,6 +1301,7 @@ mod tests {
PluginPermission::tool("Echo"), PluginPermission::tool("Echo"),
], ],
https: Vec::new(), https: Vec::new(),
fs: Vec::new(),
}, },
config: None, config: None,
}], }],
@ -1310,6 +1332,7 @@ mod tests {
digest: Some(digest), digest: Some(digest),
permissions, permissions,
https: Vec::new(), https: Vec::new(),
fs: Vec::new(),
}, },
config: None, config: None,
} }
@ -1337,6 +1360,7 @@ mod tests {
digest: None, digest: None,
permissions, permissions,
https: Vec::new(), https: Vec::new(),
fs: Vec::new(),
}, },
config: None, config: None,
} }