Merge remote-tracking branch 'refs/remotes/origin/develop' into work/T-468-remove-local-workspace-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]
|
||||
|
||||
@@ -43,7 +43,6 @@ memory = { workspace = true }
|
||||
uuid = { workspace = true, features = ["v7"] }
|
||||
session-metrics = { workspace = true }
|
||||
arc-swap = "1.9.1"
|
||||
wasmtime = { version = "45.0.2", default-features = false, features = ["std", "runtime", "cranelift", "component-model"] }
|
||||
tungstenite = { version = "0.28.0", default-features = false, features = ["handshake", "native-tls", "url"] }
|
||||
tokio-tungstenite = { version = "0.28.0", default-features = false, features = ["native-tls", "connect"] }
|
||||
futures-util = { version = "0.3", features = ["sink"] }
|
||||
@@ -53,5 +52,3 @@ dotenv = "0.15.0"
|
||||
futures = { workspace = true }
|
||||
serial_test = "3.4.0"
|
||||
tempfile = { workspace = true }
|
||||
wat = "1.241.2"
|
||||
yoi-plugin-pdk = { workspace = true }
|
||||
|
||||
@@ -1385,12 +1385,6 @@ where
|
||||
feature_registry
|
||||
.add_module(crate::feature::builtin::orchestration::orchestration_feature());
|
||||
}
|
||||
for module in crate::feature::plugin::plugin_tool_features_if_enabled(
|
||||
feature_config.plugins.enabled,
|
||||
&worker.manifest().plugins,
|
||||
) {
|
||||
feature_registry = feature_registry.with_module(module);
|
||||
}
|
||||
if let Some(workspace_root) = local_workspace_root.as_ref() {
|
||||
if let Some(module) =
|
||||
crate::feature::mcp::discover_stdio_tool_feature(&mcp_config, workspace_root).await
|
||||
|
||||
@@ -2211,7 +2211,6 @@ pub enum FeatureInstallError {
|
||||
pub mod background;
|
||||
pub mod builtin;
|
||||
pub mod mcp;
|
||||
pub mod plugin;
|
||||
pub(crate) mod session;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -2224,6 +2223,31 @@ mod tests {
|
||||
use serde_json::json;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
#[test]
|
||||
fn worker_feature_composition_has_no_dynamic_plugin_install_path() {
|
||||
let feature_source = include_str!("feature.rs")
|
||||
.split("#[cfg(test)]")
|
||||
.next()
|
||||
.unwrap();
|
||||
let controller_source = include_str!("controller.rs")
|
||||
.split("#[cfg(test)]")
|
||||
.next()
|
||||
.unwrap();
|
||||
for forbidden in [
|
||||
"pub mod plugin",
|
||||
"plugin_tool_features_if_enabled",
|
||||
"ResolvedPluginRecord",
|
||||
"read_resolved_plugin_runtime_component",
|
||||
"feature.plugins",
|
||||
] {
|
||||
assert!(
|
||||
!feature_source.contains(forbidden) && !controller_source.contains(forbidden),
|
||||
"dynamic Plugin install path returned through {forbidden}"
|
||||
);
|
||||
}
|
||||
assert_eq!(FeatureId::builtin("task").as_str(), "builtin:task");
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct DummyClient;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -127,7 +127,6 @@ where
|
||||
// parent manifest cannot accidentally grant its normal public tool surface
|
||||
// or recursively schedule Feature-owned background work.
|
||||
manifest.feature = Default::default();
|
||||
manifest.plugins = Default::default();
|
||||
manifest.mcp = Default::default();
|
||||
manifest.skills = None;
|
||||
manifest.compaction = None;
|
||||
@@ -681,7 +680,6 @@ pub(crate) fn prepare_internal_worker_from_spec(
|
||||
} = spec;
|
||||
manifest.worker.name = format!("internal-{}-{}", identity.kind, identity.run_id);
|
||||
manifest.feature = Default::default();
|
||||
manifest.plugins = Default::default();
|
||||
manifest.mcp = Default::default();
|
||||
manifest.skills = None;
|
||||
manifest.compaction = None;
|
||||
|
||||
@@ -926,7 +926,6 @@ fn manifest_to_reusable_config(manifest: &WorkerManifest) -> WorkerManifestConfi
|
||||
rules: p.rules.clone(),
|
||||
}),
|
||||
feature: manifest.feature.clone().into(),
|
||||
plugins: manifest.plugins.clone(),
|
||||
mcp: manifest.mcp.clone(),
|
||||
compaction: manifest
|
||||
.compaction
|
||||
|
||||
@@ -7750,80 +7750,6 @@ permission = "read"
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_resolved_manifest_snapshot_is_persisted_without_profile() {
|
||||
let mut manifest = WorkerManifest::from_toml(
|
||||
r#"
|
||||
[worker]
|
||||
name = "plugin-snapshot"
|
||||
|
||||
[model]
|
||||
scheme = "anthropic"
|
||||
model_id = "claude-sonnet-4-20250514"
|
||||
|
||||
[engine]
|
||||
instruction = "saved"
|
||||
|
||||
[[scope.allow]]
|
||||
target = "/snapshot/workspace"
|
||||
permission = "read"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(manifest.profile.is_none());
|
||||
assert!(
|
||||
worker_metadata_for_manifest(&manifest, None, None, None)
|
||||
.resolved_manifest_snapshot
|
||||
.is_none()
|
||||
);
|
||||
|
||||
manifest.plugins.resolved = vec![manifest::plugin::ResolvedPluginRecord {
|
||||
identity: manifest::plugin::SourceQualifiedPluginId::new(
|
||||
manifest::plugin::PluginSourceKind::Project,
|
||||
"example",
|
||||
),
|
||||
source: manifest::plugin::PluginSourceKind::Project,
|
||||
package_path: PathBuf::from("/snapshot/workspace/.yoi/plugins/example.yoi-plugin"),
|
||||
package_label: "example.yoi-plugin".to_string(),
|
||||
digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
.to_string(),
|
||||
version: "0.1.0".to_string(),
|
||||
manifest: manifest::plugin::PluginPackageManifest {
|
||||
schema_version: 1,
|
||||
id: "example".to_string(),
|
||||
name: "Example".to_string(),
|
||||
version: "0.1.0".to_string(),
|
||||
description: None,
|
||||
surfaces: vec![manifest::plugin::PluginSurface::Hook],
|
||||
runtime: None,
|
||||
hooks: vec![],
|
||||
tools: vec![],
|
||||
services: vec![],
|
||||
ingresses: vec![],
|
||||
permissions: vec![],
|
||||
request: vec![],
|
||||
websocket: vec![],
|
||||
},
|
||||
enabled_surfaces: vec![manifest::plugin::PluginSurface::Hook],
|
||||
grants: manifest::plugin::PluginGrantConfig::default(),
|
||||
config: None,
|
||||
}];
|
||||
|
||||
let metadata = worker_metadata_for_manifest(&manifest, None, None, None);
|
||||
let snapshot = metadata
|
||||
.resolved_manifest_snapshot
|
||||
.expect("plugin-resolved manifest should be snapshotted");
|
||||
let restored = manifest::read_persisted_worker_manifest_snapshot(snapshot).unwrap();
|
||||
|
||||
assert!(restored.profile.is_none());
|
||||
assert_eq!(restored.plugins.resolved.len(), 1);
|
||||
assert_eq!(
|
||||
restored.plugins.resolved[0].digest,
|
||||
"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
);
|
||||
assert_eq!(restored.plugins.resolved[0].version, "0.1.0");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+26
-75
@@ -1420,7 +1420,7 @@ async fn run_login(backend_url: &str, no_wait: bool) -> Result<(), ParseError> {
|
||||
fn parse_plugin_args(args: &[String]) -> Result<plugin_cli::PluginCliCommand, ParseError> {
|
||||
let Some((subcommand, rest)) = args.split_first() else {
|
||||
return Err(ParseError(
|
||||
"yoi plugin requires `new`, `check`, `pack`, `list`, or `show <ref>`".to_string(),
|
||||
"yoi plugin requires `new`, `check`, or `pack`".to_string(),
|
||||
));
|
||||
};
|
||||
match subcommand.as_str() {
|
||||
@@ -1472,30 +1472,6 @@ fn parse_plugin_args(args: &[String]) -> Result<plugin_cli::PluginCliCommand, Pa
|
||||
)),
|
||||
}
|
||||
}
|
||||
"list" => {
|
||||
let (plugin_args, positional) = parse_plugin_common_args(rest)?;
|
||||
if !positional.is_empty() {
|
||||
return Err(ParseError(
|
||||
"yoi plugin list does not accept positional arguments".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(plugin_cli::PluginCliCommand::List(plugin_args))
|
||||
}
|
||||
"show" => {
|
||||
let (plugin_args, positional) = parse_plugin_common_args(rest)?;
|
||||
match positional.as_slice() {
|
||||
[reference] => Ok(plugin_cli::PluginCliCommand::Show {
|
||||
reference: reference.clone(),
|
||||
args: plugin_args,
|
||||
}),
|
||||
[] => Err(ParseError(
|
||||
"yoi plugin show requires a plugin ref".to_string(),
|
||||
)),
|
||||
_ => Err(ParseError(
|
||||
"yoi plugin show accepts exactly one plugin ref".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
"--help" | "-h" => Err(ParseError(plugin_usage().to_string())),
|
||||
other => Err(ParseError(format!(
|
||||
"unknown yoi plugin subcommand `{other}`"
|
||||
@@ -1513,35 +1489,7 @@ fn parse_plugin_common_args(
|
||||
let arg = &args[index];
|
||||
match arg.as_str() {
|
||||
"--json" => parsed.json = true,
|
||||
"--workspace" => {
|
||||
index += 1;
|
||||
let Some(value) = args.get(index) else {
|
||||
return Err(ParseError("--workspace requires a value".to_string()));
|
||||
};
|
||||
parsed.workspace = Some(PathBuf::from(value));
|
||||
}
|
||||
"--profile" => {
|
||||
index += 1;
|
||||
let Some(value) = args.get(index) else {
|
||||
return Err(ParseError("--profile requires a value".to_string()));
|
||||
};
|
||||
parsed.profile = Some(value.clone());
|
||||
}
|
||||
"--help" | "-h" => return Err(ParseError(plugin_usage().to_string())),
|
||||
_ if arg.starts_with("--workspace=") => {
|
||||
let value = arg.trim_start_matches("--workspace=");
|
||||
if value.is_empty() {
|
||||
return Err(ParseError("--workspace requires a value".to_string()));
|
||||
}
|
||||
parsed.workspace = Some(PathBuf::from(value));
|
||||
}
|
||||
_ if arg.starts_with("--profile=") => {
|
||||
let value = arg.trim_start_matches("--profile=");
|
||||
if value.is_empty() {
|
||||
return Err(ParseError("--profile requires a value".to_string()));
|
||||
}
|
||||
parsed.profile = Some(value.to_string());
|
||||
}
|
||||
_ if arg.starts_with('-') => {
|
||||
return Err(ParseError(format!("unknown yoi plugin option `{arg}`")));
|
||||
}
|
||||
@@ -1581,7 +1529,7 @@ fn parse_plugin_pack_args(
|
||||
}
|
||||
|
||||
fn plugin_usage() -> &'static str {
|
||||
"usage: yoi plugin new <rust-component-tool|rust-component-service> <path-or-name> [--json]\n yoi plugin check <path-or-package> [--json]\n yoi plugin pack <path> [--output <file>] [--json]\n yoi plugin list [--workspace PATH] [--profile REF] [--json]\n yoi plugin show <ref> [--workspace PATH] [--profile REF] [--json]"
|
||||
"usage: yoi plugin new <rust-component-tool|rust-component-service> <path-or-name> [--json]\n yoi plugin check <path-or-package> [--json]\n yoi plugin pack <path> [--output <file>] [--json]"
|
||||
}
|
||||
|
||||
fn parse_mcp_args(args: &[String]) -> Result<mcp_cli::McpCliCommand, ParseError> {
|
||||
@@ -1808,7 +1756,7 @@ Host commands:
|
||||
worker [WORKER_OPTIONS] Run the direct Worker process entrypoint
|
||||
ticket <COMMAND> Manage Tickets through a Backend target
|
||||
objective <COMMAND> Manage Objectives through a Backend target
|
||||
plugin <COMMAND> Build/check/list/show plugins
|
||||
plugin <COMMAND> Author/check/pack explicit Plugin packages
|
||||
mcp <COMMAND> Inspect configured MCP servers
|
||||
memory lint Lint local memory files
|
||||
session <COMMAND> Inspect/prune Standalone session logs
|
||||
@@ -2065,6 +2013,12 @@ backend = "shared"
|
||||
assert!(TOP_LEVEL_HELP.contains("default_connection = \"local\""));
|
||||
assert!(TOP_LEVEL_HELP.contains("yoi-server"));
|
||||
assert!(TOP_LEVEL_HELP.contains("yoi-runtime"));
|
||||
assert!(
|
||||
TOP_LEVEL_HELP.contains(
|
||||
"plugin <COMMAND> Author/check/pack explicit Plugin packages"
|
||||
)
|
||||
);
|
||||
assert!(!TOP_LEVEL_HELP.contains("Build/check/list/show plugins"));
|
||||
assert!(!TOP_LEVEL_HELP.contains("yoi workspace"));
|
||||
assert!(!TOP_LEVEL_HELP.contains("yoi server"));
|
||||
assert!(!TOP_LEVEL_HELP.contains("TARGET_OPTIONS"));
|
||||
@@ -2843,29 +2797,26 @@ backend = "shared"
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_plugin_list_and_show() {
|
||||
match parse_args_from(["plugin", "list", "--workspace=/tmp/ws", "--json"]).unwrap() {
|
||||
Mode::Plugin(plugin_cli::PluginCliCommand::List(options)) => {
|
||||
assert_eq!(options.workspace, Some(PathBuf::from("/tmp/ws")));
|
||||
assert!(options.json);
|
||||
}
|
||||
_ => panic!("expected Plugin list mode"),
|
||||
fn plugin_cli_rejects_ambient_catalog_commands_and_options() {
|
||||
for args in [
|
||||
vec!["plugin", "list"],
|
||||
vec!["plugin", "show", "project:echo"],
|
||||
vec!["plugin", "check", "plugin", "--workspace=/tmp/ws"],
|
||||
vec!["plugin", "check", "plugin", "--profile", "project:inspect"],
|
||||
] {
|
||||
let error = parse_args_from(args).unwrap_err();
|
||||
assert!(
|
||||
error.0.contains("unknown yoi plugin"),
|
||||
"unexpected error: {error}"
|
||||
);
|
||||
}
|
||||
|
||||
match parse_args_from([
|
||||
"plugin",
|
||||
"show",
|
||||
"project:echo",
|
||||
"--profile",
|
||||
"project:inspect",
|
||||
])
|
||||
.unwrap()
|
||||
{
|
||||
Mode::Plugin(plugin_cli::PluginCliCommand::Show { reference, args }) => {
|
||||
assert_eq!(reference, "project:echo");
|
||||
assert_eq!(args.profile.as_deref(), Some("project:inspect"));
|
||||
match parse_args_from(["plugin", "check", "plugin.yoi-plugin", "--json"]).unwrap() {
|
||||
Mode::Plugin(plugin_cli::PluginCliCommand::Check { input, args }) => {
|
||||
assert_eq!(input, PathBuf::from("plugin.yoi-plugin"));
|
||||
assert!(args.json);
|
||||
}
|
||||
_ => panic!("expected Plugin show mode"),
|
||||
_ => panic!("expected Plugin check mode"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+71
-2267
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user