@open-mercato/ai-assistant 0.7.1-develop.7123.1.25365e718d → 0.7.1-develop.7130.1.fef2396fd8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js +1 -1
  3. package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js.map +1 -1
  4. package/dist/modules/ai_assistant/api/ai/agents/route.js +1 -1
  5. package/dist/modules/ai_assistant/api/ai/agents/route.js.map +1 -1
  6. package/dist/modules/ai_assistant/api/ai/chat/route.js +1 -1
  7. package/dist/modules/ai_assistant/api/ai/chat/route.js.map +1 -1
  8. package/dist/modules/ai_assistant/api/route/route.js +1 -1
  9. package/dist/modules/ai_assistant/api/route/route.js.map +1 -1
  10. package/dist/modules/ai_assistant/api/settings/allowlist/route.js +1 -1
  11. package/dist/modules/ai_assistant/api/settings/allowlist/route.js.map +1 -1
  12. package/dist/modules/ai_assistant/api/settings/route.js +1 -1
  13. package/dist/modules/ai_assistant/api/settings/route.js.map +1 -1
  14. package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js +1 -1
  15. package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js.map +1 -1
  16. package/dist/modules/ai_assistant/lib/agent-registry.js +1 -1
  17. package/dist/modules/ai_assistant/lib/agent-registry.js.map +1 -1
  18. package/dist/modules/ai_assistant/lib/agent-runtime.js +1 -2
  19. package/dist/modules/ai_assistant/lib/agent-runtime.js.map +2 -2
  20. package/dist/modules/ai_assistant/lib/chat-config.js +1 -2
  21. package/dist/modules/ai_assistant/lib/chat-config.js.map +2 -2
  22. package/dist/modules/ai_assistant/lib/llm-registry.js +8 -0
  23. package/dist/modules/ai_assistant/lib/llm-registry.js.map +7 -0
  24. package/dist/modules/ai_assistant/lib/model-factory.js +1 -1
  25. package/dist/modules/ai_assistant/lib/model-factory.js.map +1 -1
  26. package/package.json +6 -6
  27. package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/route.ts +1 -1
  28. package/src/modules/ai_assistant/api/ai/agents/__tests__/route.test.ts +69 -0
  29. package/src/modules/ai_assistant/api/ai/agents/route.ts +1 -1
  30. package/src/modules/ai_assistant/api/ai/chat/route.ts +1 -1
  31. package/src/modules/ai_assistant/api/route/route.ts +1 -1
  32. package/src/modules/ai_assistant/api/settings/allowlist/route.ts +1 -1
  33. package/src/modules/ai_assistant/api/settings/route.ts +1 -1
  34. package/src/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.ts +1 -1
  35. package/src/modules/ai_assistant/lib/__tests__/llm-registry-imports.test.ts +42 -0
  36. package/src/modules/ai_assistant/lib/agent-registry.ts +1 -1
  37. package/src/modules/ai_assistant/lib/agent-runtime.ts +1 -5
  38. package/src/modules/ai_assistant/lib/chat-config.ts +1 -4
  39. package/src/modules/ai_assistant/lib/llm-registry.ts +27 -0
  40. package/src/modules/ai_assistant/lib/model-factory.ts +1 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/ai_assistant/api/settings/route.ts"],
4
- "sourcesContent": ["import { existsSync, readFileSync } from 'node:fs'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { findApiKeyBySecret } from '@open-mercato/core/modules/api_keys/services/apiKeyService'\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport { joinProviderModel } from '@open-mercato/shared/lib/ai/model-id'\nimport {\n OPEN_CODE_PROVIDER_IDS,\n OPEN_CODE_PROVIDERS,\n getOpenCodeProviderConfiguredEnvKey,\n isOpenCodeProviderConfigured,\n} from '@open-mercato/shared/lib/ai/opencode-provider'\nimport { AiAgentRuntimeOverrideRepository, AiAgentRuntimeOverrideValidationError } from '../../data/repositories/AiAgentRuntimeOverrideRepository'\nimport { resolveModerationPolicy } from '../../lib/moderation-policy'\nimport { AiTenantModelAllowlistRepository } from '../../data/repositories/AiTenantModelAllowlistRepository'\nimport { isBaseurlAllowlisted, readBaseurlAllowlist } from '../../lib/baseurl-allowlist'\nimport { loadAgentRegistry, listAgents } from '../../lib/agent-registry'\nimport { createModelFactory, resolveAllowRuntimeOverride } from '../../lib/model-factory'\nimport {\n agentOverrideModelAllowlistEnvVarName,\n agentOverrideProviderAllowlistEnvVarName,\n canonicalProviderId,\n hasAllowlistSnapshotRestrictions,\n intersectEffectiveAllowlistWithSnapshot,\n intersectAllowlists,\n isProviderAllowed,\n isProviderAllowedInEffective,\n isProviderModelAllowed,\n isProviderModelAllowedInEffective,\n modelAllowlistEnvVarName,\n readAgentRuntimeOverrideAllowlist,\n readAllowedModels,\n readAllowedProviders,\n readAllowlistConfig,\n type TenantAllowlistSnapshot,\n} from '../../lib/model-allowlist'\n\nconst logger = createLogger('ai_assistant')\n\nfunction modelCatalogWithAllowlistFallback(\n models: ReadonlyArray<{ id: string; name: string; contextWindow?: number | null; tags?: readonly string[] }>,\n allowlistModelIds: string[] | undefined,\n): ReadonlyArray<{ id: string; name: string; contextWindow?: number | null; tags?: readonly string[] }> {\n if (models.length > 0) return models\n return (allowlistModelIds ?? []).map((id) => ({ id, name: id }))\n}\n\nconst runtimeOverrideUpsertSchema = z.object({\n providerId: z.string().min(1).max(64).nullable().optional(),\n modelId: z.string().min(1).max(256).nullable().optional(),\n baseURL: z.string().url().max(2048).nullable().optional(),\n agentId: z.string().min(1).max(128).nullable().optional(),\n allowedOverrideProviders: z.array(z.string().min(1).max(64)).nullable().optional(),\n allowedOverrideModelsByProvider: z\n .record(z.string().min(1).max(64), z.array(z.string().min(1).max(256)))\n .optional(),\n // Input moderation override: true = on, false = off, null = inherit.\n // Spec 2026-06-04-ai-input-moderation-and-safety-identifiers.\n inputModeration: z.boolean().nullable().optional(),\n})\n\nconst runtimeOverrideClearSchema = z.object({\n agentId: z.string().min(1).max(128).nullable().optional(),\n})\n\nexport type RuntimeOverrideUpsertBody = z.infer<typeof runtimeOverrideUpsertSchema>\nexport type RuntimeOverrideClearBody = z.infer<typeof runtimeOverrideClearSchema>\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'AI assistant settings',\n methods: {\n GET: { summary: 'Get AI provider configuration' },\n PUT: {\n summary: 'Upsert per-tenant AI runtime override',\n description:\n 'Creates or updates the per-tenant AI runtime override (provider, model, baseURL). ' +\n 'Optionally scoped to a specific agent via `agentId`. ' +\n 'Gated by `ai_assistant.settings.manage`. ' +\n 'baseURL must match AI_RUNTIME_BASEURL_ALLOWLIST when set.',\n requestBody: {\n contentType: 'application/json',\n description: 'Override payload. All fields nullable/optional; null explicitly clears the axis.',\n schema: runtimeOverrideUpsertSchema,\n },\n responses: [\n { status: 200, description: 'Override saved. Returns the saved row.' },\n ],\n errors: [\n { status: 400, description: 'Validation error: unknown provider, invalid URL, or baseURL not allowlisted.' },\n { status: 401, description: 'Unauthenticated.' },\n { status: 403, description: 'Caller lacks ai_assistant.settings.manage.' },\n ],\n },\n DELETE: {\n summary: 'Clear per-tenant AI runtime override',\n description:\n 'Soft-deletes the active per-tenant runtime override. ' +\n 'Pass `agentId` to clear only the agent-specific row; omit to clear the tenant-wide default. ' +\n 'Gated by `ai_assistant.settings.manage`. Idempotent \u2014 returns 200 with `cleared: false` when no active row existed.',\n requestBody: {\n contentType: 'application/json',\n description: 'Optional agentId to scope the delete.',\n schema: runtimeOverrideClearSchema,\n },\n responses: [\n { status: 200, description: 'Returns `{ cleared: boolean }` indicating whether a row was found and removed.' },\n ],\n errors: [\n { status: 401, description: 'Unauthenticated.' },\n { status: 403, description: 'Caller lacks ai_assistant.settings.manage.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n PUT: { requireAuth: true, requireFeatures: ['ai_assistant.settings.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['ai_assistant.settings.manage'] },\n}\n\n/**\n * GET /api/ai_assistant/settings\n *\n * Returns the current OpenCode provider configuration from environment variables\n * plus the Phase 4a additive fields: resolvedDefault, tenantOverride, agents[],\n * and availableProviders[].defaultModels.\n */\nexport async function GET(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n const env = process.env as Record<string, string | undefined>\n const configuredProviderHint = env.OM_AI_PROVIDER?.trim() || env.OPENCODE_PROVIDER?.trim() || null\n const registryProviders = llmProviderRegistry.list()\n const knownProviderIdsForAllowlist: string[] = [\n ...OPEN_CODE_PROVIDER_IDS,\n ...registryProviders\n .map((p) => p.id)\n .filter((id) => !(OPEN_CODE_PROVIDER_IDS as readonly string[]).includes(id)),\n ]\n const registryProviderId = configuredProviderHint\n ? canonicalProviderId(configuredProviderHint, registryProviders.map((provider) => provider.id))\n : null\n const registryProvider = registryProviderId ? llmProviderRegistry.get(registryProviderId) : null\n const fallbackOpenCodeProviderId = (\n (configuredProviderHint\n ? canonicalProviderId(configuredProviderHint, OPEN_CODE_PROVIDER_IDS as readonly string[])\n : null) ?? 'openai'\n ) as keyof typeof OPEN_CODE_PROVIDERS\n const fallbackOpenCodeProvider = OPEN_CODE_PROVIDERS[fallbackOpenCodeProviderId]\n\n const providerId = registryProvider?.id ?? fallbackOpenCodeProviderId\n const providerName = registryProvider?.name ?? fallbackOpenCodeProvider?.name ?? providerId\n const defaultProviderModel = registryProvider?.defaultModel ?? fallbackOpenCodeProvider?.defaultModel ?? ''\n const configuredModelHint = env.OM_AI_MODEL?.trim() || env.OPENCODE_MODEL?.trim() || defaultProviderModel\n const fallbackModelWithProvider = joinProviderModel(providerId, configuredModelHint)\n const apiKeyConfigured = registryProvider\n ? registryProvider.isConfigured(env)\n : fallbackOpenCodeProvider\n ? isOpenCodeProviderConfigured(fallbackOpenCodeProviderId)\n : false\n const displayEnvKey = registryProvider\n ? registryProvider.getConfiguredEnvKey(env)\n : fallbackOpenCodeProvider\n ? getOpenCodeProviderConfiguredEnvKey(fallbackOpenCodeProviderId)\n : null\n\n // Check if the MCP API key is configured (required for MCP authentication):\n // either directly via MCP_SERVER_API_KEY or via the shared key file that\n // the containerized mcp service provisions (MCP_SERVER_API_KEY_FILE).\n // The file variant is validated against the api_keys table \u2014 the file\n // outlives DB resets and key deletion, so mere existence would report a\n // green badge while OpenCode -> MCP auth is actually broken.\n const mcpKeyFilePath = process.env.MCP_SERVER_API_KEY_FILE?.trim()\n let mcpKeyConfigured = !!process.env.MCP_SERVER_API_KEY?.trim()\n if (!mcpKeyConfigured && mcpKeyFilePath && existsSync(mcpKeyFilePath)) {\n try {\n const fileSecret = readFileSync(mcpKeyFilePath, 'utf8').trim()\n if (fileSecret.startsWith('omk_')) {\n const keyContainer = await createRequestContainer()\n const keyEm = keyContainer.resolve<EntityManager>('em')\n mcpKeyConfigured = !!(await findApiKeyBySecret(keyEm, fileSecret))\n }\n } catch {\n mcpKeyConfigured = false\n }\n }\n\n // Phase 4a: resolve tenant override row and per-agent resolution matrix\n let tenantOverride: {\n providerId: string | null\n modelId: string | null\n baseURL: string | null\n agentId: string | null\n updatedAt: string\n } | null = null\n\n let agentResolutions: Array<{\n agentId: string\n moduleId: string\n allowRuntimeOverride: boolean\n allowRuntimeModelOverride: boolean\n codeDefaultProviderId: string | null\n codeDefaultModelId: string | null\n override: {\n providerId: string | null\n modelId: string | null\n baseURL: string | null\n updatedAt: string\n } | null\n runtimeOverrideAllowlist: {\n env: TenantAllowlistSnapshot | null\n tenant: TenantAllowlistSnapshot | null\n effective: ReturnType<typeof intersectAllowlists>\n envVarNames: {\n providers: string\n modelsByProvider: Record<string, string>\n }\n }\n providerId: string\n modelId: string\n baseURL: string | null\n source: string\n }> = []\n\n let resolvedDefault: {\n providerId: string\n modelId: string\n baseURL: string | null\n source: string\n } | null = null\n\n let tenantAllowlistSnapshot: TenantAllowlistSnapshot | null = null\n\n try {\n const container = await createRequestContainer()\n const tenantId = auth.tenantId ?? null\n const organizationId = auth.orgId ?? null\n\n if (tenantId) {\n const em = container.resolve<EntityManager>('em')\n const repo = new AiAgentRuntimeOverrideRepository(em)\n const overrideRow = await repo.getDefault({ tenantId, organizationId, agentId: null })\n const tenantWideInputModeration = overrideRow?.inputModeration ?? null\n if (overrideRow) {\n tenantOverride = {\n providerId: overrideRow.providerId ?? null,\n modelId: overrideRow.modelId ?? null,\n baseURL: overrideRow.baseUrl ?? null,\n agentId: overrideRow.agentId ?? null,\n updatedAt: overrideRow.updatedAt.toISOString(),\n }\n }\n\n const allowlistRepo = new AiTenantModelAllowlistRepository(em)\n tenantAllowlistSnapshot = await allowlistRepo.getSnapshot({\n tenantId,\n organizationId,\n })\n\n const factory = createModelFactory(container)\n const defaultResolution = factory.resolveModel({\n tenantAllowlist: tenantAllowlistSnapshot,\n tenantOverride: tenantOverride\n ? { providerId: tenantOverride.providerId, modelId: tenantOverride.modelId, baseURL: tenantOverride.baseURL }\n : undefined,\n })\n resolvedDefault = {\n providerId: defaultResolution.providerId,\n modelId: defaultResolution.modelId,\n baseURL: defaultResolution.baseURL ?? null,\n source: defaultResolution.source,\n }\n\n await loadAgentRegistry()\n const agents = listAgents()\n const agentResolutionPromises = agents.map(async (agent) => {\n const agentOverrideRow = await repo.getExact({\n tenantId,\n organizationId,\n agentId: agent.id,\n })\n const agentTenantOverride = agentOverrideRow\n ? {\n providerId: agentOverrideRow.providerId ?? null,\n modelId: agentOverrideRow.modelId ?? null,\n baseURL: agentOverrideRow.baseUrl ?? null,\n }\n : (tenantOverride ?? undefined)\n const agentResolution = factory.resolveModel({\n moduleId: agent.moduleId,\n agentDefaultModel: agent.defaultModel,\n agentDefaultProvider: agent.defaultProvider,\n agentDefaultBaseUrl: agent.defaultBaseUrl,\n allowRuntimeOverride: resolveAllowRuntimeOverride(agent),\n tenantOverride: agentTenantOverride,\n tenantAllowlist: tenantAllowlistSnapshot,\n })\n const agentEnvAllowlist = readAgentRuntimeOverrideAllowlist(\n env,\n agent.id,\n knownProviderIdsForAllowlist,\n )\n const agentTenantAllowlist = agentOverrideRow\n ? {\n allowedProviders: agentOverrideRow.allowedOverrideProviders ?? null,\n allowedModelsByProvider: agentOverrideRow.allowedOverrideModelsByProvider ?? {},\n }\n : null\n const baseEffectiveAllowlist = intersectAllowlists(\n env,\n knownProviderIdsForAllowlist,\n tenantAllowlistSnapshot,\n )\n const agentEffectiveAllowlist = intersectEffectiveAllowlistWithSnapshot(\n intersectEffectiveAllowlistWithSnapshot(\n baseEffectiveAllowlist,\n knownProviderIdsForAllowlist,\n agentEnvAllowlist,\n ),\n knownProviderIdsForAllowlist,\n agentTenantAllowlist,\n )\n const agentModelEnvVars = Object.fromEntries(\n knownProviderIdsForAllowlist.map((providerId) => [\n providerId,\n agentOverrideModelAllowlistEnvVarName(agent.id, providerId),\n ]),\n )\n return {\n agentId: agent.id,\n moduleId: agent.moduleId,\n allowRuntimeOverride: resolveAllowRuntimeOverride(agent),\n allowRuntimeModelOverride: resolveAllowRuntimeOverride(agent),\n codeDefaultProviderId: agent.defaultProvider ?? null,\n codeDefaultModelId: agent.defaultModel ?? null,\n override: agentOverrideRow\n ? {\n providerId: agentOverrideRow.providerId ?? null,\n modelId: agentOverrideRow.modelId ?? null,\n baseURL: agentOverrideRow.baseUrl ?? null,\n updatedAt: agentOverrideRow.updatedAt.toISOString(),\n }\n : null,\n runtimeOverrideAllowlist: {\n env: agentEnvAllowlist,\n tenant: hasAllowlistSnapshotRestrictions(agentTenantAllowlist)\n ? agentTenantAllowlist\n : null,\n effective: agentEffectiveAllowlist,\n envVarNames: {\n providers: agentOverrideProviderAllowlistEnvVarName(agent.id),\n modelsByProvider: agentModelEnvVars,\n },\n },\n providerId: agentResolution.providerId,\n modelId: agentResolution.modelId,\n baseURL: agentResolution.baseURL ?? null,\n source: agentResolution.source,\n moderation: {\n // `untrustedInput` agents enforce moderation; the UI renders a\n // non-editable \"Enforced\" badge for them.\n enforced: agent.untrustedInput === true,\n // Per-agent control value: null = inherit, true = on, false = off.\n override: agentOverrideRow?.inputModeration ?? null,\n // Resolved runtime policy (enforced | on | off) after precedence.\n effective: resolveModerationPolicy({\n untrustedInput: agent.untrustedInput,\n perAgentOverride: agentOverrideRow?.inputModeration ?? null,\n tenantWideOverride: tenantWideInputModeration,\n env,\n }),\n },\n }\n })\n agentResolutions = await Promise.all(agentResolutionPromises)\n }\n } catch (overrideError) {\n // Phase 4a fields are best-effort \u2014 log and continue returning the base response\n logger.warn('AI Settings \u2014 Failed to compute Phase 4a override fields', { err: overrideError })\n }\n\n // Build availableProviders with Phase 4a defaultModels, then clip to the\n // EFFECTIVE allowlist \u2014 env intersected with the per-tenant snapshot.\n // The env allowlist is the OUTER constraint; the tenant allowlist (Phase\n // 1780-6) narrows it further. The settings UI must never offer a value\n // the runtime would refuse to honor.\n const allowlistConfig = readAllowlistConfig(env, knownProviderIdsForAllowlist)\n const effectiveAllowlist = intersectAllowlists(\n env,\n knownProviderIdsForAllowlist,\n tenantAllowlistSnapshot,\n )\n\n const allRawProviders = [\n ...OPEN_CODE_PROVIDER_IDS.map((id) => {\n const info = OPEN_CODE_PROVIDERS[id]\n const registryProvider = llmProviderRegistry.get(id)\n return {\n id,\n name: info.name,\n defaultModel: info.defaultModel,\n envKey: getOpenCodeProviderConfiguredEnvKey(id),\n configured: isOpenCodeProviderConfigured(id),\n defaultModels: registryProvider?.defaultModels ?? [],\n }\n }),\n // Also surface any llmProviderRegistry providers not in OPEN_CODE_PROVIDER_IDS\n ...llmProviderRegistry.list()\n .filter((p) => !(OPEN_CODE_PROVIDER_IDS as readonly string[]).includes(p.id))\n .map((p) => ({\n id: p.id,\n name: p.name,\n defaultModel: p.defaultModels[0]?.id ?? '',\n envKey: null,\n configured: p.isConfigured(),\n defaultModels: p.defaultModels,\n })),\n ]\n\n const availableProviders = allRawProviders\n .filter((p) => isProviderAllowedInEffective(effectiveAllowlist, p.id))\n .map((p) => {\n const effectiveModelsList = effectiveAllowlist.modelsByProvider[p.id]\n const catalogModels = modelCatalogWithAllowlistFallback(\n p.defaultModels,\n effectiveModelsList,\n )\n const clippedDefaults = effectiveModelsList !== undefined\n ? catalogModels.filter((m) => effectiveModelsList.includes(m.id))\n : catalogModels\n return {\n ...p,\n defaultModel: effectiveModelsList && !effectiveModelsList.includes(p.defaultModel)\n ? (effectiveModelsList[0] ?? p.defaultModel)\n : p.defaultModel || clippedDefaults[0]?.id || '',\n defaultModels: clippedDefaults,\n }\n })\n\n const allowlistProviders = allRawProviders\n .filter((p) => isProviderAllowed(env, p.id))\n .map((p) => {\n const envModelsList = allowlistConfig.modelsByProvider[p.id]\n const catalogModels = modelCatalogWithAllowlistFallback(\n p.defaultModels,\n envModelsList,\n )\n const envClippedDefaults = envModelsList !== undefined\n ? catalogModels.filter((m) => envModelsList.includes(m.id))\n : catalogModels\n return {\n ...p,\n defaultModel: envModelsList && !envModelsList.includes(p.defaultModel)\n ? (envModelsList[0] ?? p.defaultModel)\n : p.defaultModel || envClippedDefaults[0]?.id || '',\n defaultModels: envClippedDefaults,\n }\n })\n\n return NextResponse.json({\n provider: {\n id: providerId,\n name: providerName,\n model: resolvedDefault\n ? joinProviderModel(resolvedDefault.providerId, resolvedDefault.modelId)\n : fallbackModelWithProvider,\n defaultModel: defaultProviderModel,\n envKey: displayEnvKey,\n configured: apiKeyConfigured,\n },\n availableProviders,\n // Editable universe for the tenant allowlist page. This is clipped only\n // by env so tenant-hidden models remain visible and can be re-enabled.\n allowlistProviders,\n // Snapshot of the env-driven allowlist so the UI can render hints like\n // \"limited to: openai, anthropic\" without re-implementing the parser.\n allowlist: allowlistConfig,\n // Per-tenant allowlist snapshot (Phase 1780-6). `null` when no row has\n // been persisted yet \u2014 the runtime then falls back to env-only\n // enforcement. The UI uses this to drive the editable MultiSelect.\n tenantAllowlist: tenantAllowlistSnapshot,\n // Effective allowlist after intersecting env with tenant. The UI uses\n // this to render the \"what the runtime will actually accept\" summary\n // and to clip pickers without re-implementing the intersection.\n effectiveAllowlist,\n mcpKeyConfigured,\n resolvedDefault,\n tenantOverride,\n agents: agentResolutions,\n })\n } catch (error) {\n logger.error('AI Settings \u2014 GET error', { err: error })\n return NextResponse.json({ error: 'Failed to fetch settings' }, { status: 500 })\n }\n}\n\n/**\n * PUT /api/ai_assistant/settings\n *\n * Upserts the per-tenant AI runtime override (Phase 4a). Requires\n * `ai_assistant.settings.manage`. The body is Zod-validated; a `baseURL`\n * must match `AI_RUNTIME_BASEURL_ALLOWLIST` when that env var is set.\n */\nexport async function PUT(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let parsedBody: unknown\n try {\n parsedBody = await req.json()\n } catch {\n return NextResponse.json({ error: 'Request body must be valid JSON.', code: 'validation_error' }, { status: 400 })\n }\n\n const bodyResult = runtimeOverrideUpsertSchema.safeParse(parsedBody)\n if (!bodyResult.success) {\n return NextResponse.json(\n { error: 'Invalid request body.', code: 'validation_error', issues: bodyResult.error.issues },\n { status: 400 },\n )\n }\n\n const { providerId: requestedProviderId, modelId, baseURL, agentId } = bodyResult.data\n const knownProviderIdsForRequest = llmProviderRegistry.list().map((p) => p.id)\n const providerId = requestedProviderId\n ? canonicalProviderId(requestedProviderId, knownProviderIdsForRequest) ?? requestedProviderId\n : requestedProviderId\n\n if (baseURL && baseURL.trim().length > 0) {\n const allowlist = readBaseurlAllowlist()\n if (!isBaseurlAllowlisted(baseURL.trim(), allowlist)) {\n return NextResponse.json(\n {\n error: `baseURL \"${baseURL}\" is not in AI_RUNTIME_BASEURL_ALLOWLIST.`,\n code: 'baseurl_not_allowlisted',\n },\n { status: 400 },\n )\n }\n }\n\n const allowedOverrideProviders = bodyResult.data.allowedOverrideProviders === undefined\n ? undefined\n : bodyResult.data.allowedOverrideProviders?.map((id) =>\n canonicalProviderId(id, knownProviderIdsForRequest) ?? id,\n ) ?? null\n const allowedOverrideModelsByProvider = bodyResult.data.allowedOverrideModelsByProvider === undefined\n ? undefined\n : Object.fromEntries(\n Object.entries(bodyResult.data.allowedOverrideModelsByProvider ?? {}).map(([id, models]) => [\n canonicalProviderId(id, knownProviderIdsForRequest) ?? id,\n models,\n ]),\n )\n const hasRuntimeOverrideAllowlistWrite =\n allowedOverrideProviders !== undefined || allowedOverrideModelsByProvider !== undefined\n\n // Env-driven provider/model allowlist (Phase 1780-5) intersected with the\n // per-tenant allowlist (Phase 1780-6): the EFFECTIVE allowlist clips which\n // (provider, model) pairs the runtime accepts. Reject settings PUT requests\n // for pairs outside that effective set so the settings UI never persists a\n // value the runtime would later refuse. Tenant-allowlist enforcement is\n // best-effort here: if the snapshot lookup fails we fall back to env-only\n // checks (the runtime still re-clips at resolution time).\n let putEffectiveAllowlist: ReturnType<typeof intersectAllowlists> | null = null\n if (providerId || hasRuntimeOverrideAllowlistWrite) {\n try {\n const previewContainer = await createRequestContainer()\n const knownIdsForCheck = [\n ...OPEN_CODE_PROVIDER_IDS,\n ...llmProviderRegistry\n .list()\n .map((p) => p.id)\n .filter((id) => !(OPEN_CODE_PROVIDER_IDS as readonly string[]).includes(id)),\n ]\n let snapshot: TenantAllowlistSnapshot | null = null\n if (auth.tenantId) {\n try {\n const em = previewContainer.resolve<EntityManager>('em')\n const allowlistRepo = new AiTenantModelAllowlistRepository(em)\n snapshot = await allowlistRepo.getSnapshot({\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n })\n } catch {\n snapshot = null\n }\n }\n putEffectiveAllowlist = intersectAllowlists(\n process.env as Record<string, string | undefined>,\n knownIdsForCheck,\n snapshot,\n )\n } catch {\n putEffectiveAllowlist = null\n }\n\n if (providerId && putEffectiveAllowlist) {\n if (!isProviderAllowedInEffective(putEffectiveAllowlist, providerId)) {\n const source = putEffectiveAllowlist.tenantOverridesActive\n ? 'the effective allowlist (env \u2229 tenant)'\n : 'OM_AI_AVAILABLE_PROVIDERS'\n return NextResponse.json(\n {\n error: `Provider \"${providerId}\" is not in ${source}.`,\n code: 'provider_not_allowlisted',\n },\n { status: 400 },\n )\n }\n if (modelId && !isProviderModelAllowedInEffective(putEffectiveAllowlist, providerId, modelId)) {\n const source = putEffectiveAllowlist.tenantOverridesActive\n ? `the effective allowlist (env \u2229 tenant) for \"${providerId}\"`\n : modelAllowlistEnvVarName(providerId)\n return NextResponse.json(\n {\n error: `Model \"${modelId}\" is not in ${source}.`,\n code: 'model_not_allowlisted',\n },\n { status: 400 },\n )\n }\n } else if (providerId) {\n if (!isProviderAllowed(process.env, providerId)) {\n return NextResponse.json(\n {\n error: `Provider \"${requestedProviderId}\" is not in OM_AI_AVAILABLE_PROVIDERS.`,\n code: 'provider_not_allowlisted',\n },\n { status: 400 },\n )\n }\n if (modelId && !isProviderModelAllowed(process.env, providerId, modelId)) {\n return NextResponse.json(\n {\n error: `Model \"${modelId}\" is not in ${modelAllowlistEnvVarName(providerId)}.`,\n code: 'model_not_allowlisted',\n },\n { status: 400 },\n )\n }\n }\n }\n\n if (hasRuntimeOverrideAllowlistWrite && !agentId) {\n return NextResponse.json(\n {\n error: 'agentId is required when saving chat override allowlist settings.',\n code: 'agent_required',\n },\n { status: 400 },\n )\n }\n\n if (Array.isArray(allowedOverrideProviders)) {\n for (const id of allowedOverrideProviders) {\n if (putEffectiveAllowlist && !isProviderAllowedInEffective(putEffectiveAllowlist, id)) {\n return NextResponse.json(\n {\n error: `Provider \"${id}\" is not in the effective tenant allowlist; per-agent chat override choices may not widen it.`,\n code: 'provider_not_allowlisted',\n },\n { status: 400 },\n )\n }\n }\n }\n if (allowedOverrideModelsByProvider) {\n for (const [id, models] of Object.entries(allowedOverrideModelsByProvider)) {\n if (putEffectiveAllowlist && !isProviderAllowedInEffective(putEffectiveAllowlist, id)) {\n return NextResponse.json(\n {\n error: `Provider \"${id}\" is not in the effective tenant allowlist; cannot save per-agent model choices for it.`,\n code: 'provider_not_allowlisted',\n },\n { status: 400 },\n )\n }\n for (const allowedModelId of models) {\n if (\n putEffectiveAllowlist &&\n !isProviderModelAllowedInEffective(putEffectiveAllowlist, id, allowedModelId)\n ) {\n return NextResponse.json(\n {\n error: `Model \"${allowedModelId}\" is not in the effective tenant allowlist for \"${id}\".`,\n code: 'model_not_allowlisted',\n },\n { status: 400 },\n )\n }\n }\n }\n }\n\n try {\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const canManage = await rbacService.userHasAllFeatures(\n auth.sub,\n ['ai_assistant.settings.manage'],\n { tenantId: auth.tenantId, organizationId: auth.orgId },\n )\n if (!canManage) {\n return NextResponse.json({ error: 'Forbidden', code: 'forbidden' }, { status: 403 })\n }\n\n const em = container.resolve<EntityManager>('em')\n const repo = new AiAgentRuntimeOverrideRepository(em)\n const upsertInput = {\n agentId: agentId ?? null,\n ...(Object.prototype.hasOwnProperty.call(bodyResult.data, 'providerId')\n ? { providerId: providerId ?? null }\n : {}),\n ...(Object.prototype.hasOwnProperty.call(bodyResult.data, 'modelId')\n ? { modelId: modelId ?? null }\n : {}),\n ...(Object.prototype.hasOwnProperty.call(bodyResult.data, 'baseURL')\n ? { baseURL: baseURL ?? null }\n : {}),\n ...(allowedOverrideProviders !== undefined\n ? { allowedOverrideProviders }\n : {}),\n ...(allowedOverrideModelsByProvider !== undefined\n ? { allowedOverrideModelsByProvider }\n : {}),\n ...(Object.prototype.hasOwnProperty.call(bodyResult.data, 'inputModeration')\n ? { inputModeration: bodyResult.data.inputModeration ?? null }\n : {}),\n }\n const row = await repo.upsertDefault(\n upsertInput,\n { tenantId: auth.tenantId ?? '', organizationId: auth.orgId ?? null, userId: auth.sub },\n )\n return NextResponse.json({\n id: row.id,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n agentId: row.agentId,\n providerId: row.providerId,\n modelId: row.modelId,\n baseURL: row.baseUrl,\n allowedOverrideProviders: row.allowedOverrideProviders ?? null,\n allowedOverrideModelsByProvider: row.allowedOverrideModelsByProvider ?? {},\n inputModeration: row.inputModeration ?? null,\n updatedAt: row.updatedAt,\n })\n } catch (error) {\n if (error instanceof AiAgentRuntimeOverrideValidationError) {\n return NextResponse.json({ error: error.message, code: 'provider_unknown' }, { status: 400 })\n }\n logger.error('AI Settings \u2014 PUT error', { err: error })\n return NextResponse.json({ error: 'Failed to save runtime override.' }, { status: 500 })\n }\n}\n\n/**\n * DELETE /api/ai_assistant/settings\n *\n * Soft-deletes the active per-tenant AI runtime override (Phase 4a). Requires\n * `ai_assistant.settings.manage`. Pass `agentId` in the body to clear only\n * the agent-specific row; omit (or null) to clear the tenant-wide default.\n * Idempotent \u2014 returns `{ cleared: false }` when no active row was found.\n */\nexport async function DELETE(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let parsedBody: unknown = {}\n try {\n parsedBody = await req.json()\n } catch {\n // Body is optional for DELETE \u2014 empty body is fine\n }\n\n const bodyResult = runtimeOverrideClearSchema.safeParse(parsedBody)\n if (!bodyResult.success) {\n return NextResponse.json(\n { error: 'Invalid request body.', code: 'validation_error', issues: bodyResult.error.issues },\n { status: 400 },\n )\n }\n\n try {\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const canManage = await rbacService.userHasAllFeatures(\n auth.sub,\n ['ai_assistant.settings.manage'],\n { tenantId: auth.tenantId, organizationId: auth.orgId },\n )\n if (!canManage) {\n return NextResponse.json({ error: 'Forbidden', code: 'forbidden' }, { status: 403 })\n }\n\n const em = container.resolve<EntityManager>('em')\n const repo = new AiAgentRuntimeOverrideRepository(em)\n const cleared = await repo.clearDefault({\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n agentId: bodyResult.data.agentId ?? null,\n })\n return NextResponse.json({ cleared })\n } catch (error) {\n logger.error('AI Settings \u2014 DELETE error', { err: error })\n return NextResponse.json({ error: 'Failed to clear runtime override.' }, { status: 500 })\n }\n}\n"],
4
+ "sourcesContent": ["import { existsSync, readFileSync } from 'node:fs'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\nimport { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { findApiKeyBySecret } from '@open-mercato/core/modules/api_keys/services/apiKeyService'\nimport { llmProviderRegistry } from '../../lib/llm-registry'\nimport { joinProviderModel } from '@open-mercato/shared/lib/ai/model-id'\nimport {\n OPEN_CODE_PROVIDER_IDS,\n OPEN_CODE_PROVIDERS,\n getOpenCodeProviderConfiguredEnvKey,\n isOpenCodeProviderConfigured,\n} from '@open-mercato/shared/lib/ai/opencode-provider'\nimport { AiAgentRuntimeOverrideRepository, AiAgentRuntimeOverrideValidationError } from '../../data/repositories/AiAgentRuntimeOverrideRepository'\nimport { resolveModerationPolicy } from '../../lib/moderation-policy'\nimport { AiTenantModelAllowlistRepository } from '../../data/repositories/AiTenantModelAllowlistRepository'\nimport { isBaseurlAllowlisted, readBaseurlAllowlist } from '../../lib/baseurl-allowlist'\nimport { loadAgentRegistry, listAgents } from '../../lib/agent-registry'\nimport { createModelFactory, resolveAllowRuntimeOverride } from '../../lib/model-factory'\nimport {\n agentOverrideModelAllowlistEnvVarName,\n agentOverrideProviderAllowlistEnvVarName,\n canonicalProviderId,\n hasAllowlistSnapshotRestrictions,\n intersectEffectiveAllowlistWithSnapshot,\n intersectAllowlists,\n isProviderAllowed,\n isProviderAllowedInEffective,\n isProviderModelAllowed,\n isProviderModelAllowedInEffective,\n modelAllowlistEnvVarName,\n readAgentRuntimeOverrideAllowlist,\n readAllowedModels,\n readAllowedProviders,\n readAllowlistConfig,\n type TenantAllowlistSnapshot,\n} from '../../lib/model-allowlist'\n\nconst logger = createLogger('ai_assistant')\n\nfunction modelCatalogWithAllowlistFallback(\n models: ReadonlyArray<{ id: string; name: string; contextWindow?: number | null; tags?: readonly string[] }>,\n allowlistModelIds: string[] | undefined,\n): ReadonlyArray<{ id: string; name: string; contextWindow?: number | null; tags?: readonly string[] }> {\n if (models.length > 0) return models\n return (allowlistModelIds ?? []).map((id) => ({ id, name: id }))\n}\n\nconst runtimeOverrideUpsertSchema = z.object({\n providerId: z.string().min(1).max(64).nullable().optional(),\n modelId: z.string().min(1).max(256).nullable().optional(),\n baseURL: z.string().url().max(2048).nullable().optional(),\n agentId: z.string().min(1).max(128).nullable().optional(),\n allowedOverrideProviders: z.array(z.string().min(1).max(64)).nullable().optional(),\n allowedOverrideModelsByProvider: z\n .record(z.string().min(1).max(64), z.array(z.string().min(1).max(256)))\n .optional(),\n // Input moderation override: true = on, false = off, null = inherit.\n // Spec 2026-06-04-ai-input-moderation-and-safety-identifiers.\n inputModeration: z.boolean().nullable().optional(),\n})\n\nconst runtimeOverrideClearSchema = z.object({\n agentId: z.string().min(1).max(128).nullable().optional(),\n})\n\nexport type RuntimeOverrideUpsertBody = z.infer<typeof runtimeOverrideUpsertSchema>\nexport type RuntimeOverrideClearBody = z.infer<typeof runtimeOverrideClearSchema>\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'AI assistant settings',\n methods: {\n GET: { summary: 'Get AI provider configuration' },\n PUT: {\n summary: 'Upsert per-tenant AI runtime override',\n description:\n 'Creates or updates the per-tenant AI runtime override (provider, model, baseURL). ' +\n 'Optionally scoped to a specific agent via `agentId`. ' +\n 'Gated by `ai_assistant.settings.manage`. ' +\n 'baseURL must match AI_RUNTIME_BASEURL_ALLOWLIST when set.',\n requestBody: {\n contentType: 'application/json',\n description: 'Override payload. All fields nullable/optional; null explicitly clears the axis.',\n schema: runtimeOverrideUpsertSchema,\n },\n responses: [\n { status: 200, description: 'Override saved. Returns the saved row.' },\n ],\n errors: [\n { status: 400, description: 'Validation error: unknown provider, invalid URL, or baseURL not allowlisted.' },\n { status: 401, description: 'Unauthenticated.' },\n { status: 403, description: 'Caller lacks ai_assistant.settings.manage.' },\n ],\n },\n DELETE: {\n summary: 'Clear per-tenant AI runtime override',\n description:\n 'Soft-deletes the active per-tenant runtime override. ' +\n 'Pass `agentId` to clear only the agent-specific row; omit to clear the tenant-wide default. ' +\n 'Gated by `ai_assistant.settings.manage`. Idempotent \u2014 returns 200 with `cleared: false` when no active row existed.',\n requestBody: {\n contentType: 'application/json',\n description: 'Optional agentId to scope the delete.',\n schema: runtimeOverrideClearSchema,\n },\n responses: [\n { status: 200, description: 'Returns `{ cleared: boolean }` indicating whether a row was found and removed.' },\n ],\n errors: [\n { status: 401, description: 'Unauthenticated.' },\n { status: 403, description: 'Caller lacks ai_assistant.settings.manage.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n PUT: { requireAuth: true, requireFeatures: ['ai_assistant.settings.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['ai_assistant.settings.manage'] },\n}\n\n/**\n * GET /api/ai_assistant/settings\n *\n * Returns the current OpenCode provider configuration from environment variables\n * plus the Phase 4a additive fields: resolvedDefault, tenantOverride, agents[],\n * and availableProviders[].defaultModels.\n */\nexport async function GET(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n try {\n const env = process.env as Record<string, string | undefined>\n const configuredProviderHint = env.OM_AI_PROVIDER?.trim() || env.OPENCODE_PROVIDER?.trim() || null\n const registryProviders = llmProviderRegistry.list()\n const knownProviderIdsForAllowlist: string[] = [\n ...OPEN_CODE_PROVIDER_IDS,\n ...registryProviders\n .map((p) => p.id)\n .filter((id) => !(OPEN_CODE_PROVIDER_IDS as readonly string[]).includes(id)),\n ]\n const registryProviderId = configuredProviderHint\n ? canonicalProviderId(configuredProviderHint, registryProviders.map((provider) => provider.id))\n : null\n const registryProvider = registryProviderId ? llmProviderRegistry.get(registryProviderId) : null\n const fallbackOpenCodeProviderId = (\n (configuredProviderHint\n ? canonicalProviderId(configuredProviderHint, OPEN_CODE_PROVIDER_IDS as readonly string[])\n : null) ?? 'openai'\n ) as keyof typeof OPEN_CODE_PROVIDERS\n const fallbackOpenCodeProvider = OPEN_CODE_PROVIDERS[fallbackOpenCodeProviderId]\n\n const providerId = registryProvider?.id ?? fallbackOpenCodeProviderId\n const providerName = registryProvider?.name ?? fallbackOpenCodeProvider?.name ?? providerId\n const defaultProviderModel = registryProvider?.defaultModel ?? fallbackOpenCodeProvider?.defaultModel ?? ''\n const configuredModelHint = env.OM_AI_MODEL?.trim() || env.OPENCODE_MODEL?.trim() || defaultProviderModel\n const fallbackModelWithProvider = joinProviderModel(providerId, configuredModelHint)\n const apiKeyConfigured = registryProvider\n ? registryProvider.isConfigured(env)\n : fallbackOpenCodeProvider\n ? isOpenCodeProviderConfigured(fallbackOpenCodeProviderId)\n : false\n const displayEnvKey = registryProvider\n ? registryProvider.getConfiguredEnvKey(env)\n : fallbackOpenCodeProvider\n ? getOpenCodeProviderConfiguredEnvKey(fallbackOpenCodeProviderId)\n : null\n\n // Check if the MCP API key is configured (required for MCP authentication):\n // either directly via MCP_SERVER_API_KEY or via the shared key file that\n // the containerized mcp service provisions (MCP_SERVER_API_KEY_FILE).\n // The file variant is validated against the api_keys table \u2014 the file\n // outlives DB resets and key deletion, so mere existence would report a\n // green badge while OpenCode -> MCP auth is actually broken.\n const mcpKeyFilePath = process.env.MCP_SERVER_API_KEY_FILE?.trim()\n let mcpKeyConfigured = !!process.env.MCP_SERVER_API_KEY?.trim()\n if (!mcpKeyConfigured && mcpKeyFilePath && existsSync(mcpKeyFilePath)) {\n try {\n const fileSecret = readFileSync(mcpKeyFilePath, 'utf8').trim()\n if (fileSecret.startsWith('omk_')) {\n const keyContainer = await createRequestContainer()\n const keyEm = keyContainer.resolve<EntityManager>('em')\n mcpKeyConfigured = !!(await findApiKeyBySecret(keyEm, fileSecret))\n }\n } catch {\n mcpKeyConfigured = false\n }\n }\n\n // Phase 4a: resolve tenant override row and per-agent resolution matrix\n let tenantOverride: {\n providerId: string | null\n modelId: string | null\n baseURL: string | null\n agentId: string | null\n updatedAt: string\n } | null = null\n\n let agentResolutions: Array<{\n agentId: string\n moduleId: string\n allowRuntimeOverride: boolean\n allowRuntimeModelOverride: boolean\n codeDefaultProviderId: string | null\n codeDefaultModelId: string | null\n override: {\n providerId: string | null\n modelId: string | null\n baseURL: string | null\n updatedAt: string\n } | null\n runtimeOverrideAllowlist: {\n env: TenantAllowlistSnapshot | null\n tenant: TenantAllowlistSnapshot | null\n effective: ReturnType<typeof intersectAllowlists>\n envVarNames: {\n providers: string\n modelsByProvider: Record<string, string>\n }\n }\n providerId: string\n modelId: string\n baseURL: string | null\n source: string\n }> = []\n\n let resolvedDefault: {\n providerId: string\n modelId: string\n baseURL: string | null\n source: string\n } | null = null\n\n let tenantAllowlistSnapshot: TenantAllowlistSnapshot | null = null\n\n try {\n const container = await createRequestContainer()\n const tenantId = auth.tenantId ?? null\n const organizationId = auth.orgId ?? null\n\n if (tenantId) {\n const em = container.resolve<EntityManager>('em')\n const repo = new AiAgentRuntimeOverrideRepository(em)\n const overrideRow = await repo.getDefault({ tenantId, organizationId, agentId: null })\n const tenantWideInputModeration = overrideRow?.inputModeration ?? null\n if (overrideRow) {\n tenantOverride = {\n providerId: overrideRow.providerId ?? null,\n modelId: overrideRow.modelId ?? null,\n baseURL: overrideRow.baseUrl ?? null,\n agentId: overrideRow.agentId ?? null,\n updatedAt: overrideRow.updatedAt.toISOString(),\n }\n }\n\n const allowlistRepo = new AiTenantModelAllowlistRepository(em)\n tenantAllowlistSnapshot = await allowlistRepo.getSnapshot({\n tenantId,\n organizationId,\n })\n\n const factory = createModelFactory(container)\n const defaultResolution = factory.resolveModel({\n tenantAllowlist: tenantAllowlistSnapshot,\n tenantOverride: tenantOverride\n ? { providerId: tenantOverride.providerId, modelId: tenantOverride.modelId, baseURL: tenantOverride.baseURL }\n : undefined,\n })\n resolvedDefault = {\n providerId: defaultResolution.providerId,\n modelId: defaultResolution.modelId,\n baseURL: defaultResolution.baseURL ?? null,\n source: defaultResolution.source,\n }\n\n await loadAgentRegistry()\n const agents = listAgents()\n const agentResolutionPromises = agents.map(async (agent) => {\n const agentOverrideRow = await repo.getExact({\n tenantId,\n organizationId,\n agentId: agent.id,\n })\n const agentTenantOverride = agentOverrideRow\n ? {\n providerId: agentOverrideRow.providerId ?? null,\n modelId: agentOverrideRow.modelId ?? null,\n baseURL: agentOverrideRow.baseUrl ?? null,\n }\n : (tenantOverride ?? undefined)\n const agentResolution = factory.resolveModel({\n moduleId: agent.moduleId,\n agentDefaultModel: agent.defaultModel,\n agentDefaultProvider: agent.defaultProvider,\n agentDefaultBaseUrl: agent.defaultBaseUrl,\n allowRuntimeOverride: resolveAllowRuntimeOverride(agent),\n tenantOverride: agentTenantOverride,\n tenantAllowlist: tenantAllowlistSnapshot,\n })\n const agentEnvAllowlist = readAgentRuntimeOverrideAllowlist(\n env,\n agent.id,\n knownProviderIdsForAllowlist,\n )\n const agentTenantAllowlist = agentOverrideRow\n ? {\n allowedProviders: agentOverrideRow.allowedOverrideProviders ?? null,\n allowedModelsByProvider: agentOverrideRow.allowedOverrideModelsByProvider ?? {},\n }\n : null\n const baseEffectiveAllowlist = intersectAllowlists(\n env,\n knownProviderIdsForAllowlist,\n tenantAllowlistSnapshot,\n )\n const agentEffectiveAllowlist = intersectEffectiveAllowlistWithSnapshot(\n intersectEffectiveAllowlistWithSnapshot(\n baseEffectiveAllowlist,\n knownProviderIdsForAllowlist,\n agentEnvAllowlist,\n ),\n knownProviderIdsForAllowlist,\n agentTenantAllowlist,\n )\n const agentModelEnvVars = Object.fromEntries(\n knownProviderIdsForAllowlist.map((providerId) => [\n providerId,\n agentOverrideModelAllowlistEnvVarName(agent.id, providerId),\n ]),\n )\n return {\n agentId: agent.id,\n moduleId: agent.moduleId,\n allowRuntimeOverride: resolveAllowRuntimeOverride(agent),\n allowRuntimeModelOverride: resolveAllowRuntimeOverride(agent),\n codeDefaultProviderId: agent.defaultProvider ?? null,\n codeDefaultModelId: agent.defaultModel ?? null,\n override: agentOverrideRow\n ? {\n providerId: agentOverrideRow.providerId ?? null,\n modelId: agentOverrideRow.modelId ?? null,\n baseURL: agentOverrideRow.baseUrl ?? null,\n updatedAt: agentOverrideRow.updatedAt.toISOString(),\n }\n : null,\n runtimeOverrideAllowlist: {\n env: agentEnvAllowlist,\n tenant: hasAllowlistSnapshotRestrictions(agentTenantAllowlist)\n ? agentTenantAllowlist\n : null,\n effective: agentEffectiveAllowlist,\n envVarNames: {\n providers: agentOverrideProviderAllowlistEnvVarName(agent.id),\n modelsByProvider: agentModelEnvVars,\n },\n },\n providerId: agentResolution.providerId,\n modelId: agentResolution.modelId,\n baseURL: agentResolution.baseURL ?? null,\n source: agentResolution.source,\n moderation: {\n // `untrustedInput` agents enforce moderation; the UI renders a\n // non-editable \"Enforced\" badge for them.\n enforced: agent.untrustedInput === true,\n // Per-agent control value: null = inherit, true = on, false = off.\n override: agentOverrideRow?.inputModeration ?? null,\n // Resolved runtime policy (enforced | on | off) after precedence.\n effective: resolveModerationPolicy({\n untrustedInput: agent.untrustedInput,\n perAgentOverride: agentOverrideRow?.inputModeration ?? null,\n tenantWideOverride: tenantWideInputModeration,\n env,\n }),\n },\n }\n })\n agentResolutions = await Promise.all(agentResolutionPromises)\n }\n } catch (overrideError) {\n // Phase 4a fields are best-effort \u2014 log and continue returning the base response\n logger.warn('AI Settings \u2014 Failed to compute Phase 4a override fields', { err: overrideError })\n }\n\n // Build availableProviders with Phase 4a defaultModels, then clip to the\n // EFFECTIVE allowlist \u2014 env intersected with the per-tenant snapshot.\n // The env allowlist is the OUTER constraint; the tenant allowlist (Phase\n // 1780-6) narrows it further. The settings UI must never offer a value\n // the runtime would refuse to honor.\n const allowlistConfig = readAllowlistConfig(env, knownProviderIdsForAllowlist)\n const effectiveAllowlist = intersectAllowlists(\n env,\n knownProviderIdsForAllowlist,\n tenantAllowlistSnapshot,\n )\n\n const allRawProviders = [\n ...OPEN_CODE_PROVIDER_IDS.map((id) => {\n const info = OPEN_CODE_PROVIDERS[id]\n const registryProvider = llmProviderRegistry.get(id)\n return {\n id,\n name: info.name,\n defaultModel: info.defaultModel,\n envKey: getOpenCodeProviderConfiguredEnvKey(id),\n configured: isOpenCodeProviderConfigured(id),\n defaultModels: registryProvider?.defaultModels ?? [],\n }\n }),\n // Also surface any llmProviderRegistry providers not in OPEN_CODE_PROVIDER_IDS\n ...llmProviderRegistry.list()\n .filter((p) => !(OPEN_CODE_PROVIDER_IDS as readonly string[]).includes(p.id))\n .map((p) => ({\n id: p.id,\n name: p.name,\n defaultModel: p.defaultModels[0]?.id ?? '',\n envKey: null,\n configured: p.isConfigured(),\n defaultModels: p.defaultModels,\n })),\n ]\n\n const availableProviders = allRawProviders\n .filter((p) => isProviderAllowedInEffective(effectiveAllowlist, p.id))\n .map((p) => {\n const effectiveModelsList = effectiveAllowlist.modelsByProvider[p.id]\n const catalogModels = modelCatalogWithAllowlistFallback(\n p.defaultModels,\n effectiveModelsList,\n )\n const clippedDefaults = effectiveModelsList !== undefined\n ? catalogModels.filter((m) => effectiveModelsList.includes(m.id))\n : catalogModels\n return {\n ...p,\n defaultModel: effectiveModelsList && !effectiveModelsList.includes(p.defaultModel)\n ? (effectiveModelsList[0] ?? p.defaultModel)\n : p.defaultModel || clippedDefaults[0]?.id || '',\n defaultModels: clippedDefaults,\n }\n })\n\n const allowlistProviders = allRawProviders\n .filter((p) => isProviderAllowed(env, p.id))\n .map((p) => {\n const envModelsList = allowlistConfig.modelsByProvider[p.id]\n const catalogModels = modelCatalogWithAllowlistFallback(\n p.defaultModels,\n envModelsList,\n )\n const envClippedDefaults = envModelsList !== undefined\n ? catalogModels.filter((m) => envModelsList.includes(m.id))\n : catalogModels\n return {\n ...p,\n defaultModel: envModelsList && !envModelsList.includes(p.defaultModel)\n ? (envModelsList[0] ?? p.defaultModel)\n : p.defaultModel || envClippedDefaults[0]?.id || '',\n defaultModels: envClippedDefaults,\n }\n })\n\n return NextResponse.json({\n provider: {\n id: providerId,\n name: providerName,\n model: resolvedDefault\n ? joinProviderModel(resolvedDefault.providerId, resolvedDefault.modelId)\n : fallbackModelWithProvider,\n defaultModel: defaultProviderModel,\n envKey: displayEnvKey,\n configured: apiKeyConfigured,\n },\n availableProviders,\n // Editable universe for the tenant allowlist page. This is clipped only\n // by env so tenant-hidden models remain visible and can be re-enabled.\n allowlistProviders,\n // Snapshot of the env-driven allowlist so the UI can render hints like\n // \"limited to: openai, anthropic\" without re-implementing the parser.\n allowlist: allowlistConfig,\n // Per-tenant allowlist snapshot (Phase 1780-6). `null` when no row has\n // been persisted yet \u2014 the runtime then falls back to env-only\n // enforcement. The UI uses this to drive the editable MultiSelect.\n tenantAllowlist: tenantAllowlistSnapshot,\n // Effective allowlist after intersecting env with tenant. The UI uses\n // this to render the \"what the runtime will actually accept\" summary\n // and to clip pickers without re-implementing the intersection.\n effectiveAllowlist,\n mcpKeyConfigured,\n resolvedDefault,\n tenantOverride,\n agents: agentResolutions,\n })\n } catch (error) {\n logger.error('AI Settings \u2014 GET error', { err: error })\n return NextResponse.json({ error: 'Failed to fetch settings' }, { status: 500 })\n }\n}\n\n/**\n * PUT /api/ai_assistant/settings\n *\n * Upserts the per-tenant AI runtime override (Phase 4a). Requires\n * `ai_assistant.settings.manage`. The body is Zod-validated; a `baseURL`\n * must match `AI_RUNTIME_BASEURL_ALLOWLIST` when that env var is set.\n */\nexport async function PUT(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let parsedBody: unknown\n try {\n parsedBody = await req.json()\n } catch {\n return NextResponse.json({ error: 'Request body must be valid JSON.', code: 'validation_error' }, { status: 400 })\n }\n\n const bodyResult = runtimeOverrideUpsertSchema.safeParse(parsedBody)\n if (!bodyResult.success) {\n return NextResponse.json(\n { error: 'Invalid request body.', code: 'validation_error', issues: bodyResult.error.issues },\n { status: 400 },\n )\n }\n\n const { providerId: requestedProviderId, modelId, baseURL, agentId } = bodyResult.data\n const knownProviderIdsForRequest = llmProviderRegistry.list().map((p) => p.id)\n const providerId = requestedProviderId\n ? canonicalProviderId(requestedProviderId, knownProviderIdsForRequest) ?? requestedProviderId\n : requestedProviderId\n\n if (baseURL && baseURL.trim().length > 0) {\n const allowlist = readBaseurlAllowlist()\n if (!isBaseurlAllowlisted(baseURL.trim(), allowlist)) {\n return NextResponse.json(\n {\n error: `baseURL \"${baseURL}\" is not in AI_RUNTIME_BASEURL_ALLOWLIST.`,\n code: 'baseurl_not_allowlisted',\n },\n { status: 400 },\n )\n }\n }\n\n const allowedOverrideProviders = bodyResult.data.allowedOverrideProviders === undefined\n ? undefined\n : bodyResult.data.allowedOverrideProviders?.map((id) =>\n canonicalProviderId(id, knownProviderIdsForRequest) ?? id,\n ) ?? null\n const allowedOverrideModelsByProvider = bodyResult.data.allowedOverrideModelsByProvider === undefined\n ? undefined\n : Object.fromEntries(\n Object.entries(bodyResult.data.allowedOverrideModelsByProvider ?? {}).map(([id, models]) => [\n canonicalProviderId(id, knownProviderIdsForRequest) ?? id,\n models,\n ]),\n )\n const hasRuntimeOverrideAllowlistWrite =\n allowedOverrideProviders !== undefined || allowedOverrideModelsByProvider !== undefined\n\n // Env-driven provider/model allowlist (Phase 1780-5) intersected with the\n // per-tenant allowlist (Phase 1780-6): the EFFECTIVE allowlist clips which\n // (provider, model) pairs the runtime accepts. Reject settings PUT requests\n // for pairs outside that effective set so the settings UI never persists a\n // value the runtime would later refuse. Tenant-allowlist enforcement is\n // best-effort here: if the snapshot lookup fails we fall back to env-only\n // checks (the runtime still re-clips at resolution time).\n let putEffectiveAllowlist: ReturnType<typeof intersectAllowlists> | null = null\n if (providerId || hasRuntimeOverrideAllowlistWrite) {\n try {\n const previewContainer = await createRequestContainer()\n const knownIdsForCheck = [\n ...OPEN_CODE_PROVIDER_IDS,\n ...llmProviderRegistry\n .list()\n .map((p) => p.id)\n .filter((id) => !(OPEN_CODE_PROVIDER_IDS as readonly string[]).includes(id)),\n ]\n let snapshot: TenantAllowlistSnapshot | null = null\n if (auth.tenantId) {\n try {\n const em = previewContainer.resolve<EntityManager>('em')\n const allowlistRepo = new AiTenantModelAllowlistRepository(em)\n snapshot = await allowlistRepo.getSnapshot({\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n })\n } catch {\n snapshot = null\n }\n }\n putEffectiveAllowlist = intersectAllowlists(\n process.env as Record<string, string | undefined>,\n knownIdsForCheck,\n snapshot,\n )\n } catch {\n putEffectiveAllowlist = null\n }\n\n if (providerId && putEffectiveAllowlist) {\n if (!isProviderAllowedInEffective(putEffectiveAllowlist, providerId)) {\n const source = putEffectiveAllowlist.tenantOverridesActive\n ? 'the effective allowlist (env \u2229 tenant)'\n : 'OM_AI_AVAILABLE_PROVIDERS'\n return NextResponse.json(\n {\n error: `Provider \"${providerId}\" is not in ${source}.`,\n code: 'provider_not_allowlisted',\n },\n { status: 400 },\n )\n }\n if (modelId && !isProviderModelAllowedInEffective(putEffectiveAllowlist, providerId, modelId)) {\n const source = putEffectiveAllowlist.tenantOverridesActive\n ? `the effective allowlist (env \u2229 tenant) for \"${providerId}\"`\n : modelAllowlistEnvVarName(providerId)\n return NextResponse.json(\n {\n error: `Model \"${modelId}\" is not in ${source}.`,\n code: 'model_not_allowlisted',\n },\n { status: 400 },\n )\n }\n } else if (providerId) {\n if (!isProviderAllowed(process.env, providerId)) {\n return NextResponse.json(\n {\n error: `Provider \"${requestedProviderId}\" is not in OM_AI_AVAILABLE_PROVIDERS.`,\n code: 'provider_not_allowlisted',\n },\n { status: 400 },\n )\n }\n if (modelId && !isProviderModelAllowed(process.env, providerId, modelId)) {\n return NextResponse.json(\n {\n error: `Model \"${modelId}\" is not in ${modelAllowlistEnvVarName(providerId)}.`,\n code: 'model_not_allowlisted',\n },\n { status: 400 },\n )\n }\n }\n }\n\n if (hasRuntimeOverrideAllowlistWrite && !agentId) {\n return NextResponse.json(\n {\n error: 'agentId is required when saving chat override allowlist settings.',\n code: 'agent_required',\n },\n { status: 400 },\n )\n }\n\n if (Array.isArray(allowedOverrideProviders)) {\n for (const id of allowedOverrideProviders) {\n if (putEffectiveAllowlist && !isProviderAllowedInEffective(putEffectiveAllowlist, id)) {\n return NextResponse.json(\n {\n error: `Provider \"${id}\" is not in the effective tenant allowlist; per-agent chat override choices may not widen it.`,\n code: 'provider_not_allowlisted',\n },\n { status: 400 },\n )\n }\n }\n }\n if (allowedOverrideModelsByProvider) {\n for (const [id, models] of Object.entries(allowedOverrideModelsByProvider)) {\n if (putEffectiveAllowlist && !isProviderAllowedInEffective(putEffectiveAllowlist, id)) {\n return NextResponse.json(\n {\n error: `Provider \"${id}\" is not in the effective tenant allowlist; cannot save per-agent model choices for it.`,\n code: 'provider_not_allowlisted',\n },\n { status: 400 },\n )\n }\n for (const allowedModelId of models) {\n if (\n putEffectiveAllowlist &&\n !isProviderModelAllowedInEffective(putEffectiveAllowlist, id, allowedModelId)\n ) {\n return NextResponse.json(\n {\n error: `Model \"${allowedModelId}\" is not in the effective tenant allowlist for \"${id}\".`,\n code: 'model_not_allowlisted',\n },\n { status: 400 },\n )\n }\n }\n }\n }\n\n try {\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const canManage = await rbacService.userHasAllFeatures(\n auth.sub,\n ['ai_assistant.settings.manage'],\n { tenantId: auth.tenantId, organizationId: auth.orgId },\n )\n if (!canManage) {\n return NextResponse.json({ error: 'Forbidden', code: 'forbidden' }, { status: 403 })\n }\n\n const em = container.resolve<EntityManager>('em')\n const repo = new AiAgentRuntimeOverrideRepository(em)\n const upsertInput = {\n agentId: agentId ?? null,\n ...(Object.prototype.hasOwnProperty.call(bodyResult.data, 'providerId')\n ? { providerId: providerId ?? null }\n : {}),\n ...(Object.prototype.hasOwnProperty.call(bodyResult.data, 'modelId')\n ? { modelId: modelId ?? null }\n : {}),\n ...(Object.prototype.hasOwnProperty.call(bodyResult.data, 'baseURL')\n ? { baseURL: baseURL ?? null }\n : {}),\n ...(allowedOverrideProviders !== undefined\n ? { allowedOverrideProviders }\n : {}),\n ...(allowedOverrideModelsByProvider !== undefined\n ? { allowedOverrideModelsByProvider }\n : {}),\n ...(Object.prototype.hasOwnProperty.call(bodyResult.data, 'inputModeration')\n ? { inputModeration: bodyResult.data.inputModeration ?? null }\n : {}),\n }\n const row = await repo.upsertDefault(\n upsertInput,\n { tenantId: auth.tenantId ?? '', organizationId: auth.orgId ?? null, userId: auth.sub },\n )\n return NextResponse.json({\n id: row.id,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n agentId: row.agentId,\n providerId: row.providerId,\n modelId: row.modelId,\n baseURL: row.baseUrl,\n allowedOverrideProviders: row.allowedOverrideProviders ?? null,\n allowedOverrideModelsByProvider: row.allowedOverrideModelsByProvider ?? {},\n inputModeration: row.inputModeration ?? null,\n updatedAt: row.updatedAt,\n })\n } catch (error) {\n if (error instanceof AiAgentRuntimeOverrideValidationError) {\n return NextResponse.json({ error: error.message, code: 'provider_unknown' }, { status: 400 })\n }\n logger.error('AI Settings \u2014 PUT error', { err: error })\n return NextResponse.json({ error: 'Failed to save runtime override.' }, { status: 500 })\n }\n}\n\n/**\n * DELETE /api/ai_assistant/settings\n *\n * Soft-deletes the active per-tenant AI runtime override (Phase 4a). Requires\n * `ai_assistant.settings.manage`. Pass `agentId` in the body to clear only\n * the agent-specific row; omit (or null) to clear the tenant-wide default.\n * Idempotent \u2014 returns `{ cleared: false }` when no active row was found.\n */\nexport async function DELETE(req: NextRequest) {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n let parsedBody: unknown = {}\n try {\n parsedBody = await req.json()\n } catch {\n // Body is optional for DELETE \u2014 empty body is fine\n }\n\n const bodyResult = runtimeOverrideClearSchema.safeParse(parsedBody)\n if (!bodyResult.success) {\n return NextResponse.json(\n { error: 'Invalid request body.', code: 'validation_error', issues: bodyResult.error.issues },\n { status: 400 },\n )\n }\n\n try {\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const canManage = await rbacService.userHasAllFeatures(\n auth.sub,\n ['ai_assistant.settings.manage'],\n { tenantId: auth.tenantId, organizationId: auth.orgId },\n )\n if (!canManage) {\n return NextResponse.json({ error: 'Forbidden', code: 'forbidden' }, { status: 403 })\n }\n\n const em = container.resolve<EntityManager>('em')\n const repo = new AiAgentRuntimeOverrideRepository(em)\n const cleared = await repo.clearDefault({\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n agentId: bodyResult.data.agentId ?? null,\n })\n return NextResponse.json({ cleared })\n } catch (error) {\n logger.error('AI Settings \u2014 DELETE error', { err: error })\n return NextResponse.json({ error: 'Failed to clear runtime override.' }, { status: 500 })\n }\n}\n"],
5
5
  "mappings": "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",
6
6
  "names": ["providerId", "registryProvider"]
7
7
  }
@@ -1,4 +1,4 @@
1
- import { llmProviderRegistry } from "@open-mercato/shared/lib/ai/llm-provider-registry";
1
+ import { llmProviderRegistry } from "../../lib/llm-registry.js";
2
2
  import { canonicalProviderId } from "../../lib/model-allowlist.js";
3
3
  import { AiAgentRuntimeOverride } from "../entities.js";
4
4
  class AiAgentRuntimeOverrideRepository {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.ts"],
4
- "sourcesContent": ["import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport { canonicalProviderId } from '../../lib/model-allowlist'\nimport { AiAgentRuntimeOverride } from '../entities'\nimport type { AiAgentLoopStopCondition } from '../../lib/ai-agent-definition'\n\nexport interface AiAgentRuntimeOverrideContext {\n tenantId: string\n organizationId?: string | null\n userId?: string | null\n}\n\nexport interface AiAgentRuntimeOverrideLoopInput {\n /** Kill switch \u2014 when true, runtime forces stepCountIs(1). */\n loopDisabled?: boolean | null\n /** Override loop.maxSteps. */\n loopMaxSteps?: number | null\n /** Override loop.budget.maxToolCalls. */\n loopMaxToolCalls?: number | null\n /** Override loop.budget.maxWallClockMs. */\n loopMaxWallClockMs?: number | null\n /** Override loop.budget.maxTokens. */\n loopMaxTokens?: number | null\n /** Override loop.stopWhen \u2014 JSON-safe variants only (stepCount, hasToolCall). */\n loopStopWhenJson?: AiAgentLoopStopCondition[] | null\n /** Override loop.activeTools \u2014 must be a subset of agent.allowedTools. */\n loopActiveToolsJson?: string[] | null\n}\n\nexport interface AiAgentRuntimeOverrideInput extends AiAgentRuntimeOverrideLoopInput {\n /** null means tenant-wide default (no agent pinning). */\n agentId?: string | null\n providerId?: string | null\n modelId?: string | null\n baseURL?: string | null\n allowedOverrideProviders?: string[] | null\n allowedOverrideModelsByProvider?: Record<string, string[]>\n /** Input-moderation override: true = on, false = off, null = inherit. */\n inputModeration?: boolean | null\n /**\n * Optional: the agent's declared allowedTools. When provided, loopActiveToolsJson\n * is validated to be a subset. When omitted, allowlist validation is skipped\n * (write-time defense only; the runtime re-validates at read time).\n */\n agentAllowedTools?: string[]\n}\n\n/**\n * Repository for per-tenant AI runtime overrides (Phase 4a of spec\n * `2026-04-27-ai-agents-provider-model-baseurl-overrides`).\n *\n * All reads MUST filter by `tenant_id` first. The three public methods follow\n * the same fail-safe pattern as sibling repositories: `getDefault` returns\n * null on missing tenant context; `upsertDefault` validates provider id at\n * write time; `clearDefault` soft-deletes via `deleted_at`.\n *\n * Resolution precedence returned by `getDefault`:\n * 1. Agent-specific row (non-null `agent_id`) when `agentId` is provided.\n * 2. Tenant-wide row (`agent_id IS NULL`) for the same `(tenant_id, org_id)`.\n * Both rows are always scoped to the caller's `tenant_id` \u2014 cross-tenant\n * reads are impossible because every query filters `WHERE tenant_id = ?`.\n */\nexport class AiAgentRuntimeOverrideRepository {\n constructor(private readonly em: EntityManager) {}\n\n /**\n * Returns the most-specific active runtime override for the given context.\n *\n * When `agentId` is provided, an agent-specific row takes precedence over a\n * tenant-wide null-agent row. Returns null when neither exists.\n *\n * Never returns a row with `deleted_at IS NOT NULL`.\n */\n async getDefault(ctx: {\n tenantId: string\n organizationId?: string | null\n agentId?: string | null\n }): Promise<AiAgentRuntimeOverride | null> {\n if (!ctx?.tenantId) return null\n\n const orgFilter = ctx.organizationId ?? null\n\n // Try agent-specific first when caller provided an agentId.\n if (ctx.agentId) {\n const agentRow = await this.em.findOne(AiAgentRuntimeOverride, {\n tenantId: ctx.tenantId,\n organizationId: orgFilter,\n agentId: ctx.agentId,\n deletedAt: null,\n } satisfies FilterQuery<AiAgentRuntimeOverride>)\n if (agentRow) return agentRow\n }\n\n // Fall back to tenant-wide row (agentId = null).\n const tenantRow = await this.em.findOne(AiAgentRuntimeOverride, {\n tenantId: ctx.tenantId,\n organizationId: orgFilter,\n agentId: null,\n deletedAt: null,\n } satisfies FilterQuery<AiAgentRuntimeOverride>)\n return tenantRow ?? null\n }\n\n async getExact(ctx: {\n tenantId: string\n organizationId?: string | null\n agentId?: string | null\n }): Promise<AiAgentRuntimeOverride | null> {\n if (!ctx?.tenantId) return null\n const row = await this.em.findOne(AiAgentRuntimeOverride, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n agentId: ctx.agentId ?? null,\n deletedAt: null,\n } satisfies FilterQuery<AiAgentRuntimeOverride>)\n return row ?? null\n }\n\n /**\n * Validates and normalizes the loop override fields from an input object.\n * Throws `AiAgentRuntimeOverrideValidationError` with code\n * `invalid_loop_override` for any validation failure.\n *\n * Validation rules (Phase 3 \u2014 R5 mitigation):\n * - `loopStopWhenJson`: all items must have kind `stepCount` or `hasToolCall`.\n * Items with kind `custom` are rejected \u2014 they cannot be stored as JSON.\n * - `loopActiveToolsJson`: when `agentAllowedTools` is provided, every entry\n * must be in that allowlist.\n */\n private validateLoopInput(input: AiAgentRuntimeOverrideInput): void {\n if (input.loopStopWhenJson != null) {\n if (!Array.isArray(input.loopStopWhenJson)) {\n throw new AiAgentRuntimeOverrideValidationError(\n 'loopStopWhenJson must be an array of stop condition objects.',\n 'invalid_loop_override',\n )\n }\n for (const item of input.loopStopWhenJson) {\n if (!item || typeof item !== 'object' || !('kind' in item)) {\n throw new AiAgentRuntimeOverrideValidationError(\n 'loopStopWhenJson items must have a \"kind\" field.',\n 'invalid_loop_override',\n )\n }\n const kind = (item as AiAgentLoopStopCondition).kind\n if (kind === 'custom') {\n throw new AiAgentRuntimeOverrideValidationError(\n 'loopStopWhenJson does not support kind \"custom\" \u2014 only \"stepCount\" and \"hasToolCall\" are JSON-safe and storable.',\n 'invalid_loop_override',\n )\n }\n if (kind !== 'stepCount' && kind !== 'hasToolCall') {\n throw new AiAgentRuntimeOverrideValidationError(\n `loopStopWhenJson contains unknown kind \"${String(kind)}\". Allowed: \"stepCount\", \"hasToolCall\".`,\n 'invalid_loop_override',\n )\n }\n if (kind === 'stepCount' && typeof (item as { count?: unknown }).count !== 'number') {\n throw new AiAgentRuntimeOverrideValidationError(\n 'loopStopWhenJson stepCount item must have a numeric \"count\" field.',\n 'invalid_loop_override',\n )\n }\n if (kind === 'hasToolCall' && typeof (item as { toolName?: unknown }).toolName !== 'string') {\n throw new AiAgentRuntimeOverrideValidationError(\n 'loopStopWhenJson hasToolCall item must have a string \"toolName\" field.',\n 'invalid_loop_override',\n )\n }\n }\n }\n\n if (input.loopActiveToolsJson != null) {\n if (!Array.isArray(input.loopActiveToolsJson)) {\n throw new AiAgentRuntimeOverrideValidationError(\n 'loopActiveToolsJson must be an array of tool name strings.',\n 'invalid_loop_override',\n )\n }\n for (const name of input.loopActiveToolsJson) {\n if (typeof name !== 'string' || name.length === 0) {\n throw new AiAgentRuntimeOverrideValidationError(\n 'loopActiveToolsJson entries must be non-empty strings.',\n 'invalid_loop_override',\n )\n }\n }\n if (input.agentAllowedTools && input.agentAllowedTools.length > 0) {\n const outsideAllowlist = input.loopActiveToolsJson.filter(\n (name) => !input.agentAllowedTools!.includes(name),\n )\n if (outsideAllowlist.length > 0) {\n throw new AiAgentRuntimeOverrideValidationError(\n `loopActiveToolsJson contains tools outside the agent's allowedTools: ${outsideAllowlist.join(', ')}.`,\n 'invalid_loop_override',\n )\n }\n }\n }\n }\n\n /**\n * Inserts or updates the runtime override for the given context.\n *\n * Validates `providerId` against the registry at write time so an admin\n * cannot save a typo (Phase 1.4 contract re-applied per spec \u00A7Data Models).\n * An unknown provider id throws a typed error.\n *\n * Also validates loop override fields (R5 mitigation \u2014 Phase 3):\n * - `loopStopWhenJson` items must use only JSON-safe kinds.\n * - `loopActiveToolsJson` items must be a subset of `agentAllowedTools`\n * when that is provided.\n *\n * The R6 base-URL allowlist check is intentionally NOT performed here \u2014\n * that enforcement lives at the HTTP layer (PUT settings route). The\n * repository trusts that callers have already validated the value.\n */\n async upsertDefault(\n input: AiAgentRuntimeOverrideInput,\n ctx: AiAgentRuntimeOverrideContext,\n ): Promise<AiAgentRuntimeOverride> {\n if (!ctx?.tenantId) {\n throw new Error('AiAgentRuntimeOverrideRepository.upsertDefault requires tenantId')\n }\n\n const normalizedProviderId = input.providerId\n ? canonicalProviderId(input.providerId, llmProviderRegistry.list().map((p) => p.id))\n : null\n if (input.providerId) {\n const knownProvider = normalizedProviderId ? llmProviderRegistry.get(normalizedProviderId) : null\n if (!knownProvider) {\n throw new AiAgentRuntimeOverrideValidationError(\n `Unknown provider id \"${input.providerId}\". Registered provider ids: ${llmProviderRegistry.list().map((p) => p.id).join(', ')}.`,\n )\n }\n }\n\n this.validateLoopInput(input)\n\n const orgFilter = ctx.organizationId ?? null\n const agentIdFilter = input.agentId ?? null\n const hasProviderId = Object.prototype.hasOwnProperty.call(input, 'providerId')\n const hasModelId = Object.prototype.hasOwnProperty.call(input, 'modelId')\n const hasBaseURL = Object.prototype.hasOwnProperty.call(input, 'baseURL')\n const hasAllowedOverrideProviders = Object.prototype.hasOwnProperty.call(input, 'allowedOverrideProviders')\n const hasAllowedOverrideModels = Object.prototype.hasOwnProperty.call(input, 'allowedOverrideModelsByProvider')\n\n return this.em.transactional(async (tx) => {\n const existing = await tx.findOne(AiAgentRuntimeOverride, {\n tenantId: ctx.tenantId,\n organizationId: orgFilter,\n agentId: agentIdFilter,\n deletedAt: null,\n } satisfies FilterQuery<AiAgentRuntimeOverride>)\n\n if (existing) {\n if (hasProviderId) existing.providerId = normalizedProviderId\n if (hasModelId) existing.modelId = input.modelId ?? null\n if (hasBaseURL) existing.baseUrl = input.baseURL ?? null\n if (hasAllowedOverrideProviders) {\n existing.allowedOverrideProviders = input.allowedOverrideProviders ?? null\n }\n if (hasAllowedOverrideModels) {\n existing.allowedOverrideModelsByProvider = input.allowedOverrideModelsByProvider ?? {}\n }\n existing.updatedByUserId = ctx.userId ?? null\n existing.updatedAt = new Date()\n if ('loopDisabled' in input) existing.loopDisabled = input.loopDisabled ?? null\n if ('loopMaxSteps' in input) existing.loopMaxSteps = input.loopMaxSteps ?? null\n if ('loopMaxToolCalls' in input) existing.loopMaxToolCalls = input.loopMaxToolCalls ?? null\n if ('loopMaxWallClockMs' in input) existing.loopMaxWallClockMs = input.loopMaxWallClockMs ?? null\n if ('loopMaxTokens' in input) existing.loopMaxTokens = input.loopMaxTokens ?? null\n if ('loopStopWhenJson' in input) existing.loopStopWhenJson = input.loopStopWhenJson ?? null\n if ('loopActiveToolsJson' in input) existing.loopActiveToolsJson = input.loopActiveToolsJson ?? null\n if ('inputModeration' in input) existing.inputModeration = input.inputModeration ?? null\n await tx.persist(existing).flush()\n return existing\n }\n\n const row = tx.create(AiAgentRuntimeOverride, {\n tenantId: ctx.tenantId,\n organizationId: orgFilter,\n agentId: agentIdFilter,\n providerId: hasProviderId ? normalizedProviderId : null,\n modelId: hasModelId ? (input.modelId ?? null) : null,\n baseUrl: hasBaseURL ? (input.baseURL ?? null) : null,\n allowedOverrideProviders: hasAllowedOverrideProviders\n ? (input.allowedOverrideProviders ?? null)\n : null,\n allowedOverrideModelsByProvider: hasAllowedOverrideModels\n ? (input.allowedOverrideModelsByProvider ?? {})\n : {},\n updatedByUserId: ctx.userId ?? null,\n loopDisabled: input.loopDisabled ?? null,\n loopMaxSteps: input.loopMaxSteps ?? null,\n loopMaxToolCalls: input.loopMaxToolCalls ?? null,\n loopMaxWallClockMs: input.loopMaxWallClockMs ?? null,\n loopMaxTokens: input.loopMaxTokens ?? null,\n loopStopWhenJson: input.loopStopWhenJson ?? null,\n loopActiveToolsJson: input.loopActiveToolsJson ?? null,\n inputModeration: input.inputModeration ?? null,\n } as unknown as AiAgentRuntimeOverride)\n await tx.persist(row).flush()\n return row\n })\n }\n\n /**\n * Soft-deletes the active override matching the given context by setting\n * `deleted_at = now()`. Returns true when a row was found and cleared,\n * false when no active row existed.\n */\n async clearDefault(ctx: {\n tenantId: string\n organizationId?: string | null\n agentId?: string | null\n }): Promise<boolean> {\n if (!ctx?.tenantId) return false\n\n const orgFilter = ctx.organizationId ?? null\n const agentIdFilter = ctx.agentId ?? null\n\n return this.em.transactional(async (tx) => {\n const existing = await tx.findOne(AiAgentRuntimeOverride, {\n tenantId: ctx.tenantId,\n organizationId: orgFilter,\n agentId: agentIdFilter,\n deletedAt: null,\n } satisfies FilterQuery<AiAgentRuntimeOverride>)\n if (!existing) return false\n if (\n existing.allowedOverrideProviders != null ||\n Object.keys(existing.allowedOverrideModelsByProvider ?? {}).length > 0\n ) {\n existing.providerId = null\n existing.modelId = null\n existing.baseUrl = null\n existing.updatedAt = new Date()\n await tx.persist(existing).flush()\n return true\n }\n existing.deletedAt = new Date()\n await tx.persist(existing).flush()\n return true\n })\n }\n}\n\n/**\n * Thrown by `upsertDefault` when validation fails (unknown provider id,\n * invalid loop override JSON).\n */\nexport class AiAgentRuntimeOverrideValidationError extends Error {\n readonly code: string\n\n constructor(message: string, code = 'invalid_override') {\n super(message)\n this.name = 'AiAgentRuntimeOverrideValidationError'\n this.code = code\n }\n}\n\nexport default AiAgentRuntimeOverrideRepository\n"],
4
+ "sourcesContent": ["import type { EntityManager, FilterQuery } from '@mikro-orm/postgresql'\nimport { llmProviderRegistry } from '../../lib/llm-registry'\nimport { canonicalProviderId } from '../../lib/model-allowlist'\nimport { AiAgentRuntimeOverride } from '../entities'\nimport type { AiAgentLoopStopCondition } from '../../lib/ai-agent-definition'\n\nexport interface AiAgentRuntimeOverrideContext {\n tenantId: string\n organizationId?: string | null\n userId?: string | null\n}\n\nexport interface AiAgentRuntimeOverrideLoopInput {\n /** Kill switch \u2014 when true, runtime forces stepCountIs(1). */\n loopDisabled?: boolean | null\n /** Override loop.maxSteps. */\n loopMaxSteps?: number | null\n /** Override loop.budget.maxToolCalls. */\n loopMaxToolCalls?: number | null\n /** Override loop.budget.maxWallClockMs. */\n loopMaxWallClockMs?: number | null\n /** Override loop.budget.maxTokens. */\n loopMaxTokens?: number | null\n /** Override loop.stopWhen \u2014 JSON-safe variants only (stepCount, hasToolCall). */\n loopStopWhenJson?: AiAgentLoopStopCondition[] | null\n /** Override loop.activeTools \u2014 must be a subset of agent.allowedTools. */\n loopActiveToolsJson?: string[] | null\n}\n\nexport interface AiAgentRuntimeOverrideInput extends AiAgentRuntimeOverrideLoopInput {\n /** null means tenant-wide default (no agent pinning). */\n agentId?: string | null\n providerId?: string | null\n modelId?: string | null\n baseURL?: string | null\n allowedOverrideProviders?: string[] | null\n allowedOverrideModelsByProvider?: Record<string, string[]>\n /** Input-moderation override: true = on, false = off, null = inherit. */\n inputModeration?: boolean | null\n /**\n * Optional: the agent's declared allowedTools. When provided, loopActiveToolsJson\n * is validated to be a subset. When omitted, allowlist validation is skipped\n * (write-time defense only; the runtime re-validates at read time).\n */\n agentAllowedTools?: string[]\n}\n\n/**\n * Repository for per-tenant AI runtime overrides (Phase 4a of spec\n * `2026-04-27-ai-agents-provider-model-baseurl-overrides`).\n *\n * All reads MUST filter by `tenant_id` first. The three public methods follow\n * the same fail-safe pattern as sibling repositories: `getDefault` returns\n * null on missing tenant context; `upsertDefault` validates provider id at\n * write time; `clearDefault` soft-deletes via `deleted_at`.\n *\n * Resolution precedence returned by `getDefault`:\n * 1. Agent-specific row (non-null `agent_id`) when `agentId` is provided.\n * 2. Tenant-wide row (`agent_id IS NULL`) for the same `(tenant_id, org_id)`.\n * Both rows are always scoped to the caller's `tenant_id` \u2014 cross-tenant\n * reads are impossible because every query filters `WHERE tenant_id = ?`.\n */\nexport class AiAgentRuntimeOverrideRepository {\n constructor(private readonly em: EntityManager) {}\n\n /**\n * Returns the most-specific active runtime override for the given context.\n *\n * When `agentId` is provided, an agent-specific row takes precedence over a\n * tenant-wide null-agent row. Returns null when neither exists.\n *\n * Never returns a row with `deleted_at IS NOT NULL`.\n */\n async getDefault(ctx: {\n tenantId: string\n organizationId?: string | null\n agentId?: string | null\n }): Promise<AiAgentRuntimeOverride | null> {\n if (!ctx?.tenantId) return null\n\n const orgFilter = ctx.organizationId ?? null\n\n // Try agent-specific first when caller provided an agentId.\n if (ctx.agentId) {\n const agentRow = await this.em.findOne(AiAgentRuntimeOverride, {\n tenantId: ctx.tenantId,\n organizationId: orgFilter,\n agentId: ctx.agentId,\n deletedAt: null,\n } satisfies FilterQuery<AiAgentRuntimeOverride>)\n if (agentRow) return agentRow\n }\n\n // Fall back to tenant-wide row (agentId = null).\n const tenantRow = await this.em.findOne(AiAgentRuntimeOverride, {\n tenantId: ctx.tenantId,\n organizationId: orgFilter,\n agentId: null,\n deletedAt: null,\n } satisfies FilterQuery<AiAgentRuntimeOverride>)\n return tenantRow ?? null\n }\n\n async getExact(ctx: {\n tenantId: string\n organizationId?: string | null\n agentId?: string | null\n }): Promise<AiAgentRuntimeOverride | null> {\n if (!ctx?.tenantId) return null\n const row = await this.em.findOne(AiAgentRuntimeOverride, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId ?? null,\n agentId: ctx.agentId ?? null,\n deletedAt: null,\n } satisfies FilterQuery<AiAgentRuntimeOverride>)\n return row ?? null\n }\n\n /**\n * Validates and normalizes the loop override fields from an input object.\n * Throws `AiAgentRuntimeOverrideValidationError` with code\n * `invalid_loop_override` for any validation failure.\n *\n * Validation rules (Phase 3 \u2014 R5 mitigation):\n * - `loopStopWhenJson`: all items must have kind `stepCount` or `hasToolCall`.\n * Items with kind `custom` are rejected \u2014 they cannot be stored as JSON.\n * - `loopActiveToolsJson`: when `agentAllowedTools` is provided, every entry\n * must be in that allowlist.\n */\n private validateLoopInput(input: AiAgentRuntimeOverrideInput): void {\n if (input.loopStopWhenJson != null) {\n if (!Array.isArray(input.loopStopWhenJson)) {\n throw new AiAgentRuntimeOverrideValidationError(\n 'loopStopWhenJson must be an array of stop condition objects.',\n 'invalid_loop_override',\n )\n }\n for (const item of input.loopStopWhenJson) {\n if (!item || typeof item !== 'object' || !('kind' in item)) {\n throw new AiAgentRuntimeOverrideValidationError(\n 'loopStopWhenJson items must have a \"kind\" field.',\n 'invalid_loop_override',\n )\n }\n const kind = (item as AiAgentLoopStopCondition).kind\n if (kind === 'custom') {\n throw new AiAgentRuntimeOverrideValidationError(\n 'loopStopWhenJson does not support kind \"custom\" \u2014 only \"stepCount\" and \"hasToolCall\" are JSON-safe and storable.',\n 'invalid_loop_override',\n )\n }\n if (kind !== 'stepCount' && kind !== 'hasToolCall') {\n throw new AiAgentRuntimeOverrideValidationError(\n `loopStopWhenJson contains unknown kind \"${String(kind)}\". Allowed: \"stepCount\", \"hasToolCall\".`,\n 'invalid_loop_override',\n )\n }\n if (kind === 'stepCount' && typeof (item as { count?: unknown }).count !== 'number') {\n throw new AiAgentRuntimeOverrideValidationError(\n 'loopStopWhenJson stepCount item must have a numeric \"count\" field.',\n 'invalid_loop_override',\n )\n }\n if (kind === 'hasToolCall' && typeof (item as { toolName?: unknown }).toolName !== 'string') {\n throw new AiAgentRuntimeOverrideValidationError(\n 'loopStopWhenJson hasToolCall item must have a string \"toolName\" field.',\n 'invalid_loop_override',\n )\n }\n }\n }\n\n if (input.loopActiveToolsJson != null) {\n if (!Array.isArray(input.loopActiveToolsJson)) {\n throw new AiAgentRuntimeOverrideValidationError(\n 'loopActiveToolsJson must be an array of tool name strings.',\n 'invalid_loop_override',\n )\n }\n for (const name of input.loopActiveToolsJson) {\n if (typeof name !== 'string' || name.length === 0) {\n throw new AiAgentRuntimeOverrideValidationError(\n 'loopActiveToolsJson entries must be non-empty strings.',\n 'invalid_loop_override',\n )\n }\n }\n if (input.agentAllowedTools && input.agentAllowedTools.length > 0) {\n const outsideAllowlist = input.loopActiveToolsJson.filter(\n (name) => !input.agentAllowedTools!.includes(name),\n )\n if (outsideAllowlist.length > 0) {\n throw new AiAgentRuntimeOverrideValidationError(\n `loopActiveToolsJson contains tools outside the agent's allowedTools: ${outsideAllowlist.join(', ')}.`,\n 'invalid_loop_override',\n )\n }\n }\n }\n }\n\n /**\n * Inserts or updates the runtime override for the given context.\n *\n * Validates `providerId` against the registry at write time so an admin\n * cannot save a typo (Phase 1.4 contract re-applied per spec \u00A7Data Models).\n * An unknown provider id throws a typed error.\n *\n * Also validates loop override fields (R5 mitigation \u2014 Phase 3):\n * - `loopStopWhenJson` items must use only JSON-safe kinds.\n * - `loopActiveToolsJson` items must be a subset of `agentAllowedTools`\n * when that is provided.\n *\n * The R6 base-URL allowlist check is intentionally NOT performed here \u2014\n * that enforcement lives at the HTTP layer (PUT settings route). The\n * repository trusts that callers have already validated the value.\n */\n async upsertDefault(\n input: AiAgentRuntimeOverrideInput,\n ctx: AiAgentRuntimeOverrideContext,\n ): Promise<AiAgentRuntimeOverride> {\n if (!ctx?.tenantId) {\n throw new Error('AiAgentRuntimeOverrideRepository.upsertDefault requires tenantId')\n }\n\n const normalizedProviderId = input.providerId\n ? canonicalProviderId(input.providerId, llmProviderRegistry.list().map((p) => p.id))\n : null\n if (input.providerId) {\n const knownProvider = normalizedProviderId ? llmProviderRegistry.get(normalizedProviderId) : null\n if (!knownProvider) {\n throw new AiAgentRuntimeOverrideValidationError(\n `Unknown provider id \"${input.providerId}\". Registered provider ids: ${llmProviderRegistry.list().map((p) => p.id).join(', ')}.`,\n )\n }\n }\n\n this.validateLoopInput(input)\n\n const orgFilter = ctx.organizationId ?? null\n const agentIdFilter = input.agentId ?? null\n const hasProviderId = Object.prototype.hasOwnProperty.call(input, 'providerId')\n const hasModelId = Object.prototype.hasOwnProperty.call(input, 'modelId')\n const hasBaseURL = Object.prototype.hasOwnProperty.call(input, 'baseURL')\n const hasAllowedOverrideProviders = Object.prototype.hasOwnProperty.call(input, 'allowedOverrideProviders')\n const hasAllowedOverrideModels = Object.prototype.hasOwnProperty.call(input, 'allowedOverrideModelsByProvider')\n\n return this.em.transactional(async (tx) => {\n const existing = await tx.findOne(AiAgentRuntimeOverride, {\n tenantId: ctx.tenantId,\n organizationId: orgFilter,\n agentId: agentIdFilter,\n deletedAt: null,\n } satisfies FilterQuery<AiAgentRuntimeOverride>)\n\n if (existing) {\n if (hasProviderId) existing.providerId = normalizedProviderId\n if (hasModelId) existing.modelId = input.modelId ?? null\n if (hasBaseURL) existing.baseUrl = input.baseURL ?? null\n if (hasAllowedOverrideProviders) {\n existing.allowedOverrideProviders = input.allowedOverrideProviders ?? null\n }\n if (hasAllowedOverrideModels) {\n existing.allowedOverrideModelsByProvider = input.allowedOverrideModelsByProvider ?? {}\n }\n existing.updatedByUserId = ctx.userId ?? null\n existing.updatedAt = new Date()\n if ('loopDisabled' in input) existing.loopDisabled = input.loopDisabled ?? null\n if ('loopMaxSteps' in input) existing.loopMaxSteps = input.loopMaxSteps ?? null\n if ('loopMaxToolCalls' in input) existing.loopMaxToolCalls = input.loopMaxToolCalls ?? null\n if ('loopMaxWallClockMs' in input) existing.loopMaxWallClockMs = input.loopMaxWallClockMs ?? null\n if ('loopMaxTokens' in input) existing.loopMaxTokens = input.loopMaxTokens ?? null\n if ('loopStopWhenJson' in input) existing.loopStopWhenJson = input.loopStopWhenJson ?? null\n if ('loopActiveToolsJson' in input) existing.loopActiveToolsJson = input.loopActiveToolsJson ?? null\n if ('inputModeration' in input) existing.inputModeration = input.inputModeration ?? null\n await tx.persist(existing).flush()\n return existing\n }\n\n const row = tx.create(AiAgentRuntimeOverride, {\n tenantId: ctx.tenantId,\n organizationId: orgFilter,\n agentId: agentIdFilter,\n providerId: hasProviderId ? normalizedProviderId : null,\n modelId: hasModelId ? (input.modelId ?? null) : null,\n baseUrl: hasBaseURL ? (input.baseURL ?? null) : null,\n allowedOverrideProviders: hasAllowedOverrideProviders\n ? (input.allowedOverrideProviders ?? null)\n : null,\n allowedOverrideModelsByProvider: hasAllowedOverrideModels\n ? (input.allowedOverrideModelsByProvider ?? {})\n : {},\n updatedByUserId: ctx.userId ?? null,\n loopDisabled: input.loopDisabled ?? null,\n loopMaxSteps: input.loopMaxSteps ?? null,\n loopMaxToolCalls: input.loopMaxToolCalls ?? null,\n loopMaxWallClockMs: input.loopMaxWallClockMs ?? null,\n loopMaxTokens: input.loopMaxTokens ?? null,\n loopStopWhenJson: input.loopStopWhenJson ?? null,\n loopActiveToolsJson: input.loopActiveToolsJson ?? null,\n inputModeration: input.inputModeration ?? null,\n } as unknown as AiAgentRuntimeOverride)\n await tx.persist(row).flush()\n return row\n })\n }\n\n /**\n * Soft-deletes the active override matching the given context by setting\n * `deleted_at = now()`. Returns true when a row was found and cleared,\n * false when no active row existed.\n */\n async clearDefault(ctx: {\n tenantId: string\n organizationId?: string | null\n agentId?: string | null\n }): Promise<boolean> {\n if (!ctx?.tenantId) return false\n\n const orgFilter = ctx.organizationId ?? null\n const agentIdFilter = ctx.agentId ?? null\n\n return this.em.transactional(async (tx) => {\n const existing = await tx.findOne(AiAgentRuntimeOverride, {\n tenantId: ctx.tenantId,\n organizationId: orgFilter,\n agentId: agentIdFilter,\n deletedAt: null,\n } satisfies FilterQuery<AiAgentRuntimeOverride>)\n if (!existing) return false\n if (\n existing.allowedOverrideProviders != null ||\n Object.keys(existing.allowedOverrideModelsByProvider ?? {}).length > 0\n ) {\n existing.providerId = null\n existing.modelId = null\n existing.baseUrl = null\n existing.updatedAt = new Date()\n await tx.persist(existing).flush()\n return true\n }\n existing.deletedAt = new Date()\n await tx.persist(existing).flush()\n return true\n })\n }\n}\n\n/**\n * Thrown by `upsertDefault` when validation fails (unknown provider id,\n * invalid loop override JSON).\n */\nexport class AiAgentRuntimeOverrideValidationError extends Error {\n readonly code: string\n\n constructor(message: string, code = 'invalid_override') {\n super(message)\n this.name = 'AiAgentRuntimeOverrideValidationError'\n this.code = code\n }\n}\n\nexport default AiAgentRuntimeOverrideRepository\n"],
5
5
  "mappings": "AACA,SAAS,2BAA2B;AACpC,SAAS,2BAA2B;AACpC,SAAS,8BAA8B;AA2DhC,MAAM,iCAAiC;AAAA,EAC5C,YAA6B,IAAmB;AAAnB;AAAA,EAAoB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUjD,MAAM,WAAW,KAI0B;AACzC,QAAI,CAAC,KAAK,SAAU,QAAO;AAE3B,UAAM,YAAY,IAAI,kBAAkB;AAGxC,QAAI,IAAI,SAAS;AACf,YAAM,WAAW,MAAM,KAAK,GAAG,QAAQ,wBAAwB;AAAA,QAC7D,UAAU,IAAI;AAAA,QACd,gBAAgB;AAAA,QAChB,SAAS,IAAI;AAAA,QACb,WAAW;AAAA,MACb,CAA+C;AAC/C,UAAI,SAAU,QAAO;AAAA,IACvB;AAGA,UAAM,YAAY,MAAM,KAAK,GAAG,QAAQ,wBAAwB;AAAA,MAC9D,UAAU,IAAI;AAAA,MACd,gBAAgB;AAAA,MAChB,SAAS;AAAA,MACT,WAAW;AAAA,IACb,CAA+C;AAC/C,WAAO,aAAa;AAAA,EACtB;AAAA,EAEA,MAAM,SAAS,KAI4B;AACzC,QAAI,CAAC,KAAK,SAAU,QAAO;AAC3B,UAAM,MAAM,MAAM,KAAK,GAAG,QAAQ,wBAAwB;AAAA,MACxD,UAAU,IAAI;AAAA,MACd,gBAAgB,IAAI,kBAAkB;AAAA,MACtC,SAAS,IAAI,WAAW;AAAA,MACxB,WAAW;AAAA,IACb,CAA+C;AAC/C,WAAO,OAAO;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaQ,kBAAkB,OAA0C;AAClE,QAAI,MAAM,oBAAoB,MAAM;AAClC,UAAI,CAAC,MAAM,QAAQ,MAAM,gBAAgB,GAAG;AAC1C,cAAM,IAAI;AAAA,UACR;AAAA,UACA;AAAA,QACF;AAAA,MACF;AACA,iBAAW,QAAQ,MAAM,kBAAkB;AACzC,YAAI,CAAC,QAAQ,OAAO,SAAS,YAAY,EAAE,UAAU,OAAO;AAC1D,gBAAM,IAAI;AAAA,YACR;AAAA,YACA;AAAA,UACF;AAAA,QACF;AACA,cAAM,OAAQ,KAAkC;AAChD,YAAI,SAAS,UAAU;AACrB,gBAAM,IAAI;AAAA,YACR;AAAA,YACA;AAAA,UACF;AAAA,QACF;AACA,YAAI,SAAS,eAAe,SAAS,eAAe;AAClD,gBAAM,IAAI;AAAA,YACR,2CAA2C,OAAO,IAAI,CAAC;AAAA,YACvD;AAAA,UACF;AAAA,QACF;AACA,YAAI,SAAS,eAAe,OAAQ,KAA6B,UAAU,UAAU;AACnF,gBAAM,IAAI;AAAA,YACR;AAAA,YACA;AAAA,UACF;AAAA,QACF;AACA,YAAI,SAAS,iBAAiB,OAAQ,KAAgC,aAAa,UAAU;AAC3F,gBAAM,IAAI;AAAA,YACR;AAAA,YACA;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAEA,QAAI,MAAM,uBAAuB,MAAM;AACrC,UAAI,CAAC,MAAM,QAAQ,MAAM,mBAAmB,GAAG;AAC7C,cAAM,IAAI;AAAA,UACR;AAAA,UACA;AAAA,QACF;AAAA,MACF;AACA,iBAAW,QAAQ,MAAM,qBAAqB;AAC5C,YAAI,OAAO,SAAS,YAAY,KAAK,WAAW,GAAG;AACjD,gBAAM,IAAI;AAAA,YACR;AAAA,YACA;AAAA,UACF;AAAA,QACF;AAAA,MACF;AACA,UAAI,MAAM,qBAAqB,MAAM,kBAAkB,SAAS,GAAG;AACjE,cAAM,mBAAmB,MAAM,oBAAoB;AAAA,UACjD,CAAC,SAAS,CAAC,MAAM,kBAAmB,SAAS,IAAI;AAAA,QACnD;AACA,YAAI,iBAAiB,SAAS,GAAG;AAC/B,gBAAM,IAAI;AAAA,YACR,wEAAwE,iBAAiB,KAAK,IAAI,CAAC;AAAA,YACnG;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBA,MAAM,cACJ,OACA,KACiC;AACjC,QAAI,CAAC,KAAK,UAAU;AAClB,YAAM,IAAI,MAAM,kEAAkE;AAAA,IACpF;AAEA,UAAM,uBAAuB,MAAM,aAC/B,oBAAoB,MAAM,YAAY,oBAAoB,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,IACjF;AACJ,QAAI,MAAM,YAAY;AACpB,YAAM,gBAAgB,uBAAuB,oBAAoB,IAAI,oBAAoB,IAAI;AAC7F,UAAI,CAAC,eAAe;AAClB,cAAM,IAAI;AAAA,UACR,wBAAwB,MAAM,UAAU,+BAA+B,oBAAoB,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,EAAE,KAAK,IAAI,CAAC;AAAA,QAC/H;AAAA,MACF;AAAA,IACF;AAEA,SAAK,kBAAkB,KAAK;AAE5B,UAAM,YAAY,IAAI,kBAAkB;AACxC,UAAM,gBAAgB,MAAM,WAAW;AACvC,UAAM,gBAAgB,OAAO,UAAU,eAAe,KAAK,OAAO,YAAY;AAC9E,UAAM,aAAa,OAAO,UAAU,eAAe,KAAK,OAAO,SAAS;AACxE,UAAM,aAAa,OAAO,UAAU,eAAe,KAAK,OAAO,SAAS;AACxE,UAAM,8BAA8B,OAAO,UAAU,eAAe,KAAK,OAAO,0BAA0B;AAC1G,UAAM,2BAA2B,OAAO,UAAU,eAAe,KAAK,OAAO,iCAAiC;AAE9G,WAAO,KAAK,GAAG,cAAc,OAAO,OAAO;AACzC,YAAM,WAAW,MAAM,GAAG,QAAQ,wBAAwB;AAAA,QACxD,UAAU,IAAI;AAAA,QACd,gBAAgB;AAAA,QAChB,SAAS;AAAA,QACT,WAAW;AAAA,MACb,CAA+C;AAE/C,UAAI,UAAU;AACZ,YAAI,cAAe,UAAS,aAAa;AACzC,YAAI,WAAY,UAAS,UAAU,MAAM,WAAW;AACpD,YAAI,WAAY,UAAS,UAAU,MAAM,WAAW;AACpD,YAAI,6BAA6B;AAC/B,mBAAS,2BAA2B,MAAM,4BAA4B;AAAA,QACxE;AACA,YAAI,0BAA0B;AAC5B,mBAAS,kCAAkC,MAAM,mCAAmC,CAAC;AAAA,QACvF;AACA,iBAAS,kBAAkB,IAAI,UAAU;AACzC,iBAAS,YAAY,oBAAI,KAAK;AAC9B,YAAI,kBAAkB,MAAO,UAAS,eAAe,MAAM,gBAAgB;AAC3E,YAAI,kBAAkB,MAAO,UAAS,eAAe,MAAM,gBAAgB;AAC3E,YAAI,sBAAsB,MAAO,UAAS,mBAAmB,MAAM,oBAAoB;AACvF,YAAI,wBAAwB,MAAO,UAAS,qBAAqB,MAAM,sBAAsB;AAC7F,YAAI,mBAAmB,MAAO,UAAS,gBAAgB,MAAM,iBAAiB;AAC9E,YAAI,sBAAsB,MAAO,UAAS,mBAAmB,MAAM,oBAAoB;AACvF,YAAI,yBAAyB,MAAO,UAAS,sBAAsB,MAAM,uBAAuB;AAChG,YAAI,qBAAqB,MAAO,UAAS,kBAAkB,MAAM,mBAAmB;AACpF,cAAM,GAAG,QAAQ,QAAQ,EAAE,MAAM;AACjC,eAAO;AAAA,MACT;AAEA,YAAM,MAAM,GAAG,OAAO,wBAAwB;AAAA,QAC5C,UAAU,IAAI;AAAA,QACd,gBAAgB;AAAA,QAChB,SAAS;AAAA,QACT,YAAY,gBAAgB,uBAAuB;AAAA,QACnD,SAAS,aAAc,MAAM,WAAW,OAAQ;AAAA,QAChD,SAAS,aAAc,MAAM,WAAW,OAAQ;AAAA,QAChD,0BAA0B,8BACrB,MAAM,4BAA4B,OACnC;AAAA,QACJ,iCAAiC,2BAC5B,MAAM,mCAAmC,CAAC,IAC3C,CAAC;AAAA,QACL,iBAAiB,IAAI,UAAU;AAAA,QAC/B,cAAc,MAAM,gBAAgB;AAAA,QACpC,cAAc,MAAM,gBAAgB;AAAA,QACpC,kBAAkB,MAAM,oBAAoB;AAAA,QAC5C,oBAAoB,MAAM,sBAAsB;AAAA,QAChD,eAAe,MAAM,iBAAiB;AAAA,QACtC,kBAAkB,MAAM,oBAAoB;AAAA,QAC5C,qBAAqB,MAAM,uBAAuB;AAAA,QAClD,iBAAiB,MAAM,mBAAmB;AAAA,MAC5C,CAAsC;AACtC,YAAM,GAAG,QAAQ,GAAG,EAAE,MAAM;AAC5B,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,aAAa,KAIE;AACnB,QAAI,CAAC,KAAK,SAAU,QAAO;AAE3B,UAAM,YAAY,IAAI,kBAAkB;AACxC,UAAM,gBAAgB,IAAI,WAAW;AAErC,WAAO,KAAK,GAAG,cAAc,OAAO,OAAO;AACzC,YAAM,WAAW,MAAM,GAAG,QAAQ,wBAAwB;AAAA,QACxD,UAAU,IAAI;AAAA,QACd,gBAAgB;AAAA,QAChB,SAAS;AAAA,QACT,WAAW;AAAA,MACb,CAA+C;AAC/C,UAAI,CAAC,SAAU,QAAO;AACtB,UACE,SAAS,4BAA4B,QACrC,OAAO,KAAK,SAAS,mCAAmC,CAAC,CAAC,EAAE,SAAS,GACrE;AACA,iBAAS,aAAa;AACtB,iBAAS,UAAU;AACnB,iBAAS,UAAU;AACnB,iBAAS,YAAY,oBAAI,KAAK;AAC9B,cAAM,GAAG,QAAQ,QAAQ,EAAE,MAAM;AACjC,eAAO;AAAA,MACT;AACA,eAAS,YAAY,oBAAI,KAAK;AAC9B,YAAM,GAAG,QAAQ,QAAQ,EAAE,MAAM;AACjC,aAAO;AAAA,IACT,CAAC;AAAA,EACH;AACF;AAMO,MAAM,8CAA8C,MAAM;AAAA,EAG/D,YAAY,SAAiB,OAAO,oBAAoB;AACtD,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,OAAO;AAAA,EACd;AACF;AAEA,IAAO,2CAAQ;",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,5 @@
1
1
  import { createLogger } from "@open-mercato/shared/lib/logger";
2
- import { llmProviderRegistry } from "@open-mercato/shared/lib/ai/llm-provider-registry";
2
+ import { llmProviderRegistry } from "./llm-registry.js";
3
3
  import {
4
4
  applyAgentOverrideMap,
5
5
  composeAgentExtensionEntries,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/lib/agent-registry.ts"],
4
- "sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport type { AiAgentDefinition, AiAgentExtension, AiAgentSuggestion } from './ai-agent-definition'\nimport {\n applyAgentOverrideMap,\n composeAgentExtensionEntries,\n composeAgentOverrideMap,\n type AiAgentExtensionConfigEntry,\n type AiAgentOverrideConfigEntry,\n} from './ai-overrides'\nimport { TASK_PLAN_TOOL_NAME } from './task-plan-labels'\nimport { findGeneratedFile, compileAndImportGenerated } from './generated-registry-loader'\n\nconst logger = createLogger('ai_assistant')\n\nconst agentsById = new Map<string, AiAgentDefinition>()\nlet loaded = false\n\n/**\n * Import the generated `ai-agents.generated.ts` registry.\n *\n * Dual-strategy, mirroring `tool-loader`'s `importGeneratedAiToolsModule`:\n * 1. Prefer the `@/` path-alias import \u2014 the Next.js bundler resolves it at\n * build time, so the in-app agent runtime is unchanged.\n * 2. Fall back to locating + compiling the file from disk when the alias\n * import throws. That only happens in a standalone Node process (the\n * `mcp:dev` / `mcp:serve` MCP servers), where `@/` is not a real package\n * specifier and Node throws `ERR_MODULE_NOT_FOUND`. Without this the\n * `meta.list_agents` / `meta.describe_agent` tools return an empty agent\n * registry over MCP.\n *\n * Returns `null` when no generated file exists (pre-generate builds, tests).\n */\nasync function importGeneratedAiAgentsModule(): Promise<Record<string, unknown> | null> {\n try {\n return (await import(\n '@/.mercato/generated/ai-agents.generated'\n )) as Record<string, unknown>\n } catch {\n const tsPath = findGeneratedFile('ai-agents.generated.ts')\n if (!tsPath) return null\n return compileAndImportGenerated(tsPath)\n }\n}\n\nfunction isStringArray(value: unknown): value is string[] {\n return Array.isArray(value) && value.every((item) => typeof item === 'string')\n}\n\nfunction isAiAgentSuggestion(value: unknown): value is AiAgentSuggestion {\n if (!value || typeof value !== 'object') return false\n const candidate = value as Record<string, unknown>\n return typeof candidate.label === 'string' && typeof candidate.prompt === 'string'\n}\n\nfunction isAiAgentExtension(value: unknown): value is AiAgentExtension {\n if (!value || typeof value !== 'object') return false\n const candidate = value as Record<string, unknown>\n return (\n typeof candidate.targetAgentId === 'string' &&\n (!('replaceAllowedTools' in candidate) || isStringArray(candidate.replaceAllowedTools)) &&\n (!('deleteAllowedTools' in candidate) || isStringArray(candidate.deleteAllowedTools)) &&\n (!('appendAllowedTools' in candidate) || isStringArray(candidate.appendAllowedTools)) &&\n (!('taskPlan' in candidate) ||\n (candidate.taskPlan != null &&\n typeof candidate.taskPlan === 'object' &&\n (!('enabled' in candidate.taskPlan) ||\n typeof (candidate.taskPlan as { enabled?: unknown }).enabled === 'boolean'))) &&\n (!('replaceSystemPrompt' in candidate) || typeof candidate.replaceSystemPrompt === 'string') &&\n (!('appendSystemPrompt' in candidate) || typeof candidate.appendSystemPrompt === 'string') &&\n (!('replaceSuggestions' in candidate) ||\n (Array.isArray(candidate.replaceSuggestions) && candidate.replaceSuggestions.every(isAiAgentSuggestion))) &&\n (!('deleteSuggestions' in candidate) || isStringArray(candidate.deleteSuggestions)) &&\n (!('appendSuggestions' in candidate) ||\n (Array.isArray(candidate.appendSuggestions) && candidate.appendSuggestions.every(isAiAgentSuggestion))) &&\n (!('suggestions' in candidate) ||\n (Array.isArray(candidate.suggestions) && candidate.suggestions.every(isAiAgentSuggestion)))\n )\n}\n\nfunction isAiAgentDefinition(value: unknown): value is AiAgentDefinition {\n if (!value || typeof value !== 'object') return false\n const candidate = value as Record<string, unknown>\n return (\n typeof candidate.id === 'string' &&\n typeof candidate.moduleId === 'string' &&\n typeof candidate.label === 'string' &&\n typeof candidate.description === 'string' &&\n typeof candidate.systemPrompt === 'string' &&\n isStringArray(candidate.allowedTools)\n )\n}\n\nfunction uniqueStrings(values: readonly string[]): string[] {\n return Array.from(new Set(values.filter((value) => typeof value === 'string' && value.length > 0)))\n}\n\nexport function isAgentTaskPlanEnabled(agent: Pick<AiAgentDefinition, 'taskPlan'>): boolean {\n return agent.taskPlan?.enabled === true\n}\n\nfunction normalizeTaskPlanTool(agent: AiAgentDefinition): AiAgentDefinition {\n const enabled = isAgentTaskPlanEnabled(agent)\n const withoutInternalTool = agent.allowedTools.filter((toolName) => toolName !== TASK_PLAN_TOOL_NAME)\n const allowedTools = enabled\n ? uniqueStrings([...withoutInternalTool, TASK_PLAN_TOOL_NAME])\n : uniqueStrings(withoutInternalTool)\n if (\n allowedTools.length === agent.allowedTools.length &&\n allowedTools.every((toolName, index) => toolName === agent.allowedTools[index])\n ) {\n return agent\n }\n return {\n ...agent,\n allowedTools,\n }\n}\n\nfunction applyStringListPatch(\n current: readonly string[],\n patch: {\n replace?: readonly string[]\n delete?: readonly string[]\n append?: readonly string[]\n },\n): string[] {\n const deleted = new Set(patch.delete ?? [])\n return uniqueStrings([\n ...(patch.replace ?? current).filter((value) => !deleted.has(value)),\n ...(patch.append ?? []),\n ])\n}\n\nfunction suggestionDeleteKey(suggestion: AiAgentSuggestion): string[] {\n return [suggestion.label, suggestion.prompt].filter((value) => value.length > 0)\n}\n\nfunction applySuggestionPatch(\n current: readonly AiAgentSuggestion[],\n patch: {\n replace?: readonly AiAgentSuggestion[]\n delete?: readonly string[]\n append?: readonly AiAgentSuggestion[]\n },\n): AiAgentSuggestion[] {\n const deleted = new Set(patch.delete ?? [])\n const base = patch.replace ?? current\n const out: AiAgentSuggestion[] = []\n const seen = new Set<string>()\n for (const suggestion of base) {\n if (suggestionDeleteKey(suggestion).some((key) => deleted.has(key))) continue\n const key = `${suggestion.label}\\n${suggestion.prompt}`\n if (seen.has(key)) continue\n seen.add(key)\n out.push(suggestion)\n }\n for (const suggestion of patch.append ?? []) {\n const key = `${suggestion.label}\\n${suggestion.prompt}`\n if (seen.has(key)) continue\n seen.add(key)\n out.push(suggestion)\n }\n return out\n}\n\nfunction validateAndNormalizeAgent(candidate: AiAgentDefinition): AiAgentDefinition {\n const taskPlanNormalized = normalizeTaskPlanTool(candidate)\n const rawProvider = candidate.defaultProvider\n if (typeof rawProvider !== 'string' || rawProvider.trim().length === 0) {\n return taskPlanNormalized\n }\n const providerHint = rawProvider.trim()\n const registered = llmProviderRegistry.get(providerHint)\n if (!registered) {\n logger.warn('Agent declares a defaultProvider not registered in llmProviderRegistry; registering with defaultProvider undefined', {\n agentId: candidate.id,\n defaultProvider: providerHint,\n })\n return { ...taskPlanNormalized, defaultProvider: undefined }\n }\n return taskPlanNormalized\n}\n\nfunction populateFromAgents(agents: unknown[]): void {\n for (const candidate of agents) {\n if (!isAiAgentDefinition(candidate)) {\n logger.warn('AI Agents \u2014 Skipping malformed agent entry in ai-agents.generated.ts')\n continue\n }\n const existing = agentsById.get(candidate.id)\n if (existing) {\n throw new Error(\n `[AI Agents] Duplicate agent id \"${candidate.id}\" \u2014 already registered by module \"${existing.moduleId}\", conflicts with module \"${candidate.moduleId}\". Export \\`aiAgentOverrides\\` from your module's \\`ai-agents.ts\\` (or set it on the modules.ts entry) to replace an agent across modules.`\n )\n }\n agentsById.set(candidate.id, validateAndNormalizeAgent(candidate))\n }\n}\n\nasync function loadOverrideEntries(): Promise<AiAgentOverrideConfigEntry[]> {\n try {\n const mod = (await importGeneratedAiAgentsModule()) as {\n aiAgentOverrideEntries?: unknown[]\n } | null\n return mod && Array.isArray(mod.aiAgentOverrideEntries)\n ? (mod.aiAgentOverrideEntries as AiAgentOverrideConfigEntry[])\n : []\n } catch {\n // No generated file yet \u2014 pre-generate builds and tests fall through.\n return []\n }\n}\n\nfunction applyOverridesToRegistry(entries: readonly AiAgentOverrideConfigEntry[]): void {\n const overrideMap = composeAgentOverrideMap(entries)\n if (Object.keys(overrideMap).length === 0) return\n const overridden = applyAgentOverrideMap(Array.from(agentsById.values()), overrideMap)\n agentsById.clear()\n for (const agent of overridden) agentsById.set(agent.id, agent)\n for (const [id, value] of Object.entries(overrideMap)) {\n const verb = value === null ? 'disabled' : 'replaced'\n logger.info('Agent changed by override', { agentId: id, change: verb })\n }\n}\n\nasync function loadExtensionEntries(): Promise<AiAgentExtension[]> {\n try {\n const mod = (await importGeneratedAiAgentsModule()) as {\n aiAgentExtensionEntries?: unknown[]\n } | null\n const entries =\n mod && Array.isArray(mod.aiAgentExtensionEntries)\n ? (mod.aiAgentExtensionEntries as AiAgentExtensionConfigEntry[])\n : []\n return composeAgentExtensionEntries(entries).filter(isAiAgentExtension)\n } catch {\n return []\n }\n}\n\nfunction applyExtensionsToRegistry(extensions: readonly AiAgentExtension[]): void {\n if (extensions.length === 0) return\n for (const extension of extensions) {\n const agent = agentsById.get(extension.targetAgentId)\n if (!agent) {\n logger.warn('Skipping extension for unknown agent', { agentId: extension.targetAgentId })\n continue\n }\n\n const replacementSystemPrompt = extension.replaceSystemPrompt?.trim()\n const appendSystemPrompt = extension.appendSystemPrompt?.trim()\n const systemPrompt = replacementSystemPrompt ?? agent.systemPrompt.trim()\n const patchedAgent: AiAgentDefinition = {\n ...agent,\n allowedTools: applyStringListPatch(agent.allowedTools, {\n replace: extension.replaceAllowedTools,\n delete: extension.deleteAllowedTools,\n append: extension.appendAllowedTools,\n }),\n taskPlan: extension.taskPlan !== undefined ? extension.taskPlan : agent.taskPlan,\n systemPrompt: appendSystemPrompt\n ? `${systemPrompt}\\n\\n${appendSystemPrompt}`\n : systemPrompt,\n suggestions: applySuggestionPatch(agent.suggestions ?? [], {\n replace: extension.replaceSuggestions,\n delete: extension.deleteSuggestions,\n append: [\n ...(extension.appendSuggestions ?? []),\n ...(extension.suggestions ?? []),\n ],\n }),\n }\n agentsById.set(agent.id, validateAndNormalizeAgent(patchedAgent))\n }\n}\n\nexport async function loadAgentRegistry(): Promise<void> {\n if (loaded) return\n try {\n const mod = (await importGeneratedAiAgentsModule()) as {\n allAiAgents?: unknown[]\n } | null\n const agents = mod && Array.isArray(mod.allAiAgents) ? mod.allAiAgents : []\n populateFromAgents(agents)\n } catch (error) {\n logger.error('AI Agents \u2014 Could not load ai-agents.generated.ts (agent registry empty)', { err: error })\n } finally {\n try {\n const overrideEntries = await loadOverrideEntries()\n applyOverridesToRegistry(overrideEntries)\n const extensionEntries = await loadExtensionEntries()\n applyExtensionsToRegistry(extensionEntries)\n } catch (error) {\n logger.error('AI Agents \u2014 Failed to apply agent overrides/extensions', { err: error })\n }\n loaded = true\n }\n}\n\nexport function getAgent(id: string): AiAgentDefinition | undefined {\n return agentsById.get(id)\n}\n\nexport function listAgents(): AiAgentDefinition[] {\n return Array.from(agentsById.values()).sort((a, b) => a.id.localeCompare(b.id))\n}\n\nexport function listAgentsByModule(moduleId: string): AiAgentDefinition[] {\n return listAgents().filter((agent) => agent.moduleId === moduleId)\n}\n\n/**\n * @__internal\n * Test-only hook \u2014 clears the cached registry so `loadAgentRegistry` re-evaluates its source.\n */\nexport function resetAgentRegistryForTests(): void {\n agentsById.clear()\n loaded = false\n}\n\n/**\n * @__internal\n * Test-only hook \u2014 seeds the registry directly from a fixture array without going through\n * the dynamic generated-file import. Used by the registry's own unit tests.\n */\nexport function seedAgentRegistryForTests(agents: unknown[]): void {\n agentsById.clear()\n populateFromAgents(agents)\n loaded = true\n}\n\n/**\n * @__internal\n * Test-only hook \u2014 apply override entries against the seeded registry to\n * exercise the override pipeline without round-tripping through the\n * generated file.\n */\nexport function applyAgentOverrideEntriesForTests(\n entries: readonly AiAgentOverrideConfigEntry[],\n): void {\n applyOverridesToRegistry(entries)\n}\n\n/**\n * @__internal Test-only hook \u2014 apply additive extension entries against the\n * seeded registry without round-tripping through the generated file.\n */\nexport function applyAgentExtensionEntriesForTests(\n entries: readonly AiAgentExtension[],\n): void {\n applyExtensionsToRegistry(entries)\n}\n"],
4
+ "sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport { llmProviderRegistry } from './llm-registry'\nimport type { AiAgentDefinition, AiAgentExtension, AiAgentSuggestion } from './ai-agent-definition'\nimport {\n applyAgentOverrideMap,\n composeAgentExtensionEntries,\n composeAgentOverrideMap,\n type AiAgentExtensionConfigEntry,\n type AiAgentOverrideConfigEntry,\n} from './ai-overrides'\nimport { TASK_PLAN_TOOL_NAME } from './task-plan-labels'\nimport { findGeneratedFile, compileAndImportGenerated } from './generated-registry-loader'\n\nconst logger = createLogger('ai_assistant')\n\nconst agentsById = new Map<string, AiAgentDefinition>()\nlet loaded = false\n\n/**\n * Import the generated `ai-agents.generated.ts` registry.\n *\n * Dual-strategy, mirroring `tool-loader`'s `importGeneratedAiToolsModule`:\n * 1. Prefer the `@/` path-alias import \u2014 the Next.js bundler resolves it at\n * build time, so the in-app agent runtime is unchanged.\n * 2. Fall back to locating + compiling the file from disk when the alias\n * import throws. That only happens in a standalone Node process (the\n * `mcp:dev` / `mcp:serve` MCP servers), where `@/` is not a real package\n * specifier and Node throws `ERR_MODULE_NOT_FOUND`. Without this the\n * `meta.list_agents` / `meta.describe_agent` tools return an empty agent\n * registry over MCP.\n *\n * Returns `null` when no generated file exists (pre-generate builds, tests).\n */\nasync function importGeneratedAiAgentsModule(): Promise<Record<string, unknown> | null> {\n try {\n return (await import(\n '@/.mercato/generated/ai-agents.generated'\n )) as Record<string, unknown>\n } catch {\n const tsPath = findGeneratedFile('ai-agents.generated.ts')\n if (!tsPath) return null\n return compileAndImportGenerated(tsPath)\n }\n}\n\nfunction isStringArray(value: unknown): value is string[] {\n return Array.isArray(value) && value.every((item) => typeof item === 'string')\n}\n\nfunction isAiAgentSuggestion(value: unknown): value is AiAgentSuggestion {\n if (!value || typeof value !== 'object') return false\n const candidate = value as Record<string, unknown>\n return typeof candidate.label === 'string' && typeof candidate.prompt === 'string'\n}\n\nfunction isAiAgentExtension(value: unknown): value is AiAgentExtension {\n if (!value || typeof value !== 'object') return false\n const candidate = value as Record<string, unknown>\n return (\n typeof candidate.targetAgentId === 'string' &&\n (!('replaceAllowedTools' in candidate) || isStringArray(candidate.replaceAllowedTools)) &&\n (!('deleteAllowedTools' in candidate) || isStringArray(candidate.deleteAllowedTools)) &&\n (!('appendAllowedTools' in candidate) || isStringArray(candidate.appendAllowedTools)) &&\n (!('taskPlan' in candidate) ||\n (candidate.taskPlan != null &&\n typeof candidate.taskPlan === 'object' &&\n (!('enabled' in candidate.taskPlan) ||\n typeof (candidate.taskPlan as { enabled?: unknown }).enabled === 'boolean'))) &&\n (!('replaceSystemPrompt' in candidate) || typeof candidate.replaceSystemPrompt === 'string') &&\n (!('appendSystemPrompt' in candidate) || typeof candidate.appendSystemPrompt === 'string') &&\n (!('replaceSuggestions' in candidate) ||\n (Array.isArray(candidate.replaceSuggestions) && candidate.replaceSuggestions.every(isAiAgentSuggestion))) &&\n (!('deleteSuggestions' in candidate) || isStringArray(candidate.deleteSuggestions)) &&\n (!('appendSuggestions' in candidate) ||\n (Array.isArray(candidate.appendSuggestions) && candidate.appendSuggestions.every(isAiAgentSuggestion))) &&\n (!('suggestions' in candidate) ||\n (Array.isArray(candidate.suggestions) && candidate.suggestions.every(isAiAgentSuggestion)))\n )\n}\n\nfunction isAiAgentDefinition(value: unknown): value is AiAgentDefinition {\n if (!value || typeof value !== 'object') return false\n const candidate = value as Record<string, unknown>\n return (\n typeof candidate.id === 'string' &&\n typeof candidate.moduleId === 'string' &&\n typeof candidate.label === 'string' &&\n typeof candidate.description === 'string' &&\n typeof candidate.systemPrompt === 'string' &&\n isStringArray(candidate.allowedTools)\n )\n}\n\nfunction uniqueStrings(values: readonly string[]): string[] {\n return Array.from(new Set(values.filter((value) => typeof value === 'string' && value.length > 0)))\n}\n\nexport function isAgentTaskPlanEnabled(agent: Pick<AiAgentDefinition, 'taskPlan'>): boolean {\n return agent.taskPlan?.enabled === true\n}\n\nfunction normalizeTaskPlanTool(agent: AiAgentDefinition): AiAgentDefinition {\n const enabled = isAgentTaskPlanEnabled(agent)\n const withoutInternalTool = agent.allowedTools.filter((toolName) => toolName !== TASK_PLAN_TOOL_NAME)\n const allowedTools = enabled\n ? uniqueStrings([...withoutInternalTool, TASK_PLAN_TOOL_NAME])\n : uniqueStrings(withoutInternalTool)\n if (\n allowedTools.length === agent.allowedTools.length &&\n allowedTools.every((toolName, index) => toolName === agent.allowedTools[index])\n ) {\n return agent\n }\n return {\n ...agent,\n allowedTools,\n }\n}\n\nfunction applyStringListPatch(\n current: readonly string[],\n patch: {\n replace?: readonly string[]\n delete?: readonly string[]\n append?: readonly string[]\n },\n): string[] {\n const deleted = new Set(patch.delete ?? [])\n return uniqueStrings([\n ...(patch.replace ?? current).filter((value) => !deleted.has(value)),\n ...(patch.append ?? []),\n ])\n}\n\nfunction suggestionDeleteKey(suggestion: AiAgentSuggestion): string[] {\n return [suggestion.label, suggestion.prompt].filter((value) => value.length > 0)\n}\n\nfunction applySuggestionPatch(\n current: readonly AiAgentSuggestion[],\n patch: {\n replace?: readonly AiAgentSuggestion[]\n delete?: readonly string[]\n append?: readonly AiAgentSuggestion[]\n },\n): AiAgentSuggestion[] {\n const deleted = new Set(patch.delete ?? [])\n const base = patch.replace ?? current\n const out: AiAgentSuggestion[] = []\n const seen = new Set<string>()\n for (const suggestion of base) {\n if (suggestionDeleteKey(suggestion).some((key) => deleted.has(key))) continue\n const key = `${suggestion.label}\\n${suggestion.prompt}`\n if (seen.has(key)) continue\n seen.add(key)\n out.push(suggestion)\n }\n for (const suggestion of patch.append ?? []) {\n const key = `${suggestion.label}\\n${suggestion.prompt}`\n if (seen.has(key)) continue\n seen.add(key)\n out.push(suggestion)\n }\n return out\n}\n\nfunction validateAndNormalizeAgent(candidate: AiAgentDefinition): AiAgentDefinition {\n const taskPlanNormalized = normalizeTaskPlanTool(candidate)\n const rawProvider = candidate.defaultProvider\n if (typeof rawProvider !== 'string' || rawProvider.trim().length === 0) {\n return taskPlanNormalized\n }\n const providerHint = rawProvider.trim()\n const registered = llmProviderRegistry.get(providerHint)\n if (!registered) {\n logger.warn('Agent declares a defaultProvider not registered in llmProviderRegistry; registering with defaultProvider undefined', {\n agentId: candidate.id,\n defaultProvider: providerHint,\n })\n return { ...taskPlanNormalized, defaultProvider: undefined }\n }\n return taskPlanNormalized\n}\n\nfunction populateFromAgents(agents: unknown[]): void {\n for (const candidate of agents) {\n if (!isAiAgentDefinition(candidate)) {\n logger.warn('AI Agents \u2014 Skipping malformed agent entry in ai-agents.generated.ts')\n continue\n }\n const existing = agentsById.get(candidate.id)\n if (existing) {\n throw new Error(\n `[AI Agents] Duplicate agent id \"${candidate.id}\" \u2014 already registered by module \"${existing.moduleId}\", conflicts with module \"${candidate.moduleId}\". Export \\`aiAgentOverrides\\` from your module's \\`ai-agents.ts\\` (or set it on the modules.ts entry) to replace an agent across modules.`\n )\n }\n agentsById.set(candidate.id, validateAndNormalizeAgent(candidate))\n }\n}\n\nasync function loadOverrideEntries(): Promise<AiAgentOverrideConfigEntry[]> {\n try {\n const mod = (await importGeneratedAiAgentsModule()) as {\n aiAgentOverrideEntries?: unknown[]\n } | null\n return mod && Array.isArray(mod.aiAgentOverrideEntries)\n ? (mod.aiAgentOverrideEntries as AiAgentOverrideConfigEntry[])\n : []\n } catch {\n // No generated file yet \u2014 pre-generate builds and tests fall through.\n return []\n }\n}\n\nfunction applyOverridesToRegistry(entries: readonly AiAgentOverrideConfigEntry[]): void {\n const overrideMap = composeAgentOverrideMap(entries)\n if (Object.keys(overrideMap).length === 0) return\n const overridden = applyAgentOverrideMap(Array.from(agentsById.values()), overrideMap)\n agentsById.clear()\n for (const agent of overridden) agentsById.set(agent.id, agent)\n for (const [id, value] of Object.entries(overrideMap)) {\n const verb = value === null ? 'disabled' : 'replaced'\n logger.info('Agent changed by override', { agentId: id, change: verb })\n }\n}\n\nasync function loadExtensionEntries(): Promise<AiAgentExtension[]> {\n try {\n const mod = (await importGeneratedAiAgentsModule()) as {\n aiAgentExtensionEntries?: unknown[]\n } | null\n const entries =\n mod && Array.isArray(mod.aiAgentExtensionEntries)\n ? (mod.aiAgentExtensionEntries as AiAgentExtensionConfigEntry[])\n : []\n return composeAgentExtensionEntries(entries).filter(isAiAgentExtension)\n } catch {\n return []\n }\n}\n\nfunction applyExtensionsToRegistry(extensions: readonly AiAgentExtension[]): void {\n if (extensions.length === 0) return\n for (const extension of extensions) {\n const agent = agentsById.get(extension.targetAgentId)\n if (!agent) {\n logger.warn('Skipping extension for unknown agent', { agentId: extension.targetAgentId })\n continue\n }\n\n const replacementSystemPrompt = extension.replaceSystemPrompt?.trim()\n const appendSystemPrompt = extension.appendSystemPrompt?.trim()\n const systemPrompt = replacementSystemPrompt ?? agent.systemPrompt.trim()\n const patchedAgent: AiAgentDefinition = {\n ...agent,\n allowedTools: applyStringListPatch(agent.allowedTools, {\n replace: extension.replaceAllowedTools,\n delete: extension.deleteAllowedTools,\n append: extension.appendAllowedTools,\n }),\n taskPlan: extension.taskPlan !== undefined ? extension.taskPlan : agent.taskPlan,\n systemPrompt: appendSystemPrompt\n ? `${systemPrompt}\\n\\n${appendSystemPrompt}`\n : systemPrompt,\n suggestions: applySuggestionPatch(agent.suggestions ?? [], {\n replace: extension.replaceSuggestions,\n delete: extension.deleteSuggestions,\n append: [\n ...(extension.appendSuggestions ?? []),\n ...(extension.suggestions ?? []),\n ],\n }),\n }\n agentsById.set(agent.id, validateAndNormalizeAgent(patchedAgent))\n }\n}\n\nexport async function loadAgentRegistry(): Promise<void> {\n if (loaded) return\n try {\n const mod = (await importGeneratedAiAgentsModule()) as {\n allAiAgents?: unknown[]\n } | null\n const agents = mod && Array.isArray(mod.allAiAgents) ? mod.allAiAgents : []\n populateFromAgents(agents)\n } catch (error) {\n logger.error('AI Agents \u2014 Could not load ai-agents.generated.ts (agent registry empty)', { err: error })\n } finally {\n try {\n const overrideEntries = await loadOverrideEntries()\n applyOverridesToRegistry(overrideEntries)\n const extensionEntries = await loadExtensionEntries()\n applyExtensionsToRegistry(extensionEntries)\n } catch (error) {\n logger.error('AI Agents \u2014 Failed to apply agent overrides/extensions', { err: error })\n }\n loaded = true\n }\n}\n\nexport function getAgent(id: string): AiAgentDefinition | undefined {\n return agentsById.get(id)\n}\n\nexport function listAgents(): AiAgentDefinition[] {\n return Array.from(agentsById.values()).sort((a, b) => a.id.localeCompare(b.id))\n}\n\nexport function listAgentsByModule(moduleId: string): AiAgentDefinition[] {\n return listAgents().filter((agent) => agent.moduleId === moduleId)\n}\n\n/**\n * @__internal\n * Test-only hook \u2014 clears the cached registry so `loadAgentRegistry` re-evaluates its source.\n */\nexport function resetAgentRegistryForTests(): void {\n agentsById.clear()\n loaded = false\n}\n\n/**\n * @__internal\n * Test-only hook \u2014 seeds the registry directly from a fixture array without going through\n * the dynamic generated-file import. Used by the registry's own unit tests.\n */\nexport function seedAgentRegistryForTests(agents: unknown[]): void {\n agentsById.clear()\n populateFromAgents(agents)\n loaded = true\n}\n\n/**\n * @__internal\n * Test-only hook \u2014 apply override entries against the seeded registry to\n * exercise the override pipeline without round-tripping through the\n * generated file.\n */\nexport function applyAgentOverrideEntriesForTests(\n entries: readonly AiAgentOverrideConfigEntry[],\n): void {\n applyOverridesToRegistry(entries)\n}\n\n/**\n * @__internal Test-only hook \u2014 apply additive extension entries against the\n * seeded registry without round-tripping through the generated file.\n */\nexport function applyAgentExtensionEntriesForTests(\n entries: readonly AiAgentExtension[],\n): void {\n applyExtensionsToRegistry(entries)\n}\n"],
5
5
  "mappings": "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",
6
6
  "names": ["key"]
7
7
  }
@@ -12,7 +12,7 @@ import {
12
12
  } from "ai";
13
13
  import { createModelFactory, resolveAllowRuntimeOverride } from "./model-factory.js";
14
14
  import { computeEndUserIdentifier } from "@open-mercato/shared/lib/ai/safety-identifier";
15
- import { llmProviderRegistry } from "@open-mercato/shared/lib/ai/llm-provider-registry";
15
+ import { llmProviderRegistry } from "./llm-registry.js";
16
16
  import {
17
17
  AiModerationBlockedError,
18
18
  AiModerationUnavailableError
@@ -46,7 +46,6 @@ import { isKnownMutationPolicy } from "./agent-policy.js";
46
46
  import { recordTokenUsage } from "./token-usage-recorder.js";
47
47
  import { injectTaskPlanIntoStream } from "./task-plan-stream.js";
48
48
  import { TASK_PLAN_RUNTIME_PROMPT_SECTION } from "./task-plan-labels.js";
49
- import "./llm-bootstrap.js";
50
49
  const logger = createLogger("ai_assistant").child({ component: "agent-runtime" });
51
50
  const WRAPPER_DEFAULT_LOOP_CHAT = { maxSteps: 10 };
52
51
  const WRAPPER_DEFAULT_LOOP_OBJECT = {};