Plugin policy on a Forge

Organisation allow and deny lists, trusted attesters, and required plugin checks.

Written By Dustin

Last updated About 3 hours ago

A local binding is one person's choice on one machine. It is labelled LOCAL everywhere it is listed, precisely so nobody mistakes it for an organisational gate. Server-side enforcement is a different mechanism, and this article is about that one.

The organisation policy

A Forge reads its plugin policy from .tovio/forge/plugin-policy.vex under the served root, fresh on every request — a change is live on the next transition, with no restart. If the file is absent, the policy is the permissive empty default.

ListEffect
allow_idsPlugin ids permitted to back an enforcing binding. Empty means no positive restriction.
allow_publishersThe same, one level coarser.
deny_idsIds refused as an enforcing binding regardless of any allowlist.
deny_publishersA whole publisher refused. The kill switch.
attestersDIDs whose posted plugin results the required-plugin gate will trust, in addition to any forge-admin.

Three properties are worth stating plainly. Deny wins over allow. A load or decode error refuses rather than degrading to permit-all. And the decision is computed server-side from the served policy — never from a field a client sent.

A proposal transition that names a denied plugin as a required gate is refused with TVO-PLUGIN-014 over HTTP 422, naming only the plugin id.

Required plugin checks

A required plugin's result travels as a reserved check named plugin:<plugin-id>, resolved from the same store a required CI check uses. The gate is satisfied only when all three hold:

  1. The check exists and is success.
  2. Its recorded author is a policy-approved attester — a forge-admin principal, or a DID on the attesters list.
  3. Its recorded revision equals the proposal's current revision.

Anything else blocks with TVO-PLUGIN-015. Absent, pending, failing, authored by a regular member posting their own pass, or stamped with a stale revision are all the same answer: not satisfied.

The revision stamp is the part people trip over. An amend invalidates a prior plugin attestation, exactly as it invalidates approvals. A conformant runner re-executes on amend; a hand-posted attestation has to be re-posted.

Why local results are never trusted

A Forge does not take a client's word that its local plugin passed. If it did, the gate would be satisfiable by anyone willing to type the right thing, which is not a gate. So the required-plugin decision keys on a server-recorded attestation whose author the Forge re-verifies at land time.

The closure is doubled deliberately. A non-attester posting a plugin:<id> check is refused early at the check endpoint with TVO-FORGE-009 and a 403; and even for an already-stored record, the land gate re-checks the recorded author. A self-attestation cannot satisfy the gate by any path.

An empty attesters list means only admins are trusted. The default is never everyone.

What the native Forge does and does not do

The native reference Forge runs no WASM. It is a hermetic enforcement decision over plugin results it is given. That is the whole design: policy evaluation and sandboxed execution are separate concerns, and a hosted deployment supplies the execution half. See Server-side plugin execution.

Forge webhook payloads carrying plugin results stay metadata-only for protected content, like every other Forge surface.

Getting unblocked

  • TVO-PLUGIN-014 — a forge-admin removes the id or publisher from the denylist, or adds it to the allowlist. Or drop the plugin from the required set.
  • TVO-PLUGIN-015 — have the trusted runner post the attested result, then re-run the transition. On a hosted Forge that happens automatically at propose and amend, so the remedy is usually to amend and wait rather than to post anything by hand.