Repository navigation
Conversation
The OpenCode handler previously deleted tool_handlers and never read config["tools"], so the AI Config's tool catalog did nothing and every caller silently inherited whatever opencode.json already configured. That broke the tool-handling contract every other handler follows and gave a judge (tool_handlers=None) a full, unsandboxed OpenCode agent. The offered set is now the intersection of config["tools"] and the caller's tool_handlers, same as the other handlers. A NativeTool (see the new OpenCode* built-in sentinels) is allowed on the session by its own provider name. A plain callable is served from a per-call loopback MCP bridge the handler registers on the running opencode serve with POST /mcp and disconnects when the call ends -- there is no OpenCode route that accepts a tool definition directly on a message. The session's permission ruleset now always denies everything, then allows only the offered names, so a variation with no tools gets a model with no tools (including every judge). chat and execute_tool spans are opened and closed from the same bus events the AG-UI stream already derives from, named after the AI Config key. Nothing here has been run against a real OpenCode MCP client; the bridge's own protocol behavior is unit-tested directly, and the handler's side of the contract (ruleset, registration, teardown) is tested against the existing fake opencode server. Co-authored-by: alexis <alexis@launchdarkly.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 3e1e7c2. Configure here.
| """ | ||
| ruleset = [{"permission": "*", "action": "deny", "pattern": "*"}] | ||
| for name in offered_names: | ||
| ruleset.append({"permission": name, "action": "allow", "pattern": "*"}) |
There was a problem hiding this comment.
Edit-family tools use wrong permission
High Severity
Allow rules use each native tool's own id, but OpenCode's Permission.disabled remaps write and apply_patch to the edit permission before matching. OpenCodeWrite and OpenCodeApplyPatch therefore stay denied by the leading * rule, and OpenCodeEdit also leaves write and apply_patch enabled.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 3e1e7c2. Configure here.
| tools.clear() | ||
| for span in list(models.values()): | ||
| end_span_once(span, ended, abandoned=True) | ||
| models.clear() |
There was a problem hiding this comment.
Timeouts mark spans abandoned
Medium Severity
abandon_open_spans always sets launchdarkly.stream.abandoned and has no cancelled argument. asyncio.CancelledError skips except Exception, so a timeout around config().stream() tags open chat and execute_tool spans as abandoned instead of launchdarkly.run.cancelled.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 3e1e7c2. Configure here.
|
Heads up for review: while wiring observability's Vega runtime to this commit (git pin,
So This will affect any consumer trying to git-pin |
packages/client is still version 0.4.0. Requiring >=0.4.1 made a git pin of this commit uninstallable: Poetry and pip resolve the sibling client package as 0.4.0 and reject it. The handler is written against the client source in the same commit, so the floor matches that declared version. Co-authored-by: alexis <alexis@launchdarkly.com>
|
Follow-up on the install failure: |


What
The OpenCode handler now honors
config["tools"]andtool_handlers, matching the contract every other handler follows (claude-agents,openai-agents, ...). Previously_stream_impldiddel tool_handlersand never readconfig["tools"], so the AI Config's tool catalog did nothing and every caller — including a judge, which always calls withtool_handlers=None— silently inherited whateveropencode.jsonalready configured on the running server.Design doc (internal): "OpenCode handler: the tool contract", Option C.
How
config["tools"]and the keys present intool_handlers— same request-scoped authorization rule the JS SDK states for its own handlers. ANativeToolis allowed on the session by its own provider name. A plain callable has no route into OpenCode otherwise (the message route'stoolsfield is a deprecated boolean permission map, not a place to describe a tool), so it's served from a per-call loopback MCP bridge the handler registers on the runningopencode servewithPOST /mcpand disconnects withPOST /mcp/{name}/disconnectwhen the call ends.permissionruleset (POST /session) always starts with a deny-all rule, then allows only the offered names.Permission.mergeappends a session's ruleset after the agent's staticopencode.jsonpermission, and OpenCode resolves the last matching rule, so this always wins. An empty offered set still denies everything — a variation with no tools (or a judge) gets a model with no tools, not the host's default catalog.chatandexecute_tool(constructors already existed, unused) are now opened and closed from the same bus events the AG-UI stream already derives from — onechatper assistant message, oneexecute_toolper tool call id — named after the AI Config key, not OpenCode's own tool id or the bridge's composed MCP name.close_open_spans/abandon_open_spansgive this package the same abandon/failure split the hook-based handlers have; it's now registered intest_cross_handler_parity.py'sHOOK_BASED_TOOL_HANDLERS.OpenCodeBash,OpenCodeRead,OpenCodeEdit,OpenCodeWrite,OpenCodeGlob,OpenCodeGrep,OpenCodeList,OpenCodeTask,OpenCodeSkill,OpenCodeTodoWrite,OpenCodeWebFetch,OpenCodeWebSearch,OpenCodeApplyPatch—NativeToolsentinels for OpenCode's own built-ins, the same pattern asclaude-agents'sClaude*exports.skillis not auto-allowed; a caller that wants it listsOpenCodeSkillexplicitly.http.serverthread rather than an asyncio/ASGI stack, answering in MCP streamable-HTTP "JSON response" mode (no SSE) — this package still depends on onlyhttpxandopentelemetry-api.What this is not
No change to
claude-agents(it still leaves Claude Code's built-ins on for a tool-less judge; that inconsistency is called out inagents.mdas open, not fixed here). No new AI Config field for attaching an MCP server — a remote MCP tool is stillNativeTool("<server>_<tool>"), configured host-side.Verification
Nothing here has been run against a real OpenCode MCP client (none is available). The bridge's own JSON-RPC behavior is unit-tested directly (
test_tool_bridge.py); the handler's side of the contract — ruleset sent,POST /mcpregistration body, teardown, and one test that calls the registered bridge exactly as a real client would, using the URL/token the fakeopencode serverecorded — is tested against the existing fake (test_tool_contract.py).