# v2.3 Provider lifecycle — design Detailed implementation plan for Paseo-style provider registration, readiness probing, and enable/disable toggles in BooCoder. **Audience:** Sam + future agents implementing the batch. **Paseo reference:** `/opt/forks/paseo/packages/server/src/server/agent/` (registry, snapshot manager, generic ACP), `/opt/forks/paseo/packages/app/src/screens/settings/providers-section.tsx` (UI behavior). --- ## 1. Current state vs target ### 1.1 BooCode today (v2.2) ``` ┌─────────────────┐ startup ┌──────────────────┐ │ provider- │ ───────────────► │ available_agents │ (which, version, models JSONB) │ registry.ts │ agent-probe │ (Postgres) │ │ (7 hardcoded) │ └────────┬─────────┘ └────────┬────────┘ │ │ │ ▼ ▼ ┌─────────────────┐ cache miss ┌──────────────────┐ │ getProvider │ ──────────────► │ probeAcpProvider │ (full ACP session, 30s) │ Snapshot() │ per agent │ per installed │ └────────┬────────┘ └──────────────────┘ │ ▼ Omit uninstalled ──► AgentComposerBar never sees them No enabled flag status: ready | error only ``` **Key files:** | File | Role | |------|------| | `apps/coder/src/services/provider-registry.ts` | Static `PROVIDERS[]` | | `apps/coder/src/services/agent-probe.ts` | Boot `which` + DB upsert | | `apps/coder/src/services/provider-snapshot.ts` | Cache + cold probe + merge | | `apps/coder/src/services/acp-spawn.ts` | Per-agent argv switch | | `apps/coder/src/routes/providers.ts` | snapshot + refresh | | `apps/web/src/components/AgentComposerBar.tsx` | Picker UI | ### 1.2 Target (Paseo-aligned, BooCode-native) ``` ┌──────────────────┐ │ Built-in registry│──┐ │ (provider- │ │ │ registry.ts) │ │ merge at boot + on config reload └──────────────────┘ │ ▼ ┌──────────────────┐ ┌──────────────────┐ │ /data/coder- │─►│ ResolvedProvider │ │ providers.json │ │ Registry (in-mem) │ └──────────────────┘ └────────┬───────────┘ │ ┌─────────────────────┼─────────────────────┐ ▼ ▼ ▼ agent-probe (fast) getProviderSnapshot dispatch which + version tier-1: isAvailable generic ACP for → available_agents tier-2: cold ACP config entries enabled filter │ ▼ Always emit entry per registered provider loading → ready | unavailable | error ``` **Principles copied from Paseo** (`docs/providers.md` in fork): 1. **Registration ≠ installation** — config lists what you *want*; probe tells you what’s *ready*. 2. **Warm until refresh** — no TTL re-probe on picker open; explicit `POST /api/providers/refresh` only. 3. **Disabled skips probe** — `enabled: false` → `unavailable` without spawning. 4. **Config reload replaces registry** — no redeploy to add an ACP wrapper. --- ## 2. Config file: `/data/coder-providers.json` ### 2.1 Location and loading | Env var | Default | Notes | |---------|---------|-------| | `CODER_PROVIDERS_PATH` | `/data/coder-providers.json` | Same bind-mount pattern as `SKILLS_ROOT`, `MCP_CONFIG_PATH` | - BooCoder runs on **host systemd** — path resolves to `/opt/boocode/data/coder-providers.json` in dev (add to repo as `data/coder-providers.json` + `.env.host`). - Missing file → `{}` (built-ins only, all enabled). - Invalid JSON → log error, fall back to `{}` (do not crash boot). - **Reload:** on `POST /api/providers/config` success, or `SIGHUP` optional later; v1: restart `boocoder.service` after manual edit is acceptable for solo use. ### 2.2 Schema (Zod) New file: `apps/coder/src/services/provider-config.ts` ```typescript const ProviderOverrideSchema = z.object({ extends: z.enum(['acp']).optional(), // v2.3: only 'acp' for custom; built-ins omit extends label: z.string().min(1).optional(), description: z.string().optional(), command: z.array(z.string().min(1)).min(1).optional(), // [binary, ...args] env: z.record(z.string()).optional(), enabled: z.boolean().optional(), // default true order: z.number().int().optional(), // UI sort key models: z.array(z.object({ id: z.string(), label: z.string() })).optional(), additionalModels: z.array(z.object({ id: z.string(), label: z.string() })).optional(), }); const CoderProvidersFileSchema = z.object({ providers: z.record(ProviderOverrideSchema).default({}), }); ``` **Rules:** | Case | Behavior | |------|----------| | Built-in id (e.g. `goose`) | Override merges: `enabled`, `label`, `command` (replace spawn), `env` | | New id + `extends: "acp"` | New registry entry; requires `label` + `command` | | New id without `extends` | Reject at load with log (v2.3) | | `enabled: false` on built-in | Stays in registry; snapshot `enabled: false`, status `unavailable` | | Custom id collision with built-in | Config wins for overrides only; cannot redefine `boocode` transport | ### 2.3 Example file (ship in `data/coder-providers.json`) ```json { "providers": { "goose": { "enabled": true }, "copilot": { "enabled": false }, "amp-acp": { "extends": "acp", "label": "Amp", "description": "ACP wrapper for Amp", "command": ["amp-acp"], "enabled": true } } } ``` ### 2.4 Paseo parity notes Paseo uses `~/.paseo/config.json` under `agents.providers` with the same fields (`extends`, `command`, `enabled`, `models`, …). We intentionally use a **repo-adjacent data file** instead of dotfile — matches `AGENTS.md` / skills layout and survives container/host split (coder reads host path). --- ## 3. Resolved provider registry ### 3.1 New module: `provider-config-registry.ts` **Responsibility:** Single in-memory source of truth after merge. ```typescript export interface ResolvedProviderDef extends ProviderDef { id: string; enabled: boolean; isBuiltin: boolean; isCustomAcp: boolean; /** Full argv for spawn: [binary, ...args] */ launchCommand: [string, ...string[]] | null; env: Record | undefined; configLabel?: string; configDescription?: string; } export function buildResolvedRegistry( builtins: ProviderDef[], config: CoderProvidersFile, ): Map; export function loadProviderConfig(path: string): CoderProvidersFile; export function reloadProviderConfig(): void; // called after PATCH ``` **Merge algorithm** (mirror Paseo `buildProviderRegistry` / `addDerivedProviders`): 1. For each built-in in `PROVIDERS`: - Apply config override if present - `enabled = override.enabled !== false` - `launchCommand` = override.command ?? default from `acp-spawn` + `install_path` at dispatch time 2. For each config key not in built-ins: - Require `extends: "acp"`, `label`, `command` - Insert as `isCustomAcp: true`, `transport: 'acp'`, `modelSource: 'probe'` 3. **`boocode`** always enabled; ignore `enabled: false` with warn log **Consumers:** `agent-probe`, `provider-snapshot`, `dispatcher`, `acp-dispatch`, routes. ### 3.2 agent-probe changes File: `apps/coder/src/services/agent-probe.ts` - Iterate **`getResolvedProviderIds()`** instead of `PROBED_AGENT_NAMES` only. - For custom ACP: probe `command[0]` via `which` (not agent name). - Upsert `available_agents` for custom ids (new rows). - Store `label`, `transport: 'acp'` from resolved def. - Skip probe entirely when `enabled: false` (optional: delete row or keep stale — **keep row**, set `install_path null` on disable refresh). ### 3.3 Schema migration (optional column) File: `apps/coder/src/schema.sql` ```sql ALTER TABLE available_agents ADD COLUMN IF NOT EXISTS enabled BOOLEAN NOT NULL DEFAULT true; ALTER TABLE available_agents ADD COLUMN IF NOT EXISTS source TEXT DEFAULT 'builtin'; -- source: 'builtin' | 'config' ``` Mirror `enabled` from config on each probe pass. Custom providers get `source = 'config'`. **Alternative (simpler v2.3.0):** don’t add DB column; read `enabled` only from in-memory registry at snapshot time. DB holds install facts only. Prefer this for phase 1; add column if settings page needs to show state after coder restart without re-reading JSON. --- ## 4. Snapshot lifecycle ### 4.1 Type changes Files: `apps/coder/src/services/provider-types.ts`, `apps/web/src/api/types.ts` ```typescript export type ProviderSnapshotStatus = 'loading' | 'ready' | 'unavailable' | 'error'; export interface ProviderSnapshotEntry { name: string; label: string; description?: string; transport: string; status: ProviderSnapshotStatus; enabled: boolean; installed: boolean; // binary found on last fast probe models: ProviderModel[]; modes: ProviderMode[]; defaultModeId: string | null; commands: AgentCommand[]; error?: string; fetchedAt?: string; // ISO — when tier-2 probe completed } ``` Restore `unavailable` (removed in stale cleanup — intentional regression for this batch). ### 4.2 `buildProviderEntry` rewrite File: `apps/coder/src/services/provider-snapshot.ts` **Stop returning `null` for uninstalled.** Always return an entry for every resolved registry id. ``` for each resolvedProvider: if !enabled: return { status: 'unavailable', enabled: false, installed: false, models: [], ... } if native boocode: return { status: 'ready', enabled: true, installed: true, models: llamaSwap, ... } fast = agentRow?.install_path != null // or isCommandAvailable(launchCommand[0]) if !fast: return { status: 'unavailable', enabled: true, installed: false, models: [], modes: manifest, commands: manifest } if tier2_skipped: // see §4.3 return { status: 'ready', enabled: true, installed: true, models: from DB, modes: manifest or DB, ... } cold ACP probe: ok → ready + models/modes/commands merge fail → error + error message ``` ### 4.3 Two-tier probe (implements deferred work §2) **Tier 1 — fast (always on cold read if enabled + installed):** ```typescript async function isProviderAvailable(resolved: ResolvedProviderDef, agentRow: AgentRow): Promise { if (resolved.isNative) return true; if (agentRow?.install_path) return true; if (resolved.launchCommand) return isCommandAvailable(resolved.launchCommand[0]); return false; } ``` New util: `apps/coder/src/services/command-availability.ts` — `which`-style check (lift idea from Paseo `utils/executable.ts`, ~20 lines, no full port). **Tier 2 — slow (ACP session):** Run only when: | Condition | Action | |-----------|--------| | `force === true` (`POST /refresh`) | Always cold probe installed enabled providers | | `last_probed_at` older than `PROVIDER_PROBE_TTL_MS` (default 24h, env override) | Cold probe | | DB models empty AND installed | Cold probe | | Otherwise | Use `available_agents.models` + manifest modes/commands | Env: `PROVIDER_PROBE_TTL_MS` default `86400000` (24h). Paseo uses warm-forever until refresh; 24h is a homelab compromise so stale model lists self-heal. **Paseo contract (adopt explicitly):** - Opening `AgentComposerBar` does **not** call refresh or force probe. - `POST /api/providers/refresh` clears cache + forces tier-2 for home cwd. - Document in `BOOCODER.md`. ### 4.4 Loading state On cache miss, before async probe completes: 1. Return entries with `status: 'loading'` immediately (sync). 2. Singleflight inflight map (already exists) — on completion, flip to terminal status + emit… **Tier 2 optional:** WS frame `provider_snapshot_updated` — defer to follow-up; v2.3 can rely on client polling 2s while any entry `loading` (CoderPane already polls when WS disconnected; extend: poll while snapshot has `loading`). ### 4.5 Cache keys Keep cwd-keyed cache (`resolvedCwd = cwd ?? homedir()`). Settings UI uses snapshot with **no cwd** or explicit `cwd=~` — same as Paseo home-directory snapshot for provider management. --- ## 5. Generic ACP dispatch ### 5.1 Problem `acp-spawn.ts` switch grows with every agent. Custom config entries cannot dispatch today. ### 5.2 Solution File: `apps/coder/src/services/acp-spawn.ts` ```typescript export function resolveLaunchSpec( resolved: ResolvedProviderDef, installPath: string | null, ): { binary: string; args: string[]; env?: Record } | null { if (resolved.launchCommand) { return { binary: resolved.launchCommand[0], args: resolved.launchCommand.slice(1), env: resolved.env, }; } // built-in fallback const args = resolveAcpSpawnArgs(resolved.id); if (!args || !installPath) return null; return { binary: installPath, args, env: resolved.env }; } ``` File: `apps/coder/src/services/acp-dispatch.ts` - Replace `resolveAcpSpawnArgs(agent)` + `spawn(installPath, args)` with `resolveLaunchSpec(resolved, installPath)`. - Merge `env` into spawn `env: { ...process.env, ...spec.env }`. - Dispatcher loads resolved def by task.agent name. **Do not port** Paseo `GenericACPAgentClient` class — keep procedural dispatch + existing `acp-stream.ts`. --- ## 6. HTTP API File: `apps/coder/src/routes/providers.ts` | Method | Path | Body | Response | |--------|------|------|----------| | GET | `/api/providers/snapshot?cwd=` | — | `ProviderSnapshotEntry[]` (unchanged path) | | POST | `/api/providers/refresh` | `{ providers?: string[] }` optional | `{ refreshed: number }` — if `providers` set, refresh subset only (Paseo pattern) | | GET | `/api/providers/config` | — | `{ providers: Record }` | | PATCH | `/api/providers/config` | partial providers map | merged file written, registry reload, `{ ok: true }` | | GET | `/api/providers/:id/diagnostic` | — | `{ diagnostic: string }` Tier 2 | **PATCH semantics:** shallow merge at top level per provider id (same as Paseo `patchConfig`). Writing `enabled: false` triggers registry reload + snapshot reconcile (mark unavailable without probe). **Proxy:** BooChat server may proxy `/api/coder/providers/*` — check `apps/server/src/index.ts` coder proxy prefix; add config routes if missing. **Web client:** `apps/web/src/api/client.ts` ```typescript coder: { snapshot: ... refreshProviders: (providers?: string[]) => ... getProviderConfig: () => ... patchProviderConfig: (patch) => ... getProviderDiagnostic: (id) => ... } ``` --- ## 7. Web UI ### 7.1 Settings: Provider management drawer New: `apps/web/src/components/coder/ProviderSettingsDrawer.tsx` (or section under existing settings) **Behavior lifted from Paseo `providers-section.tsx`:** | UI element | Action | |------------|--------| | Row per registered provider | Label, status dot, model count | | Switch | `PATCH config { [id]: { enabled } }` | | Refresh icon | `POST /api/providers/refresh` | | Add provider | Opens catalog modal | | Row click | Diagnostic sheet (optional phase 2) | **Status labels:** Disabled · Loading · Available · Not installed · Error Entry point: link from `AgentComposerBar` (gear icon) or CoderPane header. ### 7.2 AgentComposerBar filter File: `apps/web/src/components/AgentComposerBar.tsx` ```typescript const selectable = entries.filter( (e) => e.enabled && e.status === 'ready' && e.models.length > 0 ); // boocode: allow ready with empty models if llama-swap down? keep current fallback ``` Show subtitle when current provider becomes unavailable (toast + reset to boocode). ### 7.3 Add provider modal New: `apps/web/src/data/acp-provider-catalog.ts` Curated entries (start with 5–10 you might install): | id | command | installLink | |----|---------|-------------| | amp-acp | `["amp-acp"]` | github amp-acp | | cline | `["npx","-y","cline@…","--acp"]` | cline.bot | | pi-acp | from fork | … | Copy **structure** from Paseo `acp-provider-catalog.ts` + `buildAcpProviderConfigPatch` — trim versions aggressively; pin only when you’ve verified on homelab. Modal: search, Install → `patchProviderConfig(buildPatch(entry))` → `refreshProviders([entry.id])`. **Do not port:** React Native components, remote SVG icon pipeline — use lucide fallback icon. ### 7.4 Loading UX While any entry `status === 'loading'`, show spinner in composer provider dropdown; optional 2s poll until terminal state (reuse CoderPane poll pattern). --- ## 8. Diagnostics (Tier 2 in batch — lightweight) Paseo `getDiagnostic()` runs version probe + short ACP initialize. For solo debugging: File: `apps/coder/src/services/provider-diagnostic.ts` ```typescript export async function getProviderDiagnostic( resolved: ResolvedProviderDef, agentRow: AgentRow | undefined, cwd: string, ): Promise { // Plaintext report: // - enabled, installed, binary path // - last_probed_at, model count from DB // - optional: 8s ACP initialize probe (reuse acp-probe with shorter timeout) } ``` No need for Paseo `diagnostic-utils.ts` formatting library — a template string is fine. --- ## 9. Testing strategy | Test | File | |------|------| | Config load + merge | `provider-config-registry.test.ts` | | Snapshot: disabled → unavailable, no probe mock call | extend `provider-snapshot.test.ts` | | Snapshot: uninstalled → unavailable, installed true/false | same | | Tier-2 skip when fresh DB models | same | | force refresh calls probe | same | | PATCH config writes file | `routes/providers.test.ts` (optional integration) | | resolveLaunchSpec custom command | `acp-spawn.test.ts` | Run: `pnpm -C apps/coder test`, `npx tsc -p apps/web/tsconfig.app.json --noEmit`. Smoke: ```bash curl http://100.114.205.53:9502/api/providers/snapshot curl -X PATCH http://100.114.205.53:9502/api/providers/config -d '{"providers":{"goose":{"enabled":false}}}' curl -X POST http://100.114.205.53:9502/api/providers/refresh ``` --- ## 10. Implementation phases ### Phase 1 — Config + registry (backend only) - `provider-config.ts`, `provider-config-registry.ts` - `data/coder-providers.json` + `CODER_PROVIDERS_PATH` - Wire `agent-probe` to resolved ids - Unit tests **Exit:** custom entry in JSON → row in `available_agents` after restart. ### Phase 2 — Snapshot lifecycle - Types: `loading`, `unavailable`, `enabled` - Rewrite `buildProviderEntry` (never omit) - Tier-1 fast availability - Tier-2 skip when DB fresh - Restore warm-cache + force refresh semantics **Exit:** disabled goose visible in API as unavailable; picker filters it out. ### Phase 3 — Generic dispatch - `resolveLaunchSpec` - Dispatcher passes resolved def - Smoke: dispatch task for config-only provider (amp-acp if installed) ### Phase 4 — HTTP config API - GET/PATCH config - Reload registry on PATCH - Subset refresh ### Phase 5 — Web UI - Provider settings drawer + toggle - AgentComposerBar filter - Catalog modal (minimal list) ### Phase 6 — Docs + deploy - `BOOCODER.md` section: Provider config - `CHANGELOG.md` entry - `docs/DEFERRED-WORK.md` — mark cold-probe item resolved - `pnpm -C apps/coder build && sudo systemctl restart boocoder` --- ## 11. Tier 2 follow-ups (document, don’t build in v2.3) | Item | Paseo source | When | |------|--------------|------| | WS `provider_snapshot_updated` | `ProviderSnapshotManager` EventEmitter | When loading poll feels hacky | | MCP `list_providers` / `inspect_provider` | `mcp-server.ts` | When BooCoder MCP orchestration matures | | Profile overrides (`extends: "claude"`) | `provider-registry.ts` derived providers | When you run Z.AI / multi-endpoint | | `order` field UI sort | config schema | When catalog >10 entries | | Per-workspace snapshot in picker | cwd param | Already partial — verify project path passed from CoderPane | --- ## 12. Tier 3 reference — what Paseo has and why we don’t port it This section is **reference only**. These are large subsystems in `/opt/forks/paseo` that solve problems BooCode doesn’t have at solo scale, or that BooCode already solved differently. ### 12.1 `ACPAgentClient` base class (~2,800 lines) **Path:** `packages/server/src/server/agent/providers/acp-agent.ts` **What it does:** Full ACP lifecycle — spawn, initialize, session/new, streaming, permissions, tool calls, MCP injection, revert, persisted agent import, probe sessions. **Why Paseo needs it:** Paseo is the primary runtime for dozens of providers; one abstraction reduces duplication across copilot, cursor, generic ACP, etc. **Why BooCode skips it:** `acp-dispatch.ts` + `acp-stream.ts` + `acp-probe.ts` already cover dispatch and probe as **scripts** (~400 lines total). Replacing with the class hierarchy is a multi-week rewrite with high regression risk on v2.2 dispatch that works on homelab. **What we take instead:** Patterns only — `isAvailable()` = resolve binary; permission waiter (already shipped); derive models/modes (already shipped). --- ### 12.2 Per-provider client classes (claude, codex, opencode, pi, copilot, cursor…) **Paths:** `packages/server/src/server/agent/providers/*/agent.ts`, `codex-app-server-agent.ts` (5,000+ lines) **What they do:** Native SDK/RPC integration — not just CLI spawn. Codex uses app-server RPC; Claude uses Claude Agent SDK; OpenCode manages a sidecar server. **Why Paseo needs it:** Deep integration — voice, revert, persisted sessions, feature toggles, OAuth diagnostics. **Why BooCode skips it:** BooCode **delegates** to existing CLIs in worktrees. No embedded SDKs. PTY path for claude/qwen is stdin pipe; ACP path uses `@agentclientprotocol/sdk` at dispatch boundary only. **Lift risk:** Importing codex-app-server-agent would drag thousands of lines + unknown deps. --- ### 12.3 `ProviderSnapshotManager` class (full port) **Path:** `packages/server/src/server/agent/provider-snapshot-manager.ts` **What it does:** Per-cwd Maps, loading states, singleflight, event emitter, reconcile on registry replace, settings vs workspace refresh split. **Why not full port:** BooCode’s `provider-snapshot.ts` is ~250 lines and already has cache + inflight. **Selective lift:** loading status, reconcile on config reload, subset refresh — not a class-for-class rewrite. --- ### 12.4 React Native settings app (`packages/app`) **Paths:** `providers-section.tsx`, `add-provider-modal.tsx`, `use-providers-snapshot.ts` **What it does:** Mobile/desktop cross-platform provider UI with Unistyles, native Switch, adaptive sheets. **Why BooCode skips it:** BooChat is React web + Tailwind. Port **interaction design** (toggle, status dots, add flow), not components. --- ### 12.5 Daemon config system (`patchConfig`, migrations, Zod wire messages) **Path:** `packages/server/src/shared/messages.ts` (4000+ lines), daemon config patch RPC **What it does:** Every settings change is a typed WS/HTTP patch to daemon with validation, persistence, broadcast. **Why BooCode simplifies:** Single-user — PATCH writes JSON file + reloads in-process Map. No multi-client sync requirement. If BooChat and CLI both edit, last-write-wins on file is acceptable. --- ### 12.6 Full ACP catalog (30+ providers, version-pinned npx) **Path:** `packages/app/src/data/acp-provider-catalog.ts` (~400 lines) **Why trim:** Maintenance burden — every upstream version bump is a PR in Paseo. Solo homelab: 5–10 entries you actually install, update when you install. --- ### 12.7 Voice provider stack **Path:** `packages/server/src/server/speech/*` **Why skip:** BooCode has no voice surface; unrelated to coder provider lifecycle. --- ### 12.8 Workspace git service inside agents **Path:** Codex client integration with `WorkspaceGitService` **Why skip:** BooCode worktrees (`worktrees.ts`) are explicit per-task; agents run in worktree cwd. Different architecture. --- ### 12.9 OpenCode server manager sidecar **Path:** `packages/server/src/server/agent/providers/opencode/server-manager.ts` **What it does:** Manages long-lived OpenCode server process. **Why skip:** BooCode spawns `opencode acp` per dispatch — stateless, simpler, good enough for single user. --- ### 12.10 Pi RPC agent + session import from JSONL **Paths:** `packages/server/src/server/agent/providers/pi/agent.ts` (1,500+ lines) **Why skip until needed:** Only lift if you add `pi` as a built-in with import/revert requirements. Otherwise generic ACP + `extends: "acp"` + pi-acp catalog entry suffices. --- ### 12.11 Summary table | Paseo subsystem | Lines (approx) | BooCode v2.3 approach | |-----------------|----------------|------------------------| | ACPAgentClient | 2,800 | Keep acp-dispatch | | Codex app server agent | 5,500 | Don't import | | Provider registry merge | 700 | New 200-line module | | Snapshot manager | 490 | Extend existing snapshot | | Generic ACP agent | 300 | resolveLaunchSpec only | | RN providers UI | 400 | Web drawer ~200 lines | | MCP list_providers | 200 | Defer | | Config wire protocol | 4,000+ | JSON file PATCH | **Rule of thumb for solo project:** Lift **data models and lifecycle rules**, not **class hierarchies**. --- ## 13. Risk register | Risk | Mitigation | |------|------------| | Custom npx provider slow cold start | Show loading; subset refresh; don’t block picker on whole snapshot | | Config file edit while coder running | PATCH API primary; manual edit requires restart (document) | | `enabled: false` but task in flight | Allow running task to finish; block new sends (picker filter) | | Type drift web/coder | Update both `provider-types.ts` and `api/types.ts`; optional zod parity test | | Security: arbitrary command in config | Single-user trusted path; same trust as `AGENTS.md` — no app-layer auth | | Re-enabling cold probe slowness on refresh | Expected; refresh is explicit user action | --- ## 14. File map (new + touched) | Action | Path | |--------|------| | **New** | `apps/coder/src/services/provider-config.ts` | | **New** | `apps/coder/src/services/provider-config-registry.ts` | | **New** | `apps/coder/src/services/command-availability.ts` | | **New** | `apps/coder/src/services/provider-diagnostic.ts` | | **New** | `apps/coder/src/services/__tests__/provider-config-registry.test.ts` | | **New** | `data/coder-providers.json` | | **New** | `apps/web/src/data/acp-provider-catalog.ts` | | **New** | `apps/web/src/components/coder/ProviderSettingsDrawer.tsx` | | **New** | `apps/web/src/components/coder/AddProviderModal.tsx` | | **Edit** | `apps/coder/src/services/provider-snapshot.ts` | | **Edit** | `apps/coder/src/services/agent-probe.ts` | | **Edit** | `apps/coder/src/services/acp-spawn.ts` | | **Edit** | `apps/coder/src/services/acp-dispatch.ts` | | **Edit** | `apps/coder/src/services/dispatcher.ts` | | **Edit** | `apps/coder/src/routes/providers.ts` | | **Edit** | `apps/coder/src/config.ts` — `CODER_PROVIDERS_PATH` | | **Edit** | `apps/coder/.env.host` | | **Edit** | `apps/coder/src/services/provider-types.ts` | | **Edit** | `apps/web/src/api/types.ts` | | **Edit** | `apps/web/src/api/client.ts` | | **Edit** | `apps/web/src/components/AgentComposerBar.tsx` | | **Edit** | `BOOCODER.md` | | **Edit** | `docs/DEFERRED-WORK.md` | --- ## 15. Attribution Design patterns from [Paseo](https://github.com/getpaseo/paseo) (`/opt/forks/paseo`), especially: - `provider-registry.ts` — merge built-ins + config + `enabled` - `provider-snapshot-manager.ts` — loading/unavailable/ready lifecycle - `provider-launch-config.ts` — override schema - `providers-section.tsx` — settings UX - `public-docs/custom-providers.md` — config file semantics BooCode implementation remains original code — no copy-paste of Paseo sources required; licensing treated as irrelevant per project owner directive.