{"id":"add-opencode","name":"add-opencode","summary":"OpenCodeをエージェントプロバイダーとして使う。OpenRouter、OpenAI、Google、DeepSeekなどはOpenCodeの設定で使用可能ですが、Anthropic Agent SDKではありません。","body":"# OpenCode agent provider\n\nNanoClaw runs agents in a long-lived **poll loop** inside the container. The backend is selected per agent group by the **`provider`** key in that group's `container.json` (materialized from the `container_configs` table) — set it with `ncl groups config update --provider opencode`. Default is `claude`.\n\nTrunk ships with only the `claude` provider baked in. This skill copies the OpenCode provider files in from the `providers` branch, wires them into the host and container barrels, installs dependencies, and rebuilds the image.\n\n## Install\n\n### 1. Copy the provider payload\n\nFetch the `providers` branch from the configured remote that carries it, then\noverwrite every skill-owned provider file with its canonical registry copy:\n\n```nc:copy from-branch:providers\nsrc/providers/opencode.ts\nsrc/providers/opencode-registration.test.ts\ncontainer/agent-runner/src/providers/opencode.ts\ncontainer/agent-runner/src/providers/mcp-to-opencode.ts\ncontainer/agent-runner/src/providers/mcp-to-opencode.test.ts\ncontainer/agent-runner/src/providers/opencode-registration.test.ts\ncontainer/agent-runner/src/providers/opencode.attachments.test.ts\ncontainer/agent-runner/src/providers/opencode.compaction.test.ts\ncontainer/agent-runner/src/providers/opencode.config.test.ts\ncontainer/agent-runner/src/providers/opencode.factory.test.ts\ncontainer/agent-runner/src/providers/opencode.memory.test.ts\ncontainer/agent-runner/src/providers/opencode.question.test.ts\n```\n\n(`cwd-shim.ts` and its test are deliberately **not** in this payload even though `mcp-to-opencode.ts` imports the shim: trunk ships and owns them — the default provider imports `cwd-shim.ts` — and every path listed here becomes a skill-owned file that removal deletes.)\n\n### 2. Register the provider in both runtimes\n\nEach barrel gets one line appended at the end — skip if the line is already present.\n\n```nc:append to:src/providers/index.ts\nimport './opencode.js';\n```\n\n```nc:append to:container/agent-runner/src/providers/index.ts\nimport './opencode.js';\n```\n\n### 3. Install the matched SDK and CLI pins\n\nThe agent-runner is a separate Bun package tree. Keep its SDK on the same exact\nversion as the globally installed `opencode-ai` CLI:\n\n```nc:dep manager:bun cwd:container/agent-runner\n@opencode-ai/sdk@1.4.17\n```\n\n```nc:json-merge into:container/cli-tools.json key:name\n{\n  \"name\": \"opencode-ai\",\n  \"version\": \"1.4.17\"\n}\n```\n\nDo not use `latest`. OpenCode's CLI and SDK have changed their session API in\nlockstep before; mismatched versions can build cleanly and fail at runtime.\n\n### 4. Install the pin guard\n\nCopy the structural test that asserts the CLI manifest and SDK package stay on\nthe same exact version:\n\n```nc:copy\nopencode-cli-tools.test.ts -> src/opencode-cli-tools.test.ts\n```\n\n### 5. Build and validate\n\n```nc:run effect:build\npnpm run build\n```\n\n```nc:run effect:build\npnpm exec tsc -p container/agent-runner/tsconfig.json --noEmit\n```\n\n```nc:run effect:test\npnpm exec vitest run src/providers/opencode-registration.test.ts src/opencode-cli-tools.test.ts\n```\n\n```nc:run effect:test\ncd container/agent-runner && bun test src/providers/opencode-registration.test.ts\n```\n\n```nc:run effect:build\n./container/build.sh\n```\n\nAll checks must be clean before proceeding. The registration tests import the\nreal host and container barrels; the pin guard covers the non-importable global\nCLI; the two typechecks cover both runtime API boundaries.\n\nNanoClaw v2 mounts one shared read-only agent-runner source tree. There are no\nper-group source overlays to propagate or repair.\n\n## Configuration\n\n### Host `.env` (typical)\n\nSet model/provider strings in the form OpenCode expects (often `provider/model-id`). **Put comments on their own lines** — a `#` inside a value is kept verbatim and breaks model IDs.\n\nThese variables are read **on the host** and passed into the container only when the effective provider is `opencode`. They do not switch the provider by themselves; the group still needs `provider` set to `opencode` (see [Select the provider](#select-the-provider) below).\n\n- `OPENCODE_PROVIDER` — OpenCode provider id, e.g. `openrouter`, `anthropic`, `deepseek`.\n- `OPENCODE_MODEL` — full model id in `provider/model` form, e.g. `deepseek/deepseek-chat`.\n- `OPENCODE_SMALL_MODEL` — optional second model for lighter tasks; defaults to `OPENCODE_MODEL` if unset.\n- `ANTHROPIC_BASE_URL` — **required for non-`anthropic` providers.** The opencode container provider passes this as the `baseURL` for the upstream provider config so requests route through OneCLI's credential proxy or directly to the provider's API. Set it to the provider's API base URL (e.g. `https://api.deepseek.com/v1`, `https://openrouter.ai/api/v1`).\n\nCredentials: register provider API keys in OneCLI with the matching `--host-pattern` (e.g. `api.deepseek.com`, `openrouter.ai`). OneCLI injects them via `HTTPS_PROXY` in the container — the key never lives in `.env` or the container environment.\n\nAfter adding a secret, **grant the agent access** — agents in `selective` mode only receive secrets they've been explicitly assigned:\n\nUse the safe merge pattern — `set-secrets` replaces the entire list, so always read first:\n\n```bash\nAGENT_ID=$(onecli agents list | jq -r '.data[] | select(.identifier==\"<agentGroupId>\") | .id')\nCURRENT=$(onecli agents secrets --id \"$AGENT_ID\" | jq -r '[.data[]] | join(\",\")')\nMERGED=$(printf '%s' \"$CURRENT,<new-secret-id>\" | tr ',' '\\n' | sort -u | paste -sd ',' -)\nonecli agents set-secrets --id \"$AGENT_ID\" --secret-ids \"$MERGED\"\nonecli agents secrets --id \"$AGENT_ID\"\n```\n\n#### Example: DeepSeek\n\n```env\nOPENCODE_PROVIDER=deepseek\nOPENCODE_MODEL=deepseek/deepseek-chat\nOPENCODE_SMALL_MODEL=deepseek/deepseek-chat\nANTHROPIC_BASE_URL=https://api.deepseek.com/v1\n```\n\nRegister the key:\n\n```bash\nonecli secrets create --name \"DeepSeek\" --type generic \\\n  --value YOUR_KEY --host-pattern \"api.deepseek.com\" \\\n  --header-name \"Authorization\" --value-format \"Bearer {value}\"\n```\n\n#### Example: OpenRouter\n\n```env\nOPENCODE_PROVIDER=openrouter\nOPENCODE_MODEL=openrouter/anthropic/claude-sonnet-4\nOPENCODE_SMALL_MODEL=openrouter/anthropic/claude-haiku-4.5\nANTHROPIC_BASE_URL=https://openrouter.ai/api/v1\n```\n\nRegister the key:\n\n```bash\nonecli secrets create --name \"OpenRouter\" --type generic \\\n  --value YOUR_KEY --host-pattern \"openrouter.ai\" \\\n  --header-name \"Authorization\" --value-format \"Bearer {value}\"\n```\n\n#### Example: Anthropic (no ANTHROPIC_BASE_URL needed)\n\nWhen `OPENCODE_PROVIDER` is `anthropic`, OpenCode uses normal Anthropic env inside the container — the proxy + placeholder key pattern is unchanged and `ANTHROPIC_BASE_URL` is not required.\n\n```env\nOPENCODE_PROVIDER=anthropic\nOPENCODE_MODEL=anthropic/claude-sonnet-4-20250514\nOPENCODE_SMALL_MODEL=anthropic/claude-haiku-4-5-20251001\n```\n\n#### OpenCode Zen (`x-api-key`, not Bearer)\n\nZen's HTTP API (e.g. `POST …/zen/v1/messages`) expects the key in the **`x-api-key`** header. If OneCLI injects **`Authorization: Bearer …`** only, Zen often returns **401 / \"Missing API key\"** even though the gateway is working.\n\n**Naming:** NanoClaw's **`provider: opencode`** (the `container.json` key, set via `ncl groups config update --provider opencode`) means \"run the **OpenCode agent provider**.\" Separately, **`OPENCODE_PROVIDER=opencode`** in `.env` is OpenCode's **Zen provider id** inside the OpenCode config (see [Zen docs](https://opencode.ai/docs/zen/)).\n\n**Host `.env` (typical Zen shape):**\n\n```env\nOPENCODE_PROVIDER=opencode\nOPENCODE_MODEL=opencode/big-pickle\nOPENCODE_SMALL_MODEL=opencode/big-pickle\nANTHROPIC_BASE_URL=https://opencode.ai/zen/v1\n```\n\nUse a real Zen model id from the docs; `big-pickle` is one example.\n\n**OneCLI:** register the Zen key with **`x-api-key`**, not Bearer:\n\n```bash\nonecli secrets create --name \"OpenCode Zen\" --type generic \\\n  --value YOUR_ZEN_KEY --host-pattern opencode.ai \\\n  --header-name \"x-api-key\" --value-format \"{value}\"\n```\n\n### Select the provider\n\nPer group, from the host:\n\n```bash\nncl groups config update --id <group-id> --provider opencode\nncl groups restart --id <group-id>\n```\n\n`ncl groups config update --provider` writes the `provider` value into the `container_configs` table; the host materializes it into `groups/<folder>/container.json` at spawn time and the in-container runner reads `provider` from there (defaulting to `claude`). The restart picks up the change. Switching is an operator action — run it from the host. Memory does NOT carry over automatically between providers — run `/migrate-memory` to carry it across.\n\nExtra MCP servers still come from **`NANOCLAW_MCP_SERVERS`** / `container_config.mcpServers` on the host; the runner merges them into the same `mcpServers` object passed to **both** Claude and OpenCode providers.\n\n## Operational notes\n\n- OpenCode keeps a local **`opencode serve`** process and SSE subscription; the provider tears down with **`stream.return`** and **SIGKILL** on the server process on **`abort()`** / shared runtime reset to avoid MCP/zombie hangs.\n- Session continuation uses UUID format (SDK 1.4.x / CLI 1.4.x). Stale sessions are cleared by `isSessionInvalid` on OpenCode-specific error patterns. If you see UUID-related errors after an accidental CLI upgrade, clear `session_state` in `outbound.db` and wipe the `opencode-xdg` directory under the session folder.\n- **`NO_PROXY`** for localhost matters when the OpenCode client talks to `127.0.0.1` inside the container while HTTP(S)\\_PROXY is set (e.g. OneCLI).\n\n## Next steps\n\nThe host/container registration tests and the CLI/SDK pin guard verify the\nstatic wiring. To confirm a live round-trip, switch a test group with\n`ncl groups config update --id <group-id> --provider opencode && ncl groups\nrestart --id <group-id>`, register the matching provider key in OneCLI, and\nsend a message. A clean exchange returns the model's reply with no `Unknown\nprovider: opencode` error and no UUID/session warnings in the logs.\n\nTo remove this provider, see [REMOVE.md](REMOVE.md).","author":"@nanocoai","ownerProfile":null,"authorContacts":null,"sourceUrl":"https://github.com/nanocoai/nanoclaw/tree/main/.claude/skills/add-opencode","license":"MIT","category":"writing","lang":"en","tokens":2548,"stars":0,"calls30d":1,"claimed":false,"visibility":"public","origin":"crawler","version":"0.1.0","createdAt":"2026-08-22","updatedAt":"2026-08-22","files":[{"path":"opencode-cli-tools.test.ts","size":1227,"sha256":"4c49290cd17c0d1ac6825048e70793140c5af3a8d18565f7d7d95f73c200bfac"},{"path":"REMOVE.md","size":3315,"sha256":"f3734d36f4b4a00fae13daf7ac441a00ff3d30bd5c2419fda1ac245d68b804c6"}],"requires":{"mcp":[],"tools":[]},"safety":{"flags":[{"code":"net.endpoints","kind":"exfiltration","excerpt":"api.deepseek.com, opencode.ai, openrouter.ai","message":"bundled scripts reach 3 external host(s)","severity":"warn"}],"scannedAt":"2026-08-22","hasScripts":true,"networkEndpoints":["api.deepseek.com","opencode.ai","openrouter.ai"]}}