@open-mercato/ai-assistant 0.9.1-develop.7574.1.837288eec1 → 0.10.1-develop.7576.1.21d5b50435
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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/ai_assistant/api/ai/actions/[id]/cancel/route.js +2 -2
- package/dist/modules/ai_assistant/api/ai/actions/[id]/cancel/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/actions/[id]/confirm/route.js +1 -2
- package/dist/modules/ai_assistant/api/ai/actions/[id]/confirm/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/actions/[id]/route.js +1 -2
- package/dist/modules/ai_assistant/api/ai/actions/[id]/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js +1 -2
- package/dist/modules/ai_assistant/api/ai/agents/[agentId]/models/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/chat/route.js +2 -3
- package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/settings/route.js +4 -9
- package/dist/modules/ai_assistant/api/settings/route.js.map +2 -2
- package/dist/modules/ai_assistant/i18n/de.json +0 -2
- package/dist/modules/ai_assistant/i18n/en.json +0 -2
- package/dist/modules/ai_assistant/i18n/es.json +0 -2
- package/dist/modules/ai_assistant/i18n/ko.json +0 -2
- package/dist/modules/ai_assistant/i18n/pl.json +0 -2
- package/dist/modules/ai_assistant/lib/agent-runtime.js +1 -10
- package/dist/modules/ai_assistant/lib/agent-runtime.js.map +2 -2
- package/dist/modules/ai_assistant/lib/baseurl-allowlist.js +12 -37
- package/dist/modules/ai_assistant/lib/baseurl-allowlist.js.map +2 -2
- package/dist/modules/ai_assistant/lib/next-subpath-resolve-hook-worker.js +6 -0
- package/dist/modules/ai_assistant/lib/next-subpath-resolve-hook-worker.js.map +7 -0
- package/dist/modules/ai_assistant/lib/next-subpath-resolve-hook.js +55 -6
- package/dist/modules/ai_assistant/lib/next-subpath-resolve-hook.js.map +2 -2
- package/dist/modules/ai_assistant/lib/pending-action-cancel.js +1 -5
- package/dist/modules/ai_assistant/lib/pending-action-cancel.js.map +2 -2
- package/dist/modules/ai_assistant/lib/prepare-mutation.js +4 -21
- package/dist/modules/ai_assistant/lib/prepare-mutation.js.map +2 -2
- package/package.json +8 -8
- package/src/modules/ai_assistant/api/ai/actions/[id]/__tests__/route.test.ts +12 -29
- package/src/modules/ai_assistant/api/ai/actions/[id]/cancel/__tests__/route.test.ts +19 -36
- package/src/modules/ai_assistant/api/ai/actions/[id]/cancel/route.ts +6 -2
- package/src/modules/ai_assistant/api/ai/actions/[id]/confirm/__tests__/route.test.ts +16 -32
- package/src/modules/ai_assistant/api/ai/actions/[id]/confirm/route.ts +5 -2
- package/src/modules/ai_assistant/api/ai/actions/[id]/route.ts +5 -2
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/__tests__/route.test.ts +0 -40
- package/src/modules/ai_assistant/api/ai/agents/[agentId]/models/route.ts +1 -2
- package/src/modules/ai_assistant/api/ai/chat/__tests__/route.test.ts +0 -45
- package/src/modules/ai_assistant/api/ai/chat/route.ts +2 -3
- package/src/modules/ai_assistant/api/settings/__tests__/route.test.ts +7 -128
- package/src/modules/ai_assistant/api/settings/route.ts +6 -11
- package/src/modules/ai_assistant/i18n/de.json +0 -2
- package/src/modules/ai_assistant/i18n/en.json +0 -2
- package/src/modules/ai_assistant/i18n/es.json +0 -2
- package/src/modules/ai_assistant/i18n/ko.json +0 -2
- package/src/modules/ai_assistant/i18n/pl.json +0 -2
- package/src/modules/ai_assistant/lib/__tests__/agent-runtime-phase4a.test.ts +0 -52
- package/src/modules/ai_assistant/lib/__tests__/baseurl-allowlist.test.ts +1 -127
- package/src/modules/ai_assistant/lib/__tests__/next-subpath-resolve-hook.test.ts +174 -0
- package/src/modules/ai_assistant/lib/__tests__/pending-action-cancel.test.ts +2 -2
- package/src/modules/ai_assistant/lib/__tests__/prepare-mutation.test.ts +5 -83
- package/src/modules/ai_assistant/lib/agent-runtime.ts +1 -10
- package/src/modules/ai_assistant/lib/baseurl-allowlist.ts +19 -71
- package/src/modules/ai_assistant/lib/next-subpath-resolve-hook-worker.ts +3 -0
- package/src/modules/ai_assistant/lib/next-subpath-resolve-hook.ts +125 -6
- package/src/modules/ai_assistant/lib/pending-action-cancel.ts +1 -5
- package/src/modules/ai_assistant/lib/prepare-mutation.ts +6 -30
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:ai-assistant] found
|
|
1
|
+
[build:ai-assistant] found 238 entry points
|
|
2
2
|
[build:ai-assistant] built successfully
|
|
@@ -11,7 +11,7 @@ import { executePendingActionCancel } from "../../../../../lib/pending-action-ca
|
|
|
11
11
|
const logger = createLogger("ai_assistant");
|
|
12
12
|
const REQUIRED_FEATURE = "ai_assistant.view";
|
|
13
13
|
const idParamSchema = z.object({
|
|
14
|
-
id: z.string().trim().
|
|
14
|
+
id: z.string().trim().min(1, "id must be a non-empty string").max(128, "id exceeds the maximum length of 128 characters")
|
|
15
15
|
});
|
|
16
16
|
const bodySchema = z.object({
|
|
17
17
|
reason: z.string().max(500, "reason must be at most 500 characters").optional()
|
|
@@ -32,7 +32,7 @@ const openApi = {
|
|
|
32
32
|
}
|
|
33
33
|
],
|
|
34
34
|
errors: [
|
|
35
|
-
{ status: 400, description: "
|
|
35
|
+
{ status: 400, description: "Invalid cancel request body (unknown field, reason exceeds 500 chars, wrong type)." },
|
|
36
36
|
{ status: 401, description: "Unauthenticated caller." },
|
|
37
37
|
{ status: 403, description: "Caller lacks `ai_assistant.view`." },
|
|
38
38
|
{ status: 404, description: "Pending action not found in the caller scope." },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../../src/modules/ai_assistant/api/ai/actions/%5Bid%5D/cancel/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 { EntityManager } from '@mikro-orm/postgresql'\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 { AiPendingActionRepository } from '../../../../../data/repositories/AiPendingActionRepository'\nimport { hasRequiredFeatures } from '../../../../../lib/auth'\nimport { serializePendingActionForClient } from '../../../../../lib/pending-action-client'\nimport { checkStatusAndExpiry } from '../../../../../lib/pending-action-recheck'\nimport { executePendingActionCancel } from '../../../../../lib/pending-action-cancel'\n\nconst logger = createLogger('ai_assistant')\n\n/**\n * POST `/api/ai/actions/:id/cancel` \u2014 mutation approval gate cancel\n * endpoint (spec \u00A79.4, Step 5.9).\n *\n * Siblings the Step 5.8 confirm route: flips `pending \u2192 cancelled` and\n * emits `ai.action.cancelled`. The tool handler is NEVER invoked.\n *\n * The route re-uses only the `status + expiry + tenant-scope` guards\n * from `pending-action-recheck.ts` \u2014 the agent / tool / attachment /\n * record-version guards are confirm-only. A caller may cancel even when\n * they'd be blocked from confirming; cancelling does not touch data.\n *\n * Idempotency: calling this endpoint twice on an already-`cancelled`\n * row returns 200 with the current row without re-emitting the event.\n */\n\nconst REQUIRED_FEATURE = 'ai_assistant.view'\n\nconst idParamSchema = z.object({\n id: z.string().trim().
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,oBAAsC;AAC/C,SAAS,SAAS;AAGlB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAEvC,SAAS,iCAAiC;AAC1C,SAAS,2BAA2B;AACpC,SAAS,uCAAuC;AAChD,SAAS,4BAA4B;AACrC,SAAS,kCAAkC;AAE3C,MAAM,SAAS,aAAa,cAAc;AAkB1C,MAAM,mBAAmB;AAEzB,MAAM,gBAAgB,EAAE,OAAO;AAAA,EAC7B,IAAI,
|
|
4
|
+
"sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\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 { AiPendingActionRepository } from '../../../../../data/repositories/AiPendingActionRepository'\nimport { hasRequiredFeatures } from '../../../../../lib/auth'\nimport { serializePendingActionForClient } from '../../../../../lib/pending-action-client'\nimport { checkStatusAndExpiry } from '../../../../../lib/pending-action-recheck'\nimport { executePendingActionCancel } from '../../../../../lib/pending-action-cancel'\n\nconst logger = createLogger('ai_assistant')\n\n/**\n * POST `/api/ai/actions/:id/cancel` \u2014 mutation approval gate cancel\n * endpoint (spec \u00A79.4, Step 5.9).\n *\n * Siblings the Step 5.8 confirm route: flips `pending \u2192 cancelled` and\n * emits `ai.action.cancelled`. The tool handler is NEVER invoked.\n *\n * The route re-uses only the `status + expiry + tenant-scope` guards\n * from `pending-action-recheck.ts` \u2014 the agent / tool / attachment /\n * record-version guards are confirm-only. A caller may cancel even when\n * they'd be blocked from confirming; cancelling does not touch data.\n *\n * Idempotency: calling this endpoint twice on an already-`cancelled`\n * row returns 200 with the current row without re-emitting the event.\n */\n\nconst REQUIRED_FEATURE = 'ai_assistant.view'\n\nconst idParamSchema = z.object({\n id: z\n .string()\n .trim()\n .min(1, 'id must be a non-empty string')\n .max(128, 'id exceeds the maximum length of 128 characters'),\n})\n\nconst bodySchema = z\n .object({\n reason: z\n .string()\n .max(500, 'reason must be at most 500 characters')\n .optional(),\n })\n .strict()\n .optional()\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Pending action (mutation approval gate) cancel',\n methods: {\n POST: {\n operationId: 'aiAssistantCancelPendingAction',\n summary: 'Cancel an AI pending action without executing the wrapped tool.',\n description:\n 'Flips a pending AI action from `pending` to `cancelled` and emits the ' +\n '`ai.action.cancelled` event. The tool handler is never invoked. Idempotent: ' +\n 'a second call on a row already in `cancelled` status returns 200 with the ' +\n 'current row without re-emitting the event. Rows whose `expiresAt` is in the ' +\n 'past are flipped to `expired` and returned as 409 `expired` so the client can ' +\n 'surface the TTL loss instead of silently masking it as a cancellation.',\n responses: [\n {\n status: 200,\n description: 'Cancellation complete (or idempotent replay); body includes the serialized pending action with status `cancelled`.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 400, description: 'Invalid cancel request body (unknown field, reason exceeds 500 chars, wrong type).' },\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks `ai_assistant.view`.' },\n { status: 404, description: 'Pending action not found in the caller scope.' },\n { status: 409, description: 'Pending action is not in `pending` status (already confirmed/failed/executing) or has expired.' },\n { status: 500, description: 'Unexpected server failure during cancel.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n}\n\ninterface RouteContext {\n params: Promise<{ id: string }>\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\nasync function readRequestBody(req: NextRequest): Promise<unknown> {\n try {\n const text = await req.text()\n if (!text || text.trim().length === 0) return undefined\n return JSON.parse(text)\n } catch {\n return Symbol.for('ai_assistant.cancel.bad_json')\n }\n}\n\nexport async function POST(req: NextRequest, context: RouteContext): Promise<Response> {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return jsonError(401, 'Unauthorized', 'unauthenticated')\n }\n\n const rawParams = await context.params\n const paramResult = idParamSchema.safeParse(rawParams)\n if (!paramResult.success) {\n return jsonError(400, 'Invalid pending action id.', 'validation_error', {\n issues: paramResult.error.issues,\n })\n }\n const pendingActionId = paramResult.data.id\n\n const rawBody = await readRequestBody(req)\n if (rawBody === Symbol.for('ai_assistant.cancel.bad_json')) {\n return jsonError(400, 'Invalid JSON body.', 'validation_error')\n }\n const bodyResult = bodySchema.safeParse(rawBody)\n if (!bodyResult.success) {\n return jsonError(400, 'Invalid cancel body.', 'validation_error', {\n issues: bodyResult.error.issues,\n })\n }\n const parsedBody = bodyResult.data ?? {}\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\n if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n\n if (!auth.tenantId) {\n return jsonError(\n 404,\n `No pending action \"${pendingActionId}\" accessible to the caller.`,\n 'pending_action_not_found',\n )\n }\n\n const em = container.resolve<EntityManager>('em')\n const repo = new AiPendingActionRepository(em)\n const row = await repo.getById(pendingActionId, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n })\n if (!row) {\n return jsonError(\n 404,\n `No pending action \"${pendingActionId}\" accessible to the caller.`,\n 'pending_action_not_found',\n )\n }\n\n // Idempotent replay: second cancel on an already-cancelled row returns\n // 200 with the current row and does NOT emit a second event.\n if (row.status === 'cancelled') {\n return NextResponse.json({\n ok: true,\n pendingAction: serializePendingActionForClient(row),\n })\n }\n\n const statusCheck = checkStatusAndExpiry(row)\n if (!statusCheck.ok) {\n // Expired short-circuit: flip to `expired` + emit `ai.action.expired`\n // so Step 5.12 does not race to do it. Return 409 so the client\n // surfaces the TTL loss rather than silently cancelling.\n if (statusCheck.code === 'expired') {\n const cancelResult = await executePendingActionCancel({\n action: row,\n ctx: {\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n container,\n },\n repo,\n })\n return jsonError(409, statusCheck.message, 'expired', {\n pendingAction: serializePendingActionForClient(cancelResult.row),\n })\n }\n return jsonError(statusCheck.status, statusCheck.message, statusCheck.code, statusCheck.extra)\n }\n\n const cancelResult = await executePendingActionCancel({\n action: row,\n ctx: {\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n container,\n },\n reason: parsedBody.reason,\n repo,\n })\n\n if (cancelResult.status === 'expired') {\n return jsonError(\n 409,\n 'Pending action has expired. The model must re-propose the mutation.',\n 'expired',\n { pendingAction: serializePendingActionForClient(cancelResult.row) },\n )\n }\n\n return NextResponse.json({\n ok: true,\n pendingAction: serializePendingActionForClient(cancelResult.row),\n })\n } catch (error) {\n logger.error('AI Pending Action CANCEL \u2014 Failure', { err: error })\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to cancel pending action.',\n 'cancel_internal_error',\n )\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["cancelResult"]
|
|
7
7
|
}
|
|
@@ -20,7 +20,7 @@ import { isKnownMutationPolicy } from "../../../../../lib/agent-policy.js";
|
|
|
20
20
|
const logger = createLogger("ai_assistant");
|
|
21
21
|
const REQUIRED_FEATURE = "ai_assistant.view";
|
|
22
22
|
const idParamSchema = z.object({
|
|
23
|
-
id: z.string().trim().
|
|
23
|
+
id: z.string().trim().min(1, "id must be a non-empty string").max(128, "id exceeds the maximum length of 128 characters")
|
|
24
24
|
});
|
|
25
25
|
const openApi = {
|
|
26
26
|
tag: "AI Assistant",
|
|
@@ -38,7 +38,6 @@ const openApi = {
|
|
|
38
38
|
}
|
|
39
39
|
],
|
|
40
40
|
errors: [
|
|
41
|
-
{ status: 400, description: "The `:id` path parameter is not a UUID." },
|
|
42
41
|
{ status: 401, description: "Unauthenticated caller." },
|
|
43
42
|
{ status: 403, description: "Caller lacks `ai_assistant.view`, a required agent feature, tool whitelist, or accesses attachments outside their tenant." },
|
|
44
43
|
{ status: 404, description: "Pending action or agent not found in the caller scope." },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../../src/modules/ai_assistant/api/ai/actions/%5Bid%5D/confirm/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 { EntityManager } from '@mikro-orm/postgresql'\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 { AiPendingActionRepository } from '../../../../../data/repositories/AiPendingActionRepository'\nimport { hasRequiredFeatures } from '../../../../../lib/auth'\nimport { serializePendingActionForClient } from '../../../../../lib/pending-action-client'\nimport { getAgent, loadAgentRegistry } from '../../../../../lib/agent-registry'\nimport { toolRegistry } from '../../../../../lib/tool-registry'\nimport { loadAllModuleTools } from '../../../../../lib/tool-loader'\nimport type { AiToolDefinition } from '../../../../../lib/types'\nimport {\n runPendingActionRechecks,\n type PendingActionRecheckResult,\n} from '../../../../../lib/pending-action-recheck'\nimport {\n executePendingActionConfirm,\n type PendingActionExecuteContext,\n} from '../../../../../lib/pending-action-executor'\nimport { AiAgentMutationPolicyOverrideRepository } from '../../../../../data/repositories/AiAgentMutationPolicyOverrideRepository'\nimport type { AiAgentMutationPolicy } from '../../../../../lib/ai-agent-definition'\nimport { isKnownMutationPolicy } from '../../../../../lib/agent-policy'\n\nconst logger = createLogger('ai_assistant')\n\n/**\n * POST `/api/ai/actions/:id/confirm` \u2014 mutation approval gate confirm\n * endpoint (spec \u00A79.4, Step 5.8).\n *\n * Invariant: every check from spec \u00A79.4 is re-run on the server before the\n * tool handler executes. The original model-proposed tool arguments are\n * re-parsed through the CURRENT tool `inputSchema` so that a schema\n * migration between propose and confirm surfaces as `schema_drift`\n * (412) instead of silently passing through invalid data.\n *\n * Idempotency: calling this endpoint twice on the same pending-action id\n * returns the prior execution result without re-invoking the handler.\n */\n\nconst REQUIRED_FEATURE = 'ai_assistant.view'\n\nconst idParamSchema = z.object({\n id: z.string().trim().
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,oBAAsC;AAC/C,SAAS,SAAS;AAGlB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAEvC,SAAS,iCAAiC;AAC1C,SAAS,2BAA2B;AACpC,SAAS,uCAAuC;AAChD,SAAS,UAAU,yBAAyB;AAC5C,SAAS,oBAAoB;AAC7B,SAAS,0BAA0B;AAEnC;AAAA,EACE;AAAA,OAEK;AACP;AAAA,EACE;AAAA,OAEK;AACP,SAAS,+CAA+C;AAExD,SAAS,6BAA6B;AAEtC,MAAM,SAAS,aAAa,cAAc;AAgB1C,MAAM,mBAAmB;AAEzB,MAAM,gBAAgB,EAAE,OAAO;AAAA,EAC7B,IAAI,
|
|
4
|
+
"sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\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 { AiPendingActionRepository } from '../../../../../data/repositories/AiPendingActionRepository'\nimport { hasRequiredFeatures } from '../../../../../lib/auth'\nimport { serializePendingActionForClient } from '../../../../../lib/pending-action-client'\nimport { getAgent, loadAgentRegistry } from '../../../../../lib/agent-registry'\nimport { toolRegistry } from '../../../../../lib/tool-registry'\nimport { loadAllModuleTools } from '../../../../../lib/tool-loader'\nimport type { AiToolDefinition } from '../../../../../lib/types'\nimport {\n runPendingActionRechecks,\n type PendingActionRecheckResult,\n} from '../../../../../lib/pending-action-recheck'\nimport {\n executePendingActionConfirm,\n type PendingActionExecuteContext,\n} from '../../../../../lib/pending-action-executor'\nimport { AiAgentMutationPolicyOverrideRepository } from '../../../../../data/repositories/AiAgentMutationPolicyOverrideRepository'\nimport type { AiAgentMutationPolicy } from '../../../../../lib/ai-agent-definition'\nimport { isKnownMutationPolicy } from '../../../../../lib/agent-policy'\n\nconst logger = createLogger('ai_assistant')\n\n/**\n * POST `/api/ai/actions/:id/confirm` \u2014 mutation approval gate confirm\n * endpoint (spec \u00A79.4, Step 5.8).\n *\n * Invariant: every check from spec \u00A79.4 is re-run on the server before the\n * tool handler executes. The original model-proposed tool arguments are\n * re-parsed through the CURRENT tool `inputSchema` so that a schema\n * migration between propose and confirm surfaces as `schema_drift`\n * (412) instead of silently passing through invalid data.\n *\n * Idempotency: calling this endpoint twice on the same pending-action id\n * returns the prior execution result without re-invoking the handler.\n */\n\nconst REQUIRED_FEATURE = 'ai_assistant.view'\n\nconst idParamSchema = z.object({\n id: z\n .string()\n .trim()\n .min(1, 'id must be a non-empty string')\n .max(128, 'id exceeds the maximum length of 128 characters'),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Pending action (mutation approval gate) confirm',\n methods: {\n POST: {\n operationId: 'aiAssistantConfirmPendingAction',\n summary: 'Confirm an AI pending action, re-running every server-side check before execution.',\n description:\n 'Re-verifies the full contract from spec \u00A79.4 (status, expiry, agent registration, ' +\n 'required features, mutation policy, tool whitelist, attachment tenant scope, record ' +\n 'version, and schema drift), flips the pending-action state machine to `executing`, ' +\n 'invokes the wrapped tool handler, and persists `executionResult`. Idempotent: a ' +\n 'second call on a row already in `confirmed` state returns the prior result without ' +\n 're-executing the handler.',\n responses: [\n {\n status: 200,\n description: 'Confirmation complete; body includes the serialized pending action and the mutation result.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks `ai_assistant.view`, a required agent feature, tool whitelist, or accesses attachments outside their tenant.' },\n { status: 404, description: 'Pending action or agent not found in the caller scope.' },\n { status: 409, description: 'Pending action is not in `pending` status or has expired.' },\n { status: 412, description: 'Record version changed between propose and confirm, or the input schema no longer accepts the stored payload.' },\n { status: 500, description: 'Unexpected server failure during confirm.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n POST: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n}\n\ninterface RouteContext {\n params: Promise<{ id: string }>\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 fromRecheckFailure(result: PendingActionRecheckResult & { ok: false }): NextResponse {\n return jsonError(result.status, result.message, result.code, result.extra)\n}\n\nexport async function POST(req: NextRequest, context: RouteContext): Promise<Response> {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return jsonError(401, 'Unauthorized', 'unauthenticated')\n }\n\n const rawParams = await context.params\n const paramResult = idParamSchema.safeParse(rawParams)\n if (!paramResult.success) {\n return jsonError(400, 'Invalid pending action id.', 'validation_error', {\n issues: paramResult.error.issues,\n })\n }\n const pendingActionId = paramResult.data.id\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\n if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n\n if (!auth.tenantId) {\n return jsonError(\n 404,\n `No pending action \"${pendingActionId}\" accessible to the caller.`,\n 'pending_action_not_found',\n )\n }\n\n const em = container.resolve<EntityManager>('em')\n const repo = new AiPendingActionRepository(em)\n const row = await repo.getById(pendingActionId, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n })\n if (!row) {\n return jsonError(\n 404,\n `No pending action \"${pendingActionId}\" accessible to the caller.`,\n 'pending_action_not_found',\n )\n }\n\n // Idempotency short-circuit \u2014 a prior confirm already ran.\n if (row.status === 'confirmed' || row.status === 'failed') {\n const executionResult = row.executionResult ?? null\n return NextResponse.json({\n ok: row.status === 'confirmed',\n pendingAction: serializePendingActionForClient(row),\n mutationResult: executionResult,\n })\n }\n\n await loadAgentRegistry()\n await loadAllModuleTools()\n const agent = getAgent(row.agentId)\n const tool = toolRegistry.getTool(row.toolName) as AiToolDefinition | undefined\n\n const policyOverrideRepo = new AiAgentMutationPolicyOverrideRepository(em)\n const overrideRow = await policyOverrideRepo.get(row.agentId, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n })\n const rawOverridePolicy = overrideRow?.mutationPolicy ?? null\n const mutationPolicyOverride: AiAgentMutationPolicy | null =\n rawOverridePolicy && isKnownMutationPolicy(rawOverridePolicy) ? rawOverridePolicy : null\n\n const recheckResult = await runPendingActionRechecks({\n action: row,\n agent,\n tool,\n ctx: {\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n userFeatures: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n container,\n em,\n },\n mutationPolicyOverride,\n })\n if (!recheckResult.ok) {\n return fromRecheckFailure(recheckResult)\n }\n\n const executeCtx: PendingActionExecuteContext = {\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n userFeatures: acl.features,\n isSuperAdmin: acl.isSuperAdmin,\n container,\n }\n\n const executed = await executePendingActionConfirm({\n action: row,\n agent: agent!,\n tool: tool!,\n ctx: executeCtx,\n repo,\n failedRecords: recheckResult.failedRecords ?? null,\n })\n\n return NextResponse.json({\n ok: executed.ok,\n pendingAction: serializePendingActionForClient(executed.action),\n mutationResult: executed.executionResult,\n })\n } catch (error) {\n logger.error('AI Pending Action CONFIRM \u2014 Failure', { err: error })\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to confirm pending action.',\n 'confirm_internal_error',\n )\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -9,7 +9,7 @@ import { serializePendingActionForClient } from "../../../../lib/pending-action-
|
|
|
9
9
|
const logger = createLogger("ai_assistant");
|
|
10
10
|
const REQUIRED_FEATURE = "ai_assistant.view";
|
|
11
11
|
const idParamSchema = z.object({
|
|
12
|
-
id: z.string().trim().
|
|
12
|
+
id: z.string().trim().min(1, "id must be a non-empty string").max(128, "id exceeds the maximum length of 128 characters")
|
|
13
13
|
});
|
|
14
14
|
const openApi = {
|
|
15
15
|
tag: "AI Assistant",
|
|
@@ -27,7 +27,6 @@ const openApi = {
|
|
|
27
27
|
}
|
|
28
28
|
],
|
|
29
29
|
errors: [
|
|
30
|
-
{ status: 400, description: "The `:id` path parameter is not a UUID." },
|
|
31
30
|
{ status: 401, description: "Unauthenticated caller." },
|
|
32
31
|
{ status: 403, description: "Caller lacks the `ai_assistant.view` feature." },
|
|
33
32
|
{ status: 404, description: "No pending action with the given id is accessible to the caller." },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../src/modules/ai_assistant/api/ai/actions/%5Bid%5D/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 { EntityManager } from '@mikro-orm/postgresql'\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 { AiPendingActionRepository } from '../../../../data/repositories/AiPendingActionRepository'\nimport { hasRequiredFeatures } from '../../../../lib/auth'\nimport { serializePendingActionForClient } from '../../../../lib/pending-action-client'\n\nconst logger = createLogger('ai_assistant')\n\n/**\n * GET `/api/ai/actions/:id` \u2014 reconnect/polling endpoint for the Phase 3 WS-C\n * mutation approval gate (spec \u00A79, Step 5.7).\n *\n * When the chat UI bounces (reload, tab switch, SSE reconnect) it carries a\n * `pendingActionId` from an earlier `mutation-preview-card` UI part and needs\n * to re-hydrate the pending row's current state. This route is the authoritative\n * read-side; the confirm/cancel routes (Steps 5.8 / 5.9) use the same\n * whitelist serializer so the UI always sees the same shape.\n */\n\nconst REQUIRED_FEATURE = 'ai_assistant.view'\n\nconst idParamSchema = z.object({\n id: z.string().trim().
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,oBAAsC;AAC/C,SAAS,SAAS;AAGlB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAEvC,SAAS,iCAAiC;AAC1C,SAAS,2BAA2B;AACpC,SAAS,uCAAuC;AAEhD,MAAM,SAAS,aAAa,cAAc;AAa1C,MAAM,mBAAmB;AAEzB,MAAM,gBAAgB,EAAE,OAAO;AAAA,EAC7B,IAAI,
|
|
4
|
+
"sourcesContent": ["import { createLogger } from '@open-mercato/shared/lib/logger'\nimport { NextResponse, type NextRequest } from 'next/server'\nimport { z } from 'zod'\nimport type { EntityManager } from '@mikro-orm/postgresql'\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 { AiPendingActionRepository } from '../../../../data/repositories/AiPendingActionRepository'\nimport { hasRequiredFeatures } from '../../../../lib/auth'\nimport { serializePendingActionForClient } from '../../../../lib/pending-action-client'\n\nconst logger = createLogger('ai_assistant')\n\n/**\n * GET `/api/ai/actions/:id` \u2014 reconnect/polling endpoint for the Phase 3 WS-C\n * mutation approval gate (spec \u00A79, Step 5.7).\n *\n * When the chat UI bounces (reload, tab switch, SSE reconnect) it carries a\n * `pendingActionId` from an earlier `mutation-preview-card` UI part and needs\n * to re-hydrate the pending row's current state. This route is the authoritative\n * read-side; the confirm/cancel routes (Steps 5.8 / 5.9) use the same\n * whitelist serializer so the UI always sees the same shape.\n */\n\nconst REQUIRED_FEATURE = 'ai_assistant.view'\n\nconst idParamSchema = z.object({\n id: z\n .string()\n .trim()\n .min(1, 'id must be a non-empty string')\n .max(128, 'id exceeds the maximum length of 128 characters'),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Pending action (mutation approval gate) read-side',\n methods: {\n GET: {\n operationId: 'aiAssistantGetPendingAction',\n summary: 'Fetch the current state of an AI pending action by id.',\n description:\n 'Returns the tenant-scoped {@link AiPendingAction} addressed by `:id`. Powers the ' +\n 'chat UI reconnect/polling path: after a page reload or SSE reconnect the client ' +\n 'carries the `pendingActionId` from an earlier `mutation-preview-card` UI part and ' +\n 'calls this route to re-hydrate the card. Server-internal fields (`normalizedInput`, ' +\n '`createdByUserId`, `idempotencyKey`) are stripped by a whitelist serializer. ' +\n 'Enforces tenant/org scoping via the repository.',\n responses: [\n {\n status: 200,\n description:\n 'Serialized pending action. Never includes normalizedInput, createdByUserId, or idempotencyKey.',\n mediaType: 'application/json',\n },\n ],\n errors: [\n { status: 401, description: 'Unauthenticated caller.' },\n { status: 403, description: 'Caller lacks the `ai_assistant.view` feature.' },\n { status: 404, description: 'No pending action with the given id is accessible to the caller.' },\n { status: 500, description: 'Internal runtime failure.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] },\n}\n\ninterface RouteContext {\n params: Promise<{ id: string }>\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\nexport async function GET(req: NextRequest, context: RouteContext): Promise<Response> {\n const auth = await getAuthFromRequest(req)\n if (!auth) {\n return jsonError(401, 'Unauthorized', 'unauthenticated')\n }\n\n const rawParams = await context.params\n const paramResult = idParamSchema.safeParse(rawParams)\n if (!paramResult.success) {\n return jsonError(400, 'Invalid pending action id.', 'validation_error', {\n issues: paramResult.error.issues,\n })\n }\n const pendingActionId = paramResult.data.id\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\n if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {\n return jsonError(403, `Caller lacks required feature \"${REQUIRED_FEATURE}\".`, 'forbidden')\n }\n\n if (!auth.tenantId) {\n return jsonError(\n 404,\n `No pending action \"${pendingActionId}\" accessible to the caller.`,\n 'pending_action_not_found',\n )\n }\n\n const em = container.resolve<EntityManager>('em')\n const repo = new AiPendingActionRepository(em)\n const row = await repo.getById(pendingActionId, {\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n userId: auth.sub,\n })\n\n if (!row) {\n return jsonError(\n 404,\n `No pending action \"${pendingActionId}\" accessible to the caller.`,\n 'pending_action_not_found',\n )\n }\n\n return NextResponse.json(serializePendingActionForClient(row))\n } catch (error) {\n logger.error('AI Pending Action GET \u2014 Failure', { err: error })\n return jsonError(\n 500,\n error instanceof Error ? error.message : 'Failed to load pending action.',\n 'internal_error',\n )\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -7,7 +7,6 @@ import { llmProviderRegistry } from "../../../../../lib/llm-registry.js";
|
|
|
7
7
|
import { getAgent, loadAgentRegistry } from "../../../../../lib/agent-registry.js";
|
|
8
8
|
import { hasRequiredFeatures } from "../../../../../lib/auth.js";
|
|
9
9
|
import { createModelFactory, resolveAllowRuntimeOverride } from "../../../../../lib/model-factory.js";
|
|
10
|
-
import { resolveAllowlistedBaseurl } from "../../../../../lib/baseurl-allowlist.js";
|
|
11
10
|
import {
|
|
12
11
|
hasAllowlistSnapshotRestrictions,
|
|
13
12
|
intersectEffectiveAllowlistWithSnapshot,
|
|
@@ -113,7 +112,7 @@ async function GET(req, { params }) {
|
|
|
113
112
|
tenantRuntimeOverride = runtimeOverrideDefaultRow ? {
|
|
114
113
|
providerId: runtimeOverrideDefaultRow.providerId ?? null,
|
|
115
114
|
modelId: runtimeOverrideDefaultRow.modelId ?? null,
|
|
116
|
-
baseURL:
|
|
115
|
+
baseURL: runtimeOverrideDefaultRow.baseUrl ?? null
|
|
117
116
|
} : null;
|
|
118
117
|
const runtimeOverrideRow = await runtimeOverrideRepo.getExact({
|
|
119
118
|
tenantId: auth.tenantId,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../../../../src/modules/ai_assistant/api/ai/agents/%5BagentId%5D/models/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 type { EntityManager } from '@mikro-orm/postgresql'\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 '../../../../../lib/llm-registry'\nimport { getAgent, loadAgentRegistry } from '../../../../../lib/agent-registry'\nimport { hasRequiredFeatures } from '../../../../../lib/auth'\nimport { createModelFactory, resolveAllowRuntimeOverride } from '../../../../../lib/model-factory'\nimport { resolveAllowlistedBaseurl } from '../../../../../lib/baseurl-allowlist'\nimport {\n hasAllowlistSnapshotRestrictions,\n intersectEffectiveAllowlistWithSnapshot,\n intersectAllowlists,\n isModelAllowedForProviderInEffective,\n isProviderAllowedInEffective,\n readAgentRuntimeOverrideAllowlist,\n type TenantAllowlistSnapshot,\n} from '../../../../../lib/model-allowlist'\nimport { AiTenantModelAllowlistRepository } from '../../../../../data/repositories/AiTenantModelAllowlistRepository'\nimport { AiAgentRuntimeOverrideRepository } from '../../../../../data/repositories/AiAgentRuntimeOverrideRepository'\n\nconst logger = createLogger('ai_assistant')\n\nfunction modelsForPicker(\n provider: ReturnType<typeof llmProviderRegistry.list>[number],\n allowedModelIds: string[] | undefined,\n): ReadonlyArray<{ id: string; name: string; contextWindow?: number | null; tags?: readonly string[] }> {\n if (provider.defaultModels.length > 0) return provider.defaultModels\n return (allowedModelIds ?? []).map((id) => ({ id, name: id }))\n}\n\nconst agentIdPattern = /^[a-z0-9_]+\\.[a-z0-9_]+$/\n\nconst agentIdParamSchema = z.object({\n agentId: z\n .string()\n .regex(agentIdPattern, 'agentId must match \"<module>.<agent>\" (lowercase, digits, underscores only)'),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Available models for an AI agent',\n methods: {\n GET: {\n operationId: 'aiAssistantGetAgentModels',\n summary: 'Get the providers and curated models available for the chat-UI picker for this agent',\n description:\n 'Returns all configured providers with their curated model catalogs, filtered to providers ' +\n 'that have an API key configured in the current environment. When the agent declares ' +\n '`allowRuntimeOverride: false`, the response reflects that constraint so the ' +\n 'UI picker can hide itself. Includes the agent\\'s resolved default provider/model so ' +\n 'the picker can render a \"(default)\" badge next to the right entry. ' +\n 'When a tenant-scoped lookup (the allowlist snapshot or the per-agent runtime override) ' +\n 'cannot be loaded, the route still answers 200 with a partially resolved provider list ' +\n 'and sets `degraded: true` plus a `degradedReason` code, so callers can tell a full ' +\n 'entitlement list from a partial one. ' +\n 'RBAC: requires the same features as the agent itself (typically `ai_assistant.view`).',\n responses: [\n {\n status: 200,\n description:\n 'Providers and curated models available for the agent picker. ' +\n 'Empty `providers` array when `allowRuntimeOverride` is false. ' +\n '`degraded` is `false` and `degradedReason` is `null` on a fully resolved response; ' +\n '`degraded: true` with `degradedReason: \"tenant_allowlist_unavailable\"` means a ' +\n 'tenant-scoped lookup failed, so `providers` may be missing the tenant allowlist ' +\n 'and/or the per-agent override narrowing \u2014 one code covers the whole tenant-scoped ' +\n 'block because no client can act differently per source. Clients SHOULD render the ' +\n 'list but MUST NOT treat it as the tenant\\'s authoritative entitlement (for example, ' +\n 'do not prune a stored model selection against it).',\n },\n ],\n errors: [\n { status: 401, description: 'Unauthenticated.' },\n { status: 403, description: 'Caller lacks the agent\\'s required features.' },\n { status: 404, description: 'Unknown agent id.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\nexport async function GET(\n req: NextRequest,\n { params }: { params: Promise<{ agentId: string }> },\n): Promise<Response> {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const rawParams = await params\n const paramResult = agentIdParamSchema.safeParse(rawParams)\n if (!paramResult.success) {\n return NextResponse.json(\n { error: 'Invalid agentId path parameter.', code: 'validation_error', issues: paramResult.error.issues },\n { status: 400 },\n )\n }\n const agentId = paramResult.data.agentId\n\n try {\n await loadAgentRegistry()\n\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\n const agent = getAgent(agentId)\n if (!agent) {\n return NextResponse.json({ error: `Agent \"${agentId}\" not found.`, code: 'agent_unknown' }, { status: 404 })\n }\n\n const agentFeatures = agent.requiredFeatures ?? []\n if (agentFeatures.length > 0) {\n const permitted = hasRequiredFeatures(agentFeatures, acl.features, acl.isSuperAdmin)\n if (!permitted) {\n return NextResponse.json(\n {\n error: `Access to agent \"${agentId}\" requires features: ${agentFeatures.join(', ')}.`,\n code: 'agent_features_denied',\n },\n { status: 403 },\n )\n }\n }\n\n const allowRuntimeOverride = resolveAllowRuntimeOverride(agent)\n\n // Load the per-tenant allowlist snapshot so the picker reflects both env\n // and admin-edited tenant constraints (Phase 1780-6).\n let tenantAllowlistSnapshot: TenantAllowlistSnapshot | null = null\n let agentRuntimeOverrideAllowlist: TenantAllowlistSnapshot | null = null\n let tenantRuntimeOverride: {\n providerId: string | null\n modelId: string | null\n baseURL: string | null\n } | null = null\n let tenantAllowlistDegraded = false\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 runtimeOverrideDefaultRow = await runtimeOverrideRepo.getDefault({\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n agentId,\n })\n tenantRuntimeOverride = runtimeOverrideDefaultRow\n ? {\n providerId: runtimeOverrideDefaultRow.providerId ?? null,\n modelId: runtimeOverrideDefaultRow.modelId ?? null,\n baseURL: resolveAllowlistedBaseurl(runtimeOverrideDefaultRow.baseUrl),\n }\n : null\n const runtimeOverrideRow = await runtimeOverrideRepo.getExact({\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n agentId,\n })\n const tenantAgentAllowlist = runtimeOverrideRow\n ? {\n allowedProviders: runtimeOverrideRow.allowedOverrideProviders ?? null,\n allowedModelsByProvider: runtimeOverrideRow.allowedOverrideModelsByProvider ?? {},\n }\n : null\n agentRuntimeOverrideAllowlist = hasAllowlistSnapshotRestrictions(tenantAgentAllowlist)\n ? tenantAgentAllowlist\n : null\n } catch (snapshotError) {\n // Picker still renders against whatever resolved so the UI does not break, but log\n // at error level so an outage is operationally visible and mark the response\n // `degraded` so callers know a tenant-scoped read failed and the provider list may\n // be missing the tenant allowlist and/or the per-agent override narrowing. The chat\n // dispatcher refuses to dispatch when this lookup fails, so writes stay safe.\n tenantAllowlistDegraded = true\n logger.error('AI Agents Models \u2014 Failed to load tenant allowlist', { err: snapshotError })\n }\n }\n\n // Resolve the agent's current default provider/model for the \"(default)\" badge\n const factory = createModelFactory(container)\n const defaultResolution = factory.resolveModel({\n moduleId: agent.moduleId,\n agentDefaultModel: agent.defaultModel,\n agentDefaultProvider: agent.defaultProvider,\n agentDefaultBaseUrl: agent.defaultBaseUrl,\n allowRuntimeOverride,\n tenantOverride: tenantRuntimeOverride ?? undefined,\n tenantAllowlist: tenantAllowlistSnapshot,\n })\n const defaultProviderId = defaultResolution.providerId\n const defaultModelId = defaultResolution.modelId\n\n // Build provider list \u2014 only configured providers, with curated model\n // catalogs, clipped to the EFFECTIVE allowlist (env \u2229 tenant) so the\n // chat-UI picker can never offer a value the runtime would refuse.\n const env = process.env as Record<string, string | undefined>\n const knownProviderIds = llmProviderRegistry.list().map((p) => p.id)\n const baseEffectiveAllowlist = intersectAllowlists(\n env,\n knownProviderIds,\n tenantAllowlistSnapshot,\n )\n const envAgentAllowlist = readAgentRuntimeOverrideAllowlist(env, agentId, knownProviderIds)\n const effectiveAllowlist = intersectEffectiveAllowlistWithSnapshot(\n intersectEffectiveAllowlistWithSnapshot(\n baseEffectiveAllowlist,\n knownProviderIds,\n envAgentAllowlist,\n ),\n knownProviderIds,\n agentRuntimeOverrideAllowlist,\n )\n const providers = allowRuntimeOverride\n ? llmProviderRegistry.list()\n .filter((provider) => provider.isConfigured())\n .filter((provider) => isProviderAllowedInEffective(effectiveAllowlist, provider.id))\n .map((provider) => {\n const allowedModelIds = effectiveAllowlist.modelsByProvider[provider.id]\n const filteredModels = modelsForPicker(provider, allowedModelIds).filter((model) =>\n isModelAllowedForProviderInEffective(effectiveAllowlist, provider.id, model.id),\n )\n return {\n id: provider.id,\n name: provider.name,\n isDefault: provider.id === defaultProviderId,\n models: filteredModels.map((model) => ({\n id: model.id,\n name: model.name,\n contextWindow: model.contextWindow,\n tags: model.tags,\n isDefault: provider.id === defaultProviderId && model.id === defaultModelId,\n })),\n }\n })\n : []\n\n return NextResponse.json({\n agentId,\n allowRuntimeOverride,\n allowRuntimeModelOverride: allowRuntimeOverride,\n defaultProviderId,\n defaultModelId,\n defaultProviderName: llmProviderRegistry.get(defaultProviderId)?.name ?? defaultProviderId,\n defaultModelName:\n llmProviderRegistry\n .get(defaultProviderId)\n ?.defaultModels.find((model) => model.id === defaultModelId)?.name ?? defaultModelId,\n providers,\n degraded: tenantAllowlistDegraded,\n degradedReason: tenantAllowlistDegraded ? 'tenant_allowlist_unavailable' : null,\n })\n } catch (error) {\n logger.error('AI Agents Models \u2014 GET error', { err: error })\n return NextResponse.json({ error: 'Failed to resolve agent models.' }, { status: 500 })\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,oBAAsC;AAC/C,SAAS,SAAS;AAGlB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAEvC,SAAS,2BAA2B;AACpC,SAAS,UAAU,yBAAyB;AAC5C,SAAS,2BAA2B;AACpC,SAAS,oBAAoB,mCAAmC;AAChE
|
|
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 type { EntityManager } from '@mikro-orm/postgresql'\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 '../../../../../lib/llm-registry'\nimport { getAgent, loadAgentRegistry } from '../../../../../lib/agent-registry'\nimport { hasRequiredFeatures } from '../../../../../lib/auth'\nimport { createModelFactory, resolveAllowRuntimeOverride } from '../../../../../lib/model-factory'\nimport {\n hasAllowlistSnapshotRestrictions,\n intersectEffectiveAllowlistWithSnapshot,\n intersectAllowlists,\n isModelAllowedForProviderInEffective,\n isProviderAllowedInEffective,\n readAgentRuntimeOverrideAllowlist,\n type TenantAllowlistSnapshot,\n} from '../../../../../lib/model-allowlist'\nimport { AiTenantModelAllowlistRepository } from '../../../../../data/repositories/AiTenantModelAllowlistRepository'\nimport { AiAgentRuntimeOverrideRepository } from '../../../../../data/repositories/AiAgentRuntimeOverrideRepository'\n\nconst logger = createLogger('ai_assistant')\n\nfunction modelsForPicker(\n provider: ReturnType<typeof llmProviderRegistry.list>[number],\n allowedModelIds: string[] | undefined,\n): ReadonlyArray<{ id: string; name: string; contextWindow?: number | null; tags?: readonly string[] }> {\n if (provider.defaultModels.length > 0) return provider.defaultModels\n return (allowedModelIds ?? []).map((id) => ({ id, name: id }))\n}\n\nconst agentIdPattern = /^[a-z0-9_]+\\.[a-z0-9_]+$/\n\nconst agentIdParamSchema = z.object({\n agentId: z\n .string()\n .regex(agentIdPattern, 'agentId must match \"<module>.<agent>\" (lowercase, digits, underscores only)'),\n})\n\nexport const openApi: OpenApiRouteDoc = {\n tag: 'AI Assistant',\n summary: 'Available models for an AI agent',\n methods: {\n GET: {\n operationId: 'aiAssistantGetAgentModels',\n summary: 'Get the providers and curated models available for the chat-UI picker for this agent',\n description:\n 'Returns all configured providers with their curated model catalogs, filtered to providers ' +\n 'that have an API key configured in the current environment. When the agent declares ' +\n '`allowRuntimeOverride: false`, the response reflects that constraint so the ' +\n 'UI picker can hide itself. Includes the agent\\'s resolved default provider/model so ' +\n 'the picker can render a \"(default)\" badge next to the right entry. ' +\n 'When a tenant-scoped lookup (the allowlist snapshot or the per-agent runtime override) ' +\n 'cannot be loaded, the route still answers 200 with a partially resolved provider list ' +\n 'and sets `degraded: true` plus a `degradedReason` code, so callers can tell a full ' +\n 'entitlement list from a partial one. ' +\n 'RBAC: requires the same features as the agent itself (typically `ai_assistant.view`).',\n responses: [\n {\n status: 200,\n description:\n 'Providers and curated models available for the agent picker. ' +\n 'Empty `providers` array when `allowRuntimeOverride` is false. ' +\n '`degraded` is `false` and `degradedReason` is `null` on a fully resolved response; ' +\n '`degraded: true` with `degradedReason: \"tenant_allowlist_unavailable\"` means a ' +\n 'tenant-scoped lookup failed, so `providers` may be missing the tenant allowlist ' +\n 'and/or the per-agent override narrowing \u2014 one code covers the whole tenant-scoped ' +\n 'block because no client can act differently per source. Clients SHOULD render the ' +\n 'list but MUST NOT treat it as the tenant\\'s authoritative entitlement (for example, ' +\n 'do not prune a stored model selection against it).',\n },\n ],\n errors: [\n { status: 401, description: 'Unauthenticated.' },\n { status: 403, description: 'Caller lacks the agent\\'s required features.' },\n { status: 404, description: 'Unknown agent id.' },\n ],\n },\n },\n}\n\nexport const metadata = {\n GET: { requireAuth: true, requireFeatures: ['ai_assistant.view'] },\n}\n\nexport async function GET(\n req: NextRequest,\n { params }: { params: Promise<{ agentId: string }> },\n): Promise<Response> {\n const auth = await getAuthFromRequest(req)\n if (!auth?.sub) {\n return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const rawParams = await params\n const paramResult = agentIdParamSchema.safeParse(rawParams)\n if (!paramResult.success) {\n return NextResponse.json(\n { error: 'Invalid agentId path parameter.', code: 'validation_error', issues: paramResult.error.issues },\n { status: 400 },\n )\n }\n const agentId = paramResult.data.agentId\n\n try {\n await loadAgentRegistry()\n\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\n const agent = getAgent(agentId)\n if (!agent) {\n return NextResponse.json({ error: `Agent \"${agentId}\" not found.`, code: 'agent_unknown' }, { status: 404 })\n }\n\n const agentFeatures = agent.requiredFeatures ?? []\n if (agentFeatures.length > 0) {\n const permitted = hasRequiredFeatures(agentFeatures, acl.features, acl.isSuperAdmin)\n if (!permitted) {\n return NextResponse.json(\n {\n error: `Access to agent \"${agentId}\" requires features: ${agentFeatures.join(', ')}.`,\n code: 'agent_features_denied',\n },\n { status: 403 },\n )\n }\n }\n\n const allowRuntimeOverride = resolveAllowRuntimeOverride(agent)\n\n // Load the per-tenant allowlist snapshot so the picker reflects both env\n // and admin-edited tenant constraints (Phase 1780-6).\n let tenantAllowlistSnapshot: TenantAllowlistSnapshot | null = null\n let agentRuntimeOverrideAllowlist: TenantAllowlistSnapshot | null = null\n let tenantRuntimeOverride: {\n providerId: string | null\n modelId: string | null\n baseURL: string | null\n } | null = null\n let tenantAllowlistDegraded = false\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 runtimeOverrideDefaultRow = await runtimeOverrideRepo.getDefault({\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n agentId,\n })\n tenantRuntimeOverride = runtimeOverrideDefaultRow\n ? {\n providerId: runtimeOverrideDefaultRow.providerId ?? null,\n modelId: runtimeOverrideDefaultRow.modelId ?? null,\n baseURL: runtimeOverrideDefaultRow.baseUrl ?? null,\n }\n : null\n const runtimeOverrideRow = await runtimeOverrideRepo.getExact({\n tenantId: auth.tenantId,\n organizationId: auth.orgId ?? null,\n agentId,\n })\n const tenantAgentAllowlist = runtimeOverrideRow\n ? {\n allowedProviders: runtimeOverrideRow.allowedOverrideProviders ?? null,\n allowedModelsByProvider: runtimeOverrideRow.allowedOverrideModelsByProvider ?? {},\n }\n : null\n agentRuntimeOverrideAllowlist = hasAllowlistSnapshotRestrictions(tenantAgentAllowlist)\n ? tenantAgentAllowlist\n : null\n } catch (snapshotError) {\n // Picker still renders against whatever resolved so the UI does not break, but log\n // at error level so an outage is operationally visible and mark the response\n // `degraded` so callers know a tenant-scoped read failed and the provider list may\n // be missing the tenant allowlist and/or the per-agent override narrowing. The chat\n // dispatcher refuses to dispatch when this lookup fails, so writes stay safe.\n tenantAllowlistDegraded = true\n logger.error('AI Agents Models \u2014 Failed to load tenant allowlist', { err: snapshotError })\n }\n }\n\n // Resolve the agent's current default provider/model for the \"(default)\" badge\n const factory = createModelFactory(container)\n const defaultResolution = factory.resolveModel({\n moduleId: agent.moduleId,\n agentDefaultModel: agent.defaultModel,\n agentDefaultProvider: agent.defaultProvider,\n agentDefaultBaseUrl: agent.defaultBaseUrl,\n allowRuntimeOverride,\n tenantOverride: tenantRuntimeOverride ?? undefined,\n tenantAllowlist: tenantAllowlistSnapshot,\n })\n const defaultProviderId = defaultResolution.providerId\n const defaultModelId = defaultResolution.modelId\n\n // Build provider list \u2014 only configured providers, with curated model\n // catalogs, clipped to the EFFECTIVE allowlist (env \u2229 tenant) so the\n // chat-UI picker can never offer a value the runtime would refuse.\n const env = process.env as Record<string, string | undefined>\n const knownProviderIds = llmProviderRegistry.list().map((p) => p.id)\n const baseEffectiveAllowlist = intersectAllowlists(\n env,\n knownProviderIds,\n tenantAllowlistSnapshot,\n )\n const envAgentAllowlist = readAgentRuntimeOverrideAllowlist(env, agentId, knownProviderIds)\n const effectiveAllowlist = intersectEffectiveAllowlistWithSnapshot(\n intersectEffectiveAllowlistWithSnapshot(\n baseEffectiveAllowlist,\n knownProviderIds,\n envAgentAllowlist,\n ),\n knownProviderIds,\n agentRuntimeOverrideAllowlist,\n )\n const providers = allowRuntimeOverride\n ? llmProviderRegistry.list()\n .filter((provider) => provider.isConfigured())\n .filter((provider) => isProviderAllowedInEffective(effectiveAllowlist, provider.id))\n .map((provider) => {\n const allowedModelIds = effectiveAllowlist.modelsByProvider[provider.id]\n const filteredModels = modelsForPicker(provider, allowedModelIds).filter((model) =>\n isModelAllowedForProviderInEffective(effectiveAllowlist, provider.id, model.id),\n )\n return {\n id: provider.id,\n name: provider.name,\n isDefault: provider.id === defaultProviderId,\n models: filteredModels.map((model) => ({\n id: model.id,\n name: model.name,\n contextWindow: model.contextWindow,\n tags: model.tags,\n isDefault: provider.id === defaultProviderId && model.id === defaultModelId,\n })),\n }\n })\n : []\n\n return NextResponse.json({\n agentId,\n allowRuntimeOverride,\n allowRuntimeModelOverride: allowRuntimeOverride,\n defaultProviderId,\n defaultModelId,\n defaultProviderName: llmProviderRegistry.get(defaultProviderId)?.name ?? defaultProviderId,\n defaultModelName:\n llmProviderRegistry\n .get(defaultProviderId)\n ?.defaultModels.find((model) => model.id === defaultModelId)?.name ?? defaultModelId,\n providers,\n degraded: tenantAllowlistDegraded,\n degradedReason: tenantAllowlistDegraded ? 'tenant_allowlist_unavailable' : null,\n })\n } catch (error) {\n logger.error('AI Agents Models \u2014 GET error', { err: error })\n return NextResponse.json({ error: 'Failed to resolve agent models.' }, { status: 500 })\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,oBAAoB;AAC7B,SAAS,oBAAsC;AAC/C,SAAS,SAAS;AAGlB,SAAS,0BAA0B;AACnC,SAAS,8BAA8B;AAEvC,SAAS,2BAA2B;AACpC,SAAS,UAAU,yBAAyB;AAC5C,SAAS,2BAA2B;AACpC,SAAS,oBAAoB,mCAAmC;AAChE;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,wCAAwC;AACjD,SAAS,wCAAwC;AAEjD,MAAM,SAAS,aAAa,cAAc;AAE1C,SAAS,gBACP,UACA,iBACsG;AACtG,MAAI,SAAS,cAAc,SAAS,EAAG,QAAO,SAAS;AACvD,UAAQ,mBAAmB,CAAC,GAAG,IAAI,CAAC,QAAQ,EAAE,IAAI,MAAM,GAAG,EAAE;AAC/D;AAEA,MAAM,iBAAiB;AAEvB,MAAM,qBAAqB,EAAE,OAAO;AAAA,EAClC,SAAS,EACN,OAAO,EACP,MAAM,gBAAgB,6EAA6E;AACxG,CAAC;AAEM,MAAM,UAA2B;AAAA,EACtC,KAAK;AAAA,EACL,SAAS;AAAA,EACT,SAAS;AAAA,IACP,KAAK;AAAA,MACH,aAAa;AAAA,MACb,SAAS;AAAA,MACT,aACE;AAAA,MAUF,WAAW;AAAA,QACT;AAAA,UACE,QAAQ;AAAA,UACR,aACE;AAAA,QASJ;AAAA,MACF;AAAA,MACA,QAAQ;AAAA,QACN,EAAE,QAAQ,KAAK,aAAa,mBAAmB;AAAA,QAC/C,EAAE,QAAQ,KAAK,aAAa,8CAA+C;AAAA,QAC3E,EAAE,QAAQ,KAAK,aAAa,oBAAoB;AAAA,MAClD;AAAA,IACF;AAAA,EACF;AACF;AAEO,MAAM,WAAW;AAAA,EACtB,KAAK,EAAE,aAAa,MAAM,iBAAiB,CAAC,mBAAmB,EAAE;AACnE;AAEA,eAAsB,IACpB,KACA,EAAE,OAAO,GACU;AACnB,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,QAAM,YAAY,MAAM;AACxB,QAAM,cAAc,mBAAmB,UAAU,SAAS;AAC1D,MAAI,CAAC,YAAY,SAAS;AACxB,WAAO,aAAa;AAAA,MAClB,EAAE,OAAO,mCAAmC,MAAM,oBAAoB,QAAQ,YAAY,MAAM,OAAO;AAAA,MACvG,EAAE,QAAQ,IAAI;AAAA,IAChB;AAAA,EACF;AACA,QAAM,UAAU,YAAY,KAAK;AAEjC,MAAI;AACF,UAAM,kBAAkB;AAExB,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;AAED,UAAM,QAAQ,SAAS,OAAO;AAC9B,QAAI,CAAC,OAAO;AACV,aAAO,aAAa,KAAK,EAAE,OAAO,UAAU,OAAO,gBAAgB,MAAM,gBAAgB,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,IAC7G;AAEA,UAAM,gBAAgB,MAAM,oBAAoB,CAAC;AACjD,QAAI,cAAc,SAAS,GAAG;AAC5B,YAAM,YAAY,oBAAoB,eAAe,IAAI,UAAU,IAAI,YAAY;AACnF,UAAI,CAAC,WAAW;AACd,eAAO,aAAa;AAAA,UAClB;AAAA,YACE,OAAO,oBAAoB,OAAO,wBAAwB,cAAc,KAAK,IAAI,CAAC;AAAA,YAClF,MAAM;AAAA,UACR;AAAA,UACA,EAAE,QAAQ,IAAI;AAAA,QAChB;AAAA,MACF;AAAA,IACF;AAEA,UAAM,uBAAuB,4BAA4B,KAAK;AAI9D,QAAI,0BAA0D;AAC9D,QAAI,gCAAgE;AACpE,QAAI,wBAIO;AACX,QAAI,0BAA0B;AAC9B,QAAI,KAAK,UAAU;AACjB,UAAI;AACF,cAAM,KAAK,UAAU,QAAuB,IAAI;AAChD,cAAM,gBAAgB,IAAI,iCAAiC,EAAE;AAC7D,kCAA0B,MAAM,cAAc,YAAY;AAAA,UACxD,UAAU,KAAK;AAAA,UACf,gBAAgB,KAAK,SAAS;AAAA,QAChC,CAAC;AACD,cAAM,sBAAsB,IAAI,iCAAiC,EAAE;AACnE,cAAM,4BAA4B,MAAM,oBAAoB,WAAW;AAAA,UACrE,UAAU,KAAK;AAAA,UACf,gBAAgB,KAAK,SAAS;AAAA,UAC9B;AAAA,QACF,CAAC;AACD,gCAAwB,4BACpB;AAAA,UACE,YAAY,0BAA0B,cAAc;AAAA,UACpD,SAAS,0BAA0B,WAAW;AAAA,UAC9C,SAAS,0BAA0B,WAAW;AAAA,QAChD,IACA;AACJ,cAAM,qBAAqB,MAAM,oBAAoB,SAAS;AAAA,UAC5D,UAAU,KAAK;AAAA,UACf,gBAAgB,KAAK,SAAS;AAAA,UAC9B;AAAA,QACF,CAAC;AACD,cAAM,uBAAuB,qBACzB;AAAA,UACE,kBAAkB,mBAAmB,4BAA4B;AAAA,UACjE,yBAAyB,mBAAmB,mCAAmC,CAAC;AAAA,QAClF,IACA;AACJ,wCAAgC,iCAAiC,oBAAoB,IACjF,uBACA;AAAA,MACN,SAAS,eAAe;AAMtB,kCAA0B;AAC1B,eAAO,MAAM,2DAAsD,EAAE,KAAK,cAAc,CAAC;AAAA,MAC3F;AAAA,IACF;AAGA,UAAM,UAAU,mBAAmB,SAAS;AAC5C,UAAM,oBAAoB,QAAQ,aAAa;AAAA,MAC7C,UAAU,MAAM;AAAA,MAChB,mBAAmB,MAAM;AAAA,MACzB,sBAAsB,MAAM;AAAA,MAC5B,qBAAqB,MAAM;AAAA,MAC3B;AAAA,MACA,gBAAgB,yBAAyB;AAAA,MACzC,iBAAiB;AAAA,IACnB,CAAC;AACD,UAAM,oBAAoB,kBAAkB;AAC5C,UAAM,iBAAiB,kBAAkB;AAKzC,UAAM,MAAM,QAAQ;AACpB,UAAM,mBAAmB,oBAAoB,KAAK,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE;AACnE,UAAM,yBAAyB;AAAA,MAC7B;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACA,UAAM,oBAAoB,kCAAkC,KAAK,SAAS,gBAAgB;AAC1F,UAAM,qBAAqB;AAAA,MACzB;AAAA,QACE;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACA,UAAM,YAAY,uBACd,oBAAoB,KAAK,EACtB,OAAO,CAAC,aAAa,SAAS,aAAa,CAAC,EAC5C,OAAO,CAAC,aAAa,6BAA6B,oBAAoB,SAAS,EAAE,CAAC,EAClF,IAAI,CAAC,aAAa;AACjB,YAAM,kBAAkB,mBAAmB,iBAAiB,SAAS,EAAE;AACvE,YAAM,iBAAiB,gBAAgB,UAAU,eAAe,EAAE;AAAA,QAAO,CAAC,UACxE,qCAAqC,oBAAoB,SAAS,IAAI,MAAM,EAAE;AAAA,MAChF;AACA,aAAO;AAAA,QACL,IAAI,SAAS;AAAA,QACb,MAAM,SAAS;AAAA,QACf,WAAW,SAAS,OAAO;AAAA,QAC3B,QAAQ,eAAe,IAAI,CAAC,WAAW;AAAA,UACrC,IAAI,MAAM;AAAA,UACV,MAAM,MAAM;AAAA,UACZ,eAAe,MAAM;AAAA,UACrB,MAAM,MAAM;AAAA,UACZ,WAAW,SAAS,OAAO,qBAAqB,MAAM,OAAO;AAAA,QAC/D,EAAE;AAAA,MACJ;AAAA,IACF,CAAC,IACH,CAAC;AAEL,WAAO,aAAa,KAAK;AAAA,MACvB;AAAA,MACA;AAAA,MACA,2BAA2B;AAAA,MAC3B;AAAA,MACA;AAAA,MACA,qBAAqB,oBAAoB,IAAI,iBAAiB,GAAG,QAAQ;AAAA,MACzE,kBACE,oBACG,IAAI,iBAAiB,GACpB,cAAc,KAAK,CAAC,UAAU,MAAM,OAAO,cAAc,GAAG,QAAQ;AAAA,MAC1E;AAAA,MACA,UAAU;AAAA,MACV,gBAAgB,0BAA0B,iCAAiC;AAAA,IAC7E,CAAC;AAAA,EACH,SAAS,OAAO;AACd,WAAO,MAAM,qCAAgC,EAAE,KAAK,MAAM,CAAC;AAC3D,WAAO,aAAa,KAAK,EAAE,OAAO,kCAAkC,GAAG,EAAE,QAAQ,IAAI,CAAC;AAAA,EACxF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -85,8 +85,7 @@ const agentQuerySchema = z.object({
|
|
|
85
85
|
model: z.string().optional(),
|
|
86
86
|
/**
|
|
87
87
|
* Per-request base URL override. Must parse as a URL and match
|
|
88
|
-
* `AI_RUNTIME_BASEURL_ALLOWLIST` (comma-separated
|
|
89
|
-
* port 443, or full `scheme://host[:port]` origins). When the
|
|
88
|
+
* `AI_RUNTIME_BASEURL_ALLOWLIST` (comma-separated host patterns). When the
|
|
90
89
|
* env var is unset or empty, any non-empty value is rejected.
|
|
91
90
|
*
|
|
92
91
|
* Phase 4a of spec `2026-04-27-ai-agents-provider-model-baseurl-overrides`.
|
|
@@ -486,7 +485,7 @@ async function POST(req) {
|
|
|
486
485
|
if (!isBaseurlAllowlisted(rawBaseUrl.trim(), allowlist)) {
|
|
487
486
|
return jsonError(
|
|
488
487
|
400,
|
|
489
|
-
`baseUrl "${rawBaseUrl}" is not in the AI_RUNTIME_BASEURL_ALLOWLIST. Set that env var to a comma-separated list of allowed
|
|
488
|
+
`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.`,
|
|
490
489
|
"baseurl_not_allowlisted"
|
|
491
490
|
);
|
|
492
491
|
}
|
|
@@ -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 '../../../lib/llm-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 hosts, meaning https on\n * port 443, or full `scheme://host[:port]` origins). 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 hosts (https only) or full origins such as http://localhost:11434 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 '../../../lib/llm-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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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|