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.test.ts3.8 KB
import { test } from "node:test";
import assert from "node:assert/strict";
import { alreadyApplies, repliesAutomatically, ruleset } from "./permission.ts";
/// The server applies the last matching rule, and its `*` also matches a
/// path separator: that is what makes the order of the `.env` rules the
/// difference between asking and not.
function globMatches(pattern: string, target: string): boolean {
const parts = pattern.split("*");
const first = parts.shift() ?? "";
if (!target.startsWith(first)) return false;
let rest = target.slice(first.length);
if (!parts.length) return rest === "";
const last = parts.pop() ?? "";
for (const part of parts) {
const at = rest.indexOf(part);
if (at < 0) return false;
rest = rest.slice(at + part.length);
}
return rest.length >= last.length && rest.endsWith(last);
}
function actionFor(mode: string, permission: string, target: string): string | null {
const matching = ruleset(mode).filter(
(rule) => (rule.permission === "*" || rule.permission === permission) && globMatches(rule.pattern, target),
);
return matching.at(-1)?.action ?? null;
}
test("environment files keep asking even though reads are allowed", () => {
for (const mode of ["supervised", "auto_edits"]) {
assert.equal(actionFor(mode, "read", "src/main.rs"), "allow");
assert.equal(actionFor(mode, "read", ".env"), "ask");
assert.equal(actionFor(mode, "read", ".env.local"), "ask");
assert.equal(actionFor(mode, "read", "config/service.env"), "ask");
// The example file is committed on purpose, and its rule comes after
// the two that would otherwise catch it.
assert.equal(actionFor(mode, "read", ".env.example"), "allow");
assert.equal(actionFor(mode, "read", "config/service.env.example"), "allow");
}
});
test("only edits change between supervised and auto-accept edits", () => {
assert.equal(actionFor("supervised", "edit", "src/main.rs"), "ask");
assert.equal(actionFor("auto_edits", "edit", "src/main.rs"), "allow");
for (const mode of ["supervised", "auto_edits"]) {
assert.equal(actionFor(mode, "bash", "rm -rf /"), "ask");
assert.equal(actionFor(mode, "external_directory", "/etc"), "ask");
}
const supervised = ruleset("supervised");
const autoEdits = ruleset("auto_edits");
const differing = supervised
.filter((rule, index) => JSON.stringify(rule) !== JSON.stringify(autoEdits[index]))
.map((rule) => rule.permission);
assert.deepEqual(differing, ["edit"]);
});
test("without an approval reviewer Auto is the same ruleset as Supervised", () => {
assert.deepEqual(ruleset("auto"), ruleset("supervised"));
assert.equal(repliesAutomatically("auto"), false);
});
test("full access allows everything, other folders included", () => {
assert.deepEqual(ruleset("full"), [
{ permission: "*", pattern: "*", action: "allow" },
{ permission: "external_directory", pattern: "*", action: "allow" },
]);
assert.equal(repliesAutomatically("full"), true);
});
test("a tool nobody listed falls through to the leading wildcard, the plugin tools too", () => {
assert.equal(actionFor("supervised", "convergence_lucky_number", "*"), "ask");
assert.equal(actionFor("full", "convergence_lucky_number", "*"), "allow");
});
test("rules already in force are not pushed again, because the server appends", () => {
const wanted = ruleset("supervised");
const stored = [{ permission: "bash", pattern: "*", action: "ask" }, ...wanted];
assert.equal(alreadyApplies(stored, wanted), true, "the ruleset is already the tail");
// A mode change puts other rules last, so the push has to happen.
assert.equal(alreadyApplies([...wanted, { permission: "edit", pattern: "*", action: "allow" }], wanted), false);
assert.equal(alreadyApplies(null, wanted), false, "a session with no rules needs them");
assert.equal(alreadyApplies([], wanted), false);
});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.