Skip to content
Rymflux Documentation
Esc
navigateopen⌘Jpreview
On this page

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 (toml crate, serde integration). 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:

  1. Hand-editable by plugin authors
  2. Unambiguously parseable
  3. Language-agnostic (not Rust-specific)
  4. Compatible with serde for 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:

  • id must be non-empty and match ^[a-zA-Z0-9_-]+$
  • version must parse as SemVer (strict)
  • entry must resolve to an executable file relative to the manifest directory (validated at registration time, not parse time)
  • capabilities must 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/no being parsed as boolean true/false when the author expected a string. 01234 being parsed as octal. The Norway problem (id: "no" being parsed as id: false). These are well-documented failure modes that TOML avoids entirely. YAML also lacks a canonical Rust library — serde_yaml is 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 + toml provide 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_yaml is no longer updated as actively as toml. 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)

Was this page helpful?