dc438ea181
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.
118 lines
4.1 KiB
Markdown
118 lines
4.1 KiB
Markdown
# Plugin Trust
|
|
|
|
gnoma plugins ship arbitrary executables (hooks and MCP servers) that run with
|
|
your user privileges. To prevent silent tampering with an already-installed
|
|
plugin, gnoma pins each plugin's `plugin.json` by SHA-256 the first time it
|
|
loads, and refuses to load on subsequent runs if the manifest has changed.
|
|
|
|
This is the same Trust-On-First-Use (TOFU) discipline that SSH uses for host
|
|
keys, applied to plugin manifests.
|
|
|
|
## Pin file
|
|
|
|
```
|
|
~/.config/gnoma/plugins.pins.toml
|
|
```
|
|
|
|
Format:
|
|
|
|
```toml
|
|
[pins]
|
|
git-tools = "a3f1c5d8e9b2..."
|
|
docker-tools = "7c4b9f0e2a8d..."
|
|
```
|
|
|
|
The file is created the first time gnoma loads any plugin. It is owned by the
|
|
same trust boundary as `~/.config/gnoma/config.toml` — anyone who can write
|
|
to your config directory can also re-pin plugins.
|
|
|
|
## First load (TOFU warning)
|
|
|
|
When gnoma sees a plugin it has no pin for, it records the hash automatically
|
|
and logs a warning that names the plugin and the hash:
|
|
|
|
```
|
|
WARN enrolling new plugin (trust on first use) name=git-tools scope=user
|
|
sha256=a3f1c5d8e9b2c7f0...
|
|
```
|
|
|
|
The plugin loads normally. If this is the plugin you intended to install, you
|
|
can ignore the warning. If it appeared unexpectedly, inspect the plugin
|
|
directory and the matching `plugins.pins.toml` entry before running gnoma
|
|
again.
|
|
|
|
## Subsequent loads
|
|
|
|
- **Match:** silent. The plugin loads with no log line.
|
|
- **Mismatch:** the plugin is refused. gnoma logs an error and **does not**
|
|
overwrite the pin:
|
|
|
|
```
|
|
ERROR refusing plugin — manifest changed since enrolment name=git-tools
|
|
pinned=a3f1c5d8... actual=b9e4d2c1...
|
|
hint="remove the entry from plugins.pins.toml to re-enrol"
|
|
```
|
|
|
|
Other plugins are unaffected.
|
|
|
|
## Re-enrolling a plugin
|
|
|
|
When you legitimately update a plugin and want gnoma to accept the new
|
|
manifest:
|
|
|
|
1. Inspect the changes (`git diff`, manifest diff, or just review the new
|
|
`plugin.json`). Confirm the update is what you expected.
|
|
2. Delete the plugin's line from `plugins.pins.toml`.
|
|
3. Run gnoma. The new hash is recorded as a fresh TOFU enrollment and a
|
|
warning is logged.
|
|
|
|
If you trust the source and want to skip the inspect-and-delete step, you can
|
|
overwrite the pin directly: replace the hex value in the file with the hash
|
|
gnoma logged for the new manifest. (gnoma prints the actual hash in the
|
|
mismatch error.)
|
|
|
|
## Disabling pinning
|
|
|
|
There is no opt-out. If the pin file cannot be read or written (permissions,
|
|
disk full, etc.), gnoma logs a warning and loads plugins without pinning for
|
|
that run — but it does not silently disable the mechanism for subsequent
|
|
runs.
|
|
|
|
## What the hash covers
|
|
|
|
The full bytes of `plugin.json`. Any edit — version bump, comment, whitespace
|
|
— invalidates the pin. This is intentional: the trust surface includes every
|
|
field the loader reads, and selectively hashing fields would create bypass
|
|
opportunities.
|
|
|
|
The hash does **not** cover the rest of the plugin directory (skill files,
|
|
hook scripts, MCP server binaries). Those are referenced by paths declared
|
|
in the manifest; tampering with a hook script while leaving `plugin.json`
|
|
untouched is **not** detected. Treat the plugin directory itself as a trust
|
|
boundary at filesystem-permissions level.
|
|
|
|
## Threat model
|
|
|
|
Pinning protects against:
|
|
|
|
- Silent tampering of an installed plugin's manifest by another process or
|
|
user with write access to the plugin directory.
|
|
- Accidental drift (e.g. `git pull` in a plugin you forgot was a working copy)
|
|
pulling in capabilities you didn't intend to grant.
|
|
|
|
Pinning does **not** protect against:
|
|
|
|
- The first install — TOFU trusts whatever is on disk at first sight.
|
|
- Tampering with the pin file itself by a process that already has write
|
|
access to your config directory.
|
|
- Tampering with hook scripts or MCP binaries that live alongside the
|
|
manifest (see "What the hash covers").
|
|
|
|
For the gnoma threat model (single-user dev workstation), pinning closes the
|
|
largest realistic gap: post-install manifest drift on a machine the user
|
|
shares with other processes or where a plugin source is a live working copy.
|
|
|
|
## See also
|
|
|
|
- ADR-003: [Plugin Trust via TOFU Manifest Pinning](essentials/decisions/003-plugin-trust.md)
|