Skip to content

Commit e2bc035

Browse files
committed
fix(opencode): runAsk() actually executes ctx.ask, not just returns the Effect
THE bug behind the Discord report. User configured agent permission bash: "*": deny "terraform fmt *": allow "terraform validate *": allow "terraform plan *": allow "terraform apply *": ask and the agent ran shell commands not in the whitelist. With AFT bash removed and OpenCode native bash restored, the same config behaves correctly. Cause: a runtime-vs-types contract mismatch. OpenCode's ToolContext.ask signature changed across versions: - @opencode-ai/plugin@1.2.x: ask(input): Promise<void> - @opencode-ai/plugin@1.14.x: ask(input): Effect.Effect<void> (see opencode-source packages/plugin/src/tool.ts) AFT was depending on @opencode-ai/plugin@^1.2.26 (Promise types) and used `await ctx.ask(...)` everywhere. But the user's actual installed OpenCode runtime ships @opencode-ai/plugin@1.14.x where the runtime contract is Effect. Effects in @effect/io do NOT implement the thenable protocol — `await someEffect` resolves silently to the Effect object **without ever executing it**. So the deny/allow/ask evaluator never ran, the for-loop that pushes patterns into permissionsGranted continued, the bash retry sent permissions_granted=[<deny patterns>], permissions_granted_cover() in Rust returned true, and the command executed unchecked. Empirical confirmation: $ bun run -e 'import {Effect} from "effect"; const e = Effect.fail(new Error("deny")).pipe(Effect.orDie); console.log(await e); // resolves silently with the Effect object await Effect.runPromise(e); // throws "deny"' This affected EVERY ctx.ask call site in the opencode plugin — not just bash. read/write/edit/apply_patch shims and the bash permission loop all silently bypassed user permission rules. Fix: 1. Bump @opencode-ai/plugin and @opencode-ai/sdk from ^1.2.26 to ^1.14.39 so types match the runtime contract. peerDependencies updated to >=1.14.0 to make the requirement explicit. 2. Add effect@^3.18.6 as a direct dependency. 3. New shared helper packages/opencode-plugin/src/tools/permissions.ts `runAsk(maybe: unknown)` that: - duck-types Promise (.then is callable) → await directly (handles old 1.2.x runtime where ask returns Promise) - else treats as Effect → Effect.runPromise(maybe) (handles 1.14.x+ runtime where ask returns Effect) This works regardless of which runtime version the user has installed. On deny, runPromise rejects with the underlying defect (DeniedError / RejectedError) so callers can rely on try/catch as before. 4. Wrapped all 9 ctx.ask call sites in the OpenCode plugin: - tools/bash.ts:38 (the main bash permission loop — direct user repro) - tools/permissions.ts:askEditPermission (used by edit/write/apply_patch) - tools/hoisted.ts:8 sites in read/write/edit/apply_patch shims and the legacy mode:"write" path 5. Loosened aft_edit.execute return type from Promise<string> to Promise<ToolResult> (the upstream type that's now string | { output, metadata? }) since 1.14.x widened ToolResult. The OpenCode runtime handles both shapes; we delegate to the inner aftEditTool directly for the non-shim path so we shouldn't constrain it. Pi plugin is unaffected — Pi has no permission system and its own bash wrapper throws if Rust ever returns permission_required (which only happens in OpenCode mode). Verification: - 592 lib + 502 integration Rust tests - 45 aft-bridge + 380 Pi + 67 aft-cli + 679 opencode-plugin TS tests - typecheck clean against new 1.14.39 plugin types - biome lint clean - build clean (bundled 7 modules) For users with the same agent permission config, this means: - `terraform fmt ...` → allowed silently (no change) - `terraform apply ...` → user prompted (no change, this was already "ask") - `git status`, `ls`, `cat`, etc. → DENIED (was: silently executed)
1 parent 1ec8aaa commit e2bc035

5 files changed

Lines changed: 199 additions & 71 deletions

File tree

0 commit comments

Comments
 (0)