Files
vikingowl dc438ea181 feat(plugin): trust-on-first-use manifest pinning
Plugins are now verified against ~/.config/gnoma/plugins.pins.toml at
load time. Each plugin's plugin.json bytes are hashed (SHA-256) and:

- recorded automatically on first load (TOFU) with a prominent warning
- compared on subsequent loads
- refused with a clear error if the hash drifted, without overwriting
  the pin so the user can review and re-enrol deliberately

Pin-store I/O failures degrade to load-without-pinning rather than
locking the user out of previously-trusted plugins.

Closes audit finding C2. See ADR-003 for the decision rationale and
docs/plugins-trust.md for the end-user trust model.
2026-05-19 16:44:09 +02:00

3.9 KiB

ADR-003: Plugin Trust via TOFU Manifest Pinning

Status: Accepted Date: 2026-05-19

Context

Plugins ship arbitrary code paths: they declare hook executables (run on every matching tool call) and MCP server commands (long-lived subprocesses with stdin/stdout protocol access). The plugin loader resolves these against the plugin directory and exec's them with gnoma's full privileges.

Before this ADR, the loader had:

  • Path traversal guards in the manifest (checkSafePath).
  • An enabled/disabled allowlist by plugin name (PluginsSection).
  • No integrity check on the manifest itself.

The gap: once a plugin sits in ~/.config/gnoma/plugins/ (placed there by the user, a setup script, or any process with write access to the directory), its manifest can be edited or replaced silently. A modified plugin.json can add a new hook on a popular event, or swap the MCP command to point at a malicious binary, and the next gnoma startup will load it with no signal to the user.

Audit finding C2 flagged this as the critical gap in the plugin trust model.

Decision

The plugin loader records and verifies a SHA-256 of every enrolled plugin's plugin.json bytes using a Trust-On-First-Use (TOFU) discipline. Pins are persisted in ~/.config/gnoma/plugins.pins.toml.

For every enabled plugin on every startup:

  1. No pin recorded — compute the hash, write it to the pin store, log a prominent warning naming the plugin and the hash. The plugin is loaded.
  2. Pin matches — load silently.
  3. Pin mismatches — refuse to load. Log an error with the pinned hash, the actual hash, and a hint on re-enrollment. Other plugins continue loading.

The hash covers the entire file, not just selected fields. Any edit to the manifest — including a version bump, a typo fix, or a new skill glob — triggers re-enrollment. This is intentional: the user re-confirms trust on every change to the trust surface.

A pin-store I/O failure (read or write) is logged and downgrades to load-without-pinning so a corrupted file cannot lock the user out of plugins they previously trusted.

Alternatives Considered

Alternative A: Cryptographic signatures

  • Pros: Strong identity assertion; cross-machine portability of trust.
  • Cons: Requires plugin authors to manage signing keys, a verification key to be configured in gnoma, and a workflow to distribute fingerprints. Overkill for a tool that targets developer workstations with a single user.

Alternative B: Require explicit enrollment (no TOFU)

  • Pros: No silent first-load; user explicitly opts into every plugin.
  • Cons: Friction that punishes the common case (user puts a plugin in their plugin dir on purpose, runs gnoma, expects it to work). The first-run TOFU warning preserves the audit trail without blocking workflow.

Alternative C: Hash only the executable bits

  • Pros: Fewer benign re-enrollments on documentation tweaks.
  • Cons: Skill globs, plugin name, and gnoma_version constraints all influence what gnoma loads or how. Selecting a subset would create subtle bypasses (e.g. moving an existing hook into a newly-added Capabilities section the hash skipped). Whole-file hash has no such surface.

Consequences

Positive:

  • Tampering with an installed plugin's plugin.json between gnoma runs is loud and refused, not silent.
  • Every plugin's enrollment is auditable from a single TOML file.
  • No external infrastructure (no PKI, no registry).

Negative:

  • Legitimate plugin upgrades require a manual pin removal. For active plugin development this is friction; the workaround is to keep development plugins in a project directory (which still pins, but is throwaway per project).
  • A user who ignores the TOFU warning gains nothing.

Neutral:

  • Pin file lives next to the global config — same trust boundary as the config itself. Compromising that directory already compromises gnoma.