diff --git a/assistant/src/daemon/approval-generators.ts b/assistant/src/daemon/approval-generators.ts index 8a19897e967..8a4fc7a608b 100644 --- a/assistant/src/daemon/approval-generators.ts +++ b/assistant/src/daemon/approval-generators.ts @@ -1,6 +1,8 @@ +import { resolveCallSiteConfig } from "../config/llm-resolver.js"; import { loadConfig } from "../config/loader.js"; import { wrapWithCallSiteRouting } from "../providers/call-site-routing.js"; import { resolveDefaultProvider } from "../providers/connection-resolution.js"; +import { listProviders } from "../providers/registry.js"; import type { Provider } from "../providers/types.js"; import { APPROVAL_COPY_MAX_TOKENS, @@ -16,6 +18,7 @@ import type { ApprovalConversationResult, ApprovalCopyGenerator, } from "../runtime/http-types.js"; +import { ProviderNotConfiguredError } from "../util/errors.js"; // --------------------------------------------------------------------------- // Approval conversation generator constants @@ -142,14 +145,13 @@ export function createApprovalConversationGenerator(): ApprovalConversationGener // Connection-aware default + per-call routing. `resolveDefaultProvider` // throws `ConnectionResolutionError` on hard config errors (missing / // unknown / mismatched connection) and returns null on soft credential - // failures (vault miss, transient auth) — we treat null as "no - // provider available" and throw a domain-specific error below. We do - // not pre-gate on `listProviders()` because the default provider lives - // behind a `provider_connection` and never appears in the registry's - // initialization-time provider list. + // failures (missing credential, platform auth unavailable). const baseProvider = await resolveDefaultProvider(config); if (!baseProvider) { - throw new Error("No provider available for approval conversation"); + const resolved = resolveCallSiteConfig("mainAgent", config.llm); + throw new ProviderNotConfiguredError(resolved.provider, listProviders(), { + connectionName: resolved.provider_connection, + }); } const provider = wrapWithCallSiteRouting(baseProvider, config); diff --git a/assistant/src/daemon/conversation-store.ts b/assistant/src/daemon/conversation-store.ts index 54e836a0683..54011117e88 100644 --- a/assistant/src/daemon/conversation-store.ts +++ b/assistant/src/daemon/conversation-store.ts @@ -21,7 +21,9 @@ import { buildSystemPrompt } from "../prompts/system-prompt.js"; import { wrapWithCallSiteRouting } from "../providers/call-site-routing.js"; import { resolveDefaultProvider } from "../providers/connection-resolution.js"; import { RateLimitProvider } from "../providers/ratelimit.js"; +import { listProviders } from "../providers/registry.js"; import { getSubagentManager } from "../subagent/index.js"; +import { ProviderNotConfiguredError } from "../util/errors.js"; import { getSandboxWorkingDir } from "../util/platform.js"; import { Conversation } from "./conversation.js"; import type { ConversationEvictor } from "./conversation-evictor.js"; @@ -226,12 +228,17 @@ export async function getOrCreateConversation( // Connection-aware default-provider resolution. Throws // `ConnectionResolutionError` when the default profile's // `provider_connection` is unset / unknown / mismatched (config - // bugs). Returns null on soft credential failures (handled below - // as "default provider not registered"). + // bugs). Returns null on soft credential failures (missing + // credential, platform auth unavailable). const baseProvider = await resolveDefaultProvider(config); if (!baseProvider) { - throw new Error( - `Conversation: default provider '${resolveCallSiteConfig("mainAgent", config.llm).provider}' is not registered`, + const resolved = resolveCallSiteConfig("mainAgent", config.llm); + throw new ProviderNotConfiguredError( + resolved.provider, + listProviders(), + { + connectionName: resolved.provider_connection, + }, ); } // Per-call `callSite` routing layered on top, with connection-awareness diff --git a/assistant/src/providers/connection-resolution.ts b/assistant/src/providers/connection-resolution.ts index a95da04a088..5539e211c3f 100644 --- a/assistant/src/providers/connection-resolution.ts +++ b/assistant/src/providers/connection-resolution.ts @@ -110,6 +110,13 @@ export async function tryResolveProviderForConnectionName( `provider_connection "${connectionName}" has provider="${connection.provider}" but resolving profile declared provider="${expectedProvider}" — set the profile's provider_connection to a row matching its provider`, ); } + if (connection.status === "disabled") { + log.debug( + { connectionName }, + "provider_connection is disabled — returning null", + ); + return null; + } // `resolveProviderFromConnection` reaches into auth resolution (credential // reads, managed-proxy context). A transient failure there is a soft // miss — log and return null so the caller can treat it the same as diff --git a/assistant/src/subagent/manager.ts b/assistant/src/subagent/manager.ts index bd98f3dfd84..aafecdf4ca6 100644 --- a/assistant/src/subagent/manager.ts +++ b/assistant/src/subagent/manager.ts @@ -19,7 +19,9 @@ import { bootstrapConversation } from "../memory/conversation-bootstrap.js"; import { wrapWithCallSiteRouting } from "../providers/call-site-routing.js"; import { resolveDefaultProvider } from "../providers/connection-resolution.js"; import { RateLimitProvider } from "../providers/ratelimit.js"; +import { listProviders } from "../providers/registry.js"; import { createAbortReason } from "../util/abort-reasons.js"; +import { ProviderNotConfiguredError } from "../util/errors.js"; import { getLogger } from "../util/logger.js"; import { getSandboxWorkingDir } from "../util/platform.js"; import { @@ -186,14 +188,14 @@ export class SubagentManager { // Connection-aware default-provider resolution. Throws // `ConnectionResolutionError` if `llm.default.provider_connection` is // unset or the connection row is missing/mismatched (config bugs). - // Returns null on soft credential failures (vault miss, transient - // auth) — handled below as "no provider available". Per-call - // `callSite` routing is layered next. + // Returns null on soft credential failures (missing credential, + // platform auth unavailable). const baseProvider = await resolveDefaultProvider(appConfig); if (!baseProvider) { - throw new Error( - `Subagent: default provider '${resolveCallSiteConfig("mainAgent", appConfig.llm).provider}' is not registered`, - ); + const resolved = resolveCallSiteConfig("mainAgent", appConfig.llm); + throw new ProviderNotConfiguredError(resolved.provider, listProviders(), { + connectionName: resolved.provider_connection, + }); } // Per-call `options.config.callSite` (e.g. `subagentSpawn`) can resolve // to a profile that differs from `llm.default`. The shared wrapper