Skip to content

Workflows & diagrams

Visual, at-a-glance references for the TOVIO plugin system. Prose explanations live on the linked pages — this page is a map.

How to read these

Rectangles are states or artifacts, rounded shapes are actions, diamonds are decisions, and dashed lines cross the sandbox boundary. Colors indicate trust zones: the developer's machine, the TOVIO core, the sandbox, the Forge, and the audit log.

1. The three-layer model at a glance

The plugin system separates what a plugin is from when it runs from how it executes. Core defines the contracts; runtime hosts execute.

flowchart LR
    subgraph DEF["Definition"]
        M["manifest.toml"]
        A["Runtime artifact<br/>(plugin.wasm)"]
        I["Integrity metadata<br/>(sha256 + publisher signature)"]
    end
    subgraph BIND["Binding"]
        B1[".tovio/plugins/bindings.toml<br/>(tovio plugin bind)"]
        B2["…the same file<br/>(tovio policy hook add)"]
        B3["Forge-side required check<br/>(plugin:&lt;id&gt; attestation)"]
    end
    subgraph EXEC["Execution"]
        E1["Event fires"]
        E2["Sandbox invocation"]
        E3["Structured result"]
    end
    DEF --> BIND --> EXEC
    E3 --> AUD["PluginBlocked audit entry<br/>(on a block only)"]

    classDef def fill:#eef,stroke:#557;
    classDef bind fill:#efe,stroke:#575;
    classDef exec fill:#fee,stroke:#755;
    classDef aud fill:#ffe,stroke:#775;
    class M,A,I def;
    class B1,B2,B3 bind;
    class E1,E2,E3 exec;
    class AUD aud;

See Concepts for the prose version and Manifest reference for every field in the definition layer.

2. Install → verify → bind → run

The end-to-end journey a developer takes to put a plugin into service.

flowchart TD
    S([Start]) --> INS["tovio plugin install &lt;path or https URL&gt;"]
    INS --> PARSE{"Manifest valid?"}
    PARSE -- no --> E1[["TVO-PLUGIN-001<br/>manifest invalid"]]
    PARSE -- yes --> INTEG{"integrity"}
    INTEG -->|local_dev| DEVOK{"local source?"}
    DEVOK -- no --> E2[["TVO-PLUGIN-003<br/>integrity check failed"]]
    DEVOK -- yes --> STORE["Store in .tovio/plugins/&lt;id&gt;/<br/>verification: local-development"]
    INTEG -->|signed| INT{"artifact hashes<br/>to sha256?"}
    INT -- no --> E2
    INT -- yes --> SIG{"detached publisher<br/>signature present?"}
    SIG -- no --> HASHONLY{"local source?"}
    HASHONLY -- no --> E2
    HASHONLY -- yes --> STORE2["Store<br/>verification: sha256-only"]
    SIG -- yes --> TRUST{"signature verifies,<br/>publisher trusted,<br/>not revoked,<br/>id not pinned elsewhere?"}
    TRUST -- no --> E2
    TRUST -- yes --> STORE3["Store<br/>verification: publisher-signature+sha256"]
    STORE --> LIST["tovio plugin list / show / verify"]
    STORE2 --> LIST
    STORE3 --> LIST
    LIST --> BIND["tovio plugin bind …<br/>or tovio policy hook add …"]
    BIND --> READY(["Bound & ready"])
    READY --> EV{"Event fires?"}
    EV -->|any bound event| RUN["Sandboxed execution"]
    RUN --> RESULT{{"status: pass / warning /<br/>fail / error / skipped / unsupported"}}

    classDef err fill:#fdd,stroke:#c33,color:#600;
    class E1,E2 err;

"local source?" is the HTTPS rule in the other direction: a remote install has to reach STORE3 — local_dev and hash-only packages are refused from a URL.

Referenced pages: Use a plugin, Bindings, CLI reference, Errors.

3. Where an event's bindings come from

The client reads exactly one binding file, and adds the on-by-default first-party slots that no user authored. A local binding is never a repository, organization, or Forge policy gate — Forge-side required checks are a separate mechanism (diagram 10).

flowchart TB
    subgraph L2[" This repository — .tovio/, unsynced "]
        R[".tovio/plugins/bindings.toml<br/>written by plugin bind and policy hook add<br/>scope = local"]
    end
    subgraph L3[" Built-in default slots "]
        U["com.tovio.secret-scan on pre-snapshot<br/>enforcing + required<br/>unless secrets.scan = off"]
    end
    R --> COMPOSE{"Bindings that<br/>match this event"}
    U --> COMPOSE
    COMPOSE --> FILTER["Drop those whose path,<br/>branch, or actor filters miss"]
    FILTER --> EFF([Effective binding set for event])

Every matching binding runs; there is no precedence contest, and the first one to block is the one the error reports. A weaker user binding for the same id and event does not suppress a default slot unless it offers the same universal enforcing guarantee — otherwise a narrow shadow-bind would silently switch enforced scanning off.

See Bindings for the path/branch/actor filters and the on-disk field reference.

4. Lifecycle events — where plugins fit

Four representative events, and where each sits in a developer's or Forge's normal flow. Most of the lifecycle set fires today — post-*, sync, permission, lock, agent, and git-bridge events included; Lifecycle events records firing-site availability event by event.

sequenceDiagram
    autonumber
    actor Dev as Developer
    participant CLI as tovio-cli
    participant Core as tovio-core
    participant SB as Plugin sandbox
    participant Forge

    Dev->>CLI: edit files
    CLI->>Core: auto-snapshot working copy
    Core-->>SB: pre-snapshot(input)
    SB-->>Core: PluginResult
    Core->>CLI: allow / warn / block

    Dev->>CLI: tovio land feature/deps --into main
    CLI->>Core: prepare land
    Core-->>SB: pre-land(input)
    SB-->>Core: PluginResult
    Core->>CLI: allow / block (fail-closed if required)

    Dev->>CLI: tovio resolve --ai
    CLI->>Core: resolve request
    Core-->>SB: resolve-requested(input)
    SB-->>Core: proposed Resolution
    Core->>CLI: apply via normal resolution path

    Dev->>Forge: create proposal
    Forge->>Core: proposal-created event
    Core-->>SB: proposal-created(input, metadata-only)
    SB-->>Core: PluginResult
    Core->>Forge: surface in proposal status

Full payload details: Lifecycle events.

5. The enforcing decision (fail-closed)

The rule that decides whether an operation proceeds when an enforcing plugin has spoken.

An advisory binding is the one branch that always allows. Everything below is the enforcing branch.

stateDiagram-v2
    [*] --> Running
    Running --> pass: status = pass
    Running --> warning: status = warning
    Running --> fail: status = fail
    Running --> error: status = error
    Running --> skipped: status = skipped
    Running --> unsupported: status = unsupported
    Running --> malformed: schema invalid<br/>(REQ-PLUGIN-052)

    pass --> Allow
    warning --> Allow: (surfaced but non-blocking)
    fail --> Block: REQ-PLUGIN-054
    malformed --> error

    state ErrorCheck <<choice>>
    error --> ErrorCheck
    ErrorCheck --> Block: operation touches<br/>protected paths<br/>(REQ-PLUGIN-056)
    ErrorCheck --> Block: required = true<br/>(REQ-PLUGIN-055)
    ErrorCheck --> Allow: neither

    state DeclinedCheck <<choice>>
    skipped --> DeclinedCheck
    unsupported --> DeclinedCheck
    DeclinedCheck --> Block: required = true<br/>(REQ-PLUGIN-055)
    DeclinedCheck --> Allow: required = false

    Allow --> [*]
    Block --> [*]

skipped and unsupported are well-formed "not applicable" verdicts rather than failures, so the protected fail-closed rule does not reach them — only error blocks a protected operation a non-required binding could otherwise wave through.

Prose: Concepts → fail-closed enforcement.

6. The capability intersection

The effective capability set is the intersection of four independent grants. If any layer withholds a capability, the plugin does not get it.

flowchart LR
    M["Manifest<br/>requests"] --> X((∩))
    B["Binding<br/>approves"] --> X
    A["Actor<br/>authorization"] --> X
    T["Agent token<br/>(if agent-run)"] --> X
    X --> EFF["Effective capability set<br/>(what the sandbox actually grants)"]
    EFF --> SB[/"Passed into PluginInput.capabilities"/]

    classDef req fill:#eef,stroke:#557;
    classDef eff fill:#efe,stroke:#575;
    class M,B,A,T req;
    class EFF,SB eff;

The full deny-by-default set and per-capability rules: Capabilities & security.

7. The sandbox boundary

Exactly what crosses in and out of the WASM/WASI sandbox for a single execution.

flowchart LR
    subgraph HOST[" tovio-cli / tovio-forge / CI runner "]
        IN["PluginInput JSON<br/>(api_version, event, mode,<br/>capabilities, redacted payload)"]
        OUT["PluginResult JSON<br/>(status, findings,<br/>proposed_actions)"]
        TIMER["Deadline (manifest timeout_ms)<br/>+ a fuel budget derived from it"]
    end
    subgraph SB[" WASM/WASI sandbox "]
        CODE["Plugin code"]
    end
    IN -. stdin .-> CODE
    CODE -. stdout .-> OUT
    TIMER -. trap on exhaustion / kill on expiry .-> CODE

    NOFS["✗ Filesystem<br/>(no path_* at all)"]:::deny
    NONET["✗ Network<br/>(no sock_* at all;<br/>a granted network_access<br/>is refused at preflight,<br/>TVO-PLUGIN-012)"]:::deny
    NOKEYS["✗ Key material<br/>(ever)"]:::deny
    NOPT["✗ Protected plaintext<br/>(default)"]:::deny
    NOENV["✗ Ambient env secrets<br/>(environ_* report empty)"]:::deny
    NODOTVX["✗ .tovio/ internals"]:::deny

    CODE -.- NOFS
    CODE -.- NONET
    CODE -.- NOKEYS
    CODE -.- NOPT
    CODE -.- NOENV
    CODE -.- NODOTVX

    classDef deny fill:#fdd,stroke:#c33,color:#600;

8. Blocked-land recovery flow

When a required plugin blocks tovio land, this is the user's decision tree.

flowchart TD
    L["tovio land feature/deps --into main"] --> P["Required plugins run"]
    P --> R{"Any status<br/>&ne; pass/warning?"}
    R -- no --> OK([✓ Landed])
    R -- yes --> B[["✗ TVO-PLUGIN-009 / -010<br/>structured block, nothing changed"]]
    B --> WHY{"Why?"}
    WHY -->|fail with a finding| FIX["Fix the finding<br/>code + remediation"]
    FIX --> L
    WHY -->|error| DOC["Read the plugin's own summary<br/>then tovio plugin doctor"]
    DOC --> LIFT
    WHY -->|timeout TVO-PLUGIN-007| RETRY["Retry once,<br/>then raise timeout_ms"]
    RETRY --> L
    WHY -->|unavailable TVO-PLUGIN-010| REINST["Reinstall / update, or rebuild<br/>tovio with --features plugins-wasm"]
    REINST --> L
    WHY -->|integrity TVO-PLUGIN-003| STOP[["Do NOT work around it.<br/>Report to the publisher."]]
    WHY -->|capability denied TVO-PLUGIN-005| ADJ["Adjust binding or manifest"]
    ADJ --> L
    LIFT["Owner lifts the LOCAL binding:<br/>tovio plugin unbind &lt;id&gt; --event pre-land<br/>or re-bind it --mode advisory"]
    LIFT --> L

    classDef stop fill:#fdd,stroke:#c33,color:#600;
    class STOP,B stop;

There is no override flag on tovio land or tovio commit. A local enforcing binding is lifted by dropping it or re-binding it advisory — and the block that preceded that is already a signed PluginBlocked entry, so the sequence stays visible to anyone who verifies the chain. A Forge-side required check is a forge-admin's change, not a client-side flag.

Full catalog: Errors. Lifting a binding: Use a plugin.

9. Audit write path (enforcing blocks)

An enforcing execution that blocks produces a tamper-evident audit record. A pass or a non-blocking warning writes nothing — the entry exists to record the block. The plugin never writes to the audit log directly; the host does, from hashes and capability names, never from plugin content.

sequenceDiagram
    autonumber
    participant Host as tovio-cli runner
    participant SB as Sandbox
    participant Audit as Signed audit log
    participant Verify as tovio audit verify

    Host->>SB: PluginInput (with the effective grant)
    SB-->>Host: PluginResult
    Host->>Host: decision(mode, required, status, touches_protected)
    Host->>Host: Block ⇒ compute input_hash, output_hash
    Host->>Audit: PluginBlocked {plugin_version, plugin_mode, plugin_status,<br/>plugin_target, plugin_finding_count, binding_id,<br/>input_hash, output_hash, plugin_sandbox_runtime,<br/>plugin_granted_capabilities, plugin_touches_protected}
    Audit-->>Audit: Sign + chain to this identity's previous entry
    Verify->>Audit: tovio audit verify
    Audit-->>Verify: OK (or tamper-evident failure)

A Simple repo has no signing identity and so no chain: the block still gates the operation, but nothing is written.

10. Local, CI, and Forge parity

The same manifest + same pinned version + same input yields the same result across all three runtime hosts. Forge decides whether to trust a local result via attestation or server-side execution.

flowchart LR
    subgraph DEV[" Developer laptop "]
        LC["tovio-cli<br/>local sandbox"]
    end
    subgraph CI[" CI runner "]
        CC["tovio-cli<br/>runner sandbox"]
    end
    subgraph FORGE[" Forge "]
        FS["tovio-forge<br/>server-side sandbox"]
    end

    PLUG(["Same plugin @ pinned version<br/>Same input schema"]) --> LC
    PLUG --> CC
    PLUG --> FS

    LC --> LR["Result (local)"]
    CC --> CR["Result (CI, attestable)"]
    FS --> FR["Result (server, trusted)"]

    LR --> TRUST{"Trusted as required gate?"}
    CR --> TRUST
    FR --> TRUST
    TRUST -- server-side --> YES([Accepted])
    TRUST -- attestation-approved --> YES
    TRUST -- local only --> NO([Advisory only])

11. Agents running plugins

When an agent triggers a plugin (via MCP), the agent's capability token further constrains the effective capability set — the intersection now has four inputs, and the token is one of them.

sequenceDiagram
    autonumber
    actor Human
    participant CLI as tovio-cli
    participant Agent as MCP agent
    participant MCP as tovio-mcp-server (stdio)
    participant Core as tovio-core
    participant SB as Plugin sandbox

    Human->>CLI: tovio agent new … (issue token)
    Human-->>Agent: token
    Agent->>MCP: call tool (e.g. propose_land)
    MCP->>Core: validate token vs. request
    Core->>Core: intersect: manifest ∩ binding ∩ actor ∩ token
    Core-->>SB: PluginInput with narrowed capabilities
    SB-->>Core: PluginResult
    Core-->>MCP: structured status + findings
    MCP-->>Agent: typed response (agent branches on code)

See Agents → capability tokens and MCP server.

12. tovio resolve --ai — resolver plugin flow

The conflict-resolver seam is the same plugin framework, in resolver mode.

flowchart TD
    C["Conflict detected"] --> AI["tovio resolve --ai"]
    AI --> BOUND{"Resolver plugin<br/>bound to<br/>resolve-requested?"}
    BOUND -- no --> NORES[["Unchanged conflict<br/>+ no-resolver typed error<br/>(REQ-PLUGIN-079)"]]
    BOUND -- yes --> RUN["Sandboxed execution"]
    RUN --> PROP{"Proposed Resolution?"}
    PROP -- no --> KEEP["Leave conflict for human"]
    PROP -- yes --> ACCEPT{"Actor accepts?"}
    ACCEPT -- no --> KEEP
    ACCEPT -- yes --> APPLY["Apply via normal<br/>Resolution path<br/>(REQ-PLUGIN-080)"]
    APPLY --> DONE([✓ Conflict resolved])

See Concepts → conflicts and everyday → resolve conflicts.

13. What a result's proposed actions do

A result's status is what gates (diagram 5), and that path is the same for every plugin type. What differs is whether anything consumes the result's proposed_actions, and today exactly three kinds are consumed.

flowchart LR
    R[PluginResult] --> ST["status → the decision ladder<br/>same for every type"]
    R --> PAS{"proposed_actions kind"}
    PAS -->|protect-path| PP["Offer encrypt-in-place<br/>to the human<br/>never auto-applied"]
    PAS -->|resolution| RS{"Is the plugin<br/>type = resolver?"}
    RS -- no --> DROP["Ignored — a non-resolver<br/>cannot smuggle a resolution"]
    RS -- yes --> APPLY["Apply via the normal<br/>Resolution path"]
    PAS -->|transcript-turns| TT["Used to build the<br/>commit's session record<br/>an answer, not a gate"]
    PAS -->|anything else| IGN["Recorded in the result,<br/>consumed by nothing"]

transform, policy-adapter, and notifier are valid manifest types and their capability requests are validated, intersected, and audited like any other — but no host applies a transform, routes a policy-adapter's verdict, or delivers a notifier's output yet. Declare one and it runs and reports; only its status has an effect.

Where to go next

Last reviewed September 9, 2026

Suggest an improvement to this page Not for security reports — see disclosure