Let an AI plugin propose a resolution
resolve --ai, and what the resolver can see.
Written By Dustin
Last updated 8 minutes ago
tovio resolve <path> --aiAsks a configured resolver to propose a resolution, which is then applied exactly like --ours or --theirs. There are two different things this can call, and the difference is entirely about what they are allowed to see.
The sandboxed plugin seam
With no AI provider configured, --ai goes to a plugin bound to the resolve-requested lifecycle event:
tovio plugin install <source>tovio plugin bind <id> --event resolve-requested --mode advisoryThe event is resolve-requested. It fires only when a user, an agent, or a configured operation actually asks for plugin-assisted resolution — never on its own.
A plugin runs as sandboxed WASM under the capability model and sees what its binding grants and nothing else. Capabilities are deny-by-default, and a binding can only ever narrow what the plugin's manifest asked for — never widen it. Network access is off unless granted. Protected plaintext is not grantable by flag at all; it needs a separate approval chain.
If you want to observe conflicts rather than resolve them — logging them, opening a ticket, posting to a channel — the event you want is conflict-created, which fires once for each conflict that becomes durably reachable. It is metadata-only and cannot block anything: a failure there is a diagnostic, not a rollback.
The built-in model resolver
Set an AI provider and --ai uses the built-in resolver instead of the plugin seam. The credential is a sealed secret, provisioned first and then named — a raw API key set as a plain config value is refused, not stored:
tovio config secret set <name> --from-stdintovio config set ai.provider openaitovio config set ai.model <model>tovio config set ai.credential <name>This resolver genuinely sends the conflicting content to a third-party model, which the sandbox deliberately cannot do.
What happens before anything is sent
For a protected path, sending content off-box means decrypted plaintext leaving your machine. That is gated, fail-closed, in this order:
- Classify. A path whose policy is non-clear — or whose policy cannot be parsed — is treated as protected. The unparseable case fails safe, not open.
- Consent. An explicit per-invocation prompt on a real terminal, defaulting to no. Non-interactive runs refuse with
TVO-CONFLICT-013: a machine never auto-consents on your behalf. - Audit before egress. A signed audit entry is written and its persistence verified before a single byte reaches the model. If the disclosure cannot be audited, it is not made.
Worth being plain about: a consented disclosure is exactly the case that "your code stays on your machine" does not cover. That is why it asks every time, and why it will not ask a script.
A clear, unprotected path skips the prompt — its content is already unencrypted at rest — but the disclosure is still recorded.
What comes back
The model can keep one side verbatim, propose merged content, or decline. When it keeps a side, that side's existing stored bytes are reused — no model-authored content enters the store. When it proposes a merge on a protected path, the result is sealed under the path's policy before it is written, so decrypted plaintext never lands in the clear.
Review it
tovio difftovio undoA proposal is a resolution like any other: recorded in the op-log, reversible, and yours to reject. Read the result before landing it.
When it declines
With no resolver bound and no provider set, --ai has nothing to call and says so with TVO-CONFLICT-011. A provider whose live client is not part of your build reports the same code. A configured provider that cannot reach the model — most often an unprovisioned or expired credential — reports TVO-CONFLICT-014 and names the category, so you can tell "not set up" from "set up and failing".
In every case the conflict is left untouched and the ordinary flags still work.
See the plugins collection for installing and pinning a plugin — and verify the publisher before trusting anything with your code.