Skip to content

ADR-0004 — Plugin contract

  • Status: Accepted (2026-07-24, Opus review)
  • Related: ROADMAP v0.2/v0.6, CODE_REVIEW M4
  • Acceptance note: Confirmed. v0.2 delivers only the internal Plugin trait and migrates today’s static groups onto it (fixing modes/symlinks in collect). Declarative TOML plugins and rivet track are v0.6. The trust model (explicit, reviewed, dry-run-previewed hook commands) is non-negotiable.

Two Charter goals converge here: “every supported application is a plugin, added without changing Core” and the user’s request to “track any file” and treat the tool like “Terraform/Ansible for your environment.” Today plugins.rs is a compile-time PLUGINS: &[PluginDefinition] array of static paths. Consequences:

  • Users cannot add an app or a custom directory without editing Rust.
  • Plugins cannot carry per-app logic (VS Code extension export/import, GNOME dconf dump/load, crontab -l), so those environments restore incompletely.
  • Adding a group does currently require a Core recompile — contrary to the Charter’s “no Core changes” principle.

Split into an internal Plugin trait (built-ins, v0.2) and an external declarative plugin format plus user file tracking (v0.6).

pub trait Plugin {
fn id(&self) -> &'static str;
fn discover(&self, ctx: &HostContext) -> Vec<Candidate>; // present, named groups
fn collect(&self, sel: &Selection) -> Result<Vec<TrackedFile>>; // real mode + FileKind
fn plan_restore(&self, snap: &SnapshotTree) -> Vec<RestoreAction>;
fn validate(&self, snap: &SnapshotTree) -> Vec<Warning>;
fn hooks(&self) -> Hooks { Hooks::default() } // optional export/import commands
}

collect is where CODE_REVIEW H1/H2 are fixed: it records the real file mode and emits FileKind::{Regular,Directory,Symlink} (in-root links preserved, out-of-root flagged). hooks lets a plugin declare commands like code --list-extensions (export) / code --install-extension (import) and dconf dump / dconf load.

Built-ins ship as data and users drop in their own — no compilation:

~/.config/rivet/plugins/helix.toml
id = "helix"
title = "Helix editor"
paths = [".config/helix"]
packages = { pacman = ["helix"], apt = ["helix"], brew = ["helix"] }
[hooks]
export = [] # optional shell commands, explicit + reviewed
import = []

A loader in rivet-plugins reads these into the same Plugin interface the built-ins implement, so there is one code path.

# in the snapshot's rivet.toml
[[track]]
path = "~/dev/scripts"
include = ["**/*.sh"]
exclude = ["**/node_modules/**"]
secrets = "vault" # plaintext-blocked | vault (see ADR-0003)
mode = "preserve"

Plus imperative helpers rivet track <path> / rivet untrack <path> that edit that block. Tracked entries flow through the same collect/plan_restore path as plugins, inheriting all safety checks (exclusions, secret scan, safe_relative, rollback).

Hooks and tracked commands are explicit, user-authored, reviewable strings — never fetched or inferred. Declarative plugins run no code except the commands their author wrote. Rivet shows hook commands during scan/dry-run before they ever execute. A future signed-plugin registry is post-1.0 (ROADMAP ecosystem).

  • Keep the static array, add groups in Rust. Contradicts the Charter and the user’s “track any file” ask. Rejected.
  • WASM/dynamic-library plugins now. Powerful but heavy; a stable ABI is premature pre-1.0. Declarative TOML covers ~all real dotfile/app cases first.
  • Arbitrary tracking without the secret/exclusion pipeline. Would reintroduce the blind-copy risk the Charter forbids. Rejected — tracking reuses the guards.
  • Positive: users extend Rivet without Rust; per-app export/import makes restores actually complete; one interface for built-ins, user plugins, and tracked paths.
  • Positive: delivers the “declarative, reproducible environment” feel.
  • Cost: a manifest schema to design and version; hook execution is new surface area — mitigated by the explicit/reviewed trust model and dry-run preview.
  • Risk: schema churn pre-1.0; acceptable while versioned, frozen at v0.9.