WASM Plugin System
Artifact Keeper features a powerful plugin system built on WebAssembly (WASM) that allows you to extend functionality without modifying core code.
Overview
The WASM plugin system enables:
- Custom format handlers: Support new package formats beyond the built-in 30+
- Metadata extractors: Parse and index custom artifact metadata
- Validation hooks: Implement custom artifact validation logic
- Post-processing: Transform artifacts on upload (virus scanning, compression, etc.)
- Hot-reloading: Install, update, and remove plugins without server restart
Architecture
Built on industry-standard WebAssembly technologies:
- wasmtime 21.0+: High-performance WASM runtime
- WASI: WebAssembly System Interface for secure system access
- wit-bindgen: Interface type definitions for plugin contracts
Security Model
Plugins run in a sandboxed environment with:
- No direct filesystem access (only via provided APIs)
- No network access (unless explicitly granted)
- Limited memory allocation
- CPU time limits per operation
- Isolated from other plugins
Plugin Lifecycle
1. Installation
Install plugins from a ZIP file or Git repository:
# From local filecurl -X POST https://registry.example.com/api/v1/plugins \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -F "file=@my-plugin.zip"
# From Git repositorycurl -X POST https://registry.example.com/api/v1/plugins \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "source": "git", "repository": "https://github.com/example/plugin-repo.git", "ref": "v1.0.0" }'
# From plugin registrycurl -X POST https://registry.example.com/api/v1/plugins \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "source": "registry", "name": "format-handler-conan", "version": "1.2.0" }'2. Enabling
Installed plugins are disabled by default:
curl -X POST https://registry.example.com/api/v1/plugins/plugin-123/enable \ -H "Authorization: Bearer $ADMIN_TOKEN"3. Configuration
Configure plugin settings:
curl -X PUT https://registry.example.com/api/v1/plugins/plugin-123/config \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "max_file_size_mb": 500, "allowed_extensions": [".nupkg", ".snupkg"], "index_metadata": true }'4. Disabling
Temporarily disable without uninstalling:
curl -X POST https://registry.example.com/api/v1/plugins/plugin-123/disable \ -H "Authorization: Bearer $ADMIN_TOKEN"5. Updating
Update to a new version:
curl -X PUT https://registry.example.com/api/v1/plugins/plugin-123 \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -F "file=@my-plugin-v2.zip"6. Uninstalling
Remove plugin completely:
curl -X DELETE https://registry.example.com/api/v1/plugins/plugin-123 \ -H "Authorization: Bearer $ADMIN_TOKEN"Plugin Types
Format Handler Plugins
Add support for new package formats:
interface format-handler { // Parse artifact metadata parse-metadata: func(data: list<u8>) -> result<metadata, error>
// Validate artifact structure validate: func(data: list<u8>) -> result<validation-result, error>
// Extract dependencies extract-dependencies: func(data: list<u8>) -> result<list<dependency>, error>
// Generate index data index: func(data: list<u8>) -> result<index-data, error>}
record metadata { name: string, version: string, description: option<string>, authors: list<string>, license: option<string>, keywords: list<string>,}
record dependency { name: string, version-requirement: string, optional: bool,}Example: Conan package handler
use artifact_keeper_plugin::*;
#[plugin_export]fn parse_metadata(data: &[u8]) -> Result<Metadata, Error> { let conanfile = parse_conanfile(data)?;
Ok(Metadata { name: conanfile.name, version: conanfile.version, description: Some(conanfile.description), authors: vec![conanfile.author], license: Some(conanfile.license), keywords: conanfile.topics, })}
#[plugin_export]fn validate(data: &[u8]) -> Result<ValidationResult, Error> { // Validate conanfile.py structure // Check for required fields // Verify package contents}Metadata Extractor Plugins
Extract custom metadata for indexing:
interface metadata-extractor { extract: func(artifact-id: string, data: list<u8>) -> result<key-value-pairs, error>}
type key-value-pairs = list<tuple<string, string>>Example: Extract ML model metadata
#[plugin_export]fn extract(artifact_id: &str, data: &[u8]) -> Result<Vec<(String, String)>, Error> { let model = load_model(data)?;
Ok(vec![ ("model_type".into(), model.architecture.clone()), ("framework".into(), "pytorch".into()), ("input_shape".into(), format!("{:?}", model.input_shape)), ("parameters".into(), model.num_parameters.to_string()), ])}Validation Hook Plugins
Implement custom validation logic:
interface validation-hook { // Called before artifact is accepted validate-upload: func( artifact: artifact-info, data: list<u8> ) -> result<validation-result, error>}
record artifact-info { name: string, version: string, format: string, size-bytes: u64, uploader: string,}
record validation-result { allowed: bool, message: option<string>, warnings: list<string>,}Example: Size and naming policy
#[plugin_export]fn validate_upload(artifact: ArtifactInfo, data: &[u8]) -> Result<ValidationResult, Error> { let mut warnings = Vec::new();
// Check size limit if artifact.size_bytes > 1_000_000_000 { return Ok(ValidationResult { allowed: false, message: Some("Artifact exceeds 1GB limit".into()), warnings, }); }
// Check naming convention if !artifact.name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') { warnings.push("Artifact name contains non-standard characters".into()); }
Ok(ValidationResult { allowed: true, message: None, warnings, })}Post-Processing Plugins
Transform artifacts after upload:
interface post-processor { process: func( artifact-id: string, data: list<u8> ) -> result<processed-data, error>}
record processed-data { data: list<u8>, modified: bool, metadata: key-value-pairs,}Example: Virus scanning integration
#[plugin_export]fn process(artifact_id: &str, data: &[u8]) -> Result<ProcessedData, Error> { // Scan with ClamAV or similar let scan_result = scan_for_viruses(data)?;
if scan_result.threats_found > 0 { return Err(Error::SecurityThreat(scan_result.description)); }
Ok(ProcessedData { data: data.to_vec(), modified: false, metadata: vec![ ("scanned_at".into(), Utc::now().to_string()), ("scanner_version".into(), scan_result.version), ], })}Developing Plugins
Prerequisites
# Install Rustcurl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Add WASM targetrustup target add wasm32-wasi
# Install wit-bindgencargo install wit-bindgen-cliProject Structure
my-plugin/├── Cargo.toml├── plugin.wit # Interface definition├── src/│ └── lib.rs # Plugin implementation└── plugin.toml # Plugin manifestPlugin Manifest (plugin.toml)
[plugin]name = "format-handler-conan"version = "1.0.0"description = "Conan package format support"author = "Your Name <you@example.com>"license = "MIT"
[plugin.capabilities]format_handler = trueextensions = [".conan"]
[plugin.config]# Default configurationmax_file_size_mb = 100validate_recipes = trueBuild Plugin
# Build WASM modulecargo build --target wasm32-wasi --release
# Package pluginzip my-plugin.zip \ target/wasm32-wasi/release/my_plugin.wasm \ plugin.toml \ plugin.wit \ README.mdTesting Plugins Locally
# Test with artifact-keeper CLIartifact-keeper plugin test \ --plugin my-plugin.zip \ --artifact test-package.conanPlugin Management API
List Plugins
curl https://registry.example.com/api/v1/plugins \ -H "Authorization: Bearer $TOKEN"Response:
{ "plugins": [ { "id": "plugin-123", "name": "format-handler-conan", "version": "1.0.0", "enabled": true, "capabilities": ["format_handler"], "config": { "max_file_size_mb": 100 } } ]}Get Plugin Details
curl https://registry.example.com/api/v1/plugins/plugin-123 \ -H "Authorization: Bearer $TOKEN"Plugin Logs
curl https://registry.example.com/api/v1/plugins/plugin-123/logs \ -H "Authorization: Bearer $TOKEN"Configuration
System-Wide Settings
# Enable plugin systemPLUGINS_ENABLED=true
# Plugin storage pathPLUGINS_PATH=/var/lib/artifact-keeper/plugins
# Resource limits per pluginPLUGIN_MAX_MEMORY_MB=256PLUGIN_MAX_CPU_MS=5000PLUGIN_MAX_EXECUTION_TIME_MS=30000
# Allow network access for pluginsPLUGIN_ALLOW_NETWORK=falsePlugin Security
# Require signature verificationPLUGIN_REQUIRE_SIGNATURE=truePLUGIN_TRUSTED_KEYS=/etc/artifact-keeper/trusted-keys/
# Restrict plugin sourcesPLUGIN_ALLOWED_SOURCES=https://plugins.artifact-keeper.ioBest Practices
Performance
- Keep plugin logic lightweight
- Cache expensive computations
- Stream large files instead of loading into memory
- Use async operations where possible
Error Handling
- Return meaningful error messages
- Don’t crash on invalid input
- Validate inputs thoroughly
- Log errors for debugging
Security
- Never trust input data
- Validate all external data
- Use safe parsing libraries
- Limit resource consumption
Troubleshooting
Plugin Won’t Load
Check plugin logs:
curl https://registry.example.com/api/v1/plugins/plugin-123/logsVerify WASM module:
wasm-validate target/wasm32-wasi/release/my_plugin.wasmPlugin Crashes
Review resource limits:
PLUGIN_MAX_MEMORY_MB=512PLUGIN_MAX_EXECUTION_TIME_MS=60000Enable debug logging:
PLUGIN_LOG_LEVEL=debugPerformance Issues
Profile plugin execution:
artifact-keeper plugin profile \ --plugin plugin-123 \ --artifact test-data.binCommunity Plugins
Browse available plugins at: https://plugins.artifact-keeper.io
Popular plugins:
- format-handler-conan: Conan C++ package support
- format-handler-conda: Conda package support
- scanner-trivy: Trivy security scanning integration
- metadata-sbom: SBOM generation (SPDX, CycloneDX)
- transform-compress: Automatic compression/decompression
- validator-license: License compliance checking