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.
This commit is contained in:
@@ -33,3 +33,9 @@ essentials:
|
||||
| 10 | Milestones | complete | [milestones.md](milestones.md) | 2026-04-03 |
|
||||
| 11 | Decision Log | complete | [decisions/001-initial-decisions.md](decisions/001-initial-decisions.md) | 2026-04-03 |
|
||||
| 12 | Risk / Unknowns | complete | [risks.md](risks.md) | 2026-04-03 |
|
||||
|
||||
## Architecture Decision Records
|
||||
|
||||
- [ADR-001 — Initial Decisions](decisions/001-initial-decisions.md)
|
||||
- [ADR-002 — SLM Routing](decisions/002-slm-routing.md)
|
||||
- [ADR-003 — Plugin Trust via TOFU Manifest Pinning](decisions/003-plugin-trust.md)
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user