coder(providers): v2.3 provider-lifecycle phase 4 — config HTTP API (diagnostic returns JSON)

GET/PATCH /api/providers/config, subset POST /refresh, and
GET /api/providers/:id/diagnostic (JSON { diagnostic }, §6.4). PATCH order
is validate→save→reload→clear; a malformed body or invalid merged config
returns 422 without writing, and a save failure returns 500 without
reloading (no file/registry divergence). Web client + types extended.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-29 17:46:56 +00:00
parent 2d997ecb6c
commit f302969c71
10 changed files with 678 additions and 5 deletions

View File

@@ -0,0 +1,71 @@
/**
* v2.3 Phase 4 (design.md §8) — per-provider plaintext diagnostic report.
*
* Read-only by default: reports CACHED state (resolved registry def + the
* available_agents row + the warm snapshot-cache entry) plus a `which`-style
* PATH check for the launch binary. It does NOT spawn an ACP probe — §8 lists
* the live initialize probe as optional, and the route defaults to cached state.
*
* A template string is the whole formatter (no Paseo diagnostic-utils port).
*/
import type { ResolvedProviderDef } from './provider-config-registry.js';
import type { ProviderSnapshotEntry, ProviderModel } from './provider-types.js';
import { isCommandAvailable } from './command-availability.js';
/** The subset of an `available_agents` row the diagnostic reads. */
export interface DiagnosticAgentRow {
name: string;
install_path: string | null;
supports_acp?: boolean;
models?: ProviderModel[] | null;
last_probed_at?: string | Date | null;
}
interface DiagnosticOpts {
/** Warm snapshot-cache entry (read-only peek) — source of the last probe error. */
cachedEntry?: ProviderSnapshotEntry;
/** Injectable PATH check (defaults to the real `which`); stubbed in tests. */
checkAvailable?: (binary: string) => Promise<boolean>;
}
/** Resolve the binary the dispatcher would launch (for the PATH check + report). */
function resolveBinary(resolved: ResolvedProviderDef, agentRow: DiagnosticAgentRow | undefined): string {
return resolved.launchCommand?.[0] ?? agentRow?.install_path ?? resolved.id;
}
export async function getProviderDiagnostic(
resolved: ResolvedProviderDef,
agentRow: DiagnosticAgentRow | undefined,
opts: DiagnosticOpts = {},
): Promise<string> {
const checkAvailable = opts.checkAvailable ?? isCommandAvailable;
const installed = agentRow?.install_path != null;
const binary = resolveBinary(resolved, agentRow);
// boocode is native (no binary to launch) — short-circuit the PATH check.
const commandAvailable = resolved.transport === 'native' ? true : await checkAvailable(binary);
const lastProbedAt =
agentRow?.last_probed_at != null ? new Date(agentRow.last_probed_at).toISOString() : '(never)';
const modelCount = agentRow?.models?.length ?? 0;
const launchCommand = resolved.launchCommand
? resolved.launchCommand.join(' ')
: '(built-in default, resolved at dispatch)';
const lastError = opts.cachedEntry?.error ?? '(none recorded)';
return [
`provider: ${resolved.id}`,
`label: ${resolved.configLabel ?? resolved.label}`,
`transport: ${resolved.transport}`,
`enabled: ${resolved.enabled}`,
`builtin: ${resolved.isBuiltin}`,
`customAcp: ${resolved.isCustomAcp}`,
`installed: ${installed}`,
`install_path: ${agentRow?.install_path ?? '(none)'}`,
`binary: ${binary}`,
`command_available: ${commandAvailable}`,
`launch_command: ${launchCommand}`,
`supports_acp: ${agentRow?.supports_acp ?? '(unknown)'}`,
`last_probed_at: ${lastProbedAt}`,
`models_in_db: ${modelCount}`,
`last_probe_error: ${lastError}`,
].join('\n');
}