Files
boocode/apps/coder/src/services/provider-config.ts
indifferentketchup 3730dc9341 coder(providers): v2.3 provider-lifecycle phase 1 — config-backed registry
Adds a config layer merged over the hardcoded built-ins (tasks 1.1-1.6): CODER_PROVIDERS_PATH env (default /data/coder-providers.json); provider-config.ts (Zod schema + never-throw loader — missing/invalid file falls back to built-ins only — + save); provider-config-registry.ts (ResolvedProviderDef + buildResolvedRegistry merge: override built-ins, add custom extends:'acp' entries, boocode always enabled + singleton); agent-probe now iterates the resolved registry, probes custom-ACP command[0] via execFile (no shell), skips disabled providers (keeps the row), reads enabled from memory only (no DB column). No snapshot/dispatch/route/UI changes (Phase 2+). 6 new unit tests; empty config provably yields exactly the built-ins.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 04:09:34 +00:00

66 lines
2.4 KiB
TypeScript

/**
* v2.3 provider config file (`/data/coder-providers.json`) — schema + loader.
*
* Layers config-backed overrides/custom-ACP entries over the hardcoded built-ins
* (see provider-config-registry.ts). Loading NEVER throws at startup (design.md
* §2.1): a missing file, invalid JSON, or schema mismatch all fall back to
* `{ providers: {} }` (built-ins only, all enabled).
*/
import { readFileSync, writeFileSync } from 'node:fs';
import { z } from 'zod';
// Schemas verbatim from design.md §2.2.
export 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(),
});
export const CoderProvidersFileSchema = z.object({
providers: z.record(ProviderOverrideSchema).default({}),
});
export type ProviderOverride = z.infer<typeof ProviderOverrideSchema>;
export type CoderProvidersFile = z.infer<typeof CoderProvidersFileSchema>;
/** Read + parse + validate. Falls back to built-ins-only on any failure; never throws. */
export function load(path: string): CoderProvidersFile {
let raw: string;
try {
raw = readFileSync(path, 'utf8');
} catch {
// Missing file → built-ins only. Expected, not an error.
return { providers: {} };
}
let json: unknown;
try {
json = JSON.parse(raw);
} catch (err) {
console.error(`provider-config: invalid JSON in ${path} — using built-ins only`, err);
return { providers: {} };
}
const parsed = CoderProvidersFileSchema.safeParse(json);
if (!parsed.success) {
console.error(
`provider-config: schema validation failed for ${path} — using built-ins only`,
parsed.error.flatten(),
);
return { providers: {} };
}
return parsed.data;
}
/** Write the config back to disk (used by the Phase 4 PATCH route). */
export function save(path: string, config: CoderProvidersFile): void {
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
}