Workflow
Harbor can run OpenCode as an always-on satellite service alongside LiteLLM. An external orchestrator (another agent, a cron job, a CI pipeline) dispatches coding tasks to the satellite via docker exec harbor.opencode opencode run --model <provider>/<model> --dir <workspace> "...". The satellite connects to LiteLLM for inference, implements the task in a git workspace, and pushes a branch. The orchestrator then opens a PR, dispatches a review (optionally to a different model via the same satellite), and merges.
This enables fully autonomous development loops with no human at a terminal.
Problem
Three issues in services/compose.opencode.ts prevent this workflow from working reliably:
1. Discovery script overwrites manual config on every start
The script unconditionally writes opencode.json on container startup, destroying any manual configuration. Users who need custom provider names, model aliases, or per-model limits lose their config on every harbor up. The OPENCODE_CONFIG_CONTENT env var in override.env was intended to let users provide custom config, but the discovery script overwrites the file regardless.
2. LiteLLM authentication failure
The resolve_key() function defaults to sk-harbor for all backends. LiteLLM uses LITELLM_MASTER_KEY (defaults to sk-litellm). When the discovery script queries http://litellm:4000/v1/models with sk-harbor, it gets a 401, an empty model list, and writes a config with zero providers. The container starts but opencode run fails with ProviderModelNotFoundError.
3. No workspace init hook
When the container is recreated, workspace state (cloned repos, git config, installed tools) is lost. There's no mechanism to run a startup script from the persistent config volume to restore workspace state.
Reproduction
harbor up litellm opencode
# Manually configure a custom provider
docker exec harbor.opencode bash -c 'cat > /root/.config/opencode/opencode.json << EOF
{"$schema":"https://opencode.ai/config.json","provider":{"my-provider":{"npm":"@ai-sdk/openai-compatible","name":"My Provider","models":{"some-model":{}},"options":{"baseURL":"http://litellm:4000/v1","apiKey":"sk-litellm"}}}}
EOF'
# Restart — config is gone, replaced by empty discovery result
harbor down opencode && harbor up opencode
docker exec harbor.opencode cat /root/.config/opencode/opencode.json
# Output: discovery-generated config with provider "harbor-litellm" but zero models (401 auth)
Proposed fix
Three changes to generateDiscoveryScript() in services/compose.opencode.ts:
1. Skip discovery if config already exists:
if [ -s "$$CONFIG_FILE" ]; then
echo "Config file exists at $$CONFIG_FILE, skipping model discovery"
exec opencode serve --hostname=0.0.0.0 --port=4096
fi
2. Resolve LiteLLM key from env:
resolve_key() {
name="$$1"
case "$$name" in
litellm) [ -n "$$LITELLM_MASTER_KEY" ] && printf '%s' "$$LITELLM_MASTER_KEY" && return 0 ;;
esac
# ... existing sidecar key file logic ...
}
3. Run workspace init script from config volume:
# At the top of the discovery script, after harbor CLI init
if [ -x /root/.config/opencode/init.sh ]; then
/root/.config/opencode/init.sh || echo "workspace init failed; continuing"
fi
This lets users drop an init.sh on the persistent config volume (~/.harbor/opencode/config/init.sh) to clone repos, configure git, install tools, etc. — surviving container recreations.
Impact
Without these fixes, using OpenCode as a satellite with LiteLLM requires manually patching compose.opencode.ts after every Harbor update, and the container can't survive a restart without losing its config and workspace.
Workflow
Harbor can run OpenCode as an always-on satellite service alongside LiteLLM. An external orchestrator (another agent, a cron job, a CI pipeline) dispatches coding tasks to the satellite via
docker exec harbor.opencode opencode run --model <provider>/<model> --dir <workspace> "...". The satellite connects to LiteLLM for inference, implements the task in a git workspace, and pushes a branch. The orchestrator then opens a PR, dispatches a review (optionally to a different model via the same satellite), and merges.This enables fully autonomous development loops with no human at a terminal.
Problem
Three issues in
services/compose.opencode.tsprevent this workflow from working reliably:1. Discovery script overwrites manual config on every start
The script unconditionally writes
opencode.jsonon container startup, destroying any manual configuration. Users who need custom provider names, model aliases, or per-model limits lose their config on everyharbor up. TheOPENCODE_CONFIG_CONTENTenv var inoverride.envwas intended to let users provide custom config, but the discovery script overwrites the file regardless.2. LiteLLM authentication failure
The
resolve_key()function defaults tosk-harborfor all backends. LiteLLM usesLITELLM_MASTER_KEY(defaults tosk-litellm). When the discovery script querieshttp://litellm:4000/v1/modelswithsk-harbor, it gets a 401, an empty model list, and writes a config with zero providers. The container starts butopencode runfails withProviderModelNotFoundError.3. No workspace init hook
When the container is recreated, workspace state (cloned repos, git config, installed tools) is lost. There's no mechanism to run a startup script from the persistent config volume to restore workspace state.
Reproduction
Proposed fix
Three changes to
generateDiscoveryScript()inservices/compose.opencode.ts:1. Skip discovery if config already exists:
2. Resolve LiteLLM key from env:
3. Run workspace init script from config volume:
This lets users drop an
init.shon the persistent config volume (~/.harbor/opencode/config/init.sh) to clone repos, configure git, install tools, etc. — surviving container recreations.Impact
Without these fixes, using OpenCode as a satellite with LiteLLM requires manually patching
compose.opencode.tsafter every Harbor update, and the container can't survive a restart without losing its config and workspace.