merge: 00001KVXK0WDH plugin manifest rejection
This commit is contained in:
commit
449745ee24
|
|
@ -560,13 +560,14 @@ impl PluginPackageManifest {
|
|||
}
|
||||
}
|
||||
|
||||
pub const PLUGIN_RUNTIME_WASM_KIND: &str = "wasm";
|
||||
pub const PLUGIN_RUNTIME_WASM_ABI: &str = "yoi-plugin-wasm-1";
|
||||
/// Manifest runtime kind for WebAssembly Component Model Tool packages.
|
||||
const LEGACY_PLUGIN_RUNTIME_WASM_KIND: &str = "wasm";
|
||||
const LEGACY_PLUGIN_RUNTIME_WASM_ABI: &str = "yoi-plugin-wasm-1";
|
||||
/// Manifest runtime kind for current WebAssembly Component Model packages.
|
||||
///
|
||||
/// Component runtime manifests must set `component` to the packaged component
|
||||
/// artifact path and `world` to [`PLUGIN_COMPONENT_TOOL_WORLD`]. Raw core-Wasm
|
||||
/// packages remain explicit `kind = "wasm"` plus `abi = "yoi-plugin-wasm-1"`.
|
||||
/// artifact path and `world` to [`PLUGIN_COMPONENT_TOOL_WORLD`] or
|
||||
/// [`PLUGIN_COMPONENT_INSTANCE_WORLD`]. Legacy raw core-Wasm manifests are
|
||||
/// intentionally rejected by validation; `wasm-component` is the public runtime.
|
||||
pub const PLUGIN_RUNTIME_COMPONENT_KIND: &str = "wasm-component";
|
||||
pub const PLUGIN_COMPONENT_TOOL_WORLD: &str = "yoi:plugin/tool@1.0.0";
|
||||
pub const PLUGIN_COMPONENT_INSTANCE_WORLD: &str = "yoi:plugin/instance@1.0.0";
|
||||
|
|
@ -1060,158 +1061,6 @@ pub fn resolve_plugin_config_for_startup(
|
|||
snapshot
|
||||
}
|
||||
|
||||
/// Load the recorded WASM runtime module for a resolved plugin package.
|
||||
///
|
||||
/// Restore and execution paths use this helper instead of reading arbitrary
|
||||
/// package paths directly so module selection remains tied to the resolved
|
||||
/// package identity, runtime manifest entry, and deterministic package digest.
|
||||
pub fn read_resolved_plugin_runtime_module(
|
||||
record: &ResolvedPluginRecord,
|
||||
limits: &PluginDiscoveryLimits,
|
||||
) -> Result<Vec<u8>, PluginDiagnostic> {
|
||||
let runtime = record.manifest.runtime.as_ref().ok_or_else(|| {
|
||||
PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Missing,
|
||||
PluginDiagnosticPhase::Manifest,
|
||||
"resolved plugin package does not declare a WASM runtime",
|
||||
)
|
||||
.with_source(record.source)
|
||||
.with_identity(&record.identity)
|
||||
.with_package(&record.package_label)
|
||||
.with_digest(&record.digest)
|
||||
})?;
|
||||
|
||||
if runtime.kind != PLUGIN_RUNTIME_WASM_KIND {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Api,
|
||||
PluginDiagnosticPhase::Manifest,
|
||||
"plugin runtime kind is unsupported",
|
||||
)
|
||||
.with_source(record.source)
|
||||
.with_identity(&record.identity)
|
||||
.with_package(&record.package_label)
|
||||
.with_digest(&record.digest));
|
||||
}
|
||||
if runtime.abi.as_deref() != Some(PLUGIN_RUNTIME_WASM_ABI) {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Api,
|
||||
PluginDiagnosticPhase::Manifest,
|
||||
"plugin WASM ABI is unsupported",
|
||||
)
|
||||
.with_source(record.source)
|
||||
.with_identity(&record.identity)
|
||||
.with_package(&record.package_label)
|
||||
.with_digest(&record.digest));
|
||||
}
|
||||
|
||||
let entry = runtime.entry.as_deref().ok_or_else(|| {
|
||||
PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Missing,
|
||||
PluginDiagnosticPhase::Manifest,
|
||||
"plugin WASM runtime entry is required",
|
||||
)
|
||||
.with_source(record.source)
|
||||
.with_identity(&record.identity)
|
||||
.with_package(&record.package_label)
|
||||
.with_digest(&record.digest)
|
||||
})?;
|
||||
|
||||
let metadata = fs::metadata(&record.package_path).map_err(|error| {
|
||||
PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Io,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
format!(
|
||||
"resolved plugin package metadata could not be read: {}",
|
||||
safe_io_error(&error)
|
||||
),
|
||||
)
|
||||
.with_source(record.source)
|
||||
.with_identity(&record.identity)
|
||||
.with_package(&record.package_label)
|
||||
.with_digest(&record.digest)
|
||||
})?;
|
||||
if !metadata.is_file() {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Malformed,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
"resolved plugin package is not a regular file",
|
||||
)
|
||||
.with_source(record.source)
|
||||
.with_identity(&record.identity)
|
||||
.with_package(&record.package_label)
|
||||
.with_digest(&record.digest));
|
||||
}
|
||||
if metadata.len() > limits.max_package_size_bytes {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Bounds,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
"resolved plugin package exceeds the configured package size bound",
|
||||
)
|
||||
.with_source(record.source)
|
||||
.with_identity(&record.identity)
|
||||
.with_package(&record.package_label)
|
||||
.with_digest(&record.digest));
|
||||
}
|
||||
|
||||
let bytes = fs::read(&record.package_path).map_err(|error| {
|
||||
PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Io,
|
||||
PluginDiagnosticPhase::Discovery,
|
||||
format!(
|
||||
"resolved plugin package content could not be read: {}",
|
||||
safe_io_error(&error)
|
||||
),
|
||||
)
|
||||
.with_source(record.source)
|
||||
.with_identity(&record.identity)
|
||||
.with_package(&record.package_label)
|
||||
.with_digest(&record.digest)
|
||||
})?;
|
||||
let archive = parse_stored_zip(&bytes, &record.package_label, record.source, limits)?;
|
||||
let actual_digest = deterministic_digest(&archive.files);
|
||||
if !digest_matches(&record.digest, &actual_digest) {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Digest,
|
||||
PluginDiagnosticPhase::Resolution,
|
||||
"resolved plugin package digest does not match current package content",
|
||||
)
|
||||
.with_source(record.source)
|
||||
.with_identity(&record.identity)
|
||||
.with_package(&record.package_label)
|
||||
.with_digest(actual_digest));
|
||||
}
|
||||
|
||||
validate_manifest_path(
|
||||
entry,
|
||||
&archive,
|
||||
&record.package_label,
|
||||
record.source,
|
||||
&record.manifest.id,
|
||||
)?;
|
||||
let normalized = normalize_archive_path(entry).ok_or_else(|| {
|
||||
PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Traversal,
|
||||
PluginDiagnosticPhase::Manifest,
|
||||
"plugin manifest references a path outside the package root",
|
||||
)
|
||||
.with_source(record.source)
|
||||
.with_identity(&record.identity)
|
||||
.with_package(&record.package_label)
|
||||
.with_digest(&record.digest)
|
||||
})?;
|
||||
archive.files.get(&normalized).cloned().ok_or_else(|| {
|
||||
PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Missing,
|
||||
PluginDiagnosticPhase::Manifest,
|
||||
"plugin runtime module entry is missing from the package",
|
||||
)
|
||||
.with_source(record.source)
|
||||
.with_identity(&record.identity)
|
||||
.with_package(&record.package_label)
|
||||
.with_digest(&record.digest)
|
||||
})
|
||||
}
|
||||
|
||||
/// Reads the WebAssembly Component Model artifact selected by a resolved plugin
|
||||
/// package manifest while preserving package digest pinning.
|
||||
pub fn read_resolved_plugin_runtime_component(
|
||||
|
|
@ -2046,39 +1895,22 @@ fn validate_manifest(
|
|||
}
|
||||
if let Some(runtime) = &manifest.runtime {
|
||||
match runtime.kind.as_str() {
|
||||
PLUGIN_RUNTIME_WASM_KIND => {
|
||||
if runtime.abi.as_deref() != Some(PLUGIN_RUNTIME_WASM_ABI) {
|
||||
LEGACY_PLUGIN_RUNTIME_WASM_KIND => {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Api,
|
||||
PluginDiagnosticPhase::Manifest,
|
||||
"plugin WASM ABI is unsupported",
|
||||
format!(
|
||||
"legacy raw wasm plugin runtime `{LEGACY_PLUGIN_RUNTIME_WASM_KIND}` / `{}` is retired; use `{PLUGIN_RUNTIME_COMPONENT_KIND}`",
|
||||
runtime
|
||||
.abi
|
||||
.as_deref()
|
||||
.unwrap_or(LEGACY_PLUGIN_RUNTIME_WASM_ABI)
|
||||
),
|
||||
)
|
||||
.with_source(source)
|
||||
.with_identity(SourceQualifiedPluginId::new(source, manifest.id.clone()))
|
||||
.with_package(label));
|
||||
}
|
||||
let Some(entry) = runtime.entry.as_deref() else {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Missing,
|
||||
PluginDiagnosticPhase::Manifest,
|
||||
"plugin WASM runtime entry is required",
|
||||
)
|
||||
.with_source(source)
|
||||
.with_identity(SourceQualifiedPluginId::new(source, manifest.id.clone()))
|
||||
.with_package(label));
|
||||
};
|
||||
if runtime.component.is_some() || runtime.world.is_some() {
|
||||
return Err(PluginDiagnostic::new(
|
||||
PluginDiagnosticKind::Malformed,
|
||||
PluginDiagnosticPhase::Manifest,
|
||||
"plugin WASM runtime must not declare component metadata",
|
||||
)
|
||||
.with_source(source)
|
||||
.with_identity(SourceQualifiedPluginId::new(source, manifest.id.clone()))
|
||||
.with_package(label));
|
||||
}
|
||||
validate_manifest_path(entry, archive, label, source, &manifest.id)?;
|
||||
}
|
||||
PLUGIN_RUNTIME_COMPONENT_KIND => {
|
||||
if runtime.abi.is_some() || runtime.entry.is_some() {
|
||||
return Err(PluginDiagnostic::new(
|
||||
|
|
@ -2844,6 +2676,51 @@ description = "bad"
|
|||
assert!(err.message.contains("service/ingress"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_raw_wasm_runtime_manifest_is_rejected() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let workspace = temp.path().join("workspace");
|
||||
let plugins = workspace.join(".yoi/plugins");
|
||||
fs::create_dir_all(&plugins).unwrap();
|
||||
let manifest = r#"
|
||||
schema_version = 1
|
||||
id = "legacy"
|
||||
name = "Legacy"
|
||||
version = "0.1.0"
|
||||
surfaces = ["tool"]
|
||||
|
||||
[runtime]
|
||||
kind = "wasm"
|
||||
entry = "plugin.wasm"
|
||||
abi = "yoi-plugin-wasm-1"
|
||||
|
||||
[[tools]]
|
||||
name = "Echo"
|
||||
description = "legacy"
|
||||
input_schema = { type = "object" }
|
||||
"#;
|
||||
write_stored_zip(
|
||||
&plugins.join("legacy.yoi-plugin"),
|
||||
&[
|
||||
("plugin.toml", manifest.as_bytes().to_vec(), 0),
|
||||
("plugin.wasm", b"not wasm".to_vec(), 0),
|
||||
],
|
||||
);
|
||||
|
||||
let report = discover_plugins(&PluginDiscoveryOptions::new(&workspace));
|
||||
|
||||
assert!(report.packages.is_empty());
|
||||
let diagnostic = report
|
||||
.diagnostics
|
||||
.iter()
|
||||
.find(|diag| diag.kind == PluginDiagnosticKind::Api)
|
||||
.unwrap();
|
||||
assert_eq!(diagnostic.phase, PluginDiagnosticPhase::Manifest);
|
||||
assert_eq!(diagnostic.identity.as_deref(), Some("project:legacy"));
|
||||
assert!(diagnostic.message.contains("legacy raw wasm"));
|
||||
assert!(diagnostic.message.contains(PLUGIN_RUNTIME_COMPONENT_KIND));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discovers_valid_user_and_workspace_packages() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
|
|
@ -3521,9 +3398,9 @@ version = "1.0.0"
|
|||
surfaces = ["tool"]
|
||||
|
||||
[runtime]
|
||||
kind = "wasm"
|
||||
entry = "plugin.wasm"
|
||||
abi = "yoi-plugin-wasm-1"
|
||||
kind = "wasm-component"
|
||||
component = "plugin.component.wasm"
|
||||
world = "yoi:plugin/tool@1.0.0"
|
||||
|
||||
[[permissions]]
|
||||
kind = "host_api"
|
||||
|
|
|
|||
|
|
@ -22,10 +22,9 @@ use llm_worker::tool::{
|
|||
};
|
||||
use manifest::plugin::{
|
||||
PLUGIN_COMPONENT_INSTANCE_WORLD, PLUGIN_COMPONENT_TOOL_WORLD, PLUGIN_RUNTIME_COMPONENT_KIND,
|
||||
PLUGIN_RUNTIME_WASM_KIND, PluginConfig, PluginDiscoveryLimits, PluginFsGrant,
|
||||
PluginFsOperation, PluginHostApi, PluginPermission, PluginRequestGrant, PluginSurface,
|
||||
PluginToolManifest, PluginWebSocketGrant, ResolvedPluginRecord,
|
||||
read_resolved_plugin_runtime_component,
|
||||
PluginConfig, PluginDiscoveryLimits, PluginFsGrant, PluginFsOperation, PluginHostApi,
|
||||
PluginPermission, PluginRequestGrant, PluginSurface, PluginToolManifest, PluginWebSocketGrant,
|
||||
ResolvedPluginRecord, read_resolved_plugin_runtime_component,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
|
@ -35,6 +34,8 @@ use tokio::runtime::{
|
|||
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
|
||||
use tokio_tungstenite::tungstenite::protocol::{Message, WebSocketConfig};
|
||||
|
||||
const LEGACY_PLUGIN_RUNTIME_WASM_KIND: &str = "wasm";
|
||||
|
||||
use super::{
|
||||
FeatureDescriptor, FeatureId, FeatureInstallContext, FeatureInstallError, FeatureModule,
|
||||
FeatureRuntimeKind, ServiceDeclaration, ServiceId, ToolContribution, ToolDeclaration,
|
||||
|
|
@ -235,12 +236,12 @@ pub fn inspect_resolved_plugin_static(record: &ResolvedPluginRecord) -> PluginSt
|
|||
diagnostic: None,
|
||||
}
|
||||
}
|
||||
Some(runtime) if runtime.kind == PLUGIN_RUNTIME_WASM_KIND => {
|
||||
Some(runtime) if runtime.kind == LEGACY_PLUGIN_RUNTIME_WASM_KIND => {
|
||||
let status = runtime
|
||||
.abi
|
||||
.as_deref()
|
||||
.map(|abi| format!("{PLUGIN_RUNTIME_WASM_KIND}/{abi}"))
|
||||
.unwrap_or_else(|| format!("{PLUGIN_RUNTIME_WASM_KIND}/<missing-abi>"));
|
||||
.map(|abi| format!("{LEGACY_PLUGIN_RUNTIME_WASM_KIND}/{abi}"))
|
||||
.unwrap_or_else(|| format!("{LEGACY_PLUGIN_RUNTIME_WASM_KIND}/<missing-abi>"));
|
||||
PluginRuntimeEligibility {
|
||||
eligible: false,
|
||||
status,
|
||||
|
|
@ -3312,7 +3313,7 @@ impl PluginInstanceRuntime {
|
|||
PLUGIN_RUNTIME_COMPONENT_KIND => Err(PluginWasmError::Module(
|
||||
"unsupported or missing plugin component world".to_string(),
|
||||
)),
|
||||
PLUGIN_RUNTIME_WASM_KIND => Err(PluginWasmError::Module(
|
||||
LEGACY_PLUGIN_RUNTIME_WASM_KIND => Err(PluginWasmError::Module(
|
||||
"legacy raw wasm plugin runtime is not supported; use wasm-component".to_string(),
|
||||
)),
|
||||
other => Err(PluginWasmError::Module(format!(
|
||||
|
|
@ -6036,7 +6037,7 @@ input_schema = {{ type = "object", additionalProperties = true }}
|
|||
fn legacy_raw_wasm_runtime_is_rejected_without_fallback_execution() {
|
||||
let mut record = record(vec![tool("PluginEcho")]);
|
||||
record.manifest.runtime = Some(PluginRuntimeManifest {
|
||||
kind: PLUGIN_RUNTIME_WASM_KIND.to_string(),
|
||||
kind: LEGACY_PLUGIN_RUNTIME_WASM_KIND.to_string(),
|
||||
entry: Some("plugin.wasm".to_string()),
|
||||
abi: Some("yoi-plugin-wasm-1".to_string()),
|
||||
component: None,
|
||||
|
|
|
|||
|
|
@ -1853,6 +1853,39 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_raw_wasm_package_is_rejected_not_active_or_eligible() {
|
||||
let dir = tempdir().unwrap();
|
||||
let workspace = dir.path();
|
||||
fs::create_dir_all(workspace.join(".yoi/plugins")).unwrap();
|
||||
write_stored_zip(
|
||||
&workspace.join(".yoi/plugins/legacy.yoi-plugin"),
|
||||
&[
|
||||
("plugin.toml", plugin_legacy_manifest("legacy").as_bytes()),
|
||||
("plugin.wasm", b"not wasm"),
|
||||
],
|
||||
);
|
||||
|
||||
let snapshot = inspect_snapshot(workspace, &PluginConfig::default());
|
||||
let legacy = select_item(&snapshot, "project:legacy").unwrap();
|
||||
assert_eq!(legacy.status, "rejected");
|
||||
assert!(!legacy.discovered);
|
||||
assert!(!legacy.configured);
|
||||
assert!(legacy.enabled_surfaces.is_empty());
|
||||
assert!(legacy.diagnostics.iter().any(|diagnostic| {
|
||||
diagnostic.kind == "api"
|
||||
&& diagnostic.message.contains("legacy raw wasm")
|
||||
&& diagnostic.message.contains("wasm-component")
|
||||
}));
|
||||
|
||||
let list_output = render_list_snapshot_human(&snapshot).unwrap();
|
||||
assert!(list_output.contains("project:legacy [rejected]"));
|
||||
assert!(!list_output.contains("project:legacy [active]"));
|
||||
let show_output = render_item_human(legacy).unwrap();
|
||||
assert!(show_output.contains("status: rejected"));
|
||||
assert!(show_output.contains("legacy raw wasm"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_invalid_or_incompatible_package_is_rejected_not_missing() {
|
||||
let dir = tempdir().unwrap();
|
||||
|
|
@ -1867,7 +1900,7 @@ mod tests {
|
|||
&workspace.join(".yoi/plugins/incompat.yoi-plugin"),
|
||||
&[
|
||||
("plugin.toml", incompatible_manifest.as_bytes()),
|
||||
("plugin.wasm", b"not wasm"),
|
||||
("plugin.component.wasm", b"not wasm"),
|
||||
],
|
||||
);
|
||||
let mut config = PluginConfig::default();
|
||||
|
|
@ -1934,7 +1967,7 @@ mod tests {
|
|||
fs::create_dir_all(workspace.join(".yoi/plugins")).unwrap();
|
||||
write_stored_zip(
|
||||
&workspace.join(".yoi/plugins/no_manifest.yoi-plugin"),
|
||||
&[("plugin.wasm", b"not wasm")],
|
||||
&[("plugin.component.wasm", b"not wasm")],
|
||||
);
|
||||
let missing_runtime_manifest = plugin_manifest_missing_runtime_entry("missing_runtime");
|
||||
write_stored_zip(
|
||||
|
|
@ -2140,7 +2173,7 @@ mod tests {
|
|||
plugin_manifest("echo", "echo", "object", &["echo"]),
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(plugin.join("plugin.wasm"), b"not wasm").unwrap();
|
||||
fs::write(plugin.join("plugin.component.wasm"), b"not wasm").unwrap();
|
||||
|
||||
let human = render_check(&plugin, &PluginCliArgs::default()).unwrap();
|
||||
assert!(human.contains("[active]"));
|
||||
|
|
@ -2163,6 +2196,42 @@ mod tests {
|
|||
assert_eq!(value["safety"]["no_plugin_execution"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_check_rejects_legacy_raw_wasm_package() {
|
||||
let dir = tempdir().unwrap();
|
||||
let plugin = dir.path().join("legacy");
|
||||
fs::create_dir_all(&plugin).unwrap();
|
||||
fs::write(plugin.join("plugin.toml"), plugin_legacy_manifest("legacy")).unwrap();
|
||||
fs::write(plugin.join("plugin.wasm"), b"not wasm").unwrap();
|
||||
|
||||
let report = build_check_report(&plugin);
|
||||
assert_eq!(report.status, "rejected");
|
||||
assert!(report.diagnostics.iter().any(|diagnostic| {
|
||||
diagnostic.kind == "api"
|
||||
&& diagnostic.message.contains("legacy raw wasm")
|
||||
&& diagnostic.message.contains("wasm-component")
|
||||
}));
|
||||
let human = render_check_report(&report, &PluginCliArgs::default()).unwrap();
|
||||
assert!(human.contains("[rejected]"));
|
||||
assert!(human.contains("legacy raw wasm"));
|
||||
let json = render_check_report(
|
||||
&report,
|
||||
&PluginCliArgs {
|
||||
json: true,
|
||||
..PluginCliArgs::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let value: serde_json::Value = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(value["status"], "rejected");
|
||||
assert!(
|
||||
value["diagnostics"][0]["message"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.contains("wasm-component")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_check_rejects_invalid_manifest_and_missing_runtime_artifact() {
|
||||
let dir = tempdir().unwrap();
|
||||
|
|
@ -2228,7 +2297,7 @@ mod tests {
|
|||
plugin_manifest("echo", "echo", "object", &["echo"]),
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(plugin.join("plugin.wasm"), b"not wasm").unwrap();
|
||||
fs::write(plugin.join("plugin.component.wasm"), b"not wasm").unwrap();
|
||||
let first = dir.path().join("first.yoi-plugin");
|
||||
let second = dir.path().join("second.yoi-plugin");
|
||||
|
||||
|
|
@ -2466,9 +2535,9 @@ surfaces = ["tool"]
|
|||
permissions = [{{ kind = "surface", surface = "tool" }}, {{ kind = "tool", name = "Echo" }}]
|
||||
|
||||
[runtime]
|
||||
kind = "wasm"
|
||||
entry = "missing.wasm"
|
||||
abi = "yoi-plugin-wasm-1"
|
||||
kind = "wasm-component"
|
||||
component = "missing.component.wasm"
|
||||
world = "yoi:plugin/tool@1.0.0"
|
||||
|
||||
[[tools]]
|
||||
name = "Echo"
|
||||
|
|
@ -2500,9 +2569,9 @@ surfaces = ["tool"]
|
|||
permissions = [{{ kind = "surface", surface = "tool" }}, {permissions}]
|
||||
|
||||
[runtime]
|
||||
kind = "wasm"
|
||||
entry = "plugin.wasm"
|
||||
abi = "yoi-plugin-wasm-1"
|
||||
kind = "wasm-component"
|
||||
component = "plugin.component.wasm"
|
||||
world = "yoi:plugin/tool@1.0.0"
|
||||
|
||||
[[tools]]
|
||||
name = "{tool_name}"
|
||||
|
|
@ -2512,6 +2581,28 @@ input_schema = {{ type = "{schema_type}" }}
|
|||
)
|
||||
}
|
||||
|
||||
fn plugin_legacy_manifest(id: &str) -> String {
|
||||
format!(
|
||||
r#"
|
||||
schema_version = 1
|
||||
id = "{id}"
|
||||
name = "{id}"
|
||||
version = "0.1.0"
|
||||
surfaces = ["tool"]
|
||||
|
||||
[runtime]
|
||||
kind = "wasm"
|
||||
entry = "plugin.wasm"
|
||||
abi = "yoi-plugin-wasm-1"
|
||||
|
||||
[[tools]]
|
||||
name = "Echo"
|
||||
description = "Legacy raw wasm tool"
|
||||
input_schema = {{ type = "object" }}
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
fn write_plugin_package(workspace: &Path, id: &str) -> String {
|
||||
let manifest = format!(
|
||||
r#"
|
||||
|
|
@ -2523,9 +2614,9 @@ surfaces = ["tool"]
|
|||
permissions = [{{ kind = "surface", surface = "tool" }}, {{ kind = "tool", name = "Echo" }}]
|
||||
|
||||
[runtime]
|
||||
kind = "wasm"
|
||||
entry = "plugin.wasm"
|
||||
abi = "yoi-plugin-wasm-1"
|
||||
kind = "wasm-component"
|
||||
component = "plugin.component.wasm"
|
||||
world = "yoi:plugin/tool@1.0.0"
|
||||
|
||||
[[tools]]
|
||||
name = "Echo"
|
||||
|
|
@ -2547,9 +2638,9 @@ surfaces = ["tool"]
|
|||
permissions = [{{ kind = "surface", surface = "tool" }}, {{ kind = "tool", name = "Echo" }}, {{ kind = "tool", name = "Other" }}]
|
||||
|
||||
[runtime]
|
||||
kind = "wasm"
|
||||
entry = "plugin.wasm"
|
||||
abi = "yoi-plugin-wasm-1"
|
||||
kind = "wasm-component"
|
||||
component = "plugin.component.wasm"
|
||||
world = "yoi:plugin/tool@1.0.0"
|
||||
|
||||
[[tools]]
|
||||
name = "Echo"
|
||||
|
|
@ -2573,7 +2664,7 @@ input_schema = {{ type = "object" }}
|
|||
&package,
|
||||
&[
|
||||
("plugin.toml", manifest.as_bytes()),
|
||||
("plugin.wasm", b"not wasm"),
|
||||
("plugin.component.wasm", b"not wasm"),
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
# Plugin Component Model migration
|
||||
|
||||
Yoi's current Plugin Tool runtime uses a narrow core-WebAssembly ABI. That was the right MVP shape because it made sandboxing, bounded input/output, and fail-closed host imports explicit. It should not become the long-term authoring interface.
|
||||
Yoi's original Plugin Tool runtime used a narrow core-WebAssembly ABI. That was the right MVP shape because it made sandboxing, bounded input/output, and fail-closed host imports explicit, but it is no longer the public authoring interface.
|
||||
|
||||
The preferred direction is to adopt the WebAssembly Component Model for Plugin Tool authoring and host APIs. Component Model adoption means Plugin interfaces are described as typed WIT worlds and lowered through the canonical ABI, instead of every Plugin author or SDK wrapper hand-writing pointer/length memory plumbing.
|
||||
The supported runtime kind is now `wasm-component`, using the WebAssembly Component Model for Plugin Tool authoring and host APIs. Component Model adoption means Plugin interfaces are described as typed WIT worlds and lowered through the canonical ABI, instead of every Plugin author or SDK wrapper hand-writing pointer/length memory plumbing.
|
||||
|
||||
## What Component Model changes
|
||||
|
||||
|
|
@ -74,34 +74,15 @@ Adopting the Component Model must not change Yoi's authority model:
|
|||
|
||||
## Migration shape
|
||||
|
||||
Yoi should support Component Model as an explicit runtime kind rather than silently changing existing raw-ABI packages.
|
||||
`runtime.kind = "wasm-component"` is the sole public Plugin runtime kind. Legacy raw core-Wasm declarations (`kind = "wasm"` / `abi = "yoi-plugin-wasm-1"`) are rejected by manifest validation and are surfaced only as bounded diagnostics; they are not active/eligible Plugins and are not executed.
|
||||
|
||||
Possible manifest direction:
|
||||
The migration is now focused on the component surface:
|
||||
|
||||
```toml
|
||||
[runtime]
|
||||
kind = "wasm-component"
|
||||
component = "plugin.component.wasm"
|
||||
world = "yoi:plugin/tool@1.0.0"
|
||||
```
|
||||
|
||||
The current raw core-Wasm runtime can remain explicit during migration:
|
||||
|
||||
```toml
|
||||
[runtime]
|
||||
kind = "wasm"
|
||||
entry = "plugin.wasm"
|
||||
abi = "yoi-plugin-wasm-1"
|
||||
```
|
||||
|
||||
The migration should be phased:
|
||||
|
||||
1. Define WIT packages/worlds for Tool Plugin and initial host APIs.
|
||||
2. Add manifest/schema support for `runtime.kind = "wasm-component"` without executing it during discovery.
|
||||
3. Add a component runtime backend and typed host import/export binding.
|
||||
4. Port `https` and `fs` host API designs to WIT-compatible interfaces.
|
||||
5. Add a Rust PDK/template around the component world.
|
||||
6. Decide whether the raw ABI remains supported, becomes legacy-only, or is deprecated after examples and tests move.
|
||||
1. Keep WIT packages/worlds for Tool Plugin and initial host APIs versioned under `resources/plugin/wit`.
|
||||
2. Keep manifest/schema support centered on `runtime.kind = "wasm-component"`.
|
||||
3. Keep the component runtime backend and typed host import/export binding as the active execution path.
|
||||
4. Port future host API designs to WIT-compatible interfaces.
|
||||
5. Keep the Rust PDK/template aligned with the component world.
|
||||
|
||||
## Runtime/backend caution
|
||||
|
||||
|
|
@ -133,14 +114,7 @@ component = "plugin.component.wasm"
|
|||
world = "yoi:plugin/tool@1.0.0"
|
||||
```
|
||||
|
||||
The legacy core-Wasm ABI remains explicit metadata for migration diagnostics and is not reinterpreted as a component or executed by the active runtime path:
|
||||
|
||||
```toml
|
||||
[runtime]
|
||||
kind = "wasm"
|
||||
entry = "plugin.wasm"
|
||||
abi = "yoi-plugin-wasm-1"
|
||||
```
|
||||
Legacy core-Wasm metadata is accepted only far enough to produce migration diagnostics: package checks and discovery reject `kind = "wasm"` / `abi = "yoi-plugin-wasm-1"`, `list`/`show` report those packages as rejected rather than active/eligible, and the active runtime path does not execute them.
|
||||
|
||||
The component runtime uses `wasmtime::component` and expects the exported world
|
||||
`yoi:plugin/tool@1.0.0` with a `call(tool-name: string, input-json: string) ->
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ The initial goal is a durable `.yoi-plugin` package format that later Tickets ca
|
|||
|
||||
## Package shape
|
||||
|
||||
A `.yoi-plugin` file is a single-file archive. The initial archive format should be a constrained ZIP profile because it is easy to inspect without executing code and can carry text manifests, WASM modules, schemas, and license material.
|
||||
A `.yoi-plugin` file is a single-file archive. The archive format is a constrained ZIP profile because it is easy to inspect without executing code and can carry text manifests, WebAssembly Component Model modules, schemas, and license material.
|
||||
|
||||
The archive root must contain `plugin.toml` directly at the root. Packages should not require a wrapping directory whose name must match the plugin id.
|
||||
|
||||
|
|
@ -14,7 +14,7 @@ Recommended root layout:
|
|||
|
||||
```text
|
||||
plugin.toml # required package manifest
|
||||
module.wasm # optional; required when plugin.toml declares a WASM runtime
|
||||
plugin.component.wasm # required when plugin.toml declares the component runtime
|
||||
hooks/*.toml # optional declarative hook definitions
|
||||
schemas/*.schema.json # optional JSON schemas for configuration or tool input/output
|
||||
README.md # recommended human description
|
||||
|
|
@ -43,16 +43,7 @@ id = "summary"
|
|||
file = "hooks/summary.md"
|
||||
```
|
||||
|
||||
The package archive must contain both root `plugin.toml` and the referenced `hooks/summary.md` entry. Optional WASM metadata is accepted only for the declared future runtime boundary and is not executed:
|
||||
|
||||
```toml
|
||||
[runtime]
|
||||
kind = "wasm"
|
||||
entry = "plugin.wasm"
|
||||
abi = "yoi-plugin-wasm-1"
|
||||
```
|
||||
|
||||
The preferred WASM authoring/runtime shape is the WebAssembly Component Model, recorded in [Plugin Component Model migration](plugin-component-model.md). Component packages should be explicit and source-compatible rather than silently changing the existing raw core-Wasm runtime:
|
||||
The package archive must contain both root `plugin.toml` and referenced runtime/content entries. Component runtime metadata is explicit and static inspection never executes the artifact:
|
||||
|
||||
```toml
|
||||
[runtime]
|
||||
|
|
@ -61,13 +52,15 @@ component = "plugin.component.wasm"
|
|||
world = "yoi:plugin/tool@1.0.0"
|
||||
```
|
||||
|
||||
`wasm-component` is the public/recommended runtime kind, recorded in [Plugin Component Model migration](plugin-component-model.md). Legacy raw core-Wasm declarations (`kind = "wasm"` / `abi = "yoi-plugin-wasm-1"`) are retired: manifest validation rejects them and CLI inspection reports the package as rejected rather than active/eligible.
|
||||
|
||||
First-pass fields accepted by the parser:
|
||||
|
||||
- `schema_version`: required integer; unsupported versions fail closed.
|
||||
- `id`: required unqualified local id. It is scoped by the source that discovered the package; it is not globally unique by itself.
|
||||
- `name`, `version`, `description`: human metadata used in listings and diagnostics.
|
||||
- `surfaces`: optional declared contribution surface names.
|
||||
- `runtime`: optional WASM metadata only. Discovery records metadata and never executes it.
|
||||
- `runtime`: optional component runtime metadata. Discovery records metadata and never executes it; unsupported/retired runtime kinds fail closed.
|
||||
- `hooks`: optional hook metadata. Discovery records metadata and does not register hooks.
|
||||
|
||||
Future descriptor sections such as `[package]`, `[permissions]`, richer `contributions`, or `runtime.kind = "declarative"` are aspirational and are intentionally rejected by the current strict parser until implemented safely.
|
||||
|
|
@ -223,17 +216,4 @@ documents a future out-of-tree pinned git `rev` dependency pattern. Crates.io
|
|||
publication, remote template fetching, and package authoring commands are not
|
||||
part of the current package/runtime contract.
|
||||
|
||||
This is separate from the legacy raw core-Wasm runtime:
|
||||
|
||||
```toml
|
||||
[runtime]
|
||||
kind = "wasm"
|
||||
entry = "plugin.wasm"
|
||||
abi = "yoi-plugin-wasm-1"
|
||||
```
|
||||
|
||||
Component packages must not use `entry`/`abi`; raw packages must not use
|
||||
`component`/`world`. Discovery reports the selected runtime kind/world without
|
||||
executing the artifact. Component execution still requires explicit package
|
||||
enablement, exact source/version/digest grants, and matching Tool/host API
|
||||
permissions.
|
||||
Legacy raw core-Wasm metadata remains documented only as a rejected migration diagnostic. Packages must not use `entry`/`abi`; discovery reports `kind = "wasm"` / `abi = "yoi-plugin-wasm-1"` packages as rejected without executing the artifact. Component execution still requires explicit package enablement, exact source/version/digest grants, and matching Tool/host API permissions.
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ Keep these layers separate when designing a Plugin. Do not make package discover
|
|||
|
||||
Yoi's preferred Plugin shape is **Tool first**. A good Tool Plugin has a narrow schema, deterministic input/output behavior, explicit side-effect metadata, and a minimal grant set. Long-running services, inbound events, and autonomous routing are future Service/Ingress work; they should not be hidden inside a Tool package.
|
||||
|
||||
Component Model authoring is the preferred path for new Plugins. The raw core-Wasm ABI exists for compatibility and tests, but authors should use the Rust PDK/template unless they are deliberately testing the low-level runtime.
|
||||
Component Model authoring is the supported path for Plugins. Legacy raw core-Wasm manifests (`kind = "wasm"` / `abi = "yoi-plugin-wasm-1"`) are retired and rejected by `yoi plugin check`, discovery, `list`, and `show`; use the Rust PDK/template and `kind = "wasm-component"` instead.
|
||||
|
||||
## Current status
|
||||
|
||||
|
|
@ -35,7 +35,6 @@ Implemented foundation:
|
|||
- explicit enablement resolution;
|
||||
- Tool surface registration;
|
||||
- Plugin permission grants;
|
||||
- raw core-Wasm Tool runtime;
|
||||
- Component Model Tool runtime;
|
||||
- first-party Rust PDK helpers for Component Model Tool guests;
|
||||
- embedded Rust Component Tool starter template;
|
||||
|
|
@ -152,20 +151,13 @@ input_schema = { type = "object", properties = { text = { type = "string" } }, r
|
|||
external_write = false
|
||||
```
|
||||
|
||||
The preferred new runtime is `wasm-component`. The older raw core-Wasm runtime remains explicit for compatibility:
|
||||
|
||||
```toml
|
||||
[runtime]
|
||||
kind = "wasm"
|
||||
entry = "plugin.wasm"
|
||||
abi = "yoi-plugin-wasm-1"
|
||||
```
|
||||
`wasm-component` is the public runtime kind. Legacy raw core-Wasm declarations such as `kind = "wasm"` / `abi = "yoi-plugin-wasm-1"` are no longer compatibility paths: static validation rejects them with a bounded diagnostic and they are not displayed as active/eligible Plugins.
|
||||
|
||||
Do not rely on package presence to activate anything. Discovery only records inventory.
|
||||
|
||||
## Rust PDK authoring
|
||||
|
||||
Rust authoring with `yoi-plugin-pdk` is the preferred path for new Tool Plugins. The raw core-Wasm ABI remains available only as compatibility/transitional runtime support.
|
||||
Rust authoring with `yoi-plugin-pdk` is the supported path for new Tool Plugins. Raw core-Wasm ABI packages are retired and should be rewritten as Component Model packages before enabling.
|
||||
|
||||
Create a starter with:
|
||||
|
||||
|
|
@ -412,4 +404,4 @@ Yoi normalizes paths, rejects `..` traversal, rejects symlink/root escapes, and
|
|||
- Request only the minimal host APIs and grants needed.
|
||||
- Keep Tool output bounded and structured.
|
||||
- Prefer Component Model authoring for new Plugins.
|
||||
- Treat raw core-Wasm ABI support as transitional compatibility.
|
||||
- Treat raw core-Wasm ABI support as retired; migration diagnostics may mention it, but authors should publish `wasm-component` packages.
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user