Skip to content

feat(opencode): add optional runtime overlay#3761

Open
Danigm-dev wants to merge 33 commits intosimstudioai:stagingfrom
Danigm-dev:feat/opencode-optional-runtime
Open

feat(opencode): add optional runtime overlay#3761
Danigm-dev wants to merge 33 commits intosimstudioai:stagingfrom
Danigm-dev:feat/opencode-optional-runtime

Conversation

@Danigm-dev
Copy link
Copy Markdown
Contributor

@Danigm-dev Danigm-dev commented Mar 25, 2026

Summary

Adds OpenCode as an optional Sim capability without changing the default Sim deployment path or base UX.

This PR introduces:

  • the OpenCode workflow block, tools, API routes, and lib/opencode client/service wiring
  • async dropdown/combobox support needed by the OpenCode block selectors
  • an optional OpenCode runtime with local/prod Docker Compose overlays
  • minimal docs and env examples for opt-in setup
  • a configurable OPENCODE_REPOSITORY_ROOT contract so Sim can also target compatible external OpenCode deployments

The default Sim experience remains unchanged:

  • docker-compose.local.yml is unchanged
  • docker-compose.prod.yml is unchanged
  • the OpenCode block stays hidden unless NEXT_PUBLIC_OPENCODE_ENABLED=true

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • Other: ___________

Testing

  • timeout 90s bunx vitest run blocks/blocks.test.ts from apps/sim
  • bunx vitest run lib/opencode/service.test.ts app/api/opencode/repos/route.test.ts app/api/tools/opencode/prompt/route.test.ts from apps/sim
  • docker compose -f docker-compose.local.yml -f docker-compose.opencode.local.yml config
  • env OPENCODE_SERVER_PASSWORD=change-me docker compose -f docker-compose.prod.yml -f docker-compose.opencode.yml config
  • Manual validation:
    • Sim only: base UX unchanged and OpenCode hidden by default
    • Sim + OpenCode local overlay: OpenCode service starts, healthcheck passes, repo catalog loads, and prompts execute from the OpenCode block

Reviewer focus:

  • Confirm the default Sim path is unchanged when OpenCode is not enabled
  • Review the OpenCode runtime contract and OPENCODE_REPOSITORY_ROOT handling for external deployments
  • Review shared dropdown/combobox async-loading changes for regressions

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

Screenshots/Videos

Not included. The OpenCode block is hidden by default and only appears when NEXT_PUBLIC_OPENCODE_ENABLED=true. Manual validation covered both states and a successful prompt execution flow.

@cursor
Copy link
Copy Markdown

cursor bot commented Mar 25, 2026

PR Summary

Medium Risk
Adds a new OpenCode integration path (API routes, internal tool endpoints, and Docker runtime overlays) plus changes to shared async option-loading UI components, which could affect workflow execution and editor dropdown behavior. Risk is mitigated by the feature flag NEXT_PUBLIC_OPENCODE_ENABLED keeping the default UX/deploy path unchanged unless explicitly enabled.

Overview
Adds an opt-in OpenCode integration behind NEXT_PUBLIC_OPENCODE_ENABLED, including a new opencode workflow block with async selectors (repo/provider/model/agent) and corresponding tool registrations for prompting, listing repos, and fetching thread messages.

Introduces new server routes for OpenCode metadata (/api/opencode/*) with session/internal auth + workspace access checks, plus internal-only tool endpoints (/api/tools/opencode/*) that manage OpenCode threads, reuse/store per-workflow session IDs in memory, and retry stale sessions when needed.

Bundles a new OpenCode client/service layer (backed by @opencode-ai/sdk) with safer route error mapping, adds NEXT_PUBLIC_OPENCODE_ENABLED to env schema, updates Next/Vitest aliasing for the SDK, and ships optional Docker Compose overlays + Dockerfile/docs for running the OpenCode runtime locally or in production without changing the base compose files.

Separately hardens shared Dropdown/ComboBox async option fetching (optional options, de-duped fetch attempts, in-flight/version tracking, dependency-change resets) to support the new OpenCode selectors and reduce race conditions.

Written by Cursor Bugbot for commit 9bb48d0. This will update automatically on new commits. Configure here.

@vercel
Copy link
Copy Markdown

vercel bot commented Mar 25, 2026

@Danigm-dev is attempting to deploy a commit to the Sim Team on Vercel.

A member of the Team first needs to authorize it.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 25, 2026

Greptile Summary

This PR introduces an optional OpenCode workflow block and its supporting runtime (API routes, service layer, Docker Compose overlays, and a container image) without touching the default Sim deployment path. The feature is correctly gated behind NEXT_PUBLIC_OPENCODE_ENABLED=true, the base compose files are untouched, and all new routes carry proper auth and workspace-access checks. The async dropdown/combobox enhancements (hasAttemptedOptionsFetch flag, in-flight ref, dependency reset) are clean and non-breaking.

Key findings from the review:

  • Security – credential leak in git-askpass.sh: When GIT_TOKEN is not set, GITHUB_TOKEN is returned as the git password for any host, not just github.com. This can expose a GitHub PAT to arbitrary non-GitHub HTTPS git servers.
  • Logic – overly broad session-retry trigger: shouldRetryWithFreshOpenCodeSession checks normalized.includes('session'), which matches errors like "session limit exceeded" and causes unnecessary new-session creation. The existing 'not found' and 'does not exist' checks already cover the intended cases.
  • Reliability – unpinned opencode-ai in Dockerfile: npm install -g opencode-ai with no version produces non-deterministic builds and risks API mismatches with the pinned @opencode-ai/sdk@0.8.0 in package.json.
  • Performance – existsSync('/.dockerenv') per request: This filesystem call happens on every createOpenCodeClient() invocation and should be cached at module load time.

Confidence Score: 3/5

  • Safe to merge for default Sim deployments (OpenCode is fully opt-in), but the OpenCode runtime path has a credential-leak risk and non-deterministic builds that should be fixed before any production OpenCode rollout.
  • Three targeted P1 fixes are needed before this is production-ready for the OpenCode path: the git-askpass credential leak, the overly broad session-retry check, and the unpinned Docker image version. None of these affect the default Sim experience, so the base product is unimpacted. Once those three are addressed, the PR is on a clear path to merge.
  • docker/opencode/git-askpass.sh (credential leak), apps/sim/lib/opencode/service.ts (retry logic), docker/opencode.Dockerfile (version pin)

Important Files Changed

Filename Overview
apps/sim/lib/opencode/service.ts Core OpenCode service layer — session management, repository resolution, and DB persistence. Contains an overly broad shouldRetryWithFreshOpenCodeSession check (normalized.includes('session')) that triggers unnecessary retries on unrelated errors.
docker/opencode/git-askpass.sh Git credential helper for the OpenCode container. Has a credential leak: GITHUB_TOKEN is returned as a fallback password for non-GitHub hosts when GIT_TOKEN is unset, potentially exposing the token to arbitrary servers.
docker/opencode.Dockerfile Container definition for the optional OpenCode runtime. Uses npm install -g opencode-ai without a version pin, making builds non-deterministic and potentially incompatible with the pinned @opencode-ai/sdk@0.8.0.
apps/sim/lib/opencode/client.ts OpenCode HTTP client factory. Calls existsSync('/.dockerenv') on every client creation; should be cached at module level. Otherwise clean credential/URL handling.
apps/sim/app/api/tools/opencode/prompt/route.ts Main prompt execution route — handles session reuse, retry logic, and soft-error responses. The retry strategy delegates to shouldRetryWithFreshOpenCodeSession (which has a separate issue). Route structure and Zod validation are solid.
apps/sim/blocks/blocks/opencode.ts Block definition with async option fetching for repository, provider, model, and agent dropdowns. Correctly gated behind NEXT_PUBLIC_OPENCODE_ENABLED. Dynamic imports from stores avoid circular dependencies.
docker/opencode/entrypoint.sh Container entrypoint — writes runtime env with restricted permissions (umask 077), generates OpenCode config with deny-by-default permissions, installs cron for repo sync, and starts the server as a non-root user via gosu. Security posture is good.
docker-compose.opencode.yml Production overlay for the OpenCode service. Uses expose (not ports) to keep the service internal to the Docker network, requires OPENCODE_SERVER_PASSWORD, and gates simstudio on a healthy OpenCode service.

Sequence Diagram

sequenceDiagram
    participant UI as OpenCode Block (UI)
    participant API as /api/opencode/* (Next.js)
    participant Svc as lib/opencode/service
    participant OC as OpenCode Server
    participant DB as Sim DB (memory table)

    UI->>API: GET /api/opencode/repos?workspaceId=…
    API->>Svc: listOpenCodeRepositories()
    Svc->>OC: client.project.list()
    OC-->>Svc: Project[]
    Svc-->>API: OpenCodeRepositoryOption[]
    API-->>UI: { data: [...] }

    UI->>API: GET /api/opencode/providers?workspaceId=…&repository=…
    API->>Svc: listOpenCodeProviders(repository)
    Svc->>OC: client.config.providers({ directory })
    OC-->>Svc: Provider[]
    Svc-->>API: OpenCodeProviderOption[]
    API-->>UI: { data: [...] }

    Note over UI,DB: Workflow execution (tool call)
    UI->>API: POST /api/tools/opencode/prompt
    API->>Svc: resolveOpenCodeRepositoryOption(repo)
    API->>DB: getStoredOpenCodeSession(wsId, memKey)
    DB-->>API: null (no stored session)
    API->>OC: createOpenCodeSession(repo, title)
    OC-->>API: { id: sessionId }
    API->>OC: promptOpenCodeSession(…)
    OC-->>API: { content, threadId, cost }
    API->>DB: storeOpenCodeSession(wsId, memKey, session)
    API-->>UI: { success: true, output: { content, threadId, cost } }
Loading

Comments Outside Diff (1)

  1. apps/sim/lib/opencode/service.ts, line 2431-2436 (link)

    P1 Overly broad session retry trigger

    normalized.includes('session') will match any error message that contains the word "session" — for example "session limit exceeded", "rate limit per session", or "session token invalid". This causes the route to spin up a brand-new OpenCode session unnecessarily, wasting resources and likely failing again with the same error.

    The intent appears to be detecting stale / not-found sessions. The checks for '404', 'not found', and 'does not exist' already cover the common cases. The standalone 'session' check should be removed or replaced with a more specific pattern:

Reviews (1): Last reviewed commit: "test(opencode): cover route contracts" | Re-trigger Greptile

@Danigm-dev
Copy link
Copy Markdown
Contributor Author

Addressed the actionable Greptile findings in the latest branch state.

  • shouldRetryWithFreshOpenCodeSession() was already narrowed in 6160d1c16, so the broad includes("session") case is no longer present.
  • docker/opencode/git-askpass.sh no longer falls back to GITHUB_TOKEN for non-GitHub hosts.
  • docker/opencode.Dockerfile now pins opencode-ai@0.8.0 to keep the runtime aligned with the pinned SDK version.
  • apps/sim/lib/opencode/client.ts now caches Docker runtime detection at module load instead of checking /.dockerenv on every client creation.

Validation run after these changes:

  • bunx vitest run lib/opencode/service.test.ts app/api/opencode/repos/route.test.ts app/api/tools/opencode/prompt/route.test.ts
  • manual shell check for docker/opencode/git-askpass.sh host-specific token behavior

Latest fix commit: 8ec0c5aea.

Copy link
Copy Markdown

@cursor cursor bot left a comment

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 and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

(open: boolean) => {
if (open) {
void fetchOptionsIfNeeded()
void fetchOptionsIfNeeded(true)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Every dropdown open triggers unnecessary API refetch

Medium Severity

handleOpenChange now calls fetchOptionsIfNeeded(true) with force=true every time any async-loading dropdown or combobox is opened. This bypasses the hasAttemptedOptionsFetchRef guard, causing a full API refetch even when options were already successfully loaded. The intent was to enable retry after fetch errors, but the fix also triggers redundant refetches on every open for all async dropdowns app-wide (including the existing Agent block selectors), causing unnecessary network traffic and loading-spinner flicker.

Additional Locations (1)
Fix in Cursor Fix in Web

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.

1 participant