feat: support OpenCode 2 and scrub secret values from tool output

- Default export is { id, setup, server }: OpenCode 2 calls setup(ctx), OpenCode 1 (1.18.29+) calls server()
- Tools defined once in lib/tools.ts and registered for both versions (V2 with codemode: false)
- Blocking covers path args of any tool, patch/apply_patch envelopes, shell commands naming env files, grep output, and V2 permission checks
- Secret-looking values from the project's env files are replaced in every non-envsitter tool's output
- Protected files come from envsitter's path policy; example/template files stay readable
- npm run test:e2e drives the real OpenCode 1 and 2 CLIs with a scripted model
- @opencode-ai/plugin ^1.19.3, @opencode/plugin ^2.1.0 (peer >=2.0.32), envsitter ^0.1.0, TypeScript 6

BREAKING CHANGE: the named export EnvSitterGuard is now the plugin definition object; call EnvSitterGuard.server(input) instead of EnvSitterGuard(input).
This commit is contained in:
David Ibia
2026-10-20 15:10:32 +01:00
parent 2a53fe2d9c
commit 0595794385
17 changed files with 6110 additions and 971 deletions
+20 -1
View File
@@ -125,13 +125,14 @@ async function getTools(params: { directory: string; worktree: string }): Promis
project: {} as unknown as PluginInput["project"],
directory: params.directory,
worktree: params.worktree,
experimental_workspace: { register() {} },
serverUrl: new URL("http://localhost"),
$: (() => {
throw new Error("not used in tests");
}) as unknown as PluginInput["$"],
};
const hooks = (await EnvSitterGuard(pluginInput)) as unknown as {
const hooks = (await EnvSitterGuard.server(pluginInput)) as unknown as {
tool: ToolApi;
};
@@ -305,6 +306,24 @@ test("tool execution rejects paths outside worktree", async () => {
);
});
test("envsitter tools accept named env files such as api.env", async () => {
await withPepper(async () => {
const worktree = await createTmpDir();
await fs.writeFile(path.join(worktree, "api.env"), "API_TOKEN=secret-token\n");
const tools = await getTools({ directory: worktree, worktree });
const out = await tools.envsitter_keys.execute({ filePath: "api.env" });
assert.ok(!out.includes("secret-token"));
assert.deepEqual((JSON.parse(out) as { keys: string[] }).keys, ["API_TOKEN"]);
await assert.rejects(
() => tools.envsitter_keys.execute({ filePath: "src/index.ts" }),
(err: unknown) => err instanceof Error && err.message.includes("Only dotenv files are allowed"),
);
});
});
test("tool execution blocks .envsitter/pepper", async () => {
const worktree = await createTmpDir();
const tools = await getTools({ directory: worktree, worktree });