Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
568e675
feat(opencode): add custom block integration
Danigm-dev Mar 25, 2026
3146562
feat(opencode): add optional runtime overlay
Danigm-dev Mar 25, 2026
7014fa4
fix(opencode): harden external runtime contract
Danigm-dev Mar 25, 2026
33b834b
docs(opencode): add deployment checklists
Danigm-dev Mar 25, 2026
cacd46a
test(opencode): cover route contracts
Danigm-dev Mar 25, 2026
6160d1c
fix(opencode): address review feedback
Danigm-dev Mar 25, 2026
8ec0c5a
fix(opencode): harden runtime defaults
Danigm-dev Mar 25, 2026
d59f7a1
fix(opencode): narrow stale session retries
Danigm-dev Mar 25, 2026
1e174f7
fix(opencode): avoid redundant resolution and url leaks
Danigm-dev Mar 25, 2026
35fac8d
fix(opencode): clean up low severity review notes
Danigm-dev Mar 25, 2026
35949bb
fix(opencode): harden root path and retry errors
Danigm-dev Mar 25, 2026
3458868
refactor(opencode): keep base url helper private
Danigm-dev Mar 25, 2026
a27de0d
fix(editor): avoid stale open-change fetch gating
Danigm-dev Mar 25, 2026
a8fb073
fix(opencode): persist fresh retry sessions
Danigm-dev Mar 25, 2026
2bb744a
fix(opencode): tighten retry and entrypoint guards
Danigm-dev Mar 25, 2026
5ab2b5f
fix(editor): stabilize async option refetching
Danigm-dev Mar 25, 2026
d246b50
docs(opencode): add branch status summary
Danigm-dev Mar 25, 2026
f1156e9
docs(opencode): clarify branch scope and overlap
Danigm-dev Mar 25, 2026
cb57bad
docs(opencode): remove internal branch note from PR
Danigm-dev Mar 26, 2026
e4c40ae
fix opencode review follow-ups
Danigm-dev Mar 26, 2026
d543a9d
fix opencode async selector refresh
Danigm-dev Mar 26, 2026
59be14f
fix opencode async selector force default
Danigm-dev Mar 26, 2026
0ca5e24
clean up opencode async selector refs
Danigm-dev Mar 26, 2026
d77d875
guard opencode async selector stale fetches
Danigm-dev Mar 26, 2026
02bcfa0
Merge branch 'staging' into feat/opencode-optional-runtime
Danigm-dev Mar 26, 2026
e77557c
fix opencode selector stale reload and lint
Danigm-dev Mar 26, 2026
4ab5c68
fix opencode docker script permissions
Danigm-dev Mar 26, 2026
314e410
fix opencode client reuse and selector refetch
Danigm-dev Mar 26, 2026
780311e
fix opencode connectivity error classification
Danigm-dev Mar 26, 2026
7fc5621
fix opencode client refresh and selector loading
Danigm-dev Mar 26, 2026
2b76ffa
fix opencode client cleanup and prompt schema naming
Danigm-dev Mar 26, 2026
13f4b56
fix opencode client key helper reuse
Danigm-dev Mar 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 206 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,197 @@ Docker must be installed and running on your machine.

### Self-hosted: Docker Compose

For local Docker builds, use the local compose file:

```bash
git clone https://github.com/simstudioai/sim.git && cd sim
docker compose -f docker-compose.local.yml up -d --build
```

For a cloud or production-style deployment, use the published images:

```bash
git clone https://github.com/simstudioai/sim.git && cd sim
docker compose -f docker-compose.prod.yml up -d
```

Open [http://localhost:3000](http://localhost:3000)

Sim also supports local models via [Ollama](https://ollama.ai) and [vLLM](https://docs.vllm.ai/) — see the [Docker self-hosting docs](https://docs.sim.ai/self-hosting/docker) for setup details.
#### OpenCode Setup

OpenCode is opt-in. By default the `OpenCode` block stays hidden so the base Sim UX and deployment path remain unchanged.

Quick deploy paths:

##### Sim only

- Do not set `NEXT_PUBLIC_OPENCODE_ENABLED`.
- Do not set any `OPENCODE_*` variables.
- Do not use `docker-compose.opencode.yml` or `docker-compose.opencode.local.yml`.
- Start Sim with the normal upstream flow.

##### Sim + OpenCode local overlay

- Set `NEXT_PUBLIC_OPENCODE_ENABLED=true`.
- Set `OPENCODE_SERVER_USERNAME` and `OPENCODE_SERVER_PASSWORD`.
- Set `OPENCODE_REPOSITORY_ROOT=/app/repos` unless you intentionally changed the runtime root.
- Set `OPENCODE_REPOS` to one or more HTTPS repository URLs.
- Set at least one provider key such as `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, or `GOOGLE_GENERATIVE_AI_API_KEY`.
- Set `GIT_USERNAME` and `GIT_TOKEN` or `GITHUB_TOKEN` if any repository is private.
- For host-side `next dev`, also set `OPENCODE_BASE_URL=http://127.0.0.1:4096`.
- Start with `docker compose -f docker-compose.local.yml -f docker-compose.opencode.local.yml up -d --build`.

##### Sim + external OpenCode runtime

- Set `NEXT_PUBLIC_OPENCODE_ENABLED=true`.
- Set `OPENCODE_BASE_URL` to the external OpenCode server.
- Set `OPENCODE_SERVER_USERNAME` and `OPENCODE_SERVER_PASSWORD` to the credentials expected by that server.
- Set `OPENCODE_REPOSITORY_ROOT` to the same worktree root used by the external OpenCode deployment.
- Set `OPENCODE_REPOS` to the repository catalog you expect the runtime to clone or expose.
- Ensure the external runtime already has at least one provider key configured.
- Ensure the external runtime can clone private repositories with the right git credentials if needed.
- Verify `/global/health` and one real prompt before exposing the block to users.

Minimum setup:

```bash
cp apps/sim/.env.example apps/sim/.env
```

Then add these values to `apps/sim/.env`:

```env
NEXT_PUBLIC_OPENCODE_ENABLED=true
OPENCODE_REPOSITORY_ROOT=/app/repos
OPENCODE_SERVER_USERNAME=opencode
OPENCODE_SERVER_PASSWORD=change-me
OPENCODE_REPOS=https://github.com/octocat/Hello-World.git

# Pick at least one provider key that OpenCode can use
GEMINI_API_KEY=your-gemini-key
# or OPENAI_API_KEY=...
# or ANTHROPIC_API_KEY=...
```

If you want private repositories:

```env
# Generic HTTPS or Azure Repos
GIT_USERNAME=your-user-or-email
GIT_TOKEN=your-token-or-pat

# Optional GitHub-only fallback
GITHUB_TOKEN=your-github-token
```

Important:

- The `OpenCode` block remains hidden unless `NEXT_PUBLIC_OPENCODE_ENABLED=true` is set on the Sim app.
- `docker compose` reads environment from the shell, not from `apps/sim/.env` automatically.
- If you want the app and the OpenCode runtime to use the same credentials, load that file before starting compose:

```bash
set -a
source apps/sim/.env
set +a
docker compose -f docker-compose.local.yml -f docker-compose.opencode.local.yml up -d --build
```

Local vs production behavior:

- `docker-compose.local.yml`
- remains unchanged
- `docker-compose.opencode.local.yml`
- adds OpenCode locally without changing the base local compose file
- publishes `OPENCODE_PORT` to the host so `next dev` on the host can talk to OpenCode
- defaults `OPENCODE_REPOSITORY_ROOT=/app/repos`
- defaults `OPENCODE_SERVER_USERNAME=opencode`
- defaults `OPENCODE_SERVER_PASSWORD=dev-opencode-password` if you do not set one explicitly
- `docker-compose.prod.yml`
- contains the upstream-style base deployment only
- `docker-compose.opencode.yml`
- adds the `opencode` service as a production overlay
- builds the OpenCode runtime locally from this repository instead of requiring an official Sim-hosted image
- injects the required `NEXT_PUBLIC_OPENCODE_ENABLED` and `OPENCODE_*` variables into `simstudio`
- keeps OpenCode internal to the Docker network with `expose`, not a published host port
- defaults `OPENCODE_REPOSITORY_ROOT=/app/repos`
- requires `OPENCODE_SERVER_PASSWORD` to be set explicitly before `docker compose` starts

Production deploy command:

```bash
docker compose -f docker-compose.prod.yml -f docker-compose.opencode.yml up -d --build
```

For local hot reload with `next dev` on the host, also set this in `apps/sim/.env`:

```env
OPENCODE_BASE_URL=http://127.0.0.1:4096
```

Then start only the optional OpenCode runtime:

```bash
docker compose -f docker-compose.local.yml -f docker-compose.opencode.local.yml up -d --build opencode
```

Without that override, host-side Next.js cannot reliably reach the Docker service alias.

Notes:

- If `OPENCODE_REPOS` is empty, `opencode` still starts but no repositories are cloned.
- Repositories are cloned into `${OPENCODE_REPOSITORY_ROOT:-/app/repos}/<repo-name>`.
- Private Azure Repos must use `https` plus `GIT_USERNAME` and `GIT_TOKEN`; the container will not prompt interactively for passwords.
- `GOOGLE_GENERATIVE_AI_API_KEY` is optional; the optional overlays map it automatically from `GEMINI_API_KEY` if not set.
- If you prefer to run OpenCode in separate infrastructure, skip the overlays and point Sim at that deployment with `OPENCODE_BASE_URL`, `OPENCODE_SERVER_USERNAME`, `OPENCODE_SERVER_PASSWORD`, and the matching `OPENCODE_REPOSITORY_ROOT`.

Basic verification after startup:

```bash
curl -u "opencode:change-me" http://127.0.0.1:4096/global/health
```

If you changed the username, password, or port, use those values instead.

See [`docker/opencode/README.md`](docker/opencode/README.md) for service-specific verification steps and runtime behavior.

#### Using Local Models with Ollama

Run Sim with local AI models using [Ollama](https://ollama.ai) - no external APIs required:

```bash
# Start with GPU support (automatically downloads gemma3:4b model)
docker compose -f docker-compose.ollama.yml --profile setup up -d

# For CPU-only systems:
docker compose -f docker-compose.ollama.yml --profile cpu --profile setup up -d
```

Wait for the model to download, then visit [http://localhost:3000](http://localhost:3000). Add more models with:
```bash
docker compose -f docker-compose.ollama.yml exec ollama ollama pull llama3.1:8b
```

#### Using an External Ollama Instance

If Ollama is running on your host machine, use `host.docker.internal` instead of `localhost`:

```bash
OLLAMA_URL=http://host.docker.internal:11434 docker compose -f docker-compose.prod.yml up -d
```

On Linux, use your host's IP address or add `extra_hosts: ["host.docker.internal:host-gateway"]` to the compose file.

#### Using vLLM

Sim supports [vLLM](https://docs.vllm.ai/) for self-hosted models. Set `VLLM_BASE_URL` and optionally `VLLM_API_KEY` in your environment.

### Self-hosted: Dev Containers

1. Open VS Code with the [Remote - Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
2. Open the project and click "Reopen in Container" when prompted
3. Run `bun run dev:full` in the terminal or use the `sim-start` alias
- This starts both the main application and the realtime socket server

### Self-hosted: Manual Setup

Expand Down Expand Up @@ -104,6 +287,26 @@ cp packages/db/.env.example packages/db/.env
# Edit both .env files to set DATABASE_URL="postgresql://postgres:your_password@localhost:5432/simstudio"
```

If you want to use the OpenCode workflow block while running `next dev` on the host, also set these in `apps/sim/.env`:

```env
NEXT_PUBLIC_OPENCODE_ENABLED=true
OPENCODE_BASE_URL=http://127.0.0.1:4096
OPENCODE_SERVER_USERNAME=opencode
OPENCODE_SERVER_PASSWORD=change-me
OPENCODE_REPOS=https://github.com/octocat/Hello-World.git
GEMINI_API_KEY=your-gemini-key
```

Then export the same environment before starting the OpenCode container so the app and Docker use identical credentials:

```bash
set -a
source apps/sim/.env
set +a
docker compose -f docker-compose.local.yml -f docker-compose.opencode.local.yml up -d --build opencode
```

4. Run migrations:

```bash
Expand All @@ -114,9 +317,10 @@ cd packages/db && bunx drizzle-kit migrate --config=./drizzle.config.ts

```bash
bun run dev:full # Starts both Next.js app and realtime socket server
bun run dev:full:webpack # Same, but using Webpack instead of Turbopack
```

Or run separately: `bun run dev` (Next.js) and `cd apps/sim && bun run dev:sockets` (realtime).
Or run separately: `bun run dev` (Next.js/Turbopack), `cd apps/sim && bun run dev:webpack` (Next.js/Webpack), and `cd apps/sim && bun run dev:sockets` (realtime).

## Copilot API Keys

Expand Down
20 changes: 20 additions & 0 deletions apps/sim/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,26 @@ API_ENCRYPTION_KEY=your_api_encryption_key # Use `openssl rand -hex 32` to gener
# VLLM_BASE_URL=http://localhost:8000 # Base URL for your self-hosted vLLM (OpenAI-compatible)
# VLLM_API_KEY= # Optional bearer token if your vLLM instance requires auth

# Internal OpenCode Service (Optional, opt-in)
# NEXT_PUBLIC_OPENCODE_ENABLED=true # Required to show the OpenCode block in the UI
# # Leave unset to keep the block hidden and preserve the default Sim UX
# OPENCODE_BASE_URL=http://127.0.0.1:4096 # Use this when SIM runs on the host (for example, `bun run dev`) and OpenCode runs in Docker
# OPENCODE_BASE_URL=http://opencode:4096 # Use this when SIM and OpenCode both run in Docker Compose
# # Or point this to any separate OpenCode deployment that implements the same auth contract
# OPENCODE_PORT=4096
# OPENCODE_REPOSITORY_ROOT=/app/repos # Must match the repository root used by the OpenCode runtime, including external deployments
# OPENCODE_SERVER_USERNAME=opencode
# OPENCODE_SERVER_PASSWORD=change-me # Required for the internal OpenCode service
# OPENCODE_REPOS=https://github.com/org/ui-components,https://github.com/org/design-tokens
# OPENCODE_REPOS=https://dev.azure.com/org/project/_git/repo # Azure Repos over HTTPS also works
# GIT_USERNAME= # Optional HTTPS git username for private repos, including Azure Repos
# GIT_TOKEN= # Optional HTTPS git token/PAT for private repos, including Azure Repos
# GITHUB_TOKEN= # Optional GitHub token fallback for private GitHub repos
# OPENAI_API_KEY= # OpenCode can use any supported provider key from the environment
# ANTHROPIC_API_KEY= # Optional if you prefer Anthropic for OpenCode
# GEMINI_API_KEY= # Optional if you prefer Gemini for OpenCode
# GOOGLE_GENERATIVE_AI_API_KEY= # Optional explicit alias for OpenCode's Google provider; defaults from GEMINI_API_KEY in the optional compose overlays

# Admin API (Optional - for self-hosted GitOps)
# ADMIN_API_KEY= # Use `openssl rand -hex 32` to generate. Enables admin API for workflow export/import.
# Usage: curl -H "x-admin-key: your_key" https://your-instance/api/v1/admin/workspaces
50 changes: 50 additions & 0 deletions apps/sim/app/api/opencode/agents/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { getOpenCodeRouteError } from '@/lib/opencode/errors'
import { listOpenCodeAgents } from '@/lib/opencode/service'
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'

const logger = createLogger('OpenCodeAgentsAPI')

export const dynamic = 'force-dynamic'
export const runtime = 'nodejs'

export async function GET(request: NextRequest) {
const requestId = generateRequestId()

try {
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized OpenCode agents access attempt`)
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

const workspaceId = request.nextUrl.searchParams.get('workspaceId')
if (!workspaceId) {
return NextResponse.json({ error: 'workspaceId is required' }, { status: 400 })
}

const access = await checkWorkspaceAccess(workspaceId, authResult.userId)
if (!access.exists) {
return NextResponse.json({ error: 'Workspace not found' }, { status: 404 })
}

if (!access.hasAccess) {
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
}

const repository = request.nextUrl.searchParams.get('repository') || undefined
const agents = await listOpenCodeAgents(repository)
return NextResponse.json({ data: agents })
} catch (error) {
const routeError = getOpenCodeRouteError(error, 'agents')
logger.error(`[${requestId}] Failed to fetch OpenCode agents`, {
error,
status: routeError.status,
responseMessage: routeError.message,
})
return NextResponse.json({ error: routeError.message }, { status: routeError.status })
}
}
55 changes: 55 additions & 0 deletions apps/sim/app/api/opencode/models/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { getOpenCodeRouteError } from '@/lib/opencode/errors'
import { listOpenCodeModels } from '@/lib/opencode/service'
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'

const logger = createLogger('OpenCodeModelsAPI')

export const dynamic = 'force-dynamic'
export const runtime = 'nodejs'

export async function GET(request: NextRequest) {
const requestId = generateRequestId()

try {
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
if (!authResult.success || !authResult.userId) {
logger.warn(`[${requestId}] Unauthorized OpenCode models access attempt`)
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

const workspaceId = request.nextUrl.searchParams.get('workspaceId')
if (!workspaceId) {
return NextResponse.json({ error: 'workspaceId is required' }, { status: 400 })
}

const providerId = request.nextUrl.searchParams.get('providerId')
if (!providerId) {
return NextResponse.json({ error: 'providerId is required' }, { status: 400 })
}

const access = await checkWorkspaceAccess(workspaceId, authResult.userId)
if (!access.exists) {
return NextResponse.json({ error: 'Workspace not found' }, { status: 404 })
}

if (!access.hasAccess) {
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
}

const repository = request.nextUrl.searchParams.get('repository') || undefined
const models = await listOpenCodeModels(providerId, repository)
return NextResponse.json({ data: models })
} catch (error) {
const routeError = getOpenCodeRouteError(error, 'models')
logger.error(`[${requestId}] Failed to fetch OpenCode models`, {
error,
status: routeError.status,
responseMessage: routeError.message,
})
return NextResponse.json({ error: routeError.message }, { status: routeError.status })
}
}
Loading
Loading