Official
opencode
OpenCode agent provider: runs opencode serve and talks to it over HTTP and server-sent events.
The app opens the listing; nothing installs until an agent in your Plugins workspace has read the files and you enable the plugin. In a terminal: cvg install convergence/opencode@0.2.0
Permissions in 0.2.0
Files
permission.ts3.5 KB
// The session permission ruleset Convergence pushes for a chat (the Rust
// `permission.rs`).
//
// OpenCode decides a request against the rules stored on the session, and
// a session created without rules inherits whatever the chosen agent
// happens to allow. Pushing an explicit ruleset is the only way the
// composer's permission mode means the same thing here as anywhere else.
import { permissionMode } from "../sdk/agent.ts";
export interface Rule {
permission: string;
pattern: string;
action: string;
}
const rule = (permission: string, pattern: string, action: string): Rule => ({ permission, pattern, action });
/// The ruleset for a shared permission mode. The server applies the
/// **last** matching rule, so order carries meaning: a narrow rule only
/// wins when it comes after the broad one it refines.
///
/// Auto is read as Supervised: OpenCode has no approval reviewer, so the
/// only honest reading of "let the agent approve routine actions" is to
/// keep asking. The option this plugin publishes leaves Auto out for the
/// same reason; a chat stored before that still carries the value.
export function ruleset(mode: string): Rule[] {
if (mode === permissionMode.FULL) {
// Reads outside the workspace are a permission of their own that the
// wildcard does not cover.
return [rule("*", "*", "allow"), rule("external_directory", "*", "allow")];
}
// "Auto-accept edits" promises edits without a prompt and a prompt for
// everything else, so the mode changes this one action and no other.
const edits = mode === permissionMode.AUTO_EDITS ? "allow" : "ask";
return [
rule("*", "*", "ask"),
rule("read", "*", "allow"),
// OpenCode asks before reading environment files whatever the agent
// allows, and a blanket read rule would quietly remove that.
rule("read", "*.env", "ask"),
rule("read", "*.env.*", "ask"),
rule("read", "*.env.example", "allow"),
rule("glob", "*", "allow"),
rule("grep", "*", "allow"),
rule("lsp", "*", "allow"),
rule("skill", "*", "allow"),
rule("todowrite", "*", "allow"),
rule("bash", "*", "ask"),
rule("edit", "*", edits),
rule("webfetch", "*", "ask"),
rule("websearch", "*", "ask"),
rule("codesearch", "*", "ask"),
rule("external_directory", "*", "ask"),
rule("doom_loop", "*", "ask"),
// The question tool only elicits an answer; asking to ask is noise.
rule("question", "*", "allow"),
];
}
function sameRule(a: Rule | null | undefined, b: Rule | null | undefined): boolean {
return a?.permission === b?.permission && a?.pattern === b?.pattern && a?.action === b?.action;
}
/// Whether the rules a session carries already end with `wanted`. A pushed
/// ruleset is appended to the session's rules rather than replacing them
/// (1.18.29), which is still correct because the last match wins, but
/// pushing the same rules on every resume would grow a long-lived chat's
/// list without bound.
export function alreadyApplies(current: unknown, wanted: readonly Rule[]): boolean {
if (!Array.isArray(current) || !wanted.length || current.length < wanted.length) return false;
const tail = current.slice(current.length - wanted.length);
return tail.every((entry, index) => sameRule(entry as Rule, wanted[index]));
}
/// Whether the mode answers permission requests without the user. The
/// ruleset alone is not enough: OpenCode starts subagent sessions with
/// rules of their own, so a request can still reach us in full access.
export function repliesAutomatically(mode: string): boolean {
return mode === permissionMode.FULL;
}Versions
| Version | Published | Plugin API | Size | Permissions | Status |
|---|---|---|---|---|---|
| 0.2.0latest | Oct 5, 2026 | >=2 <3 | 94.8 KB | 5 permissions | Listed |
No comments yet.