Repository navigation
design phase 9 w12 opencode
Status: SHIPPED 2026-09-16 (daemon
0.17.0-p9w12). External ground truth inresearch-phase-9-w12-opencode.md. Scope decided with the user: opencode only (hermes runtime support later CANCELLED with the user, 2026-09-17 — dropped, not deferred), full surface (probe + install/upgrade/pin jobs + W3 provider push + W4 redacted viewer + ACP chat + C3 scan).
RUNTIME_TARGETS covered claude-code/codex/deepseek only. OpenCode (SST's
terminal agent) is a first-class citizen of the same class — npm-installable,
native ACP, JSON config — but the platform could not add it to a machine,
push a provider route to it, or chat with it. W12 gives it the standard
runtime surface with ZERO new platform concepts: every gate is
schema-driven and every UI surface lights up from the runtime arm alone.
- opencode profile install adapter (deploy axis) — not a
DEPLOYABLE_TARGET; plugins are TS code, there is no declarative hooks surface to install into. -
Native sessions rail — no
session/listover ACP; the rail answerssupported:false(the sessions handler's generic fall-through — no code). -
hooks —
HOOK_SUPPORT['opencode'] = null(route rejects withTARGET_NO_DECLARATIVE_HOOKS, same class as hermes/codex/deepseek). -
~/.local/share/opencode/auth.json— the user's own/connectstore; never read or written. -
JSONC tolerance — an unparseable (commented) global
opencode.jsonfails the apply-config job with an explicit error; we never guess.
| Axis / registry | File | Change |
|---|---|---|
AgentTarget |
shared/schemas/profile.ts + core/domain/resource.ts
|
+= 'opencode' (chat + inventory need the target id) |
RuntimeTarget |
shared/schemas/inventory.ts |
RUNTIME_TARGETS += 'opencode' |
RUNTIME_API_SUPPORT |
shared/schemas/runtime-config.ts |
opencode: ['anthropic-messages', 'openai'] |
PROVIDER_API_SUPPORT |
shared/schemas/llm-provider.ts |
opencode: ['anthropic', 'openai-chat'] — openai-responses is deliberately excluded: the spec snapshot carries only the coarse flavor, so the writer could not pick @ai-sdk/openai vs @ai-sdk/openai-compatible deterministically (the models.dev built-in already covers OpenAI proper) |
HOOK_SUPPORT |
shared/schemas/hooks.ts |
opencode: null |
SCANNABLE_TARGETS |
shared/schemas/inventory.ts |
+= 'opencode' |
| Probe | cli/inventory/runtime.ts |
RUNTIME_PROBES += { target: 'opencode', bin: 'opencode' } |
| Harness packages | cli/daemon/runtime.ts |
HARNESS_PACKAGES['opencode'] = 'opencode-ai' (npm channel, latest tag; no native self-update exception) |
| ACP row | cli/daemon/acp/adapters.ts |
opencode: ['opencode', 'acp'] (native ACP; HN_ACP_COMMAND_OPENCODE override) |
| W3 writer | cli/daemon/runtime-config.ts |
applyOpencodeConfig + dispatch case |
| W4 viewer | cli/daemon/config-view.ts |
TARGET_CONFIG_FILES['opencode'] |
| C3 scanner |
cli/inventory/scanners/opencode.ts (new) |
registered in scan.ts
|
Server: zero code (jobs/routes are schema-driven). Storage: no
migrations (machine_inventory.runtime is a JSON column; job payloads are
JSON). Web: targetBadge gains OC; everything else (Agent card,
install/upgrade/pin, provider form, view-config) renders from the runtime
arm automatically. Profile/resource target pick lists deliberately
UNCHANGED (no install adapter).
Rides the unchanged harness job pipeline. opencode uses the default npm
arm (npm install -g opencode-ai@<latest-or-version>) — no native path,
no CHANNEL_TAG row, no post-install warning hooks. Detection: the probe
row (bin: 'opencode') flows through every inventory:report, so a
post-install re-probe auto-registers the detected AgentInstance (W1 sync)
and the Agent card appears without a rescan.
Two slots, both merge-preserving and idempotent:
-
~/.config/opencode/opencode.json— read (if present,JSON.parse; failure → job error, file untouched), then write:- top-level
model="harness-nexus/<model>"(the active route; W10modelsextras become sibling keys in the provider'smodelsmap so the in-session model picker can switch them — same multi-model story as dsh's switchable set), -
provider['harness-nexus']={ npm, name: 'Harness Nexus', options: { baseURL?, apiKey: '{file:~/.config/opencode/harness-nexus.key}' }, models: { <default>: {}, …extras } }. -
npmmaps from the provider kind:anthropic→@ai-sdk/anthropic,openai-chat→@ai-sdk/openai-compatible(see PROVIDER_API_SUPPORT for the responses exclusion).baseURLis/v1-normalized (opencodeSdkBaseURL, rig-found: the AI SDK appends only the method path, and a/v1-less gateway base fails the gateway's pre-routing auth check as "Unauthorized" — NOT a 404). A baseUrl-less re-apply REMOVES ourbaseURL(the claude-codeANTHROPIC_BASE_URLsemantic — stale endpoints must not linger). Only OUR provider block-
modelkey are touched.
-
- top-level
-
~/.config/opencode/harness-nexus.key— the secret, 0600, RAW (no trailing newline — opencode reads the{file:…}target verbatim, a\nrides the key and the gateway rejects it). The config references it via{file:…}substitution, which EVERY opencode invocation resolves (TUI, ACP, headless) — unlike{env:…}, which only sees opencode's own process env. The secret therefore never rides in the JSON (the W4 viewer still masks any that do) and never depends on our spawn environment.
TARGET_CONFIG_FILES['opencode'] = ['.config/opencode/opencode.json', '.config/opencode/harness-nexus.key']. The JSON file walks the standard
key-name-aware masking (apiKey-shaped keys). The key file is a BARE
secret — no key names for the line-level rules — so the viewer
wholesale-redacts it (same treatment as .env): an explicit path
check, not an extension heuristic.
-
mcpblock ofopencode.json→mcpitems (local stdiocommand/args/env— env values redacted daemon-side to${cred:<KEY>}; remoteurl→ http transport). -
command/*.md→commanditems (frontmatterdescription). -
agent/*.md→sub_agentitems. -
skill/<name>/SKILL.md→skillitems when the dir exists (older installs: absent, skipped cleanly). -
platformMarkers:mcpkeys matching platform MCP names — unchanged pattern.
resolveAcpCommand('opencode') = ['opencode', 'acp'] (native ACP —
officially documented, Zed-registry listed). Standard dialect flows
through the UNMODIFIED mapAcpUpdate (rig-verified: message chunks,
thought chunks, usage with context occupancy). Gate order, channel
budget, W11 lifecycle all apply unchanged — chat needs no opencode code
beyond the command row. If a future dialect drift appears, EXTEND
mapAcpUpdate per the dsh precedent, never replace the standard arm.
- cli: writer goldens (fresh home / merge-preserving user keys / JSONC
refusal / baseUrl removal / extras list), viewer key-file
wholesale-redact, scanner fixture home, harness command mapping
(
opencode-ai@latest/@<version>), ACP default row. - shared:
runtimeTargetSchema/agentTargetSchemaaccept'opencode'; matrix rows present (RUNTIME_API_SUPPORT,PROVIDER_API_SUPPORT,HOOK_SUPPORTnull). - server: harness job schema accepts target
opencode(generic schema-driven path gets one regression test).
- Daemon overlay
0.17.0-p9w12→ machine reports the runtime arm (not-installed card visible). - Install via the UI harness job → card flips to installed with version;
detected AgentInstance appears;
/chatlists the opencode agent. - Provider config push (credential + model) →
opencode.json+ key file land; 查看配置 shows masked JSON + wholesale-redacted key. -
opencode acpchat turn on the pushed route — stream + usage OK. - Rail: "不支持原生会话" (honest
supported:false).