Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
38 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
9bb48d0
Merge branch 'staging' into feat/opencode-optional-runtime
Danigm-dev Apr 1, 2026
76f36ea
Merge branch 'staging' into feat/opencode-optional-runtime
Danigm-dev Apr 7, 2026
c0cd00a
fix: avoid forced async selector refetch on open
Danigm-dev Apr 7, 2026
f791d66
fix: align async selector fetch signatures
Danigm-dev Apr 7, 2026
98cae4c
fix: pass subblock id to option hydration
Danigm-dev Apr 7, 2026
ae3b633
Merge branch 'staging' into feat/opencode-optional-runtime
Danigm-dev Apr 8, 2026
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
fix(opencode): harden external runtime contract
  • Loading branch information
Danigm-dev committed Mar 25, 2026
commit 7014fa4e0512882a06f49468c5f0f158e1abc2c3
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ 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
Expand Down Expand Up @@ -134,6 +135,7 @@ Local vs production behavior:
- `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`
Expand All @@ -143,7 +145,8 @@ Local vs production behavior:
- 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
- expects `OPENCODE_SERVER_PASSWORD` to be set explicitly
- defaults `OPENCODE_REPOSITORY_ROOT=/app/repos`
- requires `OPENCODE_SERVER_PASSWORD` to be set explicitly before `docker compose` starts

Production deploy command:

Expand All @@ -168,10 +171,10 @@ Without that override, host-side Next.js cannot reliably reach the Docker servic
Notes:

- If `OPENCODE_REPOS` is empty, `opencode` still starts but no repositories are cloned.
- Repositories are cloned into `/app/repos/<repo-name>`.
- 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`, and `OPENCODE_SERVER_PASSWORD`.
- 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:

Expand Down
1 change: 1 addition & 0 deletions apps/sim/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ API_ENCRYPTION_KEY=your_api_encryption_key # Use `openssl rand -hex 32` to gener
# 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
Expand Down
25 changes: 20 additions & 5 deletions apps/sim/lib/opencode/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { and, eq, isNull } from 'drizzle-orm'
import { createOpenCodeClient } from '@/lib/opencode/client'

const logger = createLogger('OpenCodeService')
const OPEN_CODE_REPOSITORY_ROOT = '/app/repos'
const DEFAULT_OPEN_CODE_REPOSITORY_ROOT = '/app/repos'

export interface OpenCodeRepositoryOption {
id: string
Expand Down Expand Up @@ -77,6 +77,19 @@ export interface OpenCodeMessageItem {
createdAt: number
}

function getOpenCodeRepositoryRoot(): string {
const configuredRoot = process.env.OPENCODE_REPOSITORY_ROOT?.trim()
if (!configuredRoot) {
return DEFAULT_OPEN_CODE_REPOSITORY_ROOT
}

if (configuredRoot === '/') {
return configuredRoot
}

return configuredRoot.replace(/\/+$/, '')
}

function stripGitSuffix(value: string): string {
return value.endsWith('.git') ? value.slice(0, -4) : value
}
Expand Down Expand Up @@ -140,19 +153,21 @@ function listConfiguredOpenCodeRepositoryNames(): string[] {
}

function getRepositoryName(repository: string): string {
if (repository.startsWith(OPEN_CODE_REPOSITORY_ROOT)) {
return repository.slice(OPEN_CODE_REPOSITORY_ROOT.length + 1)
const repositoryRoot = getOpenCodeRepositoryRoot()

if (repository.startsWith(`${repositoryRoot}/`)) {
return repository.slice(repositoryRoot.length + 1)
}

return repository
}

function buildOpenCodeRepositoryDirectory(repository: string): string {
return `${OPEN_CODE_REPOSITORY_ROOT}/${repository}`
return `${getOpenCodeRepositoryRoot()}/${repository}`
}

function isProjectInsideRepositoryRoot(project: Project): boolean {
return project.worktree.startsWith(`${OPEN_CODE_REPOSITORY_ROOT}/`)
return project.worktree.startsWith(`${getOpenCodeRepositoryRoot()}/`)
}
Comment thread
cursor[bot] marked this conversation as resolved.

function mapProjectToRepositoryOption(project: Project): OpenCodeRepositoryOption {
Expand Down
4 changes: 3 additions & 1 deletion docker-compose.opencode.local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ services:
- '4096'
environment:
- OPENCODE_PORT=${OPENCODE_PORT:-4096}
- OPENCODE_REPOSITORY_ROOT=${OPENCODE_REPOSITORY_ROOT:-/app/repos}
- OPENCODE_SERVER_USERNAME=${OPENCODE_SERVER_USERNAME:-opencode}
- OPENCODE_SERVER_PASSWORD=${OPENCODE_SERVER_PASSWORD:-dev-opencode-password}
- OPENCODE_REPOS=${OPENCODE_REPOS:-}
Expand All @@ -21,7 +22,7 @@ services:
- GEMINI_API_KEY=${GEMINI_API_KEY:-${GEMINI_API_KEY_1:-}}
- GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY:-${GEMINI_API_KEY:-${GEMINI_API_KEY_1:-}}}
volumes:
- opencode_repos:/app/repos
- opencode_repos:${OPENCODE_REPOSITORY_ROOT:-/app/repos}
- opencode_data:/home/opencode/.local/share/opencode
healthcheck:
test: ['CMD', '/usr/local/bin/opencode-healthcheck.sh']
Expand All @@ -35,6 +36,7 @@ services:
- NEXT_PUBLIC_OPENCODE_ENABLED=${NEXT_PUBLIC_OPENCODE_ENABLED:-true}
- OPENCODE_BASE_URL=${OPENCODE_BASE_URL:-http://opencode:${OPENCODE_PORT:-4096}}
- OPENCODE_PORT=${OPENCODE_PORT:-4096}
- OPENCODE_REPOSITORY_ROOT=${OPENCODE_REPOSITORY_ROOT:-/app/repos}
- OPENCODE_SERVER_USERNAME=${OPENCODE_SERVER_USERNAME:-opencode}
- OPENCODE_SERVER_PASSWORD=${OPENCODE_SERVER_PASSWORD:-dev-opencode-password}
- OPENCODE_REPOS=${OPENCODE_REPOS:-}
Expand Down
8 changes: 5 additions & 3 deletions docker-compose.opencode.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ services:
- '4096'
environment:
- OPENCODE_PORT=${OPENCODE_PORT:-4096}
- OPENCODE_REPOSITORY_ROOT=${OPENCODE_REPOSITORY_ROOT:-/app/repos}
- OPENCODE_SERVER_USERNAME=${OPENCODE_SERVER_USERNAME:-opencode}
- OPENCODE_SERVER_PASSWORD=${OPENCODE_SERVER_PASSWORD}
- OPENCODE_SERVER_PASSWORD=${OPENCODE_SERVER_PASSWORD:?OPENCODE_SERVER_PASSWORD is required for docker-compose.opencode.yml}
- OPENCODE_REPOS=${OPENCODE_REPOS:-}
- GIT_USERNAME=${GIT_USERNAME:-}
- GIT_TOKEN=${GIT_TOKEN:-}
Expand All @@ -19,7 +20,7 @@ services:
- GEMINI_API_KEY=${GEMINI_API_KEY:-${GEMINI_API_KEY_1:-}}
- GOOGLE_GENERATIVE_AI_API_KEY=${GOOGLE_GENERATIVE_AI_API_KEY:-${GEMINI_API_KEY:-${GEMINI_API_KEY_1:-}}}
volumes:
- opencode_repos:/app/repos
- opencode_repos:${OPENCODE_REPOSITORY_ROOT:-/app/repos}
- opencode_data:/home/opencode/.local/share/opencode
healthcheck:
test: ['CMD', '/usr/local/bin/opencode-healthcheck.sh']
Expand All @@ -33,8 +34,9 @@ services:
- NEXT_PUBLIC_OPENCODE_ENABLED=${NEXT_PUBLIC_OPENCODE_ENABLED:-true}
- OPENCODE_BASE_URL=${OPENCODE_BASE_URL:-http://opencode:${OPENCODE_PORT:-4096}}
- OPENCODE_PORT=${OPENCODE_PORT:-4096}
- OPENCODE_REPOSITORY_ROOT=${OPENCODE_REPOSITORY_ROOT:-/app/repos}
- OPENCODE_SERVER_USERNAME=${OPENCODE_SERVER_USERNAME:-opencode}
- OPENCODE_SERVER_PASSWORD=${OPENCODE_SERVER_PASSWORD}
- OPENCODE_SERVER_PASSWORD=${OPENCODE_SERVER_PASSWORD:?OPENCODE_SERVER_PASSWORD is required for docker-compose.opencode.yml}
- OPENCODE_REPOS=${OPENCODE_REPOS:-}
depends_on:
opencode:
Expand Down
13 changes: 8 additions & 5 deletions docker/opencode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ This service runs `opencode serve` for Sim. It backs the optional `OpenCode` wor
At minimum, set:

```env
OPENCODE_REPOSITORY_ROOT=/app/repos
OPENCODE_SERVER_USERNAME=opencode
OPENCODE_SERVER_PASSWORD=change-me
OPENCODE_REPOS=https://github.com/octocat/Hello-World.git
Expand All @@ -24,9 +25,10 @@ GEMINI_API_KEY=your-gemini-key
Notes:

- The UI block is intentionally hidden until `NEXT_PUBLIC_OPENCODE_ENABLED=true` is set on the Sim app.
- `OPENCODE_REPOSITORY_ROOT` defaults to `/app/repos` and must match the path Sim uses when it resolves repository directories.
- `OPENCODE_SERVER_USERNAME` defaults to `opencode` in the optional compose overlays if omitted.
- `docker-compose.opencode.local.yml` defaults `OPENCODE_SERVER_PASSWORD` to `dev-opencode-password`, but setting it explicitly is safer and avoids app/container credential drift.
- `docker-compose.opencode.yml` requires `OPENCODE_SERVER_PASSWORD` to be provided from the environment.
- `docker-compose.opencode.yml` requires `OPENCODE_SERVER_PASSWORD` to be provided from the environment before `docker compose` starts.
- OpenCode needs at least one provider key to answer prompts:
- `OPENAI_API_KEY`
- `ANTHROPIC_API_KEY`
Expand All @@ -48,7 +50,7 @@ Azure Repos over HTTPS is also supported. Example:
OPENCODE_REPOS=https://dev.azure.com/org/project/_git/repo
```

Each repository is cloned into `/app/repos/<repo-name>`. On restart, existing clones are updated with `git pull --ff-only`. A background cron sync retries every 15 minutes.
Each repository is cloned into `${OPENCODE_REPOSITORY_ROOT:-/app/repos}/<repo-name>`. On restart, existing clones are updated with `git pull --ff-only`. A background cron sync retries every 15 minutes.

For private repositories, provide HTTPS credentials with one of these options:

Expand All @@ -68,6 +70,7 @@ Add this to `apps/sim/.env`:
```env
NEXT_PUBLIC_OPENCODE_ENABLED=true
OPENCODE_BASE_URL=http://127.0.0.1:4096
OPENCODE_REPOSITORY_ROOT=/app/repos
OPENCODE_SERVER_USERNAME=opencode
OPENCODE_SERVER_PASSWORD=change-me
```
Expand Down Expand Up @@ -119,9 +122,9 @@ Production should use the base compose plus the OpenCode overlay:
docker compose -f docker-compose.prod.yml -f docker-compose.opencode.yml up -d --build
```

The overlay injects `NEXT_PUBLIC_OPENCODE_ENABLED`, `OPENCODE_BASE_URL`, `OPENCODE_PORT`, `OPENCODE_SERVER_USERNAME`, and `OPENCODE_SERVER_PASSWORD` into `simstudio`, so the app can authenticate against the internal OpenCode server without changing `docker-compose.prod.yml`.
The overlay injects `NEXT_PUBLIC_OPENCODE_ENABLED`, `OPENCODE_BASE_URL`, `OPENCODE_PORT`, `OPENCODE_REPOSITORY_ROOT`, `OPENCODE_SERVER_USERNAME`, and `OPENCODE_SERVER_PASSWORD` into `simstudio`, so the app can authenticate against the internal OpenCode server without changing `docker-compose.prod.yml`.

If you prefer to run OpenCode in separate infrastructure, skip the overlay and set the same app variables directly on the Sim deployment.
If you prefer to run OpenCode in separate infrastructure, skip the overlay and set the same app variables directly on the Sim deployment. The external OpenCode runtime must expose project worktrees under the same `OPENCODE_REPOSITORY_ROOT` that Sim is configured to use.

OpenCode stays internal to the Docker network, so verify from another container:

Expand Down Expand Up @@ -166,4 +169,4 @@ The SDK also supports injecting extra per-session context without triggering a r
## Notes

- Session retention is not managed yet. OpenCode data persists until the `opencode_data` volume is pruned.
- The compose overlays are convenience wrappers. The app can also target any compatible external OpenCode deployment through `OPENCODE_BASE_URL` plus the same server credentials.
- The compose overlays are convenience wrappers. The app can also target any compatible external OpenCode deployment through `OPENCODE_BASE_URL`, the same server credentials, and the same `OPENCODE_REPOSITORY_ROOT`.
6 changes: 4 additions & 2 deletions docker/opencode/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ write_runtime_env() {
HOME
PATH
OPENCODE_REPOS
OPENCODE_REPOSITORY_ROOT
GIT_USERNAME
GIT_TOKEN
GITHUB_TOKEN
Expand Down Expand Up @@ -76,6 +77,7 @@ EOF
main() {
: "${OPENCODE_PORT:=4096}"
: "${OPENCODE_SERVER_USERNAME:=opencode}"
: "${OPENCODE_REPOSITORY_ROOT:=/app/repos}"

if [[ -z "${OPENCODE_SERVER_PASSWORD:-}" ]]; then
log "OPENCODE_SERVER_PASSWORD is required"
Expand All @@ -86,8 +88,8 @@ main() {
export GOOGLE_GENERATIVE_AI_API_KEY="${GEMINI_API_KEY}"
fi

mkdir -p /app/repos /home/opencode/.config/opencode /home/opencode/.local/share/opencode /home/opencode/.local/state
chown -R opencode:opencode /app/repos /home/opencode/.config /home/opencode/.local/share /home/opencode/.local/state
mkdir -p "${OPENCODE_REPOSITORY_ROOT}" /home/opencode/.config/opencode /home/opencode/.local/share/opencode /home/opencode/.local/state
chown -R opencode:opencode "${OPENCODE_REPOSITORY_ROOT}" /home/opencode/.config /home/opencode/.local/share /home/opencode/.local/state

write_runtime_env
write_global_config
Expand Down
5 changes: 3 additions & 2 deletions docker/opencode/sync-repos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ fi
export HOME="${HOME:-/home/opencode}"
export GIT_TERMINAL_PROMPT=0
export GIT_ASKPASS=/usr/local/bin/git-askpass.sh
export OPENCODE_REPOSITORY_ROOT="${OPENCODE_REPOSITORY_ROOT:-/app/repos}"

log() {
printf '[opencode-sync] %s\n' "$*"
Expand All @@ -23,7 +24,7 @@ trim() {
sync_repo() {
local repo_url="$1"
local repo_name="$2"
local repo_dir="/app/repos/${repo_name}"
local repo_dir="${OPENCODE_REPOSITORY_ROOT}/${repo_name}"

if [[ -d "$repo_dir/.git" ]]; then
if git -C "$repo_dir" pull --ff-only; then
Expand Down Expand Up @@ -53,7 +54,7 @@ sync_repo() {
main() {
local repos_raw="${OPENCODE_REPOS:-}"

mkdir -p /app/repos
mkdir -p "${OPENCODE_REPOSITORY_ROOT}"

if [[ -z "$repos_raw" ]]; then
log "No repositories configured"
Expand Down