@open-mercato/ai-assistant 0.6.7-develop.6862.1.c11a64ce0a → 0.6.7

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 (114) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +29 -12
  3. package/dist/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.js +0 -30
  4. package/dist/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.js.map +2 -2
  5. package/dist/modules/ai_assistant/api/ai/chat/route.js +0 -7
  6. package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
  7. package/dist/modules/ai_assistant/api/settings/allowlist/route.js +10 -10
  8. package/dist/modules/ai_assistant/api/settings/allowlist/route.js.map +2 -2
  9. package/dist/modules/ai_assistant/api/settings/route.js +13 -34
  10. package/dist/modules/ai_assistant/api/settings/route.js.map +2 -2
  11. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js +0 -106
  12. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js.map +2 -2
  13. package/dist/modules/ai_assistant/data/entities.js +0 -42
  14. package/dist/modules/ai_assistant/data/entities.js.map +2 -2
  15. package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js +1 -3
  16. package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js.map +2 -2
  17. package/dist/modules/ai_assistant/di.js +2 -6
  18. package/dist/modules/ai_assistant/di.js.map +2 -2
  19. package/dist/modules/ai_assistant/events.js +0 -6
  20. package/dist/modules/ai_assistant/events.js.map +2 -2
  21. package/dist/modules/ai_assistant/i18n/de.json +0 -42
  22. package/dist/modules/ai_assistant/i18n/en.json +0 -42
  23. package/dist/modules/ai_assistant/i18n/es.json +0 -42
  24. package/dist/modules/ai_assistant/i18n/pl.json +0 -42
  25. package/dist/modules/ai_assistant/lib/agent-runtime.js +5 -158
  26. package/dist/modules/ai_assistant/lib/agent-runtime.js.map +2 -2
  27. package/dist/modules/ai_assistant/lib/ai-agent-definition.js.map +2 -2
  28. package/dist/modules/ai_assistant/lib/auth.js +7 -6
  29. package/dist/modules/ai_assistant/lib/auth.js.map +2 -2
  30. package/dist/modules/ai_assistant/lib/entity-graph.js +3 -3
  31. package/dist/modules/ai_assistant/lib/entity-graph.js.map +2 -2
  32. package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js +0 -3
  33. package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js.map +2 -2
  34. package/dist/modules/ai_assistant/lib/llm-adapters/openai.js +1 -8
  35. package/dist/modules/ai_assistant/lib/llm-adapters/openai.js.map +2 -2
  36. package/dist/modules/ai_assistant/lib/model-factory.js +1 -6
  37. package/dist/modules/ai_assistant/lib/model-factory.js.map +2 -2
  38. package/dist/modules/ai_assistant/lib/openai-compatible-presets.js +0 -2
  39. package/dist/modules/ai_assistant/lib/openai-compatible-presets.js.map +2 -2
  40. package/dist/modules/ai_assistant/lib/tool-search.js +12 -4
  41. package/dist/modules/ai_assistant/lib/tool-search.js.map +2 -2
  42. package/generated/entities/ai_agent_runtime_override/index.ts +0 -1
  43. package/generated/entities.ids.generated.ts +1 -2
  44. package/generated/entity-fields-registry.ts +1 -13
  45. package/package.json +12 -13
  46. package/src/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.ts +0 -48
  47. package/src/modules/ai_assistant/api/ai/chat/route.ts +0 -10
  48. package/src/modules/ai_assistant/api/settings/__tests__/route.test.ts +8 -58
  49. package/src/modules/ai_assistant/api/settings/allowlist/route.ts +12 -10
  50. package/src/modules/ai_assistant/api/settings/route.ts +10 -33
  51. package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx +0 -146
  52. package/src/modules/ai_assistant/data/entities.ts +0 -56
  53. package/src/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.ts +0 -4
  54. package/src/modules/ai_assistant/di.ts +1 -5
  55. package/src/modules/ai_assistant/events.ts +0 -21
  56. package/src/modules/ai_assistant/i18n/de.json +0 -42
  57. package/src/modules/ai_assistant/i18n/en.json +0 -42
  58. package/src/modules/ai_assistant/i18n/es.json +0 -42
  59. package/src/modules/ai_assistant/i18n/pl.json +0 -42
  60. package/src/modules/ai_assistant/lib/__tests__/auth.test.ts +5 -2
  61. package/src/modules/ai_assistant/lib/__tests__/llm-adapters-anthropic.test.ts +0 -13
  62. package/src/modules/ai_assistant/lib/__tests__/llm-adapters-openai.test.ts +0 -41
  63. package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +0 -60
  64. package/src/modules/ai_assistant/lib/agent-runtime.ts +0 -259
  65. package/src/modules/ai_assistant/lib/ai-agent-definition.ts +0 -10
  66. package/src/modules/ai_assistant/lib/auth.ts +15 -6
  67. package/src/modules/ai_assistant/lib/entity-graph.ts +3 -3
  68. package/src/modules/ai_assistant/lib/llm-adapters/anthropic.ts +0 -10
  69. package/src/modules/ai_assistant/lib/llm-adapters/openai.ts +1 -32
  70. package/src/modules/ai_assistant/lib/model-factory.ts +0 -33
  71. package/src/modules/ai_assistant/lib/openai-compatible-presets.ts +0 -2
  72. package/src/modules/ai_assistant/lib/tool-search.ts +17 -4
  73. package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +16 -388
  74. package/dist/modules/ai_assistant/__integration__/TC-AI-MODERATION-008-settings-and-audit.spec.js +0 -80
  75. package/dist/modules/ai_assistant/__integration__/TC-AI-MODERATION-008-settings-and-audit.spec.js.map +0 -7
  76. package/dist/modules/ai_assistant/__integration__/TC-AI-MODERATION-009-ui.spec.js +0 -124
  77. package/dist/modules/ai_assistant/__integration__/TC-AI-MODERATION-009-ui.spec.js.map +0 -7
  78. package/dist/modules/ai_assistant/api/moderation-flags/route.js +0 -121
  79. package/dist/modules/ai_assistant/api/moderation-flags/route.js.map +0 -7
  80. package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.js +0 -139
  81. package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.js.map +0 -7
  82. package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.js +0 -10
  83. package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.js.map +0 -7
  84. package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.meta.js +0 -26
  85. package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.meta.js.map +0 -7
  86. package/dist/modules/ai_assistant/data/repositories/AiModerationFlagRepository.js +0 -44
  87. package/dist/modules/ai_assistant/data/repositories/AiModerationFlagRepository.js.map +0 -7
  88. package/dist/modules/ai_assistant/i18n/ko.json +0 -527
  89. package/dist/modules/ai_assistant/lib/moderation-flag-recorder.js +0 -43
  90. package/dist/modules/ai_assistant/lib/moderation-flag-recorder.js.map +0 -7
  91. package/dist/modules/ai_assistant/lib/moderation-policy.js +0 -24
  92. package/dist/modules/ai_assistant/lib/moderation-policy.js.map +0 -7
  93. package/dist/modules/ai_assistant/lib/moderation.js +0 -109
  94. package/dist/modules/ai_assistant/lib/moderation.js.map +0 -7
  95. package/dist/modules/ai_assistant/migrations/Migration20260610134045_ai_assistant.js +0 -19
  96. package/dist/modules/ai_assistant/migrations/Migration20260610134045_ai_assistant.js.map +0 -7
  97. package/generated/entities/ai_moderation_flag/index.ts +0 -9
  98. package/src/modules/ai_assistant/__integration__/TC-AI-MODERATION-008-settings-and-audit.spec.ts +0 -126
  99. package/src/modules/ai_assistant/__integration__/TC-AI-MODERATION-009-ui.spec.ts +0 -161
  100. package/src/modules/ai_assistant/api/moderation-flags/__tests__/route.test.ts +0 -122
  101. package/src/modules/ai_assistant/api/moderation-flags/route.ts +0 -137
  102. package/src/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.tsx +0 -186
  103. package/src/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.meta.ts +0 -24
  104. package/src/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.tsx +0 -12
  105. package/src/modules/ai_assistant/data/repositories/AiModerationFlagRepository.ts +0 -79
  106. package/src/modules/ai_assistant/i18n/ko.json +0 -527
  107. package/src/modules/ai_assistant/lib/__tests__/input-moderation-gate.test.ts +0 -121
  108. package/src/modules/ai_assistant/lib/__tests__/moderation-flag-recorder.test.ts +0 -76
  109. package/src/modules/ai_assistant/lib/__tests__/moderation-policy.test.ts +0 -86
  110. package/src/modules/ai_assistant/lib/__tests__/moderation.test.ts +0 -114
  111. package/src/modules/ai_assistant/lib/moderation-flag-recorder.ts +0 -75
  112. package/src/modules/ai_assistant/lib/moderation-policy.ts +0 -77
  113. package/src/modules/ai_assistant/lib/moderation.ts +0 -179
  114. package/src/modules/ai_assistant/migrations/Migration20260610134045_ai_assistant.ts +0 -21
@@ -1,2 +1,2 @@
1
- [build:ai-assistant] found 231 entry points
1
+ [build:ai-assistant] found 220 entry points
2
2
  [build:ai-assistant] built successfully
package/AGENTS.md CHANGED
@@ -880,16 +880,6 @@ yarn mcp:serve
880
880
 
881
881
  Typed pending-action lifecycle events live in `src/modules/ai_assistant/events.ts` and are emitted via the shared `emitAiAssistantEvent` helper (`createModuleEvents`). The three ids are FROZEN per `BACKWARD_COMPATIBILITY.md` §5 and MUST NOT be renamed; payload fields are additive-only. `ai.action.confirmed` fires from `executePendingActionConfirm` with `{ pendingActionId, agentId, toolName, status, tenantId, organizationId, userId, resolvedByUserId, resolvedAt, executionResult, failedRecords? }`; `ai.action.cancelled` fires from `executePendingActionCancel` with the same shape plus an optional `reason`; `ai.action.expired` fires from the cancel helper's TTL short-circuit (and the Step 5.12 cleanup worker) with `resolvedByUserId: null` and additional `expiresAt` / `expiredAt` timestamps. All three use `category: 'system'` and `entity: 'ai_pending_action'`.
882
882
 
883
- `ai_assistant.moderation_flag.created` (`entity: 'ai_moderation_flag'`) fires best-effort from the input-moderation gate; payload carries flagged category names only, never prompt content.
884
-
885
- ## Input moderation & safety identifiers
886
-
887
- Guide: [`moderation.mdx`](../../apps/docs/docs/framework/ai-assistant/moderation.mdx) + spec `.ai/specs/2026-06-04-ai-input-moderation-and-safety-identifiers.md`. Envs: `OM_AI_INPUT_MODERATION`, `OM_AI_MODERATION_MODEL`.
888
-
889
- - Enforced surfaces (`untrustedInput`) fail **closed**; opt-in surfaces fail **open**.
890
- - Flagged categories are audit-only — never send them to the client.
891
- - The audit write is best-effort and MUST NOT block the rejection.
892
-
893
883
  ## Rules for the OpenCode Client
894
884
 
895
885
  Located in `lib/opencode-client.ts`. Use these methods when interacting with OpenCode:
@@ -1630,6 +1620,12 @@ Note: the guard is intentionally NOT added to `mcp-client.ts` `connectHttp`, whi
1630
1620
  - `src/modules/ai_assistant/lib/opencode-handlers.ts` - Fixed Promise.race completion bug
1631
1621
  - `src/frontend/hooks/useCommandPalette.ts` - Added ref pattern for sessionId
1632
1622
 
1623
+ **Diagnostic logging added** (can be removed after verification):
1624
+ - `[handleSubmit] DIAGNOSTIC` - Session check before routing
1625
+ - `[sendAgenticMessage] DIAGNOSTIC` - Request payload before fetch
1626
+ - `[startAgenticChat] DIAGNOSTIC` - Done event handling
1627
+ - `[AI Chat] DIAGNOSTIC` - Backend request received
1628
+
1633
1629
  ### 2026-01 - OpenCode Integration
1634
1630
 
1635
1631
  **Lesson learned:** When replacing an AI backend, preserve the session management contract — the frontend depends on `sessionId` in `done` events regardless of the underlying AI engine.
@@ -1649,9 +1645,30 @@ Note: the guard is intentionally NOT added to `mcp-client.ts` `connectHttp`, whi
1649
1645
  - `src/frontend/components/CommandPalette/ToolChatPage.tsx` - Added thinking UI
1650
1646
  - `src/frontend/types.ts` - Added ChatSSEEvent, isThinking
1651
1647
 
1652
- ### 2026-01 - API Discovery Tools / Hybrid Tool Discovery (superseded)
1648
+ ### 2026-01 - API Discovery Tools
1649
+
1650
+ **Lesson learned:** Exposing hundreds of individual tools overwhelms the AI context. Use meta-tools (discover, schema, execute) to let the agent dynamically find what it needs.
1653
1651
 
1654
- **Lesson learned:** Exposing hundreds of individual tools overwhelms the AI context — use meta-tools the agent can search. Superseded by Code Mode (2026-02-22); the tools and files these entries described (`api_discover`, `api_schema`, `lib/api-discovery-tools.ts`, `lib/entity-graph-tools.ts`) were deleted in #1876. See git history for the detail.
1652
+ **Major change**: Replaced 600+ individual tools with 3 meta-tools.
1653
+
1654
+ **What changed**:
1655
+ - Added `api_discover`, `api_execute`, `api_schema` tools
1656
+ - Created `ApiEndpointIndex` for OpenAPI introspection
1657
+ - Hybrid discovery: search + OpenAPI
1658
+ - 405 endpoints available via discovery
1659
+
1660
+ **Files created**:
1661
+ - `lib/api-discovery-tools.ts`
1662
+ - `lib/api-endpoint-index.ts`
1663
+
1664
+ ### 2026-01 - Hybrid Tool Discovery
1665
+
1666
+ **Lesson learned:** Neither search-based nor OpenAPI-based discovery alone covers all tools — combine both for comprehensive results.
1667
+
1668
+ **What changed**:
1669
+ - Combined semantic search with OpenAPI introspection
1670
+ - Tools indexed for fulltext search
1671
+ - API endpoints indexed from OpenAPI spec
1655
1672
 
1656
1673
  ### Previous Changes
1657
1674
 
@@ -100,34 +100,6 @@ async function deleteTenantAllowlistInDb(tenantId) {
100
100
  await client.query("delete from ai_tenant_model_allowlists where tenant_id = $1", [tenantId]);
101
101
  });
102
102
  }
103
- async function seedModerationFlagInDb(input) {
104
- const categories = JSON.stringify(input.categories ?? { hate: { flagged: true, score: 0.97 } });
105
- return withClient(async (client) => {
106
- const result = await client.query(
107
- `insert into ai_moderation_flags
108
- (id, tenant_id, organization_id, agent_id, user_id, provider_id, model_id, categories, created_at)
109
- values
110
- (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7::jsonb, now())
111
- returning id`,
112
- [
113
- input.tenantId,
114
- input.organizationId ?? null,
115
- input.agentId ?? "it_agent.moderation_fixture",
116
- input.userId ?? `it-user-${randomUUID()}`,
117
- input.providerId ?? "openai",
118
- input.modelId ?? "gpt-5-mini",
119
- categories
120
- ]
121
- );
122
- return result.rows[0].id;
123
- });
124
- }
125
- async function deleteModerationFlagsInDb(tenantId) {
126
- if (!tenantId) return;
127
- await withClient(async (client) => {
128
- await client.query("delete from ai_moderation_flags where tenant_id = $1", [tenantId]);
129
- });
130
- }
131
103
  async function deleteConversationCascadeInDb(input) {
132
104
  if (!input.tenantId || !input.conversationId) return;
133
105
  await withClient(async (client) => {
@@ -148,10 +120,8 @@ async function deleteConversationCascadeInDb(input) {
148
120
  export {
149
121
  deleteAgentOverridesInDb,
150
122
  deleteConversationCascadeInDb,
151
- deleteModerationFlagsInDb,
152
123
  deletePendingActionInDb,
153
124
  deleteTenantAllowlistInDb,
154
- seedModerationFlagInDb,
155
125
  seedPendingActionInDb
156
126
  };
157
127
  //# sourceMappingURL=aiAssistantFixtures.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.ts"],
4
- "sourcesContent": ["import { readFileSync } from 'node:fs';\nimport { randomUUID } from 'node:crypto';\nimport path from 'node:path';\nimport { Client } from 'pg';\n\n/**\n * Direct-Postgres fixtures for AI Assistant integration specs.\n *\n * Mirrors the sanctioned pattern in\n * `@open-mercato/core/helpers/integration/dbFixtures` (raw SQL against\n * `DATABASE_URL`) for the surfaces that have NO public create route:\n * - `ai_pending_actions`: a pending mutation approval is only ever born from\n * the internal `prepareMutation` path during a real LLM agent turn. Seeding\n * the row directly keeps confirm/cancel/GET coverage deterministic and\n * provider-free.\n * - prompt overrides: the `prompt-override` route exposes no DELETE, so\n * versioned rows must be swept via SQL after a test.\n *\n * The app server and this helper MUST share one `DATABASE_URL`, so specs using\n * these helpers only run under the coherent app+DB harness\n * (`yarn test:integration` / `:ephemeral`), never against an arbitrary dev\n * server whose `DATABASE_URL` differs from `apps/mercato/.env`.\n */\n\nfunction resolveAppRoot(): string {\n const fromEnv = process.env.OM_TEST_APP_ROOT?.trim();\n return fromEnv ? path.resolve(fromEnv) : path.resolve(process.cwd(), 'apps/mercato');\n}\n\nfunction readEnvValue(key: string): string | undefined {\n if (process.env[key]) return process.env[key];\n const candidatePaths = [\n path.resolve(resolveAppRoot(), '.env'),\n path.resolve(process.cwd(), 'apps/mercato/.env'),\n path.resolve(process.cwd(), '.env'),\n ];\n for (const envPath of candidatePaths) {\n try {\n const content = readFileSync(envPath, 'utf-8');\n const match = content.match(new RegExp(`^${key}=(.+)$`, 'm'));\n if (match?.[1]) return match[1].trim();\n } catch {\n continue;\n }\n }\n return undefined;\n}\n\nfunction resolveDatabaseUrl(): string {\n const url = readEnvValue('DATABASE_URL');\n if (!url) throw new Error('[internal] DATABASE_URL is not configured for AI Assistant integration DB fixtures');\n return url;\n}\n\nasync function withClient<T>(run: (client: Client) => Promise<T>): Promise<T> {\n const client = new Client({ connectionString: resolveDatabaseUrl() });\n await client.connect();\n try {\n return await run(client);\n } finally {\n await client.end();\n }\n}\n\nexport type SeededPendingActionStatus =\n | 'pending'\n | 'confirmed'\n | 'cancelled'\n | 'expired'\n | 'executing'\n | 'failed';\n\nexport interface SeedPendingActionInput {\n tenantId: string;\n organizationId?: string | null;\n createdByUserId: string;\n status?: SeededPendingActionStatus;\n agentId?: string;\n toolName?: string;\n /** Minutes from now until expiry. Negative => the row is already expired. */\n expiresInMinutes?: number;\n normalizedInput?: Record<string, unknown>;\n executionResult?: Record<string, unknown> | null;\n idempotencyKey?: string;\n}\n\nexport interface SeededPendingAction {\n id: string;\n idempotencyKey: string;\n}\n\n/**\n * Inserts an `ai_pending_actions` row directly. Returns the new id so the test\n * can act on it and clean it up. Defaults produce an actionable `pending` row\n * with a future TTL; override `status`/`expiresInMinutes`/`executionResult` to\n * cover the idempotency short-circuit and 409 error branches.\n */\nexport async function seedPendingActionInDb(input: SeedPendingActionInput): Promise<SeededPendingAction> {\n const idempotencyKey = input.idempotencyKey ?? `it-pending-${randomUUID()}`;\n const status = input.status ?? 'pending';\n const expiresInMinutes = input.expiresInMinutes ?? 60;\n const normalizedInput = JSON.stringify(input.normalizedInput ?? {});\n const executionResult =\n input.executionResult === undefined || input.executionResult === null\n ? null\n : JSON.stringify(input.executionResult);\n return withClient(async (client) => {\n const result = await client.query<{ id: string }>(\n `insert into ai_pending_actions\n (id, tenant_id, organization_id, agent_id, tool_name, normalized_input,\n field_diff, attachment_ids, idempotency_key, created_by_user_id, status,\n queue_mode, execution_result, created_at, expires_at)\n values\n (gen_random_uuid(), $1, $2, $3, $4, $5::jsonb,\n '[]'::jsonb, '[]'::jsonb, $6, $7, $8,\n 'inline', $9::jsonb, now(), now() + make_interval(mins => $10::int))\n returning id`,\n [\n input.tenantId,\n input.organizationId ?? null,\n input.agentId ?? 'it_agent.pending_fixture',\n input.toolName ?? 'it_tool.noop',\n normalizedInput,\n idempotencyKey,\n input.createdByUserId,\n status,\n executionResult,\n expiresInMinutes,\n ],\n );\n return { id: result.rows[0].id, idempotencyKey };\n });\n}\n\n/** Hard-deletes a seeded pending action row (best-effort test cleanup). */\nexport async function deletePendingActionInDb(id: string | null): Promise<void> {\n if (!id) return;\n await withClient(async (client) => {\n await client.query('delete from ai_pending_actions where id = $1', [id]);\n });\n}\n\n/**\n * Sweeps every per-agent override row for a tenant across the three override\n * tables. Required for `prompt-override` cleanup (no DELETE route) and used as a\n * belt-and-braces teardown for mutation-policy / loop overrides.\n */\nexport async function deleteAgentOverridesInDb(input: { tenantId: string; agentId: string }): Promise<void> {\n if (!input.tenantId || !input.agentId) return;\n await withClient(async (client) => {\n await client.query('delete from ai_agent_prompt_overrides where tenant_id = $1 and agent_id = $2', [\n input.tenantId,\n input.agentId,\n ]);\n await client.query('delete from ai_agent_mutation_policy_overrides where tenant_id = $1 and agent_id = $2', [\n input.tenantId,\n input.agentId,\n ]);\n await client.query('delete from ai_agent_runtime_overrides where tenant_id = $1 and agent_id = $2', [\n input.tenantId,\n input.agentId,\n ]);\n });\n}\n\n/** Hard-deletes any tenant model-allowlist rows (best-effort cleanup). */\nexport async function deleteTenantAllowlistInDb(tenantId: string | null): Promise<void> {\n if (!tenantId) return;\n await withClient(async (client) => {\n await client.query('delete from ai_tenant_model_allowlists where tenant_id = $1', [tenantId]);\n });\n}\n\nexport interface SeedModerationFlagInput {\n tenantId: string;\n organizationId?: string | null;\n agentId?: string;\n userId?: string;\n providerId?: string;\n modelId?: string;\n /** Defaults to a single flagged `hate` category. */\n categories?: Record<string, { flagged: boolean; score: number }>;\n}\n\n/**\n * Inserts an `ai_moderation_flags` audit row directly. A real flag is only ever\n * born from the gate during a flagged LLM turn (needs a live moderation call),\n * so seeding the row keeps audit-listing + tenant-isolation coverage\n * deterministic and provider-free. Returns the new id for assertions/cleanup.\n */\nexport async function seedModerationFlagInDb(input: SeedModerationFlagInput): Promise<string> {\n const categories = JSON.stringify(input.categories ?? { hate: { flagged: true, score: 0.97 } });\n return withClient(async (client) => {\n const result = await client.query<{ id: string }>(\n `insert into ai_moderation_flags\n (id, tenant_id, organization_id, agent_id, user_id, provider_id, model_id, categories, created_at)\n values\n (gen_random_uuid(), $1, $2, $3, $4, $5, $6, $7::jsonb, now())\n returning id`,\n [\n input.tenantId,\n input.organizationId ?? null,\n input.agentId ?? 'it_agent.moderation_fixture',\n input.userId ?? `it-user-${randomUUID()}`,\n input.providerId ?? 'openai',\n input.modelId ?? 'gpt-5-mini',\n categories,\n ],\n );\n return result.rows[0].id;\n });\n}\n\n/** Hard-deletes every moderation-flag row for a tenant (best-effort cleanup). */\nexport async function deleteModerationFlagsInDb(tenantId: string | null): Promise<void> {\n if (!tenantId) return;\n await withClient(async (client) => {\n await client.query('delete from ai_moderation_flags where tenant_id = $1', [tenantId]);\n });\n}\n\n/**\n * Hard-deletes a conversation and its participant + message rows by the\n * client-facing `conversation_id`. The DELETE route only soft-deletes, so this\n * gives specs a true teardown for the rows they created.\n */\nexport async function deleteConversationCascadeInDb(input: {\n tenantId: string;\n conversationId: string;\n}): Promise<void> {\n if (!input.tenantId || !input.conversationId) return;\n await withClient(async (client) => {\n await client.query('delete from ai_chat_messages where tenant_id = $1 and conversation_id = $2', [\n input.tenantId,\n input.conversationId,\n ]);\n await client.query(\n 'delete from ai_chat_conversation_participants where tenant_id = $1 and conversation_id = $2',\n [input.tenantId, input.conversationId],\n );\n await client.query('delete from ai_chat_conversations where tenant_id = $1 and conversation_id = $2', [\n input.tenantId,\n input.conversationId,\n ]);\n });\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { readFileSync } from 'node:fs';\nimport { randomUUID } from 'node:crypto';\nimport path from 'node:path';\nimport { Client } from 'pg';\n\n/**\n * Direct-Postgres fixtures for AI Assistant integration specs.\n *\n * Mirrors the sanctioned pattern in\n * `@open-mercato/core/helpers/integration/dbFixtures` (raw SQL against\n * `DATABASE_URL`) for the surfaces that have NO public create route:\n * - `ai_pending_actions`: a pending mutation approval is only ever born from\n * the internal `prepareMutation` path during a real LLM agent turn. Seeding\n * the row directly keeps confirm/cancel/GET coverage deterministic and\n * provider-free.\n * - prompt overrides: the `prompt-override` route exposes no DELETE, so\n * versioned rows must be swept via SQL after a test.\n *\n * The app server and this helper MUST share one `DATABASE_URL`, so specs using\n * these helpers only run under the coherent app+DB harness\n * (`yarn test:integration` / `:ephemeral`), never against an arbitrary dev\n * server whose `DATABASE_URL` differs from `apps/mercato/.env`.\n */\n\nfunction resolveAppRoot(): string {\n const fromEnv = process.env.OM_TEST_APP_ROOT?.trim();\n return fromEnv ? path.resolve(fromEnv) : path.resolve(process.cwd(), 'apps/mercato');\n}\n\nfunction readEnvValue(key: string): string | undefined {\n if (process.env[key]) return process.env[key];\n const candidatePaths = [\n path.resolve(resolveAppRoot(), '.env'),\n path.resolve(process.cwd(), 'apps/mercato/.env'),\n path.resolve(process.cwd(), '.env'),\n ];\n for (const envPath of candidatePaths) {\n try {\n const content = readFileSync(envPath, 'utf-8');\n const match = content.match(new RegExp(`^${key}=(.+)$`, 'm'));\n if (match?.[1]) return match[1].trim();\n } catch {\n continue;\n }\n }\n return undefined;\n}\n\nfunction resolveDatabaseUrl(): string {\n const url = readEnvValue('DATABASE_URL');\n if (!url) throw new Error('[internal] DATABASE_URL is not configured for AI Assistant integration DB fixtures');\n return url;\n}\n\nasync function withClient<T>(run: (client: Client) => Promise<T>): Promise<T> {\n const client = new Client({ connectionString: resolveDatabaseUrl() });\n await client.connect();\n try {\n return await run(client);\n } finally {\n await client.end();\n }\n}\n\nexport type SeededPendingActionStatus =\n | 'pending'\n | 'confirmed'\n | 'cancelled'\n | 'expired'\n | 'executing'\n | 'failed';\n\nexport interface SeedPendingActionInput {\n tenantId: string;\n organizationId?: string | null;\n createdByUserId: string;\n status?: SeededPendingActionStatus;\n agentId?: string;\n toolName?: string;\n /** Minutes from now until expiry. Negative => the row is already expired. */\n expiresInMinutes?: number;\n normalizedInput?: Record<string, unknown>;\n executionResult?: Record<string, unknown> | null;\n idempotencyKey?: string;\n}\n\nexport interface SeededPendingAction {\n id: string;\n idempotencyKey: string;\n}\n\n/**\n * Inserts an `ai_pending_actions` row directly. Returns the new id so the test\n * can act on it and clean it up. Defaults produce an actionable `pending` row\n * with a future TTL; override `status`/`expiresInMinutes`/`executionResult` to\n * cover the idempotency short-circuit and 409 error branches.\n */\nexport async function seedPendingActionInDb(input: SeedPendingActionInput): Promise<SeededPendingAction> {\n const idempotencyKey = input.idempotencyKey ?? `it-pending-${randomUUID()}`;\n const status = input.status ?? 'pending';\n const expiresInMinutes = input.expiresInMinutes ?? 60;\n const normalizedInput = JSON.stringify(input.normalizedInput ?? {});\n const executionResult =\n input.executionResult === undefined || input.executionResult === null\n ? null\n : JSON.stringify(input.executionResult);\n return withClient(async (client) => {\n const result = await client.query<{ id: string }>(\n `insert into ai_pending_actions\n (id, tenant_id, organization_id, agent_id, tool_name, normalized_input,\n field_diff, attachment_ids, idempotency_key, created_by_user_id, status,\n queue_mode, execution_result, created_at, expires_at)\n values\n (gen_random_uuid(), $1, $2, $3, $4, $5::jsonb,\n '[]'::jsonb, '[]'::jsonb, $6, $7, $8,\n 'inline', $9::jsonb, now(), now() + make_interval(mins => $10::int))\n returning id`,\n [\n input.tenantId,\n input.organizationId ?? null,\n input.agentId ?? 'it_agent.pending_fixture',\n input.toolName ?? 'it_tool.noop',\n normalizedInput,\n idempotencyKey,\n input.createdByUserId,\n status,\n executionResult,\n expiresInMinutes,\n ],\n );\n return { id: result.rows[0].id, idempotencyKey };\n });\n}\n\n/** Hard-deletes a seeded pending action row (best-effort test cleanup). */\nexport async function deletePendingActionInDb(id: string | null): Promise<void> {\n if (!id) return;\n await withClient(async (client) => {\n await client.query('delete from ai_pending_actions where id = $1', [id]);\n });\n}\n\n/**\n * Sweeps every per-agent override row for a tenant across the three override\n * tables. Required for `prompt-override` cleanup (no DELETE route) and used as a\n * belt-and-braces teardown for mutation-policy / loop overrides.\n */\nexport async function deleteAgentOverridesInDb(input: { tenantId: string; agentId: string }): Promise<void> {\n if (!input.tenantId || !input.agentId) return;\n await withClient(async (client) => {\n await client.query('delete from ai_agent_prompt_overrides where tenant_id = $1 and agent_id = $2', [\n input.tenantId,\n input.agentId,\n ]);\n await client.query('delete from ai_agent_mutation_policy_overrides where tenant_id = $1 and agent_id = $2', [\n input.tenantId,\n input.agentId,\n ]);\n await client.query('delete from ai_agent_runtime_overrides where tenant_id = $1 and agent_id = $2', [\n input.tenantId,\n input.agentId,\n ]);\n });\n}\n\n/** Hard-deletes any tenant model-allowlist rows (best-effort cleanup). */\nexport async function deleteTenantAllowlistInDb(tenantId: string | null): Promise<void> {\n if (!tenantId) return;\n await withClient(async (client) => {\n await client.query('delete from ai_tenant_model_allowlists where tenant_id = $1', [tenantId]);\n });\n}\n\n/**\n * Hard-deletes a conversation and its participant + message rows by the\n * client-facing `conversation_id`. The DELETE route only soft-deletes, so this\n * gives specs a true teardown for the rows they created.\n */\nexport async function deleteConversationCascadeInDb(input: {\n tenantId: string;\n conversationId: string;\n}): Promise<void> {\n if (!input.tenantId || !input.conversationId) return;\n await withClient(async (client) => {\n await client.query('delete from ai_chat_messages where tenant_id = $1 and conversation_id = $2', [\n input.tenantId,\n input.conversationId,\n ]);\n await client.query(\n 'delete from ai_chat_conversation_participants where tenant_id = $1 and conversation_id = $2',\n [input.tenantId, input.conversationId],\n );\n await client.query('delete from ai_chat_conversations where tenant_id = $1 and conversation_id = $2', [\n input.tenantId,\n input.conversationId,\n ]);\n });\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -11,7 +11,6 @@ import {
11
11
  resolveLoopBudgetPreset
12
12
  } from "../../../lib/agent-runtime.js";
13
13
  import { AgentPolicyError } from "../../../lib/agent-tools.js";
14
- import { AiModerationBlockedError, AiModerationUnavailableError } from "../../../lib/moderation.js";
15
14
  import { readBaseurlAllowlist, isBaseurlAllowlisted } from "../../../lib/baseurl-allowlist.js";
16
15
  import {
17
16
  canonicalProviderId,
@@ -550,12 +549,6 @@ async function POST(req) {
550
549
  if (error instanceof AgentPolicyError) {
551
550
  return jsonError(statusForDenyCode(error.code), error.message, error.code);
552
551
  }
553
- if (error instanceof AiModerationBlockedError) {
554
- return jsonError(400, "Input rejected by the content safety filter.", "moderation_blocked");
555
- }
556
- if (error instanceof AiModerationUnavailableError) {
557
- return jsonError(503, "Content safety check temporarily unavailable.", "moderation_unavailable");
558
- }
559
552
  logger.error("AI Chat Agent \u2014 Dispatch failure", { err: error });
560
553
  return jsonError(
561
554
  500,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/ai_assistant/api/ai/chat/route.ts"],
4
- "sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport { NextResponse, type NextRequest } from 'next/server'\nimport type { UIMessage } from 'ai'\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 { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport { loadAgentRegistry } from '../../../lib/agent-registry'\nimport { checkAgentPolicy, type AgentPolicyDenyCode } from '../../../lib/agent-policy'\nimport {\n runAiAgentText,\n resolveLoopBudgetPreset,\n type AiAgentLoopBudgetPreset,\n} from '../../../lib/agent-runtime'\nimport { AgentPolicyError } from '../../../lib/agent-tools'\nimport { AiModerationBlockedError, AiModerationUnavailableError } from '../../../lib/moderation'\nimport { readBaseurlAllowlist, isBaseurlAllowlisted } from '../../../lib/baseurl-allowlist'\nimport {\n canonicalProviderId,\n hasAllowlistSnapshotRestrictions,\n intersectEffectiveAllowlistWithSnapshot,\n intersectAllowlists,\n isModelAllowedForProviderInEffective,\n isProviderAllowedInEffective,\n modelAllowlistEnvVarName,\n readAgentRuntimeOverrideAllowlist,\n type TenantAllowlistSnapshot,\n} from '../../../lib/model-allowlist'\nimport { AiTenantModelAllowlistRepository } from '../../../data/repositories/AiTenantModelAllowlistRepository'\nimport { AiAgentRuntimeOverrideRepository } from '../../../data/repositories/AiAgentRuntimeOverrideRepository'\nimport { createConversationStorage } from '../../../lib/conversation-storage'\nimport { checkAiChatRateLimit } from '../../../lib/rate-limit'\nimport type { EntityManager } from '@mikro-orm/postgresql'\n\nconst logger = createLogger('ai_assistant')\n\nconst MAX_MESSAGES = 100\n\nconst agentIdPattern = /^[a-z0-9_]+\\.[a-z0-9_]+$/\n\nconst chatMessageSchema = z.object({\n id: z.string().min(1).max(128).optional(),\n role: z.enum(['user', 'assistant', 'system']),\n content: z.string(),\n uiParts: z.array(z.unknown()).optional(),\n files: z\n .array(\n z\n .object({\n id: z.string().optional(),\n name: z.string().optional(),\n type: z.string().optional(),\n mimeType: z.string().optional(),\n size: z.number().optional(),\n })\n .passthrough(),\n )\n .optional(),\n})\n\nconst pageContextSchema = z\n .object({\n pageId: z.string().nullable().optional(),\n entityType: z.string().nullable().optional(),\n recordId: z.string().nullable().optional(),\n })\n .passthrough()\n\nconst chatRequestSchema = z.object({\n messages: z\n .array(chatMessageSchema)\n .min(1, 'messages must contain at least one message')\n .max(MAX_MESSAGES, `messages must contain at most ${MAX_MESSAGES} entries`),\n attachmentIds: z.array(z.string()).optional(),\n debug: z.boolean().optional(),\n pageContext: pageContextSchema.optional(),\n /**\n * Stable per-conversation id (Phase 6.2). Wins over `conversationId` when\n * both are provided. The server echoes the resolved id on the SSE\n * `loop-finish` event so clients can persist it for the next turn.\n */\n sessionId: z.string().uuid().optional(),\n /**\n * @deprecated Use `sessionId` instead.\n */\n conversationId: z.string().min(1).max(128).optional(),\n})\n\nexport type AiChatRequest = z.infer<typeof chatRequestSchema>\n\nconst agentQuerySchema = z.object({\n agent: z\n .string()\n .regex(agentIdPattern, 'agent must match \"<module>.<agent>\" (lowercase, digits, underscores only)'),\n /**\n * Per-request provider override. Must match a registered + configured\n * provider id. Validated against `llmProviderRegistry` at dispatch time.\n * Rejected when the agent has `allowRuntimeOverride: false`.\n *\n * Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n provider: z.string().optional(),\n /**\n * Per-request model id override. Free-form string. Logged (not rejected)\n * when not in the provider's curated `defaultModels` catalog.\n *\n * Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n model: z.string().optional(),\n /**\n * Per-request base URL override. Must parse as a URL and match\n * `AI_RUNTIME_BASEURL_ALLOWLIST` (comma-separated host patterns). When the\n * env var is unset or empty, any non-empty value is rejected.\n *\n * Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n baseUrl: z.string().optional(),\n /**\n * Named loop-budget preset. Maps to a fixed `loop.budget` triple:\n * tight \u2192 maxSteps: 3, maxWallClockMs: 10_000, maxTokens: 50_000\n * default \u2192 no override (agent default applies)\n * loose \u2192 maxSteps: 20, maxWallClockMs: 120_000, maxTokens: 500_000\n *\n * Rejected when the agent has `allowRuntimeOverride: false` or\n * `loop.allowRuntimeOverride: false`.\n *\n * Phase 4 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\n loopBudget: z.enum(['tight', 'default', 'loose']).optional(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'AI agent dispatcher',\n methods: {\n POST: {\n operationId: 'aiAssistantChatAgent',\n summary: 'Stream a chat turn for a registered AI agent',\n description:\n 'Dispatches a chat turn to the focused AI agent identified by `?agent=<module>.<agent>`. ' +\n 'Enforces agent-level `requiredFeatures`, tool whitelisting, read-only / mutationPolicy, ' +\n 'execution-mode compatibility, and attachment media-type policy. The streaming response ' +\n 'body uses an AI SDK-compatible `text/event-stream` transport. ' +\n 'Optional `?provider=`, `?model=`, and `?baseUrl=` query params let callers ' +\n 'override the resolved provider/model/base-URL for this turn (Phase 4a). ' +\n 'Provider must be registered and configured; baseUrl must match ' +\n '`AI_RUNTIME_BASEURL_ALLOWLIST` when set. Both are suppressed when the ' +\n 'agent declares `allowRuntimeOverride: false`.',\n query: agentQuerySchema,\n requestBody: {\n contentType: 'application/json',\n description: 'Chat turn payload. `messages` is required; `attachmentIds`, `debug`, and `pageContext` are optional.',\n schema: chatRequestSchema,\n },\n responses: [\n { status: 200, description: 'Streaming text/event-stream response compatible with AI SDK chat transports.', mediaType: 'text/event-stream' },\n ],\n errors: [\n {\n status: 400,\n description:\n 'Invalid query param, malformed payload, or message count above the cap. ' +\n 'Typed codes: `runtime_override_disabled` (agent has allowRuntimeOverride:false), ' +\n '`provider_unknown` (provider id not registered), ' +\n '`provider_not_configured` (provider registered but no API key in env), ' +\n '`baseurl_not_allowlisted` (baseUrl not in AI_RUNTIME_BASEURL_ALLOWLIST).',\n },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks agent-level or tool-level required features.' },\n { status: 404, description: 'Unknown agent id.' },\n { status: 409, description: 'Agent/tool/execution-mode policy violation.' },\n { status: 500, description: 'Internal runtime failure.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\nfunction jsonError(\n status: number,\n message: string,\n code: string,\n extra?: Record<string, unknown>,\n): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\nfunction statusForDenyCode(code: AgentPolicyDenyCode): number {\n switch (code) {\n case 'agent_unknown':\n return 404\n case 'agent_features_denied':\n case 'tool_features_denied':\n return 403\n case 'tool_not_whitelisted':\n case 'tool_unknown':\n case 'mutation_blocked_by_readonly':\n case 'mutation_blocked_by_policy':\n case 'execution_mode_not_supported':\n return 409\n case 'attachment_type_not_accepted':\n return 400\n default:\n return 409\n }\n}\n\nfunction extractDataPayload(eventBlock: string): string | null {\n const dataLines = eventBlock\n .split('\\n')\n .filter((line) => line.startsWith('data:'))\n .map((line) => (line.startsWith('data: ') ? line.slice(6) : line.slice(5)))\n if (dataLines.length === 0) return null\n return dataLines.join('\\n')\n}\n\nfunction extractUiPartsFromToolOutput(output: unknown): unknown[] {\n let parsed = output\n if (typeof output === 'string') {\n const trimmed = output.trim()\n if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return []\n try {\n parsed = JSON.parse(trimmed) as unknown\n } catch {\n return []\n }\n }\n if (!parsed || typeof parsed !== 'object') return []\n const value = parsed as Record<string, unknown>\n const parts: unknown[] = []\n if (value.status === 'pending-confirmation' || value.status === 'awaiting-confirmation') {\n const pendingActionId =\n typeof value.pendingActionId === 'string' && value.pendingActionId.length > 0\n ? value.pendingActionId\n : null\n if (pendingActionId) {\n parts.push({\n componentId: 'mutation-preview-card',\n pendingActionId,\n payload: {\n pendingActionId,\n expiresAt: typeof value.expiresAt === 'string' ? value.expiresAt : undefined,\n agentId:\n typeof value.agentId === 'string'\n ? value.agentId\n : typeof value.agent === 'string'\n ? value.agent\n : undefined,\n toolName: typeof value.toolName === 'string' ? value.toolName : undefined,\n },\n })\n }\n }\n if (value.uiPart && typeof value.uiPart === 'object') parts.push(value.uiPart)\n if (Array.isArray(value.uiParts)) parts.push(...value.uiParts)\n return parts\n}\n\nfunction extractAssistantSnapshot(\n raw: string,\n contentType: string | null,\n): { content: string; uiParts: unknown[] } {\n if (!contentType?.includes('event-stream')) {\n return { content: raw, uiParts: [] }\n }\n let content = ''\n const uiParts: unknown[] = []\n for (const block of raw.split('\\n\\n')) {\n const data = extractDataPayload(block)\n if (!data || data === '[DONE]') continue\n try {\n const parsed = JSON.parse(data) as Record<string, unknown>\n if (parsed.type === 'text-delta' && typeof parsed.delta === 'string') {\n content += parsed.delta\n } else if (parsed.type === 'text' && typeof parsed.content === 'string') {\n content += parsed.content\n } else if (parsed.type === 'tool-output-available') {\n uiParts.push(...extractUiPartsFromToolOutput(parsed.output))\n }\n } catch {\n // Ignore SSE comments and malformed provider chunks.\n }\n }\n return { content, uiParts }\n}\n\nasync function persistChatTurnStart(input: {\n container: Awaited<ReturnType<typeof createRequestContainer>>\n tenantId: string | null | undefined\n organizationId: string | null | undefined\n userId: string\n agentId: string\n conversationId: string | null\n pageContext?: Record<string, unknown>\n messages: AiChatRequest['messages']\n attachmentIds?: string[]\n}): Promise<{ conversationId: string; userClientMessageId: string | null } | null> {\n if (!input.tenantId || !input.conversationId) return null\n const repo = createConversationStorage(input.container)\n const ctx = {\n tenantId: input.tenantId,\n organizationId: input.organizationId ?? null,\n userId: input.userId,\n }\n await repo.createOrGet(\n {\n conversationId: input.conversationId,\n agentId: input.agentId,\n pageContext: input.pageContext ?? null,\n },\n ctx,\n )\n const userMessage = [...input.messages].reverse().find((message) => message.role === 'user')\n if (!userMessage) return { conversationId: input.conversationId, userClientMessageId: null }\n await repo.appendMessage(\n input.conversationId,\n {\n clientMessageId: userMessage.id,\n role: 'user',\n content: userMessage.content,\n uiParts: userMessage.uiParts,\n attachmentIds: input.attachmentIds,\n files: userMessage.files?.map((file, index) => {\n const id = file.id ?? input.attachmentIds?.[index]\n const mimeType = file.mimeType ?? file.type\n return {\n ...(id ? { id } : {}),\n ...(file.name ? { name: file.name } : {}),\n ...(mimeType ? { mimeType } : {}),\n ...(typeof file.size === 'number' ? { size: file.size } : {}),\n }\n }),\n },\n ctx,\n )\n return {\n conversationId: input.conversationId,\n userClientMessageId: userMessage.id ?? null,\n }\n}\n\nfunction persistAssistantOnStreamCompletion(input: {\n response: Response\n container: Awaited<ReturnType<typeof createRequestContainer>>\n tenantId: string | null | undefined\n organizationId: string | null | undefined\n userId: string\n conversationId: string\n userClientMessageId: string | null\n}): Response {\n if (!input.response.body || !input.tenantId) return input.response\n const tenantId = input.tenantId\n const { readable, writable } = new TransformStream<Uint8Array, Uint8Array>()\n const writer = writable.getWriter()\n const decoder = new TextDecoder()\n const contentType = input.response.headers.get('content-type')\n\n async function pump(): Promise<void> {\n const reader = input.response.body!.getReader()\n let raw = ''\n try {\n for (;;) {\n const { value, done } = await reader.read()\n if (done) break\n if (!value) continue\n raw += decoder.decode(value, { stream: true })\n await writer.write(value)\n }\n raw += decoder.decode()\n const assistant = extractAssistantSnapshot(raw, contentType)\n if (assistant.content.trim() || assistant.uiParts.length > 0) {\n const repo = createConversationStorage(input.container)\n await repo.appendMessage(\n input.conversationId,\n {\n clientMessageId: input.userClientMessageId\n ? `${input.userClientMessageId}:assistant`\n : undefined,\n role: 'assistant',\n content: assistant.content,\n uiParts: assistant.uiParts,\n },\n {\n tenantId,\n organizationId: input.organizationId ?? null,\n userId: input.userId,\n },\n )\n }\n } catch (error) {\n logger.error('AI Chat Agent \u2014 Conversation persistence failure', { err: error })\n } finally {\n reader.releaseLock()\n await writer.close().catch(() => undefined)\n }\n }\n\n void pump()\n return new Response(readable, {\n status: input.response.status,\n statusText: input.response.statusText,\n headers: input.response.headers,\n })\n}\n\nexport async function POST(req: NextRequest): Promise<Response> {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return jsonError(401, 'Unauthorized', 'unauthenticated')\n }\n\n const requestUrl = new URL(req.url)\n const queryResult = agentQuerySchema.safeParse({\n agent: requestUrl.searchParams.get('agent') ?? undefined,\n provider: requestUrl.searchParams.get('provider') ?? undefined,\n model: requestUrl.searchParams.get('model') ?? undefined,\n baseUrl: requestUrl.searchParams.get('baseUrl') ?? undefined,\n loopBudget: requestUrl.searchParams.get('loopBudget') ?? undefined,\n })\n if (!queryResult.success) {\n return jsonError(400, 'Invalid or missing \"agent\" query parameter.', 'validation_error', {\n issues: queryResult.error.issues,\n })\n }\n const agentId = queryResult.data.agent\n const rawProvider = queryResult.data.provider\n const rawModel = queryResult.data.model\n const rawBaseUrl = queryResult.data.baseUrl\n const rawLoopBudget = queryResult.data.loopBudget as AiAgentLoopBudgetPreset | undefined\n\n let parsedBody: unknown\n try {\n parsedBody = await req.json()\n } catch {\n return jsonError(400, 'Request body must be valid JSON.', 'validation_error')\n }\n\n const bodyResult = chatRequestSchema.safeParse(parsedBody)\n if (!bodyResult.success) {\n return jsonError(400, 'Invalid request body.', 'validation_error', {\n issues: bodyResult.error.issues,\n })\n }\n\n try {\n await loadAgentRegistry()\n\n const container = await createRequestContainer()\n\n const rateLimited = await checkAiChatRateLimit({\n req,\n container,\n userId: auth.sub,\n tenantId: auth.tenantId,\n })\n if (rateLimited) return rateLimited\n\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n\n const decision = checkAgentPolicy({\n agentId,\n authContext: {\n userFeatures: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n },\n requestedExecutionMode: 'chat',\n // TODO(step-3.7): resolve attachmentIds -> media types via attachment-bridge\n // once the attachment-to-model conversion bridge lands. Until then the\n // policy gate skips attachment-type validation because media types are\n // not known at dispatch time.\n attachmentMediaTypes: undefined,\n })\n\n if (!decision.ok) {\n return jsonError(statusForDenyCode(decision.code), decision.message, decision.code)\n }\n\n const agentDef = decision.agent\n\n // --- Phase 4a: validate runtime override query params ---\n const hasRuntimeOverride =\n (rawProvider && rawProvider.trim().length > 0) ||\n (rawModel && rawModel.trim().length > 0) ||\n (rawBaseUrl && rawBaseUrl.trim().length > 0) ||\n (rawLoopBudget !== undefined && rawLoopBudget !== 'default')\n\n // `allowRuntimeOverride` is the canonical flag (renamed from\n // `allowRuntimeModelOverride` in Phase 4 of this spec). Both are checked\n // here to cover agents declared before the rename lands; the deprecated\n // alias has lower priority.\n const runtimeOverrideAllowed =\n agentDef.allowRuntimeOverride !== false &&\n agentDef.allowRuntimeModelOverride !== false\n\n if (hasRuntimeOverride && !runtimeOverrideAllowed) {\n return jsonError(\n 400,\n `Agent \"${agentId}\" has runtime override disabled (allowRuntimeOverride: false).`,\n 'runtime_override_disabled',\n )\n }\n\n let tenantAllowlistSnapshot: TenantAllowlistSnapshot | null = null\n let agentRuntimeOverrideAllowlist: TenantAllowlistSnapshot | null = null\n if (auth.tenantId) {\n try {\n const em = container.resolve<EntityManager>('em')\n const allowlistRepo = new AiTenantModelAllowlistRepository(em)\n tenantAllowlistSnapshot = await allowlistRepo.getSnapshot({\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n })\n const runtimeOverrideRepo = new AiAgentRuntimeOverrideRepository(em)\n const agentRuntimeOverrideRow = await runtimeOverrideRepo.getExact({\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n agentId,\n })\n const tenantAgentAllowlist = agentRuntimeOverrideRow\n ? {\n allowedProviders: agentRuntimeOverrideRow.allowedOverrideProviders ?? null,\n allowedModelsByProvider: agentRuntimeOverrideRow.allowedOverrideModelsByProvider ?? {},\n }\n : null\n agentRuntimeOverrideAllowlist = hasAllowlistSnapshotRestrictions(tenantAgentAllowlist)\n ? tenantAgentAllowlist\n : null\n } catch (snapshotError) {\n // Fail closed: refuse to dispatch if we cannot confirm the tenant allowlist.\n // Silently falling back to env-only would widen the effective allowlist when\n // the DB is unavailable, which is the opposite of what an admin intends.\n logger.error('Tenant allowlist lookup failed; refusing to dispatch', { err: snapshotError })\n return jsonError(\n 503,\n 'Tenant allowlist is temporarily unavailable. Try again shortly.',\n 'tenant_allowlist_unavailable',\n )\n }\n }\n const knownProviderIds = llmProviderRegistry.list().map((p) => p.id)\n const baseEffectiveAllowlist = intersectAllowlists(\n process.env as Record<string, string | undefined>,\n knownProviderIds,\n tenantAllowlistSnapshot,\n )\n const envAgentAllowlist = readAgentRuntimeOverrideAllowlist(\n process.env as Record<string, string | undefined>,\n agentId,\n knownProviderIds,\n )\n const effectiveAllowlist = intersectEffectiveAllowlistWithSnapshot(\n intersectEffectiveAllowlistWithSnapshot(\n baseEffectiveAllowlist,\n knownProviderIds,\n envAgentAllowlist,\n ),\n knownProviderIds,\n agentRuntimeOverrideAllowlist,\n )\n\n const normalizedProvider = rawProvider && rawProvider.trim().length > 0\n ? canonicalProviderId(rawProvider.trim(), llmProviderRegistry.list().map((p) => p.id))\n : null\n\n if (rawProvider && rawProvider.trim().length > 0) {\n const providerEntry = normalizedProvider ? llmProviderRegistry.get(normalizedProvider) : null\n if (!providerEntry) {\n return jsonError(\n 400,\n `Provider \"${rawProvider}\" is not registered. Registered provider ids: ${llmProviderRegistry.list().map((p) => p.id).join(', ')}.`,\n 'provider_unknown',\n )\n }\n if (!providerEntry.isConfigured()) {\n return jsonError(\n 400,\n `Provider \"${rawProvider}\" is registered but not configured in this environment (missing API key).`,\n 'provider_not_configured',\n )\n }\n if (!isProviderAllowedInEffective(effectiveAllowlist, normalizedProvider!)) {\n const source = effectiveAllowlist.tenantOverridesActive\n ? 'the effective allowlist (env \u2229 tenant)'\n : 'OM_AI_AVAILABLE_PROVIDERS'\n return jsonError(\n 400,\n `Provider \"${rawProvider}\" is not in ${source}.`,\n 'provider_not_allowlisted',\n )\n }\n if (\n rawModel\n && rawModel.trim().length > 0\n && !isModelAllowedForProviderInEffective(\n effectiveAllowlist,\n normalizedProvider!,\n rawModel.trim(),\n )\n ) {\n const source = effectiveAllowlist.tenantOverridesActive\n ? `the effective allowlist (env \u2229 tenant) for \"${normalizedProvider}\"`\n : modelAllowlistEnvVarName(normalizedProvider!)\n return jsonError(\n 400,\n `Model \"${rawModel}\" is not in ${source}.`,\n 'model_not_allowlisted',\n )\n }\n }\n\n if (rawBaseUrl && rawBaseUrl.trim().length > 0) {\n const allowlist = readBaseurlAllowlist()\n if (!isBaseurlAllowlisted(rawBaseUrl.trim(), allowlist)) {\n return jsonError(\n 400,\n `baseUrl \"${rawBaseUrl}\" is not in the AI_RUNTIME_BASEURL_ALLOWLIST. Set that env var to a comma-separated list of allowed host patterns to enable per-request baseUrl overrides.`,\n 'baseurl_not_allowlisted',\n )\n }\n }\n // --- end Phase 4a + Phase 4 validation ---\n\n const requestOverride =\n hasRuntimeOverride\n ? {\n providerId: normalizedProvider,\n modelId: rawModel && rawModel.trim().length > 0 ? rawModel.trim() : null,\n baseURL: rawBaseUrl && rawBaseUrl.trim().length > 0 ? rawBaseUrl.trim() : null,\n }\n : undefined\n\n // Resolve the loopBudget preset to a loop config override (Phase 4).\n const loopFromPreset =\n rawLoopBudget !== undefined && rawLoopBudget !== 'default'\n ? resolveLoopBudgetPreset(rawLoopBudget)\n : undefined\n\n const effectiveConversationId = bodyResult.data.sessionId ?? bodyResult.data.conversationId ?? null\n let persistedTurn:\n | { conversationId: string; userClientMessageId: string | null }\n | null = null\n try {\n persistedTurn = await persistChatTurnStart({\n container,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n agentId,\n conversationId: effectiveConversationId,\n pageContext: bodyResult.data.pageContext,\n messages: bodyResult.data.messages,\n attachmentIds: bodyResult.data.attachmentIds,\n })\n } catch (error) {\n if (error instanceof Error && error.name === 'AiChatConversationOrgNotFoundError') {\n return jsonError(400, error.message, 'organization_not_found')\n }\n logger.error('AI Chat Agent \u2014 Failed to persist user message', { err: error })\n }\n\n const response = await runAiAgentText({\n agentId,\n messages: bodyResult.data.messages as unknown as UIMessage[],\n attachmentIds: bodyResult.data.attachmentIds,\n pageContext: bodyResult.data.pageContext,\n debug: bodyResult.data.debug,\n sessionId: bodyResult.data.sessionId ?? null,\n conversationId: bodyResult.data.conversationId ?? null,\n authContext: {\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n features: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n },\n container,\n requestOverride,\n loop: loopFromPreset,\n emitLoopTrace: true,\n })\n if (!persistedTurn) return response\n return persistAssistantOnStreamCompletion({\n response,\n container,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n conversationId: persistedTurn.conversationId,\n userClientMessageId: persistedTurn.userClientMessageId,\n })\n } catch (error) {\n if (error instanceof AgentPolicyError) {\n return jsonError(statusForDenyCode(error.code), error.message, error.code)\n }\n // Input pre-moderation outcomes (spec 2026-06-04). The blocked categories\n // are NEVER sent to the client \u2014 only the typed code, which the chat UI\n // maps to a generic translated message.\n if (error instanceof AiModerationBlockedError) {\n return jsonError(400, 'Input rejected by the content safety filter.', 'moderation_blocked')\n }\n if (error instanceof AiModerationUnavailableError) {\n return jsonError(503, 'Content safety check temporarily unavailable.', 'moderation_unavailable')\n }\n logger.error('AI Chat Agent \u2014 Dispatch failure', { err: error })\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Agent dispatch failed.',\n 'internal_error',\n )\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport { NextResponse, type NextRequest } from 'next/server'\nimport type { UIMessage } from 'ai'\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 { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport { loadAgentRegistry } from '../../../lib/agent-registry'\nimport { checkAgentPolicy, type AgentPolicyDenyCode } from '../../../lib/agent-policy'\nimport {\n runAiAgentText,\n resolveLoopBudgetPreset,\n type AiAgentLoopBudgetPreset,\n} from '../../../lib/agent-runtime'\nimport { AgentPolicyError } from '../../../lib/agent-tools'\nimport { readBaseurlAllowlist, isBaseurlAllowlisted } from '../../../lib/baseurl-allowlist'\nimport {\n canonicalProviderId,\n hasAllowlistSnapshotRestrictions,\n intersectEffectiveAllowlistWithSnapshot,\n intersectAllowlists,\n isModelAllowedForProviderInEffective,\n isProviderAllowedInEffective,\n modelAllowlistEnvVarName,\n readAgentRuntimeOverrideAllowlist,\n type TenantAllowlistSnapshot,\n} from '../../../lib/model-allowlist'\nimport { AiTenantModelAllowlistRepository } from '../../../data/repositories/AiTenantModelAllowlistRepository'\nimport { AiAgentRuntimeOverrideRepository } from '../../../data/repositories/AiAgentRuntimeOverrideRepository'\nimport { createConversationStorage } from '../../../lib/conversation-storage'\nimport { checkAiChatRateLimit } from '../../../lib/rate-limit'\nimport type { EntityManager } from '@mikro-orm/postgresql'\n\nconst logger = createLogger('ai_assistant')\n\nconst MAX_MESSAGES = 100\n\nconst agentIdPattern = /^[a-z0-9_]+\\.[a-z0-9_]+$/\n\nconst chatMessageSchema = z.object({\n id: z.string().min(1).max(128).optional(),\n role: z.enum(['user', 'assistant', 'system']),\n content: z.string(),\n uiParts: z.array(z.unknown()).optional(),\n files: z\n .array(\n z\n .object({\n id: z.string().optional(),\n name: z.string().optional(),\n type: z.string().optional(),\n mimeType: z.string().optional(),\n size: z.number().optional(),\n })\n .passthrough(),\n )\n .optional(),\n})\n\nconst pageContextSchema = z\n .object({\n pageId: z.string().nullable().optional(),\n entityType: z.string().nullable().optional(),\n recordId: z.string().nullable().optional(),\n })\n .passthrough()\n\nconst chatRequestSchema = z.object({\n messages: z\n .array(chatMessageSchema)\n .min(1, 'messages must contain at least one message')\n .max(MAX_MESSAGES, `messages must contain at most ${MAX_MESSAGES} entries`),\n attachmentIds: z.array(z.string()).optional(),\n debug: z.boolean().optional(),\n pageContext: pageContextSchema.optional(),\n /**\n * Stable per-conversation id (Phase 6.2). Wins over `conversationId` when\n * both are provided. The server echoes the resolved id on the SSE\n * `loop-finish` event so clients can persist it for the next turn.\n */\n sessionId: z.string().uuid().optional(),\n /**\n * @deprecated Use `sessionId` instead.\n */\n conversationId: z.string().min(1).max(128).optional(),\n})\n\nexport type AiChatRequest = z.infer<typeof chatRequestSchema>\n\nconst agentQuerySchema = z.object({\n agent: z\n .string()\n .regex(agentIdPattern, 'agent must match \"<module>.<agent>\" (lowercase, digits, underscores only)'),\n /**\n * Per-request provider override. Must match a registered + configured\n * provider id. Validated against `llmProviderRegistry` at dispatch time.\n * Rejected when the agent has `allowRuntimeOverride: false`.\n *\n * Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n provider: z.string().optional(),\n /**\n * Per-request model id override. Free-form string. Logged (not rejected)\n * when not in the provider's curated `defaultModels` catalog.\n *\n * Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n model: z.string().optional(),\n /**\n * Per-request base URL override. Must parse as a URL and match\n * `AI_RUNTIME_BASEURL_ALLOWLIST` (comma-separated host patterns). When the\n * env var is unset or empty, any non-empty value is rejected.\n *\n * Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.\n */\n baseUrl: z.string().optional(),\n /**\n * Named loop-budget preset. Maps to a fixed `loop.budget` triple:\n * tight \u2192 maxSteps: 3, maxWallClockMs: 10_000, maxTokens: 50_000\n * default \u2192 no override (agent default applies)\n * loose \u2192 maxSteps: 20, maxWallClockMs: 120_000, maxTokens: 500_000\n *\n * Rejected when the agent has `allowRuntimeOverride: false` or\n * `loop.allowRuntimeOverride: false`.\n *\n * Phase 4 of spec `2026-04-28-ai-agents-agentic-loop-controls`.\n */\n loopBudget: z.enum(['tight', 'default', 'loose']).optional(),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'AI agent dispatcher',\n methods: {\n POST: {\n operationId: 'aiAssistantChatAgent',\n summary: 'Stream a chat turn for a registered AI agent',\n description:\n 'Dispatches a chat turn to the focused AI agent identified by `?agent=<module>.<agent>`. ' +\n 'Enforces agent-level `requiredFeatures`, tool whitelisting, read-only / mutationPolicy, ' +\n 'execution-mode compatibility, and attachment media-type policy. The streaming response ' +\n 'body uses an AI SDK-compatible `text/event-stream` transport. ' +\n 'Optional `?provider=`, `?model=`, and `?baseUrl=` query params let callers ' +\n 'override the resolved provider/model/base-URL for this turn (Phase 4a). ' +\n 'Provider must be registered and configured; baseUrl must match ' +\n '`AI_RUNTIME_BASEURL_ALLOWLIST` when set. Both are suppressed when the ' +\n 'agent declares `allowRuntimeOverride: false`.',\n query: agentQuerySchema,\n requestBody: {\n contentType: 'application/json',\n description: 'Chat turn payload. `messages` is required; `attachmentIds`, `debug`, and `pageContext` are optional.',\n schema: chatRequestSchema,\n },\n responses: [\n { status: 200, description: 'Streaming text/event-stream response compatible with AI SDK chat transports.', mediaType: 'text/event-stream' },\n ],\n errors: [\n {\n status: 400,\n description:\n 'Invalid query param, malformed payload, or message count above the cap. ' +\n 'Typed codes: `runtime_override_disabled` (agent has allowRuntimeOverride:false), ' +\n '`provider_unknown` (provider id not registered), ' +\n '`provider_not_configured` (provider registered but no API key in env), ' +\n '`baseurl_not_allowlisted` (baseUrl not in AI_RUNTIME_BASEURL_ALLOWLIST).',\n },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks agent-level or tool-level required features.' },\n { status: 404, description: 'Unknown agent id.' },\n { status: 409, description: 'Agent/tool/execution-mode policy violation.' },\n { status: 500, description: 'Internal runtime failure.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\nfunction jsonError(\n status: number,\n message: string,\n code: string,\n extra?: Record<string, unknown>,\n): NextResponse {\n return NextResponse.json({ error: message, code, ...(extra ?? {}) }, { status })\n}\n\nfunction statusForDenyCode(code: AgentPolicyDenyCode): number {\n switch (code) {\n case 'agent_unknown':\n return 404\n case 'agent_features_denied':\n case 'tool_features_denied':\n return 403\n case 'tool_not_whitelisted':\n case 'tool_unknown':\n case 'mutation_blocked_by_readonly':\n case 'mutation_blocked_by_policy':\n case 'execution_mode_not_supported':\n return 409\n case 'attachment_type_not_accepted':\n return 400\n default:\n return 409\n }\n}\n\nfunction extractDataPayload(eventBlock: string): string | null {\n const dataLines = eventBlock\n .split('\\n')\n .filter((line) => line.startsWith('data:'))\n .map((line) => (line.startsWith('data: ') ? line.slice(6) : line.slice(5)))\n if (dataLines.length === 0) return null\n return dataLines.join('\\n')\n}\n\nfunction extractUiPartsFromToolOutput(output: unknown): unknown[] {\n let parsed = output\n if (typeof output === 'string') {\n const trimmed = output.trim()\n if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) return []\n try {\n parsed = JSON.parse(trimmed) as unknown\n } catch {\n return []\n }\n }\n if (!parsed || typeof parsed !== 'object') return []\n const value = parsed as Record<string, unknown>\n const parts: unknown[] = []\n if (value.status === 'pending-confirmation' || value.status === 'awaiting-confirmation') {\n const pendingActionId =\n typeof value.pendingActionId === 'string' && value.pendingActionId.length > 0\n ? value.pendingActionId\n : null\n if (pendingActionId) {\n parts.push({\n componentId: 'mutation-preview-card',\n pendingActionId,\n payload: {\n pendingActionId,\n expiresAt: typeof value.expiresAt === 'string' ? value.expiresAt : undefined,\n agentId:\n typeof value.agentId === 'string'\n ? value.agentId\n : typeof value.agent === 'string'\n ? value.agent\n : undefined,\n toolName: typeof value.toolName === 'string' ? value.toolName : undefined,\n },\n })\n }\n }\n if (value.uiPart && typeof value.uiPart === 'object') parts.push(value.uiPart)\n if (Array.isArray(value.uiParts)) parts.push(...value.uiParts)\n return parts\n}\n\nfunction extractAssistantSnapshot(\n raw: string,\n contentType: string | null,\n): { content: string; uiParts: unknown[] } {\n if (!contentType?.includes('event-stream')) {\n return { content: raw, uiParts: [] }\n }\n let content = ''\n const uiParts: unknown[] = []\n for (const block of raw.split('\\n\\n')) {\n const data = extractDataPayload(block)\n if (!data || data === '[DONE]') continue\n try {\n const parsed = JSON.parse(data) as Record<string, unknown>\n if (parsed.type === 'text-delta' && typeof parsed.delta === 'string') {\n content += parsed.delta\n } else if (parsed.type === 'text' && typeof parsed.content === 'string') {\n content += parsed.content\n } else if (parsed.type === 'tool-output-available') {\n uiParts.push(...extractUiPartsFromToolOutput(parsed.output))\n }\n } catch {\n // Ignore SSE comments and malformed provider chunks.\n }\n }\n return { content, uiParts }\n}\n\nasync function persistChatTurnStart(input: {\n container: Awaited<ReturnType<typeof createRequestContainer>>\n tenantId: string | null | undefined\n organizationId: string | null | undefined\n userId: string\n agentId: string\n conversationId: string | null\n pageContext?: Record<string, unknown>\n messages: AiChatRequest['messages']\n attachmentIds?: string[]\n}): Promise<{ conversationId: string; userClientMessageId: string | null } | null> {\n if (!input.tenantId || !input.conversationId) return null\n const repo = createConversationStorage(input.container)\n const ctx = {\n tenantId: input.tenantId,\n organizationId: input.organizationId ?? null,\n userId: input.userId,\n }\n await repo.createOrGet(\n {\n conversationId: input.conversationId,\n agentId: input.agentId,\n pageContext: input.pageContext ?? null,\n },\n ctx,\n )\n const userMessage = [...input.messages].reverse().find((message) => message.role === 'user')\n if (!userMessage) return { conversationId: input.conversationId, userClientMessageId: null }\n await repo.appendMessage(\n input.conversationId,\n {\n clientMessageId: userMessage.id,\n role: 'user',\n content: userMessage.content,\n uiParts: userMessage.uiParts,\n attachmentIds: input.attachmentIds,\n files: userMessage.files?.map((file, index) => {\n const id = file.id ?? input.attachmentIds?.[index]\n const mimeType = file.mimeType ?? file.type\n return {\n ...(id ? { id } : {}),\n ...(file.name ? { name: file.name } : {}),\n ...(mimeType ? { mimeType } : {}),\n ...(typeof file.size === 'number' ? { size: file.size } : {}),\n }\n }),\n },\n ctx,\n )\n return {\n conversationId: input.conversationId,\n userClientMessageId: userMessage.id ?? null,\n }\n}\n\nfunction persistAssistantOnStreamCompletion(input: {\n response: Response\n container: Awaited<ReturnType<typeof createRequestContainer>>\n tenantId: string | null | undefined\n organizationId: string | null | undefined\n userId: string\n conversationId: string\n userClientMessageId: string | null\n}): Response {\n if (!input.response.body || !input.tenantId) return input.response\n const tenantId = input.tenantId\n const { readable, writable } = new TransformStream<Uint8Array, Uint8Array>()\n const writer = writable.getWriter()\n const decoder = new TextDecoder()\n const contentType = input.response.headers.get('content-type')\n\n async function pump(): Promise<void> {\n const reader = input.response.body!.getReader()\n let raw = ''\n try {\n for (;;) {\n const { value, done } = await reader.read()\n if (done) break\n if (!value) continue\n raw += decoder.decode(value, { stream: true })\n await writer.write(value)\n }\n raw += decoder.decode()\n const assistant = extractAssistantSnapshot(raw, contentType)\n if (assistant.content.trim() || assistant.uiParts.length > 0) {\n const repo = createConversationStorage(input.container)\n await repo.appendMessage(\n input.conversationId,\n {\n clientMessageId: input.userClientMessageId\n ? `${input.userClientMessageId}:assistant`\n : undefined,\n role: 'assistant',\n content: assistant.content,\n uiParts: assistant.uiParts,\n },\n {\n tenantId,\n organizationId: input.organizationId ?? null,\n userId: input.userId,\n },\n )\n }\n } catch (error) {\n logger.error('AI Chat Agent \u2014 Conversation persistence failure', { err: error })\n } finally {\n reader.releaseLock()\n await writer.close().catch(() => undefined)\n }\n }\n\n void pump()\n return new Response(readable, {\n status: input.response.status,\n statusText: input.response.statusText,\n headers: input.response.headers,\n })\n}\n\nexport async function POST(req: NextRequest): Promise<Response> {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return jsonError(401, 'Unauthorized', 'unauthenticated')\n }\n\n const requestUrl = new URL(req.url)\n const queryResult = agentQuerySchema.safeParse({\n agent: requestUrl.searchParams.get('agent') ?? undefined,\n provider: requestUrl.searchParams.get('provider') ?? undefined,\n model: requestUrl.searchParams.get('model') ?? undefined,\n baseUrl: requestUrl.searchParams.get('baseUrl') ?? undefined,\n loopBudget: requestUrl.searchParams.get('loopBudget') ?? undefined,\n })\n if (!queryResult.success) {\n return jsonError(400, 'Invalid or missing \"agent\" query parameter.', 'validation_error', {\n issues: queryResult.error.issues,\n })\n }\n const agentId = queryResult.data.agent\n const rawProvider = queryResult.data.provider\n const rawModel = queryResult.data.model\n const rawBaseUrl = queryResult.data.baseUrl\n const rawLoopBudget = queryResult.data.loopBudget as AiAgentLoopBudgetPreset | undefined\n\n let parsedBody: unknown\n try {\n parsedBody = await req.json()\n } catch {\n return jsonError(400, 'Request body must be valid JSON.', 'validation_error')\n }\n\n const bodyResult = chatRequestSchema.safeParse(parsedBody)\n if (!bodyResult.success) {\n return jsonError(400, 'Invalid request body.', 'validation_error', {\n issues: bodyResult.error.issues,\n })\n }\n\n try {\n await loadAgentRegistry()\n\n const container = await createRequestContainer()\n\n const rateLimited = await checkAiChatRateLimit({\n req,\n container,\n userId: auth.sub,\n tenantId: auth.tenantId,\n })\n if (rateLimited) return rateLimited\n\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n\n const decision = checkAgentPolicy({\n agentId,\n authContext: {\n userFeatures: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n },\n requestedExecutionMode: 'chat',\n // TODO(step-3.7): resolve attachmentIds -> media types via attachment-bridge\n // once the attachment-to-model conversion bridge lands. Until then the\n // policy gate skips attachment-type validation because media types are\n // not known at dispatch time.\n attachmentMediaTypes: undefined,\n })\n\n if (!decision.ok) {\n return jsonError(statusForDenyCode(decision.code), decision.message, decision.code)\n }\n\n const agentDef = decision.agent\n\n // --- Phase 4a: validate runtime override query params ---\n const hasRuntimeOverride =\n (rawProvider && rawProvider.trim().length > 0) ||\n (rawModel && rawModel.trim().length > 0) ||\n (rawBaseUrl && rawBaseUrl.trim().length > 0) ||\n (rawLoopBudget !== undefined && rawLoopBudget !== 'default')\n\n // `allowRuntimeOverride` is the canonical flag (renamed from\n // `allowRuntimeModelOverride` in Phase 4 of this spec). Both are checked\n // here to cover agents declared before the rename lands; the deprecated\n // alias has lower priority.\n const runtimeOverrideAllowed =\n agentDef.allowRuntimeOverride !== false &&\n agentDef.allowRuntimeModelOverride !== false\n\n if (hasRuntimeOverride && !runtimeOverrideAllowed) {\n return jsonError(\n 400,\n `Agent \"${agentId}\" has runtime override disabled (allowRuntimeOverride: false).`,\n 'runtime_override_disabled',\n )\n }\n\n let tenantAllowlistSnapshot: TenantAllowlistSnapshot | null = null\n let agentRuntimeOverrideAllowlist: TenantAllowlistSnapshot | null = null\n if (auth.tenantId) {\n try {\n const em = container.resolve<EntityManager>('em')\n const allowlistRepo = new AiTenantModelAllowlistRepository(em)\n tenantAllowlistSnapshot = await allowlistRepo.getSnapshot({\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n })\n const runtimeOverrideRepo = new AiAgentRuntimeOverrideRepository(em)\n const agentRuntimeOverrideRow = await runtimeOverrideRepo.getExact({\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n agentId,\n })\n const tenantAgentAllowlist = agentRuntimeOverrideRow\n ? {\n allowedProviders: agentRuntimeOverrideRow.allowedOverrideProviders ?? null,\n allowedModelsByProvider: agentRuntimeOverrideRow.allowedOverrideModelsByProvider ?? {},\n }\n : null\n agentRuntimeOverrideAllowlist = hasAllowlistSnapshotRestrictions(tenantAgentAllowlist)\n ? tenantAgentAllowlist\n : null\n } catch (snapshotError) {\n // Fail closed: refuse to dispatch if we cannot confirm the tenant allowlist.\n // Silently falling back to env-only would widen the effective allowlist when\n // the DB is unavailable, which is the opposite of what an admin intends.\n logger.error('Tenant allowlist lookup failed; refusing to dispatch', { err: snapshotError })\n return jsonError(\n 503,\n 'Tenant allowlist is temporarily unavailable. Try again shortly.',\n 'tenant_allowlist_unavailable',\n )\n }\n }\n const knownProviderIds = llmProviderRegistry.list().map((p) => p.id)\n const baseEffectiveAllowlist = intersectAllowlists(\n process.env as Record<string, string | undefined>,\n knownProviderIds,\n tenantAllowlistSnapshot,\n )\n const envAgentAllowlist = readAgentRuntimeOverrideAllowlist(\n process.env as Record<string, string | undefined>,\n agentId,\n knownProviderIds,\n )\n const effectiveAllowlist = intersectEffectiveAllowlistWithSnapshot(\n intersectEffectiveAllowlistWithSnapshot(\n baseEffectiveAllowlist,\n knownProviderIds,\n envAgentAllowlist,\n ),\n knownProviderIds,\n agentRuntimeOverrideAllowlist,\n )\n\n const normalizedProvider = rawProvider && rawProvider.trim().length > 0\n ? canonicalProviderId(rawProvider.trim(), llmProviderRegistry.list().map((p) => p.id))\n : null\n\n if (rawProvider && rawProvider.trim().length > 0) {\n const providerEntry = normalizedProvider ? llmProviderRegistry.get(normalizedProvider) : null\n if (!providerEntry) {\n return jsonError(\n 400,\n `Provider \"${rawProvider}\" is not registered. Registered provider ids: ${llmProviderRegistry.list().map((p) => p.id).join(', ')}.`,\n 'provider_unknown',\n )\n }\n if (!providerEntry.isConfigured()) {\n return jsonError(\n 400,\n `Provider \"${rawProvider}\" is registered but not configured in this environment (missing API key).`,\n 'provider_not_configured',\n )\n }\n if (!isProviderAllowedInEffective(effectiveAllowlist, normalizedProvider!)) {\n const source = effectiveAllowlist.tenantOverridesActive\n ? 'the effective allowlist (env \u2229 tenant)'\n : 'OM_AI_AVAILABLE_PROVIDERS'\n return jsonError(\n 400,\n `Provider \"${rawProvider}\" is not in ${source}.`,\n 'provider_not_allowlisted',\n )\n }\n if (\n rawModel\n && rawModel.trim().length > 0\n && !isModelAllowedForProviderInEffective(\n effectiveAllowlist,\n normalizedProvider!,\n rawModel.trim(),\n )\n ) {\n const source = effectiveAllowlist.tenantOverridesActive\n ? `the effective allowlist (env \u2229 tenant) for \"${normalizedProvider}\"`\n : modelAllowlistEnvVarName(normalizedProvider!)\n return jsonError(\n 400,\n `Model \"${rawModel}\" is not in ${source}.`,\n 'model_not_allowlisted',\n )\n }\n }\n\n if (rawBaseUrl && rawBaseUrl.trim().length > 0) {\n const allowlist = readBaseurlAllowlist()\n if (!isBaseurlAllowlisted(rawBaseUrl.trim(), allowlist)) {\n return jsonError(\n 400,\n `baseUrl \"${rawBaseUrl}\" is not in the AI_RUNTIME_BASEURL_ALLOWLIST. Set that env var to a comma-separated list of allowed host patterns to enable per-request baseUrl overrides.`,\n 'baseurl_not_allowlisted',\n )\n }\n }\n // --- end Phase 4a + Phase 4 validation ---\n\n const requestOverride =\n hasRuntimeOverride\n ? {\n providerId: normalizedProvider,\n modelId: rawModel && rawModel.trim().length > 0 ? rawModel.trim() : null,\n baseURL: rawBaseUrl && rawBaseUrl.trim().length > 0 ? rawBaseUrl.trim() : null,\n }\n : undefined\n\n // Resolve the loopBudget preset to a loop config override (Phase 4).\n const loopFromPreset =\n rawLoopBudget !== undefined && rawLoopBudget !== 'default'\n ? resolveLoopBudgetPreset(rawLoopBudget)\n : undefined\n\n const effectiveConversationId = bodyResult.data.sessionId ?? bodyResult.data.conversationId ?? null\n let persistedTurn:\n | { conversationId: string; userClientMessageId: string | null }\n | null = null\n try {\n persistedTurn = await persistChatTurnStart({\n container,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n agentId,\n conversationId: effectiveConversationId,\n pageContext: bodyResult.data.pageContext,\n messages: bodyResult.data.messages,\n attachmentIds: bodyResult.data.attachmentIds,\n })\n } catch (error) {\n if (error instanceof Error && error.name === 'AiChatConversationOrgNotFoundError') {\n return jsonError(400, error.message, 'organization_not_found')\n }\n logger.error('AI Chat Agent \u2014 Failed to persist user message', { err: error })\n }\n\n const response = await runAiAgentText({\n agentId,\n messages: bodyResult.data.messages as unknown as UIMessage[],\n attachmentIds: bodyResult.data.attachmentIds,\n pageContext: bodyResult.data.pageContext,\n debug: bodyResult.data.debug,\n sessionId: bodyResult.data.sessionId ?? null,\n conversationId: bodyResult.data.conversationId ?? null,\n authContext: {\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n features: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n },\n container,\n requestOverride,\n loop: loopFromPreset,\n emitLoopTrace: true,\n })\n if (!persistedTurn) return response\n return persistAssistantOnStreamCompletion({\n response,\n container,\n tenantId: auth.tenantId ?? null,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n conversationId: persistedTurn.conversationId,\n userClientMessageId: persistedTurn.userClientMessageId,\n })\n } catch (error) {\n if (error instanceof AgentPolicyError) {\n return jsonError(statusForDenyCode(error.code), error.message, error.code)\n }\n logger.error('AI Chat Agent \u2014 Dispatch failure', { err: error })\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Agent dispatch failed.',\n 'internal_error',\n )\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -123,11 +123,11 @@ async function PUT(req) {
123
123
  try {
124
124
  const container = await createRequestContainer();
125
125
  const rbacService = container.resolve("rbacService");
126
- const canManage = await rbacService.userHasAllFeatures(
127
- auth.sub,
128
- ["ai_assistant.settings.manage"],
129
- { tenantId: auth.tenantId, organizationId: auth.orgId }
130
- );
126
+ const acl = await rbacService.loadAcl(auth.sub, {
127
+ tenantId: auth.tenantId,
128
+ organizationId: auth.orgId
129
+ });
130
+ const canManage = acl.isSuperAdmin || acl.features.includes("ai_assistant.settings.manage");
131
131
  if (!canManage) {
132
132
  return NextResponse.json({ error: "Forbidden", code: "forbidden" }, { status: 403 });
133
133
  }
@@ -165,11 +165,11 @@ async function DELETE(req) {
165
165
  try {
166
166
  const container = await createRequestContainer();
167
167
  const rbacService = container.resolve("rbacService");
168
- const canManage = await rbacService.userHasAllFeatures(
169
- auth.sub,
170
- ["ai_assistant.settings.manage"],
171
- { tenantId: auth.tenantId, organizationId: auth.orgId }
172
- );
168
+ const acl = await rbacService.loadAcl(auth.sub, {
169
+ tenantId: auth.tenantId,
170
+ organizationId: auth.orgId
171
+ });
172
+ const canManage = acl.isSuperAdmin || acl.features.includes("ai_assistant.settings.manage");
173
173
  if (!canManage) {
174
174
  return NextResponse.json({ error: "Forbidden", code: "forbidden" }, { status: 403 });
175
175
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../src/modules/ai_assistant/api/settings/allowlist/route.ts"],
4
- "sourcesContent": ["import { 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 { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport { AiTenantModelAllowlistRepository } from '../../../data/repositories/AiTenantModelAllowlistRepository'\nimport {\n canonicalProviderId,\n isModelAllowedForProvider,\n isProviderAllowed,\n modelAllowlistEnvVarName,\n} from '../../../lib/model-allowlist'\n\nconst logger = createLogger('ai_assistant')\n\nconst allowlistUpsertSchema = z.object({\n allowedProviders: z.array(z.string().min(1).max(64)).nullable().optional(),\n allowedModelsByProvider: z\n .record(z.string().min(1).max(64), z.array(z.string().min(1).max(256)))\n .optional(),\n})\n\nexport type AllowlistUpsertBody = z.infer<typeof allowlistUpsertSchema>\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'AI assistant tenant allowlist',\n methods: {\n PUT: {\n summary: 'Upsert per-tenant AI provider/model allowlist',\n description:\n 'Persists the per-tenant allowlist of providers and models. The runtime intersects this with the env allowlist (`OM_AI_AVAILABLE_*`) at resolution time. ' +\n 'Tenant values that fall outside the env allowlist are rejected with `provider_not_in_env_allowlist` / `model_not_in_env_allowlist` 400 codes. ' +\n 'Gated by `ai_assistant.settings.manage`.',\n requestBody: {\n contentType: 'application/json',\n description:\n 'Allowlist payload. `allowedProviders: null` clears tenant provider restriction (inherit env). Missing key in `allowedModelsByProvider` inherits env for that provider.',\n schema: allowlistUpsertSchema,\n },\n responses: [\n { status: 200, description: 'Allowlist saved. Returns the saved snapshot.' },\n ],\n errors: [\n { status: 400, description: 'Validation error or values outside env allowlist.' },\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 provider/model allowlist',\n description:\n 'Soft-deletes the tenant allowlist row. Tenant overrides revert to env-only enforcement. Idempotent \u2014 returns `{ cleared: false }` when no active row existed.',\n responses: [\n { status: 200, description: 'Returns `{ cleared: boolean }`.' },\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 PUT: { requireAuth: true, requireFeatures: ['ai_assistant.settings.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['ai_assistant.settings.manage'] },\n}\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(\n { error: 'Request body must be valid JSON.', code: 'validation_error' },\n { status: 400 },\n )\n }\n\n const bodyResult = allowlistUpsertSchema.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 env = process.env as Record<string, string | undefined>\n const knownProviderIds = llmProviderRegistry.list().map((provider) => provider.id)\n const canonicalize = (providerId: string) =>\n canonicalProviderId(providerId, knownProviderIds) ?? providerId\n const allowedProviders = bodyResult.data.allowedProviders === undefined\n ? null\n : bodyResult.data.allowedProviders?.map(canonicalize) ?? null\n const allowedModelsByProvider = Object.fromEntries(\n Object.entries(bodyResult.data.allowedModelsByProvider ?? {}).map(([providerId, models]) => [\n canonicalize(providerId),\n models,\n ]),\n )\n\n // Reject tenant entries that escape the env allowlist. The runtime would\n // intersect them away anyway; failing fast at write time keeps stored\n // tenant snapshots honest and visible in admin audits.\n if (Array.isArray(allowedProviders)) {\n for (const providerId of allowedProviders) {\n if (!isProviderAllowed(env, providerId)) {\n return NextResponse.json(\n {\n error: `Provider \"${providerId}\" is not in OM_AI_AVAILABLE_PROVIDERS; tenant allowlist may not widen the env allowlist.`,\n code: 'provider_not_in_env_allowlist',\n },\n { status: 400 },\n )\n }\n }\n }\n for (const providerId of Object.keys(allowedModelsByProvider)) {\n if (!isProviderAllowed(env, providerId)) {\n return NextResponse.json(\n {\n error: `Provider \"${providerId}\" is not in OM_AI_AVAILABLE_PROVIDERS; cannot save tenant model allowlist for it.`,\n code: 'provider_not_in_env_allowlist',\n },\n { status: 400 },\n )\n }\n for (const modelId of allowedModelsByProvider[providerId] ?? []) {\n if (!isModelAllowedForProvider(env, providerId, modelId)) {\n return NextResponse.json(\n {\n error: `Model \"${modelId}\" is not in ${modelAllowlistEnvVarName(providerId)}; tenant allowlist may not widen the env allowlist.`,\n code: 'model_not_in_env_allowlist',\n },\n { status: 400 },\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 AiTenantModelAllowlistRepository(em)\n const row = await repo.upsert(\n { allowedProviders, allowedModelsByProvider },\n {\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n },\n )\n return NextResponse.json({\n id: row.id,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n allowedProviders: row.allowedProviders ?? null,\n allowedModelsByProvider: row.allowedModelsByProvider ?? {},\n updatedAt: row.updatedAt,\n })\n } catch (error) {\n logger.error('AI Settings Allowlist \u2014 PUT error', { err: error })\n return NextResponse.json(\n { error: 'Failed to save tenant allowlist.' },\n { status: 500 },\n )\n }\n}\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 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 AiTenantModelAllowlistRepository(em)\n const cleared = await repo.clear({\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n })\n return NextResponse.json({ cleared })\n } catch (error) {\n logger.error('AI Settings Allowlist \u2014 DELETE error', { err: error })\n return NextResponse.json(\n { error: 'Failed to clear tenant allowlist.' },\n { status: 500 },\n )\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,oBAAsC;AAC/C,SAAS,SAAS;AAElB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAGvC,SAAS,2BAA2B;AACpC,SAAS,wCAAwC;AACjD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,MAAM,SAAS,aAAa,cAAc;AAE1C,MAAM,wBAAwB,EAAE,OAAO;AAAA,EACrC,kBAAkB,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,SAAS,EAAE,SAAS;AAAA,EACzE,yBAAyB,EACtB,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,CAAC,CAAC,EACrE,SAAS;AACd,CAAC;AAIM,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,aACE;AAAA,MAGF,aAAa;AAAA,QACX,aAAa;AAAA,QACb,aACE;AAAA,QACF,QAAQ;AAAA,MACV;AAAA,MACA,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,+CAA+C;AAAA,MAC7E;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,oDAAoD;AAAA,QAChF,EAAE,QAAQ,KAAK,aAAa,mBAAmB;AAAA,QAC/C,EAAE,QAAQ,KAAK,aAAa,6CAA6C;AAAA,MAC3E;AAAA,IACF;AAAA,IACA,QAAQ;AAAA,MACN,SAAS;AAAA,MACT,aACE;AAAA,MACF,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,kCAAkC;AAAA,MAChE;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,mBAAmB;AAAA,QAC/C,EAAE,QAAQ,KAAK,aAAa,6CAA6C;AAAA,MAC3E;AAAA,IACF;AAAA,EACF;AACF;AAEO,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,8BAA8B,EAAE;AAAA,EAC5E,QAAQ,EAAE,aAAa,MAAM,iBAAiB,CAAC,8BAA8B,EAAE;AACjF;AAEA,eAAsB,IAAI,KAAkB;AAC1C,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,KAAK;AACd,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AAEA,MAAI;AACJ,MAAI;AACF,iBAAa,MAAM,IAAI,KAAK;AAAA,EAC9B,QAAQ;AACN,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,oCAAoC,MAAM,mBAAmB;AAAA,MACtE,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AAEA,QAAM,aAAa,sBAAsB,UAAU,UAAU;AAC7D,MAAI,CAAC,WAAW,SAAS;AACvB,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,yBAAyB,MAAM,oBAAoB,QAAQ,WAAW,MAAM,OAAO;AAAA,MAC5F,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AAEA,QAAM,MAAM,QAAQ;AACpB,QAAM,mBAAmB,oBAAoB,KAAK,EAAE,IAAI,CAAC,aAAa,SAAS,EAAE;AACjF,QAAM,eAAe,CAAC,eACpB,oBAAoB,YAAY,gBAAgB,KAAK;AACvD,QAAM,mBAAmB,WAAW,KAAK,qBAAqB,SAC1D,OACA,WAAW,KAAK,kBAAkB,IAAI,YAAY,KAAK;AAC3D,QAAM,0BAA0B,OAAO;AAAA,IACrC,OAAO,QAAQ,WAAW,KAAK,2BAA2B,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,YAAY,MAAM,MAAM;AAAA,MAC1F,aAAa,UAAU;AAAA,MACvB;AAAA,IACF,CAAC;AAAA,EACH;AAKA,MAAI,MAAM,QAAQ,gBAAgB,GAAG;AACnC,eAAW,cAAc,kBAAkB;AACzC,UAAI,CAAC,kBAAkB,KAAK,UAAU,GAAG;AACvC,eAAO,aAAa;AAAA,UAClB;AAAA,YACE,OAAO,aAAa,UAAU;AAAA,YAC9B,MAAM;AAAA,UACR;AAAA,UACA,EAAE,QAAQ,IAAI;AAAA,QAChB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACA,aAAW,cAAc,OAAO,KAAK,uBAAuB,GAAG;AAC7D,QAAI,CAAC,kBAAkB,KAAK,UAAU,GAAG;AACvC,aAAO,aAAa;AAAA,QAClB;AAAA,UACE,OAAO,aAAa,UAAU;AAAA,UAC9B,MAAM;AAAA,QACR;AAAA,QACA,EAAE,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AACA,eAAW,WAAW,wBAAwB,UAAU,KAAK,CAAC,GAAG;AAC/D,UAAI,CAAC,0BAA0B,KAAK,YAAY,OAAO,GAAG;AACxD,eAAO,aAAa;AAAA,UAClB;AAAA,YACE,OAAO,UAAU,OAAO,eAAe,yBAAyB,UAAU,CAAC;AAAA,YAC3E,MAAM;AAAA,UACR;AAAA,UACA,EAAE,QAAQ,IAAI;AAAA,QAChB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,MAAI;AACF,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,cAAc,UAAU,QAAqB,aAAa;AAChE,UAAM,YAAY,MAAM,YAAY;AAAA,MAClC,KAAK;AAAA,MACL,CAAC,8BAA8B;AAAA,MAC/B,EAAE,UAAU,KAAK,UAAU,gBAAgB,KAAK,MAAM;AAAA,IACxD;AACA,QAAI,CAAC,WAAW;AACd,aAAO,aAAa,KAAK,EAAE,OAAO,aAAa,MAAM,YAAY,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACrF;AAEA,UAAM,KAAK,UAAU,QAAuB,IAAI;AAChD,UAAM,OAAO,IAAI,iCAAiC,EAAE;AACpD,UAAM,MAAM,MAAM,KAAK;AAAA,MACrB,EAAE,kBAAkB,wBAAwB;AAAA,MAC5C;AAAA,QACE,UAAU,KAAK,YAAY;AAAA,QAC3B,gBAAgB,KAAK,SAAS;AAAA,QAC9B,QAAQ,KAAK;AAAA,MACf;AAAA,IACF;AACA,WAAO,aAAa,KAAK;AAAA,MACvB,IAAI,IAAI;AAAA,MACR,UAAU,IAAI;AAAA,MACd,gBAAgB,IAAI;AAAA,MACpB,kBAAkB,IAAI,oBAAoB;AAAA,MAC1C,yBAAyB,IAAI,2BAA2B,CAAC;AAAA,MACzD,WAAW,IAAI;AAAA,IACjB,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,MAAM,0CAAqC,EAAE,KAAK,MAAM,CAAC;AAChE,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,mCAAmC;AAAA,MAC5C,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACF;AAEA,eAAsB,OAAO,KAAkB;AAC7C,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,KAAK;AACd,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AAEA,MAAI;AACF,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,cAAc,UAAU,QAAqB,aAAa;AAChE,UAAM,YAAY,MAAM,YAAY;AAAA,MAClC,KAAK;AAAA,MACL,CAAC,8BAA8B;AAAA,MAC/B,EAAE,UAAU,KAAK,UAAU,gBAAgB,KAAK,MAAM;AAAA,IACxD;AACA,QAAI,CAAC,WAAW;AACd,aAAO,aAAa,KAAK,EAAE,OAAO,aAAa,MAAM,YAAY,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACrF;AAEA,UAAM,KAAK,UAAU,QAAuB,IAAI;AAChD,UAAM,OAAO,IAAI,iCAAiC,EAAE;AACpD,UAAM,UAAU,MAAM,KAAK,MAAM;AAAA,MAC/B,UAAU,KAAK,YAAY;AAAA,MAC3B,gBAAgB,KAAK,SAAS;AAAA,IAChC,CAAC;AACD,WAAO,aAAa,KAAK,EAAE,QAAQ,CAAC;AAAA,EACtC,SAAS,OAAO;AACd,WAAO,MAAM,6CAAwC,EAAE,KAAK,MAAM,CAAC;AACnE,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,oCAAoC;AAAA,MAC7C,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import { 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 { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport { AiTenantModelAllowlistRepository } from '../../../data/repositories/AiTenantModelAllowlistRepository'\nimport {\n canonicalProviderId,\n isModelAllowedForProvider,\n isProviderAllowed,\n modelAllowlistEnvVarName,\n} from '../../../lib/model-allowlist'\n\nconst logger = createLogger('ai_assistant')\n\nconst allowlistUpsertSchema = z.object({\n allowedProviders: z.array(z.string().min(1).max(64)).nullable().optional(),\n allowedModelsByProvider: z\n .record(z.string().min(1).max(64), z.array(z.string().min(1).max(256)))\n .optional(),\n})\n\nexport type AllowlistUpsertBody = z.infer<typeof allowlistUpsertSchema>\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'AI assistant tenant allowlist',\n methods: {\n PUT: {\n summary: 'Upsert per-tenant AI provider/model allowlist',\n description:\n 'Persists the per-tenant allowlist of providers and models. The runtime intersects this with the env allowlist (`OM_AI_AVAILABLE_*`) at resolution time. ' +\n 'Tenant values that fall outside the env allowlist are rejected with `provider_not_in_env_allowlist` / `model_not_in_env_allowlist` 400 codes. ' +\n 'Gated by `ai_assistant.settings.manage`.',\n requestBody: {\n contentType: 'application/json',\n description:\n 'Allowlist payload. `allowedProviders: null` clears tenant provider restriction (inherit env). Missing key in `allowedModelsByProvider` inherits env for that provider.',\n schema: allowlistUpsertSchema,\n },\n responses: [\n { status: 200, description: 'Allowlist saved. Returns the saved snapshot.' },\n ],\n errors: [\n { status: 400, description: 'Validation error or values outside env allowlist.' },\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 provider/model allowlist',\n description:\n 'Soft-deletes the tenant allowlist row. Tenant overrides revert to env-only enforcement. Idempotent \u2014 returns `{ cleared: false }` when no active row existed.',\n responses: [\n { status: 200, description: 'Returns `{ cleared: boolean }`.' },\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 PUT: { requireAuth: true, requireFeatures: ['ai_assistant.settings.manage'] },\n DELETE: { requireAuth: true, requireFeatures: ['ai_assistant.settings.manage'] },\n}\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(\n { error: 'Request body must be valid JSON.', code: 'validation_error' },\n { status: 400 },\n )\n }\n\n const bodyResult = allowlistUpsertSchema.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 env = process.env as Record<string, string | undefined>\n const knownProviderIds = llmProviderRegistry.list().map((provider) => provider.id)\n const canonicalize = (providerId: string) =>\n canonicalProviderId(providerId, knownProviderIds) ?? providerId\n const allowedProviders = bodyResult.data.allowedProviders === undefined\n ? null\n : bodyResult.data.allowedProviders?.map(canonicalize) ?? null\n const allowedModelsByProvider = Object.fromEntries(\n Object.entries(bodyResult.data.allowedModelsByProvider ?? {}).map(([providerId, models]) => [\n canonicalize(providerId),\n models,\n ]),\n )\n\n // Reject tenant entries that escape the env allowlist. The runtime would\n // intersect them away anyway; failing fast at write time keeps stored\n // tenant snapshots honest and visible in admin audits.\n if (Array.isArray(allowedProviders)) {\n for (const providerId of allowedProviders) {\n if (!isProviderAllowed(env, providerId)) {\n return NextResponse.json(\n {\n error: `Provider \"${providerId}\" is not in OM_AI_AVAILABLE_PROVIDERS; tenant allowlist may not widen the env allowlist.`,\n code: 'provider_not_in_env_allowlist',\n },\n { status: 400 },\n )\n }\n }\n }\n for (const providerId of Object.keys(allowedModelsByProvider)) {\n if (!isProviderAllowed(env, providerId)) {\n return NextResponse.json(\n {\n error: `Provider \"${providerId}\" is not in OM_AI_AVAILABLE_PROVIDERS; cannot save tenant model allowlist for it.`,\n code: 'provider_not_in_env_allowlist',\n },\n { status: 400 },\n )\n }\n for (const modelId of allowedModelsByProvider[providerId] ?? []) {\n if (!isModelAllowedForProvider(env, providerId, modelId)) {\n return NextResponse.json(\n {\n error: `Model \"${modelId}\" is not in ${modelAllowlistEnvVarName(providerId)}; tenant allowlist may not widen the env allowlist.`,\n code: 'model_not_in_env_allowlist',\n },\n { status: 400 },\n )\n }\n }\n }\n\n try {\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n const canManage =\n acl.isSuperAdmin || acl.features.includes('ai_assistant.settings.manage')\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 AiTenantModelAllowlistRepository(em)\n const row = await repo.upsert(\n { allowedProviders, allowedModelsByProvider },\n {\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n },\n )\n return NextResponse.json({\n id: row.id,\n tenantId: row.tenantId,\n organizationId: row.organizationId,\n allowedProviders: row.allowedProviders ?? null,\n allowedModelsByProvider: row.allowedModelsByProvider ?? {},\n updatedAt: row.updatedAt,\n })\n } catch (error) {\n logger.error('AI Settings Allowlist \u2014 PUT error', { err: error })\n return NextResponse.json(\n { error: 'Failed to save tenant allowlist.' },\n { status: 500 },\n )\n }\n}\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 try {\n const container = await createRequestContainer()\n const rbacService = container.resolve<RbacService>('rbacService')\n const acl = await rbacService.loadAcl(auth.sub, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId,\n })\n const canManage =\n acl.isSuperAdmin || acl.features.includes('ai_assistant.settings.manage')\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 AiTenantModelAllowlistRepository(em)\n const cleared = await repo.clear({\n tenantId: auth.tenantId ?? '',\n organizationId: auth.orgId ?? null,\n })\n return NextResponse.json({ cleared })\n } catch (error) {\n logger.error('AI Settings Allowlist \u2014 DELETE error', { err: error })\n return NextResponse.json(\n { error: 'Failed to clear tenant allowlist.' },\n { status: 500 },\n )\n }\n}\n"],
5
+ "mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,oBAAsC;AAC/C,SAAS,SAAS;AAElB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAGvC,SAAS,2BAA2B;AACpC,SAAS,wCAAwC;AACjD;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,MAAM,SAAS,aAAa,cAAc;AAE1C,MAAM,wBAAwB,EAAE,OAAO;AAAA,EACrC,kBAAkB,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,SAAS,EAAE,SAAS;AAAA,EACzE,yBAAyB,EACtB,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,CAAC,CAAC,EACrE,SAAS;AACd,CAAC;AAIM,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,MACH,SAAS;AAAA,MACT,aACE;AAAA,MAGF,aAAa;AAAA,QACX,aAAa;AAAA,QACb,aACE;AAAA,QACF,QAAQ;AAAA,MACV;AAAA,MACA,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,+CAA+C;AAAA,MAC7E;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,oDAAoD;AAAA,QAChF,EAAE,QAAQ,KAAK,aAAa,mBAAmB;AAAA,QAC/C,EAAE,QAAQ,KAAK,aAAa,6CAA6C;AAAA,MAC3E;AAAA,IACF;AAAA,IACA,QAAQ;AAAA,MACN,SAAS;AAAA,MACT,aACE;AAAA,MACF,WAAW;AAAA,QACT,EAAE,QAAQ,KAAK,aAAa,kCAAkC;AAAA,MAChE;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,mBAAmB;AAAA,QAC/C,EAAE,QAAQ,KAAK,aAAa,6CAA6C;AAAA,MAC3E;AAAA,IACF;AAAA,EACF;AACF;AAEO,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,8BAA8B,EAAE;AAAA,EAC5E,QAAQ,EAAE,aAAa,MAAM,iBAAiB,CAAC,8BAA8B,EAAE;AACjF;AAEA,eAAsB,IAAI,KAAkB;AAC1C,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,KAAK;AACd,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AAEA,MAAI;AACJ,MAAI;AACF,iBAAa,MAAM,IAAI,KAAK;AAAA,EAC9B,QAAQ;AACN,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,oCAAoC,MAAM,mBAAmB;AAAA,MACtE,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AAEA,QAAM,aAAa,sBAAsB,UAAU,UAAU;AAC7D,MAAI,CAAC,WAAW,SAAS;AACvB,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,yBAAyB,MAAM,oBAAoB,QAAQ,WAAW,MAAM,OAAO;AAAA,MAC5F,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AAEA,QAAM,MAAM,QAAQ;AACpB,QAAM,mBAAmB,oBAAoB,KAAK,EAAE,IAAI,CAAC,aAAa,SAAS,EAAE;AACjF,QAAM,eAAe,CAAC,eACpB,oBAAoB,YAAY,gBAAgB,KAAK;AACvD,QAAM,mBAAmB,WAAW,KAAK,qBAAqB,SAC1D,OACA,WAAW,KAAK,kBAAkB,IAAI,YAAY,KAAK;AAC3D,QAAM,0BAA0B,OAAO;AAAA,IACrC,OAAO,QAAQ,WAAW,KAAK,2BAA2B,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,YAAY,MAAM,MAAM;AAAA,MAC1F,aAAa,UAAU;AAAA,MACvB;AAAA,IACF,CAAC;AAAA,EACH;AAKA,MAAI,MAAM,QAAQ,gBAAgB,GAAG;AACnC,eAAW,cAAc,kBAAkB;AACzC,UAAI,CAAC,kBAAkB,KAAK,UAAU,GAAG;AACvC,eAAO,aAAa;AAAA,UAClB;AAAA,YACE,OAAO,aAAa,UAAU;AAAA,YAC9B,MAAM;AAAA,UACR;AAAA,UACA,EAAE,QAAQ,IAAI;AAAA,QAChB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACA,aAAW,cAAc,OAAO,KAAK,uBAAuB,GAAG;AAC7D,QAAI,CAAC,kBAAkB,KAAK,UAAU,GAAG;AACvC,aAAO,aAAa;AAAA,QAClB;AAAA,UACE,OAAO,aAAa,UAAU;AAAA,UAC9B,MAAM;AAAA,QACR;AAAA,QACA,EAAE,QAAQ,IAAI;AAAA,MAChB;AAAA,IACF;AACA,eAAW,WAAW,wBAAwB,UAAU,KAAK,CAAC,GAAG;AAC/D,UAAI,CAAC,0BAA0B,KAAK,YAAY,OAAO,GAAG;AACxD,eAAO,aAAa;AAAA,UAClB;AAAA,YACE,OAAO,UAAU,OAAO,eAAe,yBAAyB,UAAU,CAAC;AAAA,YAC3E,MAAM;AAAA,UACR;AAAA,UACA,EAAE,QAAQ,IAAI;AAAA,QAChB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,MAAI;AACF,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,cAAc,UAAU,QAAqB,aAAa;AAChE,UAAM,MAAM,MAAM,YAAY,QAAQ,KAAK,KAAK;AAAA,MAC9C,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK;AAAA,IACvB,CAAC;AACD,UAAM,YACJ,IAAI,gBAAgB,IAAI,SAAS,SAAS,8BAA8B;AAC1E,QAAI,CAAC,WAAW;AACd,aAAO,aAAa,KAAK,EAAE,OAAO,aAAa,MAAM,YAAY,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACrF;AAEA,UAAM,KAAK,UAAU,QAAuB,IAAI;AAChD,UAAM,OAAO,IAAI,iCAAiC,EAAE;AACpD,UAAM,MAAM,MAAM,KAAK;AAAA,MACrB,EAAE,kBAAkB,wBAAwB;AAAA,MAC5C;AAAA,QACE,UAAU,KAAK,YAAY;AAAA,QAC3B,gBAAgB,KAAK,SAAS;AAAA,QAC9B,QAAQ,KAAK;AAAA,MACf;AAAA,IACF;AACA,WAAO,aAAa,KAAK;AAAA,MACvB,IAAI,IAAI;AAAA,MACR,UAAU,IAAI;AAAA,MACd,gBAAgB,IAAI;AAAA,MACpB,kBAAkB,IAAI,oBAAoB;AAAA,MAC1C,yBAAyB,IAAI,2BAA2B,CAAC;AAAA,MACzD,WAAW,IAAI;AAAA,IACjB,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,MAAM,0CAAqC,EAAE,KAAK,MAAM,CAAC;AAChE,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,mCAAmC;AAAA,MAC5C,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACF;AAEA,eAAsB,OAAO,KAAkB;AAC7C,QAAM,OAAO,MAAM,mBAAmB,GAAG;AACzC,MAAI,CAAC,MAAM,KAAK;AACd,WAAO,aAAa,KAAK,EAAE,OAAO,eAAe,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACrE;AAEA,MAAI;AACF,UAAM,YAAY,MAAM,uBAAuB;AAC/C,UAAM,cAAc,UAAU,QAAqB,aAAa;AAChE,UAAM,MAAM,MAAM,YAAY,QAAQ,KAAK,KAAK;AAAA,MAC9C,UAAU,KAAK;AAAA,MACf,gBAAgB,KAAK;AAAA,IACvB,CAAC;AACD,UAAM,YACJ,IAAI,gBAAgB,IAAI,SAAS,SAAS,8BAA8B;AAC1E,QAAI,CAAC,WAAW;AACd,aAAO,aAAa,KAAK,EAAE,OAAO,aAAa,MAAM,YAAY,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IACrF;AAEA,UAAM,KAAK,UAAU,QAAuB,IAAI;AAChD,UAAM,OAAO,IAAI,iCAAiC,EAAE;AACpD,UAAM,UAAU,MAAM,KAAK,MAAM;AAAA,MAC/B,UAAU,KAAK,YAAY;AAAA,MAC3B,gBAAgB,KAAK,SAAS;AAAA,IAChC,CAAC;AACD,WAAO,aAAa,KAAK,EAAE,QAAQ,CAAC;AAAA,EACtC,SAAS,OAAO;AACd,WAAO,MAAM,6CAAwC,EAAE,KAAK,MAAM,CAAC;AACnE,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,oCAAoC;AAAA,MAC7C,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }
@@ -14,7 +14,6 @@ import {
14
14
  isOpenCodeProviderConfigured
15
15
  } from "@open-mercato/shared/lib/ai/opencode-provider";
16
16
  import { AiAgentRuntimeOverrideRepository, AiAgentRuntimeOverrideValidationError } from "../../data/repositories/AiAgentRuntimeOverrideRepository.js";
17
- import { resolveModerationPolicy } from "../../lib/moderation-policy.js";
18
17
  import { AiTenantModelAllowlistRepository } from "../../data/repositories/AiTenantModelAllowlistRepository.js";
19
18
  import { isBaseurlAllowlisted, readBaseurlAllowlist } from "../../lib/baseurl-allowlist.js";
20
19
  import { loadAgentRegistry, listAgents } from "../../lib/agent-registry.js";
@@ -45,10 +44,7 @@ const runtimeOverrideUpsertSchema = z.object({
45
44
  baseURL: z.string().url().max(2048).nullable().optional(),
46
45
  agentId: z.string().min(1).max(128).nullable().optional(),
47
46
  allowedOverrideProviders: z.array(z.string().min(1).max(64)).nullable().optional(),
48
- allowedOverrideModelsByProvider: z.record(z.string().min(1).max(64), z.array(z.string().min(1).max(256))).optional(),
49
- // Input moderation override: true = on, false = off, null = inherit.
50
- // Spec 2026-06-04-ai-input-moderation-and-safety-identifiers.
51
- inputModeration: z.boolean().nullable().optional()
47
+ allowedOverrideModelsByProvider: z.record(z.string().min(1).max(64), z.array(z.string().min(1).max(256))).optional()
52
48
  });
53
49
  const runtimeOverrideClearSchema = z.object({
54
50
  agentId: z.string().min(1).max(128).nullable().optional()
@@ -148,7 +144,6 @@ async function GET(req) {
148
144
  const em = container.resolve("em");
149
145
  const repo = new AiAgentRuntimeOverrideRepository(em);
150
146
  const overrideRow = await repo.getDefault({ tenantId, organizationId, agentId: null });
151
- const tenantWideInputModeration = overrideRow?.inputModeration ?? null;
152
147
  if (overrideRow) {
153
148
  tenantOverride = {
154
149
  providerId: overrideRow.providerId ?? null,
@@ -250,21 +245,7 @@ async function GET(req) {
250
245
  providerId: agentResolution.providerId,
251
246
  modelId: agentResolution.modelId,
252
247
  baseURL: agentResolution.baseURL ?? null,
253
- source: agentResolution.source,
254
- moderation: {
255
- // `untrustedInput` agents enforce moderation; the UI renders a
256
- // non-editable "Enforced" badge for them.
257
- enforced: agent.untrustedInput === true,
258
- // Per-agent control value: null = inherit, true = on, false = off.
259
- override: agentOverrideRow?.inputModeration ?? null,
260
- // Resolved runtime policy (enforced | on | off) after precedence.
261
- effective: resolveModerationPolicy({
262
- untrustedInput: agent.untrustedInput,
263
- perAgentOverride: agentOverrideRow?.inputModeration ?? null,
264
- tenantWideOverride: tenantWideInputModeration,
265
- env
266
- })
267
- }
248
+ source: agentResolution.source
268
249
  };
269
250
  });
270
251
  agentResolutions = await Promise.all(agentResolutionPromises);
@@ -524,11 +505,11 @@ async function PUT(req) {
524
505
  try {
525
506
  const container = await createRequestContainer();
526
507
  const rbacService = container.resolve("rbacService");
527
- const canManage = await rbacService.userHasAllFeatures(
528
- auth.sub,
529
- ["ai_assistant.settings.manage"],
530
- { tenantId: auth.tenantId, organizationId: auth.orgId }
531
- );
508
+ const acl = await rbacService.loadAcl(auth.sub, {
509
+ tenantId: auth.tenantId,
510
+ organizationId: auth.orgId
511
+ });
512
+ const canManage = acl.isSuperAdmin || acl.features.includes("ai_assistant.settings.manage");
532
513
  if (!canManage) {
533
514
  return NextResponse.json({ error: "Forbidden", code: "forbidden" }, { status: 403 });
534
515
  }
@@ -540,8 +521,7 @@ async function PUT(req) {
540
521
  ...Object.prototype.hasOwnProperty.call(bodyResult.data, "modelId") ? { modelId: modelId ?? null } : {},
541
522
  ...Object.prototype.hasOwnProperty.call(bodyResult.data, "baseURL") ? { baseURL: baseURL ?? null } : {},
542
523
  ...allowedOverrideProviders !== void 0 ? { allowedOverrideProviders } : {},
543
- ...allowedOverrideModelsByProvider !== void 0 ? { allowedOverrideModelsByProvider } : {},
544
- ...Object.prototype.hasOwnProperty.call(bodyResult.data, "inputModeration") ? { inputModeration: bodyResult.data.inputModeration ?? null } : {}
524
+ ...allowedOverrideModelsByProvider !== void 0 ? { allowedOverrideModelsByProvider } : {}
545
525
  };
546
526
  const row = await repo.upsertDefault(
547
527
  upsertInput,
@@ -557,7 +537,6 @@ async function PUT(req) {
557
537
  baseURL: row.baseUrl,
558
538
  allowedOverrideProviders: row.allowedOverrideProviders ?? null,
559
539
  allowedOverrideModelsByProvider: row.allowedOverrideModelsByProvider ?? {},
560
- inputModeration: row.inputModeration ?? null,
561
540
  updatedAt: row.updatedAt
562
541
  });
563
542
  } catch (error) {
@@ -588,11 +567,11 @@ async function DELETE(req) {
588
567
  try {
589
568
  const container = await createRequestContainer();
590
569
  const rbacService = container.resolve("rbacService");
591
- const canManage = await rbacService.userHasAllFeatures(
592
- auth.sub,
593
- ["ai_assistant.settings.manage"],
594
- { tenantId: auth.tenantId, organizationId: auth.orgId }
595
- );
570
+ const acl = await rbacService.loadAcl(auth.sub, {
571
+ tenantId: auth.tenantId,
572
+ organizationId: auth.orgId
573
+ });
574
+ const canManage = acl.isSuperAdmin || acl.features.includes("ai_assistant.settings.manage");
596
575
  if (!canManage) {
597
576
  return NextResponse.json({ error: "Forbidden", code: "forbidden" }, { status: 403 });
598
577
  }