use super::{
CURRENT_WIT_WORLD, EnabledPluginConfig, PluginCapabilities, PluginComponentPath,
PluginIdentity, PluginWitWorld, ValidatedPluginRuntimeLimits,
error::{PluginLoadError, PluginManifestOpenError},
};
use crate::{
fs_utils::{EscapedDisplayText, FilesystemConfig, read_existing_file},
plugin::{host::PluginHostContext, runtime::PluginRuntimeSpec},
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{
fmt::{Debug, Formatter},
path::Path,
};
#[derive(Clone, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize)]
#[serde(default, deny_unknown_fields)]
pub struct PluginManifest {
identity: String,
wit_world: String,
requested_capabilities: PluginCapabilities,
}
impl Default for PluginManifest {
fn default() -> Self {
Self {
identity: String::new(),
wit_world: String::from(CURRENT_WIT_WORLD),
requested_capabilities: PluginCapabilities::default(),
}
}
}
impl PluginManifest {
#[must_use]
pub fn new_untrusted(
identity: impl Into<String>,
wit_world: impl Into<String>,
requested_capabilities: PluginCapabilities,
) -> Self {
Self {
identity: identity.into(),
wit_world: wit_world.into(),
requested_capabilities,
}
}
pub fn from_json_slice(bytes: &[u8], path: Option<&Path>) -> Result<Self, PluginLoadError> {
serde_json::from_slice(bytes).map_err(|source| PluginLoadError::ManifestParse {
path: path.map(Path::to_owned),
source,
})
}
#[must_use]
pub fn identity(&self) -> &str {
&self.identity
}
#[must_use]
pub fn wit_world(&self) -> &str {
&self.wit_world
}
#[must_use]
pub const fn requested_capabilities(&self) -> &PluginCapabilities {
&self.requested_capabilities
}
pub fn authorize_for_enabled_config(
&self,
config: EnabledPluginConfig<'_>,
) -> Result<AuthorizedPluginManifest, PluginLoadError> {
let manifest_identity =
PluginIdentity::try_new(self.identity.clone()).map_err(|source| {
PluginLoadError::InvalidIdentity {
identity: self.identity.clone(),
source,
}
})?;
if config.identity() != &manifest_identity {
return Err(PluginLoadError::IdentityMismatch {
configured: config.identity().clone(),
manifest: manifest_identity,
});
}
if config.wit_world() != self.wit_world {
return Err(PluginLoadError::WitWorldMismatch {
configured: config.wit_world().to_owned(),
manifest: self.wit_world.clone(),
});
}
Ok(AuthorizedPluginManifest {
identity: manifest_identity,
wit_world: config.wit_world_proof(),
requested_capabilities: self.requested_capabilities.clone(),
})
}
}
#[derive(Eq, PartialEq)]
pub struct PolicyOpenedPluginManifest {
display_path: EscapedDisplayText,
bytes: Vec<u8>,
}
impl PolicyOpenedPluginManifest {
pub fn open(
path: impl AsRef<Path>,
filesystem: &FilesystemConfig,
max_bytes: u64,
) -> Result<Self, PluginManifestOpenError> {
let file = read_existing_file(path, filesystem, max_bytes)
.map_err(|source| PluginManifestOpenError::Read { source })?;
Ok(Self {
display_path: EscapedDisplayText::from_path(&file.path),
bytes: file.bytes,
})
}
pub fn parse(&self) -> Result<PluginManifest, PluginManifestOpenError> {
serde_json::from_slice(&self.bytes).map_err(|source| PluginManifestOpenError::Parse {
display_path: self.display_path.clone(),
source,
})
}
#[must_use]
pub const fn display_path(&self) -> &EscapedDisplayText {
&self.display_path
}
#[must_use]
pub fn bytes(&self) -> &[u8] {
&self.bytes
}
}
impl Debug for PolicyOpenedPluginManifest {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("PolicyOpenedPluginManifest")
.field("display_path_byte_len", &self.display_path.as_str().len())
.field("byte_len", &self.bytes.len())
.finish()
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct AuthorizedPluginManifest {
identity: PluginIdentity,
wit_world: PluginWitWorld,
requested_capabilities: PluginCapabilities,
}
impl AuthorizedPluginManifest {
#[must_use]
pub const fn identity(&self) -> &PluginIdentity {
&self.identity
}
#[must_use]
pub const fn wit_world(&self) -> &'static str {
self.wit_world.as_str()
}
#[must_use]
pub const fn requested_capabilities(&self) -> &PluginCapabilities {
&self.requested_capabilities
}
}
#[derive(Debug, Eq, PartialEq)]
pub struct EffectivePlugin {
identity: PluginIdentity,
component_path: PluginComponentPath,
wit_world: PluginWitWorld,
capabilities: PluginCapabilities,
runtime_limits: ValidatedPluginRuntimeLimits,
}
impl EffectivePlugin {
pub fn from_enabled_config_and_manifest(
config: EnabledPluginConfig<'_>,
manifest: &PluginManifest,
) -> Result<Self, PluginLoadError> {
let manifest = manifest.authorize_for_enabled_config(config)?;
Self::from_authorized_manifest(config, &manifest)
}
pub fn from_authorized_manifest(
config: EnabledPluginConfig<'_>,
manifest: &AuthorizedPluginManifest,
) -> Result<Self, PluginLoadError> {
if config.identity() != manifest.identity() {
return Err(PluginLoadError::IdentityMismatch {
configured: config.identity().clone(),
manifest: manifest.identity().clone(),
});
}
if config.wit_world() != manifest.wit_world() {
return Err(PluginLoadError::WitWorldMismatch {
configured: config.wit_world().to_owned(),
manifest: manifest.wit_world().to_owned(),
});
}
Ok(Self {
identity: manifest.identity().clone(),
component_path: config.component_path().clone(),
wit_world: manifest.wit_world,
capabilities: config
.capabilities()
.intersection(manifest.requested_capabilities()),
runtime_limits: config.runtime_limits(),
})
}
#[must_use]
pub fn identity(&self) -> &str {
self.identity_proof().as_str()
}
#[must_use]
pub const fn identity_proof(&self) -> &PluginIdentity {
&self.identity
}
#[must_use]
pub const fn wit_world(&self) -> &'static str {
self.wit_world.as_str()
}
#[must_use]
pub const fn capabilities(&self) -> &PluginCapabilities {
&self.capabilities
}
#[must_use]
pub const fn runtime_limits(&self) -> super::PluginRuntimeLimits {
self.runtime_limits.as_config()
}
#[must_use]
pub fn host_context(&self) -> PluginHostContext {
PluginHostContext::new(self.identity.clone(), self.capabilities.clone())
}
pub fn runtime_spec(
self,
filesystem: &FilesystemConfig,
) -> Result<PluginRuntimeSpec, super::PluginComponentOpenError> {
let component = super::AuthorizedPluginComponent::open(
self.identity.clone(),
&self.component_path,
filesystem,
self.runtime_limits.max_message_bytes(),
)?;
Ok(PluginRuntimeSpec::new(
component,
self.wit_world,
self.capabilities.clone(),
self.runtime_limits,
))
}
}
#[cfg(test)]
mod tests {
use super::{EffectivePlugin, PolicyOpenedPluginManifest};
use crate::fs_utils::FilesystemConfig;
use crate::plugin::{
PluginCapabilities, PluginConfig, PluginIdentity, PluginLoadError, PluginManifest,
PluginRegistryConfig, PluginRuntimeLimits, WorkspacePathGrant,
};
use std::{
fs,
path::{Path, PathBuf},
time::{SystemTime, UNIX_EPOCH},
};
#[test]
fn effective_plugin_requires_enabled_matching_identity_and_world() {
let config = PluginConfig {
identity: String::from("formatter"),
component_path: "plugins/formatter.wasm".into(),
enabled: true,
capabilities: PluginCapabilities {
workspace_observe: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
},
..PluginConfig::default()
};
let manifest = PluginManifest {
identity: String::from("formatter"),
requested_capabilities: PluginCapabilities {
workspace_observe: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
},
..PluginManifest::default()
};
let effective = effective_from_config(config, &manifest)
.expect("matching enabled plugin should project");
assert_eq!(effective.identity(), "formatter");
assert_eq!(effective.identity_proof().as_str(), "formatter");
assert_eq!(
effective.capabilities().workspace_observe,
vec![WorkspacePathGrant::new("docs")]
);
assert_eq!(effective.runtime_limits(), PluginRuntimeLimits::default());
}
#[test]
fn authorized_manifest_is_required_before_effective_projection() {
let config = PluginConfig {
identity: String::from("formatter"),
component_path: "plugins/formatter.wasm".into(),
enabled: true,
capabilities: PluginCapabilities {
status_publish: true,
..PluginCapabilities::default()
},
..PluginConfig::default()
};
let manifest = PluginManifest {
identity: String::from("formatter"),
requested_capabilities: PluginCapabilities {
status_publish: true,
..PluginCapabilities::default()
},
..PluginManifest::default()
};
let registry = PluginRegistryConfig {
plugins: vec![config],
}
.validate()
.expect("registry should validate");
let enabled = registry
.enabled_plugin(&identity("formatter"))
.expect("enabled plugin should be loadable");
let authorized = manifest
.authorize_for_enabled_config(enabled)
.expect("matching manifest should authorize");
assert_eq!(authorized.identity().as_str(), "formatter");
assert_eq!(authorized.wit_world(), crate::plugin::CURRENT_WIT_WORLD);
let effective = EffectivePlugin::from_authorized_manifest(enabled, &authorized)
.expect("authorized manifest should project");
assert_eq!(effective.identity(), "formatter");
assert_eq!(effective.identity_proof().as_str(), "formatter");
assert!(effective.capabilities().status_publish);
}
#[test]
fn effective_projection_rejects_authorized_manifest_for_another_config() {
let registry = PluginRegistryConfig {
plugins: vec![
PluginConfig {
identity: String::from("formatter"),
component_path: "plugins/formatter.wasm".into(),
enabled: true,
..PluginConfig::default()
},
PluginConfig {
identity: String::from("other"),
component_path: "plugins/other.wasm".into(),
enabled: true,
..PluginConfig::default()
},
],
}
.validate()
.expect("registry should validate");
let formatter = registry
.enabled_plugin(&identity("formatter"))
.expect("formatter should be enabled");
let other = registry
.enabled_plugin(&identity("other"))
.expect("other should be enabled");
let manifest = PluginManifest {
identity: String::from("formatter"),
..PluginManifest::default()
};
let authorized = manifest
.authorize_for_enabled_config(formatter)
.expect("formatter manifest should authorize");
let error = EffectivePlugin::from_authorized_manifest(other, &authorized)
.expect_err("authorized manifest is scoped to formatter config");
match error {
PluginLoadError::IdentityMismatch {
configured,
manifest,
} => {
assert_eq!(configured, identity("other"));
assert_eq!(manifest, identity("formatter"));
}
other => panic!("expected identity mismatch, got {other:?}"),
}
}
#[test]
fn runtime_spec_uses_configured_limits() {
let limits = PluginRuntimeLimits::try_new(32 * 1024 * 1024, 2 * 1024 * 1024, 10_000, 100)
.expect("limits should be valid");
let config = PluginConfig {
identity: String::from("formatter"),
component_path: "plugins/formatter.wasm".into(),
enabled: true,
runtime_limits: limits,
..PluginConfig::default()
};
let manifest = PluginManifest {
identity: String::from("formatter"),
..PluginManifest::default()
};
let root = temp_dir("runtime-spec-component");
fs::create_dir(root.join("plugins")).expect("plugin directory should be created");
fs::write(root.join("plugins/formatter.wasm"), b"\0asm")
.expect("component fixture should be written");
let filesystem = FilesystemConfig::from_workspace_root(&root)
.expect("test workspace root should validate");
let spec = effective_from_config(config, &manifest)
.expect("matching enabled plugin should project")
.runtime_spec(&filesystem)
.expect("runtime spec should open component through policy");
assert_eq!(spec.limits(), limits);
assert_eq!(spec.component_bytes(), b"\0asm");
assert!(
spec.component_display_path()
.as_str()
.contains("formatter.wasm")
);
let _cleanup = fs::remove_dir_all(root);
}
#[test]
fn disabled_config_has_no_enabled_registry_entry() {
let config = PluginConfig {
identity: String::from("formatter"),
enabled: false,
..PluginConfig::default()
};
let registry = PluginRegistryConfig {
plugins: vec![config],
}
.validate()
.expect("disabled config should still validate");
assert!(registry.enabled_plugin(&identity("formatter")).is_none());
}
#[test]
fn invalid_enabled_identity_rejects_before_effective_plugin_construction() {
let config = PluginConfig {
enabled: true,
..PluginConfig::default()
};
let registry = PluginRegistryConfig {
plugins: vec![config],
};
assert!(matches!(
registry.validate(),
Err(crate::plugin::PluginRegistryError::InvalidIdentity { .. })
));
}
#[test]
fn effective_plugin_rejects_invalid_manifest_identity() {
let config = PluginConfig {
identity: String::from("formatter"),
component_path: "plugins/formatter.wasm".into(),
enabled: true,
..PluginConfig::default()
};
let manifest = PluginManifest {
identity: String::from("Formatter"),
..PluginManifest::default()
};
assert!(matches!(
effective_from_config(config, &manifest),
Err(PluginLoadError::InvalidIdentity { .. })
));
}
#[test]
fn identity_mismatch_error_carries_validated_identity_proofs() {
let config = PluginConfig {
identity: String::from("formatter"),
component_path: "plugins/formatter.wasm".into(),
enabled: true,
..PluginConfig::default()
};
let manifest = PluginManifest {
identity: String::from("other"),
..PluginManifest::default()
};
let error = effective_from_config(config, &manifest)
.expect_err("mismatched manifest identity should reject");
match error {
PluginLoadError::IdentityMismatch {
configured,
manifest,
} => {
assert_eq!(configured, identity("formatter"));
assert_eq!(manifest, identity("other"));
}
other => panic!("expected identity mismatch, got {other:?}"),
}
}
#[test]
fn invalid_enabled_component_path_rejects_before_effective_plugin_construction() {
let config = PluginConfig {
identity: String::from("formatter"),
component_path: "/tmp/formatter.wasm".into(),
enabled: true,
..PluginConfig::default()
};
let registry = PluginRegistryConfig {
plugins: vec![config],
};
assert!(matches!(
registry.validate(),
Err(crate::plugin::PluginRegistryError::InvalidEnabledComponentPath { .. })
));
}
#[test]
fn effective_plugin_projects_runtime_and_host_authorization_boundaries() {
let limits = PluginRuntimeLimits::try_new(32 * 1024 * 1024, 2 * 1024 * 1024, 10_000, 100)
.expect("limits should validate");
let config = PluginConfig {
identity: String::from("formatter"),
component_path: "plugins/formatter.wasm".into(),
enabled: true,
capabilities: PluginCapabilities {
buffer_observe: true,
buffer_propose_edit: true,
workspace_observe: vec![WorkspacePathGrant::new("docs")],
workspace_artifact_write: vec![WorkspacePathGrant::new("docs/generated")],
status_publish: true,
},
runtime_limits: limits,
..PluginConfig::default()
};
let manifest = PluginManifest {
identity: String::from("formatter"),
requested_capabilities: PluginCapabilities {
buffer_observe: true,
workspace_observe: vec![WorkspacePathGrant::new("docs")],
workspace_artifact_write: vec![WorkspacePathGrant::new("docs")],
status_publish: true,
..PluginCapabilities::default()
},
..PluginManifest::default()
};
let effective =
effective_from_config(config, &manifest).expect("effective plugin should project");
let host = effective.host_context();
let root = temp_dir("runtime-boundary-component");
fs::create_dir(root.join("plugins")).expect("plugin directory should be created");
fs::write(root.join("plugins/formatter.wasm"), b"\0asm")
.expect("component fixture should be written");
let filesystem = FilesystemConfig::from_workspace_root(&root)
.expect("test workspace root should validate");
let runtime = effective
.runtime_spec(&filesystem)
.expect("runtime spec should open component through policy");
assert_eq!(runtime.identity(), "formatter");
assert_eq!(runtime.component_bytes(), b"\0asm");
assert_eq!(runtime.limits(), limits);
assert!(host.capabilities().buffer_observe);
assert!(!host.capabilities().buffer_propose_edit);
assert_eq!(
host.capabilities().workspace_artifact_write,
vec![WorkspacePathGrant::new("docs/generated")]
);
let _cleanup = fs::remove_dir_all(root);
}
#[test]
fn runtime_spec_rejects_missing_component_after_filesystem_policy() {
let root = temp_dir("missing-runtime-component");
let filesystem = FilesystemConfig::from_workspace_root(&root)
.expect("test workspace root should validate");
let config = PluginConfig {
identity: String::from("formatter"),
component_path: "plugins/missing.wasm".into(),
enabled: true,
..PluginConfig::default()
};
let manifest = PluginManifest {
identity: String::from("formatter"),
..PluginManifest::default()
};
let error = effective_from_config(config, &manifest)
.expect("effective plugin should project before component open")
.runtime_spec(&filesystem)
.expect_err("missing component should not become an empty runtime spec");
match &error {
crate::plugin::PluginComponentOpenError::Read { identity, .. } => {
assert_eq!(identity.as_str(), "formatter");
}
}
assert!(
error
.to_string()
.contains("failed to open component for plugin \"formatter\"")
);
let _cleanup = fs::remove_dir_all(root);
}
#[test]
fn policy_opened_manifest_parses_only_after_filesystem_read() {
let root = temp_dir("policy-opened-manifest");
fs::create_dir(root.join("plugins")).expect("plugin directory should be created");
let manifest_path = root.join("plugins/manifest.json");
fs::write(
&manifest_path,
br#"{"identity":"formatter","wit_world":"alma:editor/plugin@0.2.0","requested_capabilities":{"status.publish":true}}"#,
)
.expect("manifest fixture should be written");
let filesystem = FilesystemConfig::from_workspace_root(&root)
.expect("test workspace root should validate");
let opened = PolicyOpenedPluginManifest::open(&manifest_path, &filesystem, 1024)
.expect("manifest should open through policy");
let manifest = opened.parse().expect("manifest should parse");
assert_eq!(manifest.identity(), "formatter");
assert!(manifest.requested_capabilities().status_publish);
assert!(opened.display_path().as_str().contains("manifest.json"));
let debug = format!("{opened:?}");
assert!(debug.contains("display_path_byte_len"));
assert!(debug.contains("byte_len"));
assert!(!debug.contains("manifest.json"));
assert!(!debug.contains("formatter"));
assert!(!debug.contains("status.publish"));
let _cleanup = fs::remove_dir_all(root);
}
#[test]
fn policy_opened_manifest_rejects_missing_file() {
let root = temp_dir("missing-policy-manifest");
let filesystem = FilesystemConfig::from_workspace_root(&root)
.expect("test workspace root should validate");
let error =
PolicyOpenedPluginManifest::open(root.join("plugins/missing.json"), &filesystem, 1024)
.expect_err("missing manifest should reject before parsing");
assert!(matches!(
error,
crate::plugin::PluginManifestOpenError::Read { .. }
));
let _cleanup = fs::remove_dir_all(root);
}
#[test]
fn manifest_unknown_fields_fail_closed() {
let error = PluginManifest::from_json_slice(
br#"{"identity": "formatter", "unknown": true}"#,
Some(std::path::Path::new("plugins/formatter/manifest.json")),
)
.expect_err("unknown manifest fields should reject");
assert!(matches!(error, PluginLoadError::ManifestParse { .. }));
}
#[test]
fn plugin_manifest_golden_parses_and_projects_effective_grants() {
let manifest_path = plugin_golden_path("manifest-valid.json");
let manifest = PluginManifest::from_json_slice(
&std::fs::read(&manifest_path).expect("manifest fixture should read"),
Some(&manifest_path),
)
.expect("valid manifest fixture should parse");
let config = PluginConfig {
identity: String::from("formatter"),
component_path: "plugins/formatter.wasm".into(),
enabled: true,
capabilities: PluginCapabilities {
buffer_observe: true,
workspace_observe: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
},
..PluginConfig::default()
};
let effective = effective_from_config(config, &manifest)
.expect("valid manifest fixture should project");
assert_eq!(effective.identity(), "formatter");
assert!(effective.capabilities().buffer_observe);
assert_eq!(
effective.capabilities().workspace_observe,
vec![WorkspacePathGrant::new("docs")]
);
assert!(!effective.capabilities().status_publish);
}
#[test]
fn plugin_manifest_goldens_fail_closed_for_unknown_capability_and_world_mismatch() {
let unknown_path = plugin_golden_path("manifest-unknown-capability.json");
let unknown_error = PluginManifest::from_json_slice(
&std::fs::read(&unknown_path).expect("manifest fixture should read"),
Some(&unknown_path),
)
.expect_err("unknown manifest capability should reject");
assert!(matches!(
unknown_error,
PluginLoadError::ManifestParse { .. }
));
let mismatch_path = plugin_golden_path("manifest-mismatched-world.json");
let mismatch = PluginManifest::from_json_slice(
&std::fs::read(&mismatch_path).expect("manifest fixture should read"),
Some(&mismatch_path),
)
.expect("mismatched world is a load-time failure, not a parse failure");
let config = PluginConfig {
identity: String::from("formatter"),
enabled: true,
component_path: "plugins/formatter.wasm".into(),
..PluginConfig::default()
};
assert!(matches!(
effective_from_config(config, &mismatch),
Err(PluginLoadError::WitWorldMismatch { .. })
));
}
#[test]
fn plugin_config_goldens_cover_enabled_disabled_and_broad_grants() {
let minimal = load_config_golden("config-minimal-valid.json");
assert_eq!(minimal.plugins().enabled_plugins().count(), 1);
assert!(minimal.plugins().plugin(&identity("formatter")).is_some());
let disabled = load_config_golden("config-disabled.json");
assert_eq!(disabled.plugins().enabled_plugins().count(), 0);
assert!(disabled.plugins().plugin(&identity("formatter")).is_some());
let broad = load_config_golden("config-whole-workspace-grant.json");
let plugin = broad
.plugins()
.enabled_plugin(&identity("formatter"))
.expect("broad-grant fixture should include formatter");
assert_eq!(
plugin.capabilities().workspace_observe,
vec![WorkspacePathGrant::all_workspace()]
);
}
#[test]
fn unknown_wit_capability_names_fail_closed() {
assert!(crate::plugin::WitCapability::try_from("style.propose").is_err());
assert!(crate::plugin::WitCapability::try_from("workspace.delete").is_err());
}
fn load_config_golden(name: &str) -> crate::config::AppConfig {
let path = plugin_golden_path(name);
crate::config::AppConfig::from_json_slice(
&std::fs::read(&path).expect("config fixture should read"),
&path,
)
.expect("config fixture should load")
}
fn plugin_golden_path(name: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/goldens/plugin")
.join(name)
}
fn temp_dir(name: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time should be after unix epoch")
.as_nanos();
let path =
std::env::temp_dir().join(format!("alma-plugin-{name}-{}-{nanos}", std::process::id()));
fs::create_dir(&path).expect("temporary directory should be created");
path
}
fn identity(identity: &str) -> PluginIdentity {
PluginIdentity::try_new(identity).expect("test identity should validate")
}
fn effective_from_config(
config: PluginConfig,
manifest: &PluginManifest,
) -> Result<EffectivePlugin, PluginLoadError> {
let registry = PluginRegistryConfig {
plugins: vec![config],
}
.validate()
.expect("test config should validate");
let enabled = registry
.enabled_plugins()
.next()
.expect("test config should be enabled");
EffectivePlugin::from_enabled_config_and_manifest(enabled, manifest)
}
}