- MCP secrets: substituteEnvVars recursively resolves {env:NAME} in mcp.json string values from process.env before Zod (opencode-compatible); unset -> '' + boot warning, and invalid-config log names the unset vars (an empty {env:VAR} in a strict url/command field invalidates the whole config)
- data/mcp.json now untracked (.gitignore flips !data/mcp.json -> !data/mcp.example.json); tracked template data/mcp.example.json carries "{env:CONTEXT7_API_KEY}"; .env.example documents the key (9 mcp-config tests)
- Coder fix: message_complete frame model widened string -> string|null (server+web ws-frames parity); dispatcher publishes model: task.model at all 4 external completion points — a null model otherwise fail-closed in publishFrame and dropped the whole frame incl. status:'complete' (regression test)
- Coder fix: claude-sdk mapUserToolResults maps user-message tool_result blocks -> terminal tool_update events (completed/failed w/ output) so tool snapshots resolve instead of spinning forever
- Composer: AgentComposerBar drops §9b resumed/history/new chip + token readout, loses flex-wrap so the row stays one line; CoderPane gains a per-chat localStorage agent-config cache (restores last model on reopen) + threads model into the timeline/chip
- Docs: root CLAUDE.md slimmed (~190 lines), per-app refs split to apps/{coder,server,web}/CLAUDE.md; new docs/coder-backends.md, docs/project-discovery.md, docs/coding-standards/ (cross-app-contract-parity); ARCHITECTURE.md links the backends doc
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.9 KiB
7.9 KiB
apps/coder — BooCoder (deep reference)
Per-app engineering notes for
apps/coder/src/. BooCoder runs as a systemd service on the host (boocoder.service), NOT in Docker — Fastify at port 9502, postgres at127.0.0.1:5500. Cross-cutting commands, database, environment, workflow, and cross-app contracts live in the rootCLAUDE.md. This file auto-loads when you read/edit files underapps/coder/.
Probe & provider discovery
services/provider-registry.ts— Static registry of provider metadata (label, transport, model source).PROVIDERSarray,PROVIDERS_BY_NAMEmap. 5 providers: boocode (native), opencode (acp), goose (pty), claude (pty), qwen (pty).PROBED_AGENT_NAMESderives from it — adding/removing providers means editing this file, not the frontend.services/agent-probe.ts— Startup probe via directexec()(not SSH): discovers installed agents, versions, ACP support, models. Qwen models from~/.qwen/settings.json; Claude models static from the registry. Persisted toavailable_agents.routes/providers.ts—GET /api/providersreturns installed providers with models. Transport reflects actual capability (checkssupports_acpfrom DB, not just registry preference). The apps/server side is "Provider picker dispatch" (seeapps/server/CLAUDE.md).- Provider snapshot lifecycle (
services/):provider-config.ts(Zod config, never-throws) →provider-config-registry.ts(buildResolvedRegistry, singleton) →provider-snapshot.ts(two-tier probe: tier-1 fast presence, tier-2 cold ACP probe skipped unless force / stalePROVIDER_PROBE_TTL_MS24h / dbEmpty; cached). Verify live:curl http://100.114.205.53:9502/api/providers/snapshot— returns providers + models + commands, the exact shapeAgentComposerBarrenders. PATCH /api/providers/configreplaces a provider id's override object wholesale (per-id shallow merge) — to flip one field send{...existing, enabled}, or a custom ACP entry'scommand/labelis wiped and it drops out of the resolved registry.data/coder-providers.jsonis gitignored (live runtime config — the coder reads AND writes it on UI toggles); tracked reference isdata/coder-providers.example.json. The loader falls back to{providers:{}}(built-ins only) when absent, so a fresh checkout needs no copy.
Build, deploy, dispatch
- Workspace dependency on
@boocode/server: importscreateInferenceRunner,createBroker,ALL_TOOLS,appendMcpToolsfrom the server's compileddist/. apps/server'spackage.jsonhas anexportsmap withtypesconditions for NodeNext resolution. apps/server must build FIRST. - Build + deploy:
pnpm -C apps/server build && pnpm -C apps/coder build && sudo systemctl restart boocoder. Env file atapps/coder/.env.host. Service file at/etc/systemd/system/boocoder.service. - After
pnpm -C apps/coder buildthe host service keeps running the OLD process untilsudo systemctl restart boocoder— a stale process shows new routes 404 with{error:'not found'}while old routes still 200 (the/apinot-found handler shape). Restart, don't re-debug. :9502/api/healthis down ~15–20s after a boocoder restart while the startup agent-probe scan runs — retry; an early connection-refused is not a failed deploy.- Agent dispatch spawns binaries directly using
install_pathfromavailable_agents— nospawn('sh', ['-c', ...])(fails under systemd). Paseo's pattern:spawn(fullBinaryPath, argsArray, { cwd }). - systemd hardening: only
NoNewPrivileges=trueis safe.ProtectSystem,ProtectHome,PrivateTmpall break agent dispatch (agents need full filesystem access to read configs, write to worktrees). apps/server/tsconfig.jsonhasdeclaration: trueso.d.tsfiles exist for workspace consumers. The provider'spackage.jsonneedsexportswithtypes+defaultconditions per subpath ("./inference": { "types": "./dist/.../index.d.ts", "default": "./dist/.../index.js" }) — without thetypescondition, NodeNext can't find.d.tsfiles and tsc fails "Cannot find module" here.- Write tools (
edit_file,create_file,delete_file,apply_pending,rewind) queue inpending_changes. Nothing hits disk untilapply_pending.write_guard.tsvalidates paths (resolve + prefix-check, no realpath since files may not exist for creates).
Backends
Behavioral overview + flows + data model: see /docs/coder-backends.md. The notes below are the deep per-fact reference.
- opencode runs as a warm HTTP server (
services/backends/opencode-server.ts—opencode serveper BooCoder process, one opencode session per BooCode session, resumed viaagent_sessions). goose/qwen/claude dispatch one-shot ACP/PTY with no ctx/token usage; only nativeboocode(llama-swap) tracks ctx. - opencode SSE (
opencode-server.ts): live streaming issession.next.text.delta/.reasoning.delta/.tool.{called,success,failed}— NOTmessage.part.*(terminal/post-hoc).client.event.subscribe({ directory })MUST pass the session's worktree dir; omit it and opencode scopes events to the serverprocess.cwd()→ zero session events (empty turns, 180s timeout). Each live session owns its own subscribe loop + AbortController (asessionIDdemux guard drops cross-session events when two share a dir). Turn completes onsession.idle;promptAsyncis fire-and-forget (204). - opencode model strings must be provider-prefixed (
llama-swap/<model>) AND exist in~/.config/opencode/opencode.jsonprovider.llama-swap.models— not merely loadable by llama-swap.parseModelinfersllama-swap/for a bare id; the dispatcher coalesces empty→DEFAULT_MODEL then prefixes.agent-probepopulates opencode'savailable_agents.modelsviamergeLlamaSwap(fetches/v1/models); empty model list → frontend sends''→ no inference (empty turn). - agent_sessions resume:
config_hash = sha256('opencode_server|<model>')— must NOT include the server port (random per boot; breaks cross-restart resume). Keyed(chat_id, agent)— the tab/chat is the context unit (two opencode tabs = two contexts sharing one worktree).chat_idCASCADEs fromchats;session_id/worktree_idare informationalSET NULL. Theworktreestable (one-per-session, survives session delete) supersedes the defangedsession_worktrees.tasks.chat_idthreads the tab id to the dispatcher;runOpenCodeServerTaskresolves-or-creates a chat when null. The@opencode-ai/sdkv2 client takes flattened params ({sessionID, directory, parts, model:{providerID,modelID}}),createOpencodeClientfrom@opencode-ai/sdk/v2/client. - Claude SDK backend tool RESULTS arrive as
type:'user'SDK messages (tool_result content blocks):mapSdkMessage(claude-sdk-map.ts) MUST map theusercase → a terminaltool_update(completed/failed + output), else the tool_call persistsstatus:'running'and the UI spinner never stops. The dispatcher'stool_updatepath then publishes + persists it. - ACP command discovery is async:
acp-probe.tsmust poll afternewSessionforavailable_commands_update(commands arrive in a later notification; reading synchronously captures 0). PTY providers (claude) discover from disk viaclaude-command-discovery.ts(~/.claude/commands+enabledPlugins, bare names, deduped).AgentCommand.kindtags'command'vs'skill';CoderPane'sslashGroupssplits them into icon'd groups.SlashCommandPicker'sgroups?prop is opt-in. - A new per-message coder field silently drops unless you update every mapper: server read SELECT +
mapCoderMessageRow(apps/coder/src/routes/messages.ts),CoderPane.tsx(RawCoderMessage/CoderMessage/mapCoderTimelineRow+ the livemessage_completeWS reducer),CoderMessageWire(CoderMessageList.tsx), andapi/types.ts. The clientmapCoderTimelineRowwhitelists fields — easiest to forget (this is how themodelchip silently vanished in the coder).