ADR-011: Plugin manifest format — TOML
ADR-011: Plugin manifest format — TOML
Status
Accepted
Context
FR-PLUGIN-2 defines the plugin manifest schema: a metadata file that declares the plugin’s identity, entry point, and capabilities. The manifest is hand-edited by plugin authors and parsed by the core at startup.
Three serialization formats are viable:
- TOML: Designed for human-edited config files. Comments are valid syntax. Rust-native (
tomlcrate,serdeintegration). Canonical in the Rust ecosystem (Cargo.toml sets the expectation). - YAML: Human-readable, widely used for CI configs and Kubernetes manifests. Rust support via
serde_yaml. But indentation-sensitive parsing makes error messages ambiguous, and implicit type coercion (yes/no/true/false/on/off) causes real bugs. - JSON: Universal, every language parses it. But no comment support, comma-before-newline errors are common in hand-edited files, and the syntax is noisier for nested structures.
The manifest format must be:
- Hand-editable by plugin authors
- Unambiguously parseable
- Language-agnostic (not Rust-specific)
- Compatible with
serdefor Rust-side deserialization
Decision
Use TOML for all plugin manifests (plugin.toml). The canonical schema is defined by the PluginManifest Rust struct:
use serde::Deserialize;
use std::collections::HashSet;
#[derive(Debug, Deserialize)]
pub struct PluginManifest {
pub id: String, // Unique plugin identifier (reverse domain or slug)
pub name: String, // Human-readable name
pub version: String, // SemVer version string
pub entry: String, // Path to executable (relative to manifest dir)
pub capabilities: HashSet<PluginCapability>, // Declared capabilities
}
#[derive(Debug, Deserialize, Hash, Eq, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum PluginCapability {
Search,
GetTrack,
GetAlbum,
GetArtist,
Stream,
// GetLyrics reserved for v2 — omitted until pipe mode or concrete request
}
Example manifest:
id = "local-files"
name = "Local Files"
version = "1.0.0"
entry = "main.py"
capabilities = ["search", "get_track", "get_album", "get_artist", "stream"]
# get_lyrics is reserved for v2
Validation rules:
idmust be non-empty and match^[a-zA-Z0-9_-]+$versionmust parse as SemVer (strict)entrymust resolve to an executable file relative to the manifest directory (validated at registration time, not parse time)capabilitiesmust be non-empty. Unknown capabilities are ignored with a warning (FR-PLUGIN-2).- Unknown fields in the manifest are ignored with a warning (forward compatibility).
- All fields are required. Omitting any field causes rejection with an error message naming the missing field.
The #[serde(deny_unknown_fields)] attribute is intentionally NOT used to allow forward-compatible manifest evolution. Unknown fields are logged and ignored.
Alternatives considered
- YAML: The implicit type coercion is a real source of bugs in practice.
yes/nobeing parsed as booleantrue/falsewhen the author expected a string.01234being parsed as octal. The Norway problem (id: "no"being parsed asid: false). These are well-documented failure modes that TOML avoids entirely. YAML also lacks a canonical Rust library —serde_yamlis maintained but the spec surface area is significantly larger than TOML’s. - JSON: No comment support. Plugin authors (target audience: hobbyists, enthusiasts) are likely to want inline comments explaining fields. JSON’s comma rules are error-prone in hand-edited files. TOML is strictly less noisy for the nested structures in a plugin manifest (arrays of strings, optional fields).
- RON (Rusty Object Notation): Rust-specific syntax. Defeats the language-agnostic purpose — a Python plugin author should not need to learn Rust syntax to write a manifest.
- protobuf / flatbuffers / cap’n proto: Schema languages that require a compilation step. Overkill for a 10-line config file. Plugin authors should be able to write a manifest in a text editor and drop it in a directory.
Consequences
Positive:
- TOML is the Rust ecosystem standard (Cargo.toml, Cargo configuration). Every Rust developer reading the project immediately understands the format convention.
- Comments in manifests help plugin authors document their configuration.
serde+tomlprovide zero-boilerplate deserialization with strong typing and clear error messages on parse failure.- Unknown-field tolerance provides forward compatibility without explicit version negotiation.
- The normative schema is the Rust struct — it cannot drift from implementation.
Negative:
- Plugin authors who don’t know TOML must learn it. Mitigation: TOML is deliberately simple (spec fits on one page). The reference plugin ships with a well-commented example
plugin.toml. - TOML arrays-of-tables for complex nested structures (if plugins later need sub-manifests or multi-entry manifests) are verbose compared to YAML. Mitigation: plugin manifests are flat in v1 (id, name, version, entry, capabilities). If nested structures are needed in v2, the schema can evolve.
serde_yamlis no longer updated as actively astoml. The TOML crate has first-class support in the Rust ecosystem.
References
- FR-PLUGIN-2 (functional.md)
- Constraint C2 (language-agnostic protocol) — (constraints-assumptions.md)
- ADR-001 (JSON-RPC over stdin/stdout)