Skip to content

feat(opencode-agents): sandbox loopback Python tool callables - #232

Closed
apucacao wants to merge 1 commit into
mainfrom
cursor/opencode-tool-sandbox-2960
Closed

apucacao wants to merge 1 commit into
mainfrom
cursor/opencode-tool-sandbox-2960

Conversation

@apucacao

Copy link
Copy Markdown
Contributor

Summary

  • Adds Sandbox, SandboxError, and invoke_tool_callable to launchdarkly_ai_server for spawned-process tool execution (picklable, module-scope callables only).
  • OpenCode loopback MCP bridge (tool_bridge._invoke) always runs plain Python tool callables in that sandbox. $ld:ai:tool_call tracking still fires in the parent via _ld_tool_track / _ld_tool_target metadata on tracked wrappers.
  • wrap_tool_handlers treats Sandbox(fn) as opt-in sandboxing on other handlers (track in parent, run fn in a child).

Sandboxed

  • Plain Python functions registered on the per-call ToolBridge (AI Config tool key → callable in tool_handlers, not NativeTool).

Explicitly not sandboxed

  • NativeTool entries (OpenCode built-ins and observability MCP tools wired as NativeTool("observability_*")) — OpenCode runs those inside opencode serve; this PR does not touch them.
  • Tool callables on non-OpenCode handlers unless the author wraps with Sandbox(fn).

Tests

  • packages/client/tests/test_tool_sandbox.py — spawn isolation + pickling errors
  • packages/opencode-agents/tests/test_tool_bridge.py — bridge MCP tools/call path
  • Updated bridge e2e in test_tool_contract.py
Open in Web Open in Cursor 

Run every plain callable served through the OpenCode MCP tool bridge in a
spawned child process via invoke_tool_callable, while emitting tool_call
tracking in the parent from the tracked wrapper metadata.

Add Sandbox and SandboxError to launchdarkly_ai_server for opt-in sandboxing
on other handlers; wrap_tool_handlers keeps tracking in-process for Sandbox(fn).

NativeTool entries are unchanged and never pass through the bridge.

Co-authored-by: alexis <alexis@launchdarkly.com>
@apucacao
apucacao requested a review from a team as a code owner October 27, 2026 21:42

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ 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 b77e721. Configure here.

track()
result = await invoke_tool_callable(_sandbox_target(target), arguments)
else:
result = await invoke_tool_callable(fn, arguments)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sandbox pickles trajectory wrapper closures

High Severity

_invoke sandboxes _ld_tool_target from the tracking wrapper. execute_and_track and execute_and_stream apply TrajectoryRecorder.wrap first, so that target is a nested closure and pickling fails. OpenCode Python tools then error with a picklable-callable message even when the author defined the function at module scope.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b77e721. Configure here.

except Exception as exc:
if isinstance(exc, SandboxError):
raise
raise SandboxError(str(exc)) from exc

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sandbox timeout leaves child running

Medium Severity

After future.result times out, the ProcessPoolExecutor context still shuts down with wait=True, so a hung child is never killed and the worker thread blocks. The outer asyncio.wait_for then raises TimeoutError while that process keeps running and occupies the default executor.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b77e721. Configure here.

@apucacao apucacao closed this Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants