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_ignored",
"serde_json",
"sha2 0.10.9",
"tempfile",
"thiserror 2.0.18",
"toml",

View File

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

View File

@ -15,6 +15,7 @@ use serde::{Deserialize, Serialize};
use crate::defaults;
use crate::model::{AuthRef, ModelManifest, ReasoningControl};
use crate::plugin::PluginConfig;
use crate::{
CompactionConfig, FeatureConfig, FeatureFlagConfig, FileUploadLimits, MemoryConfig,
PodManifest, PodMeta, ScopeConfig, SessionConfig, SkillsConfig, TicketFeatureAccessConfig,
@ -52,6 +53,10 @@ pub struct PodManifestConfig {
/// disabled after cascade merge.
#[serde(default)]
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)]
pub compaction: Option<CompactionConfigPartial>,
/// First-class web tool opt-in. See [`WebConfig`].
@ -444,6 +449,7 @@ impl PodManifestConfig {
PermissionConfigPartial::merge,
),
feature: self.feature.merge(upper.feature),
plugins: merge_plugin_config(self.plugins, upper.plugins),
compaction: merge_option(
self.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 {
fn merge(self, upper: Self) -> Self {
Self {
@ -827,6 +838,7 @@ impl TryFrom<PodManifestConfig> for PodManifest {
session,
permissions,
feature: FeatureConfig::from(cfg.feature),
plugins: cfg.plugins,
compaction,
web: cfg.web,
memory: cfg.memory,
@ -873,6 +885,7 @@ mod tests {
delegation_scope: ScopeConfig::default(),
permissions: None,
feature: FeatureConfigPartial::default(),
plugins: PluginConfig::default(),
session: None,
compaction: None,
web: None,

View File

@ -2,6 +2,7 @@ mod config;
pub mod defaults;
mod model;
pub mod paths;
pub mod plugin;
mod profile;
mod scope;
@ -57,6 +58,10 @@ pub struct PodManifest {
/// resolve disabled so Profile authors choose the exposed built-in surfaces.
#[serde(default)]
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)]
pub compaction: Option<CompactionConfig>,
/// 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());
}
#[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]
fn parse_max_turns() {
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,
};
use crate::model::{AuthRef, ModelManifest};
use crate::plugin::PluginConfig;
use crate::{
MemoryConfig, Permission, PodManifest, PodManifestConfig, PodMetaConfig, ResolveError,
ScopeConfig, ScopeRule, SkillsConfig, WebConfig, WorkerManifestConfig, paths,
@ -626,6 +627,7 @@ fn resolve_lua_profile_value(
session: profile.session,
permissions: profile.permissions,
feature: profile.feature,
plugins: profile.plugins,
compaction,
web: profile.web,
memory: profile.memory,
@ -687,6 +689,8 @@ struct ProfileConfig {
#[serde(default)]
feature: FeatureConfigPartial,
#[serde(default)]
plugins: PluginConfig,
#[serde(default)]
compaction: Option<serde_json::Value>,
#[serde(default)]
web: Option<WebConfig>,

View File

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

View File

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