refactor: remove ambient plugin authority
This commit is contained in:
@@ -15,7 +15,6 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::defaults;
|
||||
use crate::model::{AuthRef, ModelManifest, ReasoningControl};
|
||||
use crate::plugin::PluginConfig;
|
||||
use crate::{
|
||||
CompactionConfig, EngineManifest, FeatureConfig, FeatureFlagConfig, FileUploadLimits,
|
||||
McpConfig, McpEnvValue, McpStdioCwdPolicy, MemoryConsolidationProfileConfig,
|
||||
@@ -55,10 +54,6 @@ pub struct WorkerManifestConfig {
|
||||
/// 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,
|
||||
/// Explicit Model Context Protocol provider declarations. Config parsing
|
||||
/// never starts a local MCP subprocess.
|
||||
#[serde(default)]
|
||||
@@ -74,6 +69,7 @@ pub struct WorkerManifestConfig {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct FeatureConfigPartial {
|
||||
#[serde(default)]
|
||||
pub task: Option<FeatureFlagConfigPartial>,
|
||||
@@ -101,8 +97,6 @@ pub struct FeatureConfigPartial {
|
||||
pub merge_request: Option<MergeRequestFeatureConfigPartial>,
|
||||
#[serde(default)]
|
||||
pub orchestration: Option<FeatureFlagConfigPartial>,
|
||||
#[serde(default)]
|
||||
pub plugins: Option<FeatureFlagConfigPartial>,
|
||||
}
|
||||
|
||||
impl FeatureConfigPartial {
|
||||
@@ -145,7 +139,6 @@ impl FeatureConfigPartial {
|
||||
other.orchestration,
|
||||
FeatureFlagConfigPartial::merge,
|
||||
),
|
||||
plugins: merge_option(self.plugins, other.plugins, FeatureFlagConfigPartial::merge),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -370,10 +363,6 @@ impl From<FeatureConfigPartial> for FeatureConfig {
|
||||
.orchestration
|
||||
.map(FeatureFlagConfig::from)
|
||||
.unwrap_or_default(),
|
||||
plugins: value
|
||||
.plugins
|
||||
.map(FeatureFlagConfig::from)
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -517,7 +506,6 @@ impl From<FeatureConfig> for FeatureConfigPartial {
|
||||
ticket: Some(value.ticket.into()),
|
||||
merge_request: Some(value.merge_request.into()),
|
||||
orchestration: Some(value.orchestration.into()),
|
||||
plugins: Some(value.plugins.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -654,6 +642,20 @@ pub(crate) fn reject_removed_manifest_fields(s: &str) -> Result<(), toml::de::Er
|
||||
"unknown field in manifest: memory (removed; configure feature.memory)",
|
||||
));
|
||||
}
|
||||
if value.get("plugins").is_some() {
|
||||
return Err(toml::de::Error::custom(
|
||||
"unknown field in manifest: plugins (dynamic Plugins are not supported)",
|
||||
));
|
||||
}
|
||||
if value
|
||||
.get("feature")
|
||||
.and_then(toml::Value::as_table)
|
||||
.is_some_and(|table| table.contains_key("plugins"))
|
||||
{
|
||||
return Err(toml::de::Error::custom(
|
||||
"unknown field in manifest: feature.plugins (dynamic Plugins are not supported)",
|
||||
));
|
||||
}
|
||||
if value
|
||||
.get("feature")
|
||||
.and_then(toml::Value::as_table)
|
||||
@@ -771,7 +773,6 @@ impl WorkerManifestConfig {
|
||||
PermissionConfigPartial::merge,
|
||||
),
|
||||
feature: self.feature.merge(upper.feature),
|
||||
plugins: merge_plugin_config(self.plugins, upper.plugins),
|
||||
mcp: merge_mcp_config(self.mcp, upper.mcp),
|
||||
compaction: merge_option(
|
||||
self.compaction,
|
||||
@@ -791,16 +792,6 @@ impl SkillsConfig {
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_plugin_config(mut base: PluginConfig, upper: PluginConfig) -> PluginConfig {
|
||||
let upper_has_resolved_plan = upper.has_resolved_plan();
|
||||
base.enabled.extend(upper.enabled);
|
||||
if upper_has_resolved_plan {
|
||||
base.resolved = upper.resolved;
|
||||
base.diagnostics = upper.diagnostics;
|
||||
}
|
||||
base
|
||||
}
|
||||
|
||||
fn merge_mcp_config(mut base: McpConfig, upper: McpConfig) -> McpConfig {
|
||||
base.stdio_servers.extend(upper.stdio_servers);
|
||||
base
|
||||
@@ -1289,7 +1280,6 @@ impl TryFrom<WorkerManifestConfig> for WorkerManifest {
|
||||
session,
|
||||
permissions,
|
||||
feature: FeatureConfig::from(cfg.feature),
|
||||
plugins: cfg.plugins,
|
||||
mcp: cfg.mcp,
|
||||
compaction,
|
||||
web: cfg.web,
|
||||
@@ -1335,7 +1325,6 @@ mod tests {
|
||||
delegation_scope: ScopeConfig::default(),
|
||||
permissions: None,
|
||||
feature: FeatureConfigPartial::default(),
|
||||
plugins: PluginConfig::default(),
|
||||
mcp: McpConfig::default(),
|
||||
session: None,
|
||||
compaction: None,
|
||||
|
||||
+52
-32
@@ -69,10 +69,6 @@ pub struct WorkerManifest {
|
||||
/// 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,
|
||||
/// Explicit external Model Context Protocol provider configuration. This
|
||||
/// is config data only: declaring a server never starts a subprocess or
|
||||
/// grants OS sandboxing. Runtime MCP lifecycle/registration is a separate
|
||||
@@ -106,6 +102,7 @@ pub struct WorkerManifest {
|
||||
/// sessions, secrets, or resolved host state. Tool registration still applies
|
||||
/// the normal scope, host-authority, backend, memory, and network checks.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct FeatureConfig {
|
||||
#[serde(default)]
|
||||
pub task: FeatureFlagConfig,
|
||||
@@ -135,8 +132,6 @@ pub struct FeatureConfig {
|
||||
pub merge_request: MergeRequestFeatureConfig,
|
||||
#[serde(default)]
|
||||
pub orchestration: FeatureFlagConfig,
|
||||
#[serde(default)]
|
||||
pub plugins: FeatureFlagConfig,
|
||||
}
|
||||
|
||||
impl Default for FeatureConfig {
|
||||
@@ -155,7 +150,6 @@ impl Default for FeatureConfig {
|
||||
ticket: TicketFeatureConfig::default(),
|
||||
merge_request: MergeRequestFeatureConfig::default(),
|
||||
orchestration: FeatureFlagConfig::disabled(),
|
||||
plugins: FeatureFlagConfig::disabled(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -941,9 +935,7 @@ impl Default for CompactionConfig {
|
||||
|
||||
impl WorkerManifest {
|
||||
pub fn requires_persisted_execution_snapshot(&self) -> bool {
|
||||
self.profile.is_some()
|
||||
|| self.plugins.has_resolved_plan()
|
||||
|| self.feature.memory.workspace_settings.is_some()
|
||||
self.profile.is_some() || self.feature.memory.workspace_settings.is_some()
|
||||
}
|
||||
|
||||
/// Parse a manifest from a TOML string.
|
||||
@@ -1322,33 +1314,61 @@ model_id = "claude-sonnet-4-20250514"
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_plugin_enablement_config() {
|
||||
fn dynamic_plugin_manifest_config_is_rejected() {
|
||||
let toml = format!(
|
||||
"{MINIMAL_REQUIRED}\n\
|
||||
[[plugins.enabled]]\n\
|
||||
id = \"project:example\"\n\
|
||||
version = \"0.1.0\"\n\
|
||||
digest = \"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"\n\
|
||||
surfaces = [\"hook\"]\n\n\
|
||||
[plugins.enabled.config]\n\
|
||||
greeting = \"hello\"\n"
|
||||
id = \"project:example\"\n"
|
||||
);
|
||||
let manifest = WorkerManifest::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.version.as_ref().map(|version| version.0.as_str()),
|
||||
Some("0.1.0")
|
||||
let error = WorkerManifest::from_toml(&toml).unwrap_err();
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains("dynamic Plugins are not supported"),
|
||||
"unexpected error: {error}"
|
||||
);
|
||||
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 persisted_manifest_with_dynamic_plugin_plan_is_rejected() {
|
||||
let base =
|
||||
serde_json::to_value(WorkerManifest::from_toml(MINIMAL_REQUIRED).unwrap()).unwrap();
|
||||
|
||||
let mut top_level = base.clone();
|
||||
top_level.as_object_mut().unwrap().insert(
|
||||
"plugins".to_string(),
|
||||
serde_json::json!({
|
||||
"resolved": [{
|
||||
"package_path": "/tmp/ambient.yoi-plugin"
|
||||
}]
|
||||
}),
|
||||
);
|
||||
let error = serde_json::from_value::<WorkerManifest>(top_level).unwrap_err();
|
||||
assert!(error.to_string().contains("unknown field `plugins`"));
|
||||
|
||||
let mut nested = base;
|
||||
nested
|
||||
.get_mut("feature")
|
||||
.unwrap()
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.insert(
|
||||
"plugins".to_string(),
|
||||
serde_json::json!({ "enabled": true }),
|
||||
);
|
||||
let error = serde_json::from_value::<WorkerManifest>(nested).unwrap_err();
|
||||
assert!(error.to_string().contains("unknown field `plugins`"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dynamic_plugin_feature_flag_is_rejected() {
|
||||
let toml = format!("{MINIMAL_REQUIRED}\n[feature.plugins]\nenabled = true\n");
|
||||
let error = WorkerManifest::from_toml(&toml).unwrap_err();
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains("dynamic Plugins are not supported"),
|
||||
"unexpected error: {error}"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+100
-1874
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,6 @@ use crate::config::{
|
||||
CompactionConfigPartial, FeatureConfigPartial, PermissionConfigPartial, SessionConfigPartial,
|
||||
};
|
||||
use crate::model::{AuthRef, ModelManifest};
|
||||
use crate::plugin::PluginConfig;
|
||||
use crate::{
|
||||
EngineManifestConfig, McpConfig, McpStdioCwdPolicy, Permission, ResolveError, ScopeConfig,
|
||||
ScopeRule, SkillsConfig, WebConfig, WorkerManifest, WorkerManifestConfig, WorkerMetaConfig,
|
||||
@@ -148,7 +147,6 @@ pub enum WorkspaceAuthorityRequirement {
|
||||
MergeRequest,
|
||||
Objective,
|
||||
Orchestration,
|
||||
Plugins,
|
||||
Ticket,
|
||||
Worker,
|
||||
}
|
||||
@@ -162,7 +160,6 @@ impl fmt::Display for WorkspaceAuthorityRequirement {
|
||||
Self::MergeRequest => formatter.write_str("feature.merge_request"),
|
||||
Self::Objective => formatter.write_str("feature.objective"),
|
||||
Self::Orchestration => formatter.write_str("feature.orchestration"),
|
||||
Self::Plugins => formatter.write_str("feature.plugins or plugin packages"),
|
||||
Self::Ticket => formatter.write_str("feature.ticket"),
|
||||
Self::Worker => formatter.write_str("feature.worker"),
|
||||
}
|
||||
@@ -202,9 +199,6 @@ pub fn validate_profile_execution_target(
|
||||
if feature.orchestration.enabled {
|
||||
requirements.insert(WorkspaceAuthorityRequirement::Orchestration);
|
||||
}
|
||||
if feature.plugins.enabled || !manifest.plugins.is_empty() {
|
||||
requirements.insert(WorkspaceAuthorityRequirement::Plugins);
|
||||
}
|
||||
if feature.ticket.enabled
|
||||
|| feature.ticket.authoring
|
||||
|| feature.ticket.thread
|
||||
@@ -638,7 +632,6 @@ fn resolve_profile_value(
|
||||
session: profile.session,
|
||||
permissions: profile.permissions,
|
||||
feature: profile.feature,
|
||||
plugins: profile.plugins,
|
||||
mcp: profile.mcp,
|
||||
compaction,
|
||||
web: profile.web,
|
||||
@@ -684,8 +677,6 @@ struct ProfileConfig {
|
||||
#[serde(default)]
|
||||
feature: FeatureConfigPartial,
|
||||
#[serde(default)]
|
||||
plugins: PluginConfig,
|
||||
#[serde(default)]
|
||||
mcp: McpConfig,
|
||||
#[serde(default)]
|
||||
compaction: Option<serde_json::Value>,
|
||||
@@ -1267,6 +1258,51 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ambient_plugin_directories_do_not_affect_builtin_profile_resolution() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let workspace = tmp.path().join("workspace/nested");
|
||||
std::fs::create_dir_all(&workspace).unwrap();
|
||||
for root in [tmp.path(), tmp.path().join("workspace").as_path()] {
|
||||
let package = root.join(".yoi/plugins/broken.yoi-plugin");
|
||||
std::fs::create_dir_all(package.parent().unwrap()).unwrap();
|
||||
std::fs::write(package, b"malformed ambient package").unwrap();
|
||||
}
|
||||
|
||||
let resolved = ProfileResolver::new()
|
||||
.with_workspace_base(&workspace)
|
||||
.resolve_for_target(
|
||||
&ProfileSelector::source_named(ProfileRegistrySource::Builtin, "default"),
|
||||
ProfileResolveOptions::with_worker_name("standalone-worker"),
|
||||
ProfileExecutionTarget::Standalone,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(resolved.manifest.worker.name, "standalone-worker");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_rejects_dynamic_plugin_configuration() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
for body in [
|
||||
"[feature.plugins]\nenabled = true\n",
|
||||
"[[plugins.enabled]]\nid = \"explicit:example\"\n",
|
||||
] {
|
||||
let profile = write_profile(tmp.path(), "plugin.toml", body);
|
||||
let error = ProfileResolver::new()
|
||||
.with_workspace_base(tmp.path())
|
||||
.resolve(
|
||||
&ProfileSelector::path(profile),
|
||||
ProfileResolveOptions::with_worker_name("runtime-worker"),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
error.to_string().contains("unknown field"),
|
||||
"unexpected error: {error}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builtin_default_resolves_as_a_standalone_local_capability_profile() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
@@ -1305,8 +1341,6 @@ mod tests {
|
||||
assert!(!resolved.manifest.feature.flow.enabled);
|
||||
assert!(!resolved.manifest.feature.worker.enabled);
|
||||
assert!(!resolved.manifest.feature.manage_workdir.enabled);
|
||||
assert!(!resolved.manifest.feature.plugins.enabled);
|
||||
assert!(resolved.manifest.plugins.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user