feat: add plugin package resolver

This commit is contained in:
Keisuke Hirata 2026-06-15 23:26:46 +09:00
parent 4772c4d6a5
commit a03a9da64a
No known key found for this signature in database
8 changed files with 1732 additions and 1 deletions

1
Cargo.lock generated
View File

@ -1798,6 +1798,7 @@ dependencies = [
"serde", "serde",
"serde_ignored", "serde_ignored",
"serde_json", "serde_json",
"sha2 0.10.9",
"tempfile", "tempfile",
"thiserror 2.0.18", "thiserror 2.0.18",
"toml", "toml",

View File

@ -12,6 +12,7 @@ protocol = { workspace = true }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true } serde_json = { workspace = true }
serde_ignored = "0.1.14" serde_ignored = "0.1.14"
sha2 = "0.10"
thiserror = { workspace = true } thiserror = { workspace = true }
toml = { workspace = true } toml = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }

View File

@ -15,6 +15,7 @@ use serde::{Deserialize, Serialize};
use crate::defaults; use crate::defaults;
use crate::model::{AuthRef, ModelManifest, ReasoningControl}; use crate::model::{AuthRef, ModelManifest, ReasoningControl};
use crate::plugin::PluginConfig;
use crate::{ use crate::{
CompactionConfig, FeatureConfig, FeatureFlagConfig, FileUploadLimits, MemoryConfig, CompactionConfig, FeatureConfig, FeatureFlagConfig, FileUploadLimits, MemoryConfig,
PodManifest, PodMeta, ScopeConfig, SessionConfig, SkillsConfig, TicketFeatureAccessConfig, PodManifest, PodMeta, ScopeConfig, SessionConfig, SkillsConfig, TicketFeatureAccessConfig,
@ -52,6 +53,10 @@ pub struct PodManifestConfig {
/// disabled after cascade merge. /// disabled after cascade merge.
#[serde(default)] #[serde(default)]
pub feature: FeatureConfigPartial, pub feature: FeatureConfigPartial,
/// Explicit plugin package enablement entries. Discovery/resolution is a
/// separate step and does not run during config merge.
#[serde(default)]
pub plugins: PluginConfig,
#[serde(default)] #[serde(default)]
pub compaction: Option<CompactionConfigPartial>, pub compaction: Option<CompactionConfigPartial>,
/// First-class web tool opt-in. See [`WebConfig`]. /// First-class web tool opt-in. See [`WebConfig`].
@ -444,6 +449,7 @@ impl PodManifestConfig {
PermissionConfigPartial::merge, PermissionConfigPartial::merge,
), ),
feature: self.feature.merge(upper.feature), feature: self.feature.merge(upper.feature),
plugins: merge_plugin_config(self.plugins, upper.plugins),
compaction: merge_option( compaction: merge_option(
self.compaction, self.compaction,
upper.compaction, upper.compaction,
@ -463,6 +469,11 @@ impl SkillsConfig {
} }
} }
fn merge_plugin_config(mut base: PluginConfig, upper: PluginConfig) -> PluginConfig {
base.enabled.extend(upper.enabled);
base
}
impl WebConfig { impl WebConfig {
fn merge(self, upper: Self) -> Self { fn merge(self, upper: Self) -> Self {
Self { Self {
@ -827,6 +838,7 @@ impl TryFrom<PodManifestConfig> for PodManifest {
session, session,
permissions, permissions,
feature: FeatureConfig::from(cfg.feature), feature: FeatureConfig::from(cfg.feature),
plugins: cfg.plugins,
compaction, compaction,
web: cfg.web, web: cfg.web,
memory: cfg.memory, memory: cfg.memory,
@ -873,6 +885,7 @@ mod tests {
delegation_scope: ScopeConfig::default(), delegation_scope: ScopeConfig::default(),
permissions: None, permissions: None,
feature: FeatureConfigPartial::default(), feature: FeatureConfigPartial::default(),
plugins: PluginConfig::default(),
session: None, session: None,
compaction: None, compaction: None,
web: None, web: None,

View File

@ -2,6 +2,7 @@ mod config;
pub mod defaults; pub mod defaults;
mod model; mod model;
pub mod paths; pub mod paths;
pub mod plugin;
mod profile; mod profile;
mod scope; mod scope;
@ -57,6 +58,10 @@ pub struct PodManifest {
/// resolve disabled so Profile authors choose the exposed built-in surfaces. /// resolve disabled so Profile authors choose the exposed built-in surfaces.
#[serde(default)] #[serde(default)]
pub feature: FeatureConfig, pub feature: FeatureConfig,
/// Explicit plugin package enablement. Discovery remains read-only; only
/// source-qualified entries listed here may resolve to active plugin metadata.
#[serde(default)]
pub plugins: plugin::PluginConfig,
#[serde(default)] #[serde(default)]
pub compaction: Option<CompactionConfig>, pub compaction: Option<CompactionConfig>,
/// Memory subsystem configuration. Presence of `[memory]` configures memory /// Memory subsystem configuration. Presence of `[memory]` configures memory
@ -867,6 +872,32 @@ model_id = "claude-sonnet-4-20250514"
assert!(PodManifest::from_toml(toml).is_err()); assert!(PodManifest::from_toml(toml).is_err());
} }
#[test]
fn parse_plugin_enablement_config() {
let toml = format!(
"{MINIMAL_REQUIRED}\n\
[[plugins.enabled]]\n\
id = \"project:example\"\n\
digest = \"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"\n\
surfaces = [\"hook\"]\n\n\
[plugins.enabled.config]\n\
greeting = \"hello\"\n"
);
let manifest = PodManifest::from_toml(&toml).unwrap();
assert_eq!(manifest.plugins.enabled.len(), 1);
let enabled = &manifest.plugins.enabled[0];
assert_eq!(enabled.id, "project:example");
assert_eq!(enabled.surfaces, vec![plugin::PluginSurface::Hook]);
assert_eq!(
enabled
.config
.as_ref()
.and_then(|value| value.get("greeting"))
.and_then(|value| value.as_str()),
Some("hello")
);
}
#[test] #[test]
fn parse_max_turns() { fn parse_max_turns() {
let toml = MINIMAL_REQUIRED.replace("[worker]\n", "[worker]\nmax_turns = 50\n"); let toml = MINIMAL_REQUIRED.replace("[worker]\n", "[worker]\nmax_turns = 50\n");

File diff suppressed because it is too large Load Diff

View File

@ -17,6 +17,7 @@ use crate::config::{
CompactionConfigPartial, FeatureConfigPartial, PermissionConfigPartial, SessionConfigPartial, CompactionConfigPartial, FeatureConfigPartial, PermissionConfigPartial, SessionConfigPartial,
}; };
use crate::model::{AuthRef, ModelManifest}; use crate::model::{AuthRef, ModelManifest};
use crate::plugin::PluginConfig;
use crate::{ use crate::{
MemoryConfig, Permission, PodManifest, PodManifestConfig, PodMetaConfig, ResolveError, MemoryConfig, Permission, PodManifest, PodManifestConfig, PodMetaConfig, ResolveError,
ScopeConfig, ScopeRule, SkillsConfig, WebConfig, WorkerManifestConfig, paths, ScopeConfig, ScopeRule, SkillsConfig, WebConfig, WorkerManifestConfig, paths,
@ -626,6 +627,7 @@ fn resolve_lua_profile_value(
session: profile.session, session: profile.session,
permissions: profile.permissions, permissions: profile.permissions,
feature: profile.feature, feature: profile.feature,
plugins: profile.plugins,
compaction, compaction,
web: profile.web, web: profile.web,
memory: profile.memory, memory: profile.memory,
@ -687,6 +689,8 @@ struct ProfileConfig {
#[serde(default)] #[serde(default)]
feature: FeatureConfigPartial, feature: FeatureConfigPartial,
#[serde(default)] #[serde(default)]
plugins: PluginConfig,
#[serde(default)]
compaction: Option<serde_json::Value>, compaction: Option<serde_json::Value>,
#[serde(default)] #[serde(default)]
web: Option<WebConfig>, web: Option<WebConfig>,

View File

@ -775,6 +775,7 @@ fn manifest_to_reusable_config(manifest: &PodManifest) -> PodManifestConfig {
rules: p.rules.clone(), rules: p.rules.clone(),
}), }),
feature: manifest.feature.clone().into(), feature: manifest.feature.clone().into(),
plugins: manifest.plugins.clone(),
compaction: manifest compaction: manifest
.compaction .compaction
.as_ref() .as_ref()

View File

@ -40,7 +40,7 @@ rustPlatform.buildRustPackage rec {
filter = sourceFilter; filter = sourceFilter;
}; };
cargoHash = "sha256-pIDYnbBs3U8Z3IndgH10rirv8/IdFv1WlgwpCbKXy+M="; cargoHash = "sha256-Y1siH1oDe9It7ntx83DJO5fzV9LtC7+qq9V6RPlRxUY=";
depsExtraArgs = { depsExtraArgs = {
# Older fetchCargoVendor utilities used crates.io's API download endpoint, # Older fetchCargoVendor utilities used crates.io's API download endpoint,