@open-mercato/ai-assistant 0.6.3-develop.3894.1.352abf4240 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/ai_assistant/__integration__/TC-AI-sharing-06-deep-link.spec.js +87 -0
  3. package/dist/modules/ai_assistant/__integration__/TC-AI-sharing-06-deep-link.spec.js.map +7 -0
  4. package/dist/modules/ai_assistant/__integration__/TC-AI-sharing-07-owner-label.spec.js +119 -0
  5. package/dist/modules/ai_assistant/__integration__/TC-AI-sharing-07-owner-label.spec.js.map +7 -0
  6. package/dist/modules/ai_assistant/acl.js +1 -0
  7. package/dist/modules/ai_assistant/acl.js.map +2 -2
  8. package/dist/modules/ai_assistant/api/ai/chat/route.js +3 -0
  9. package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
  10. package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/[userId]/route.js +128 -0
  11. package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/[userId]/route.js.map +7 -0
  12. package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/route.js +271 -0
  13. package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/route.js.map +7 -0
  14. package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/route.js +9 -1
  15. package/dist/modules/ai_assistant/api/ai/conversations/[conversationId]/route.js.map +2 -2
  16. package/dist/modules/ai_assistant/api/ai/conversations/route.js +4 -1
  17. package/dist/modules/ai_assistant/api/ai/conversations/route.js.map +2 -2
  18. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js +5 -1
  19. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js.map +2 -2
  20. package/dist/modules/ai_assistant/components/ConversationShareButton.js +5 -0
  21. package/dist/modules/ai_assistant/components/ConversationShareButton.js.map +7 -0
  22. package/dist/modules/ai_assistant/components/ConversationShareDialog.js +5 -0
  23. package/dist/modules/ai_assistant/components/ConversationShareDialog.js.map +7 -0
  24. package/dist/modules/ai_assistant/data/entities.js +3 -0
  25. package/dist/modules/ai_assistant/data/entities.js.map +2 -2
  26. package/dist/modules/ai_assistant/data/repositories/AiChatConversationRepository.js +235 -5
  27. package/dist/modules/ai_assistant/data/repositories/AiChatConversationRepository.js.map +2 -2
  28. package/dist/modules/ai_assistant/events.js +14 -0
  29. package/dist/modules/ai_assistant/events.js.map +2 -2
  30. package/dist/modules/ai_assistant/i18n/de.json +17 -0
  31. package/dist/modules/ai_assistant/i18n/en.json +17 -0
  32. package/dist/modules/ai_assistant/i18n/es.json +17 -0
  33. package/dist/modules/ai_assistant/i18n/pl.json +17 -0
  34. package/dist/modules/ai_assistant/lib/conversation-storage.js +12 -3
  35. package/dist/modules/ai_assistant/lib/conversation-storage.js.map +2 -2
  36. package/dist/modules/ai_assistant/migrations/Migration20260522120000_ai_assistant.js +15 -0
  37. package/dist/modules/ai_assistant/migrations/Migration20260522120000_ai_assistant.js.map +7 -0
  38. package/dist/modules/ai_assistant/notifications.client.js +30 -0
  39. package/dist/modules/ai_assistant/notifications.client.js.map +7 -0
  40. package/dist/modules/ai_assistant/notifications.js +27 -0
  41. package/dist/modules/ai_assistant/notifications.js.map +7 -0
  42. package/dist/modules/ai_assistant/setup.js +2 -1
  43. package/dist/modules/ai_assistant/setup.js.map +2 -2
  44. package/dist/modules/ai_assistant/subscribers/conversation-shared-notify.js +59 -0
  45. package/dist/modules/ai_assistant/subscribers/conversation-shared-notify.js.map +7 -0
  46. package/dist/modules/ai_assistant/widgets/notifications/ConversationSharedRenderer.js +123 -0
  47. package/dist/modules/ai_assistant/widgets/notifications/ConversationSharedRenderer.js.map +7 -0
  48. package/generated/entities/ai_chat_conversation_participant/index.ts +1 -0
  49. package/generated/entity-fields-registry.ts +1 -0
  50. package/package.json +7 -8
  51. package/src/modules/ai_assistant/__integration__/TC-AI-sharing-06-deep-link.spec.ts +117 -0
  52. package/src/modules/ai_assistant/__integration__/TC-AI-sharing-07-owner-label.spec.ts +159 -0
  53. package/src/modules/ai_assistant/__tests__/integration/ai-chat-sharing.test.ts +406 -0
  54. package/src/modules/ai_assistant/acl.ts +1 -0
  55. package/src/modules/ai_assistant/api/ai/chat/route.ts +3 -0
  56. package/src/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/[userId]/route.ts +149 -0
  57. package/src/modules/ai_assistant/api/ai/conversations/[conversationId]/participants/route.ts +314 -0
  58. package/src/modules/ai_assistant/api/ai/conversations/[conversationId]/route.ts +9 -1
  59. package/src/modules/ai_assistant/api/ai/conversations/route.ts +4 -1
  60. package/src/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.tsx +4 -0
  61. package/src/modules/ai_assistant/components/ConversationShareButton.tsx +1 -0
  62. package/src/modules/ai_assistant/components/ConversationShareDialog.tsx +1 -0
  63. package/src/modules/ai_assistant/data/entities.ts +4 -0
  64. package/src/modules/ai_assistant/data/repositories/AiChatConversationRepository.ts +270 -7
  65. package/src/modules/ai_assistant/data/repositories/__tests__/AiChatConversationRepository.test.ts +297 -3
  66. package/src/modules/ai_assistant/events.ts +31 -0
  67. package/src/modules/ai_assistant/i18n/__tests__/conversation-share-translations.test.ts +59 -0
  68. package/src/modules/ai_assistant/i18n/de.json +17 -0
  69. package/src/modules/ai_assistant/i18n/en.json +17 -0
  70. package/src/modules/ai_assistant/i18n/es.json +17 -0
  71. package/src/modules/ai_assistant/i18n/pl.json +17 -0
  72. package/src/modules/ai_assistant/lib/conversation-storage.ts +22 -1
  73. package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +25 -0
  74. package/src/modules/ai_assistant/migrations/Migration20260522120000_ai_assistant.ts +15 -0
  75. package/src/modules/ai_assistant/notifications.client.ts +29 -0
  76. package/src/modules/ai_assistant/notifications.ts +25 -0
  77. package/src/modules/ai_assistant/setup.ts +2 -1
  78. package/src/modules/ai_assistant/subscribers/__tests__/conversation-shared-notify.test.ts +116 -0
  79. package/src/modules/ai_assistant/subscribers/conversation-shared-notify.ts +78 -0
  80. package/src/modules/ai_assistant/widgets/notifications/ConversationSharedRenderer.tsx +121 -0
@@ -1,2 +1,2 @@
1
- [build:ai-assistant] found 192 entry points
1
+ [build:ai-assistant] found 203 entry points
2
2
  [build:ai-assistant] built successfully
@@ -0,0 +1,87 @@
1
+ import { test, expect } from "@playwright/test";
2
+ import { login } from "@open-mercato/core/modules/core/__integration__/helpers/auth";
3
+ test.describe("TC-AI-sharing-06: deep-link auto-open from ?openAiConversation", () => {
4
+ const STUB_CONV_ID = "test-shared-conv-01";
5
+ const STUB_AGENT_ID = "customers.account_assistant";
6
+ const agentsStub = {
7
+ agents: [
8
+ {
9
+ id: STUB_AGENT_ID,
10
+ moduleId: "customers",
11
+ label: "Account assistant",
12
+ description: "Helps with customer accounts.",
13
+ executionMode: "chat",
14
+ mutationPolicy: "read-only",
15
+ allowedTools: [],
16
+ requiredFeatures: [],
17
+ acceptedMediaTypes: [],
18
+ hasOutputSchema: false
19
+ }
20
+ ],
21
+ total: 1,
22
+ aiConfigured: true
23
+ };
24
+ const conversationStub = {
25
+ conversation: {
26
+ conversationId: STUB_CONV_ID,
27
+ agentId: STUB_AGENT_ID,
28
+ title: "Shared conversation",
29
+ status: "open",
30
+ visibility: "shared",
31
+ pageContext: null,
32
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
33
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
34
+ lastMessageAt: null,
35
+ importedFromLocalAt: null,
36
+ isOwner: false
37
+ },
38
+ messages: [],
39
+ nextCursor: null
40
+ };
41
+ async function setupStubs(page) {
42
+ await page.route(
43
+ "**/api/ai_assistant/health",
44
+ (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ healthy: true }) })
45
+ );
46
+ await page.route(
47
+ "**/api/ai_assistant/ai/agents",
48
+ (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(agentsStub) })
49
+ );
50
+ await page.route(
51
+ `**/api/ai_assistant/ai/conversations/${STUB_CONV_ID}**`,
52
+ (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(conversationStub) })
53
+ );
54
+ }
55
+ test("chat sheet opens when page loads with ?openAiConversation param", async ({ page }) => {
56
+ await login(page, "superadmin");
57
+ await setupStubs(page);
58
+ await page.goto(`/backend?openAiConversation=${STUB_CONV_ID}`, { waitUntil: "domcontentloaded" });
59
+ await expect(page.locator("[data-ai-launcher-sheet]")).toBeVisible({ timeout: 1e4 });
60
+ });
61
+ test("URL param is stripped after deep-link is handled", async ({ page }) => {
62
+ await login(page, "superadmin");
63
+ await setupStubs(page);
64
+ await page.goto(`/backend?openAiConversation=${STUB_CONV_ID}`, { waitUntil: "domcontentloaded" });
65
+ await expect(page.locator("[data-ai-launcher-sheet]")).toBeVisible({ timeout: 1e4 });
66
+ await expect(page).not.toHaveURL(/openAiConversation/, { timeout: 5e3 });
67
+ });
68
+ test("deep-link is ignored when conversation fetch returns 403", async ({ page }) => {
69
+ await login(page, "superadmin");
70
+ await page.route(
71
+ "**/api/ai_assistant/health",
72
+ (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ healthy: true }) })
73
+ );
74
+ await page.route(
75
+ "**/api/ai_assistant/ai/agents",
76
+ (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(agentsStub) })
77
+ );
78
+ await page.route(
79
+ `**/api/ai_assistant/ai/conversations/${STUB_CONV_ID}**`,
80
+ (route) => route.fulfill({ status: 403, contentType: "application/json", body: JSON.stringify({ error: "forbidden" }) })
81
+ );
82
+ await page.goto(`/backend?openAiConversation=${STUB_CONV_ID}`, { waitUntil: "domcontentloaded" });
83
+ await expect(page.locator("[data-ai-launcher-trigger]")).toBeVisible({ timeout: 1e4 });
84
+ await expect(page.locator("[data-ai-launcher-sheet]")).not.toBeVisible();
85
+ });
86
+ });
87
+ //# sourceMappingURL=TC-AI-sharing-06-deep-link.spec.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/ai_assistant/__integration__/TC-AI-sharing-06-deep-link.spec.ts"],
4
+ "sourcesContent": ["import { test, expect } from '@playwright/test';\nimport { login } from '@open-mercato/core/modules/core/__integration__/helpers/auth';\n\n/**\n * TC-AI-sharing-06: Deep-link auto-open via ?openAiConversation=<id>\n *\n * Regression test for the bug where navigating to\n * `/backend?openAiConversation=<id>` failed to open the AiAssistantLauncher\n * chat sheet \u2014 because the launcher read window.location.search only on mount\n * and AppShell never remounts during client-side navigation.\n *\n * Fix: replaced mount-only useEffect with useSearchParams() from next/navigation,\n * which is reactive to URL changes in both hard and client-side navigations.\n *\n * Also verifies the URL-cleanup behaviour: after the deep-link is handled the\n * launcher strips ?openAiConversation from the URL via router.replace() so\n * subsequent normal chat opens don't inherit the shared conversation.\n *\n * API routes are fully stubbed \u2014 no live LLM is called.\n */\ntest.describe('TC-AI-sharing-06: deep-link auto-open from ?openAiConversation', () => {\n const STUB_CONV_ID = 'test-shared-conv-01';\n const STUB_AGENT_ID = 'customers.account_assistant';\n\n const agentsStub = {\n agents: [\n {\n id: STUB_AGENT_ID,\n moduleId: 'customers',\n label: 'Account assistant',\n description: 'Helps with customer accounts.',\n executionMode: 'chat',\n mutationPolicy: 'read-only',\n allowedTools: [],\n requiredFeatures: [],\n acceptedMediaTypes: [],\n hasOutputSchema: false,\n },\n ],\n total: 1,\n aiConfigured: true,\n };\n\n const conversationStub = {\n conversation: {\n conversationId: STUB_CONV_ID,\n agentId: STUB_AGENT_ID,\n title: 'Shared conversation',\n status: 'open',\n visibility: 'shared',\n pageContext: null,\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n lastMessageAt: null,\n importedFromLocalAt: null,\n isOwner: false,\n },\n messages: [],\n nextCursor: null,\n };\n\n async function setupStubs(page: import('@playwright/test').Page) {\n await page.route('**/api/ai_assistant/health', (route) =>\n route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ healthy: true }) }),\n );\n await page.route('**/api/ai_assistant/ai/agents', (route) =>\n route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(agentsStub) }),\n );\n await page.route(`**/api/ai_assistant/ai/conversations/${STUB_CONV_ID}**`, (route) =>\n route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(conversationStub) }),\n );\n }\n\n test('chat sheet opens when page loads with ?openAiConversation param', async ({ page }) => {\n await login(page, 'superadmin');\n await setupStubs(page);\n\n await page.goto(`/backend?openAiConversation=${STUB_CONV_ID}`, { waitUntil: 'domcontentloaded' });\n\n await expect(page.locator('[data-ai-launcher-sheet]')).toBeVisible({ timeout: 10_000 });\n });\n\n test('URL param is stripped after deep-link is handled', async ({ page }) => {\n await login(page, 'superadmin');\n await setupStubs(page);\n\n await page.goto(`/backend?openAiConversation=${STUB_CONV_ID}`, { waitUntil: 'domcontentloaded' });\n\n // Wait for the chat to open (deep-link was handled).\n await expect(page.locator('[data-ai-launcher-sheet]')).toBeVisible({ timeout: 10_000 });\n\n // The launcher calls router.replace(pathname) after opening, stripping the\n // param. The URL should no longer contain openAiConversation.\n await expect(page).not.toHaveURL(/openAiConversation/, { timeout: 5_000 });\n });\n\n test('deep-link is ignored when conversation fetch returns 403', async ({ page }) => {\n await login(page, 'superadmin');\n\n await page.route('**/api/ai_assistant/health', (route) =>\n route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ healthy: true }) }),\n );\n await page.route('**/api/ai_assistant/ai/agents', (route) =>\n route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(agentsStub) }),\n );\n // Simulate conversation that the viewer has no access to.\n await page.route(`**/api/ai_assistant/ai/conversations/${STUB_CONV_ID}**`, (route) =>\n route.fulfill({ status: 403, contentType: 'application/json', body: JSON.stringify({ error: 'forbidden' }) }),\n );\n\n await page.goto(`/backend?openAiConversation=${STUB_CONV_ID}`, { waitUntil: 'domcontentloaded' });\n\n // Launcher should be visible but chat sheet must NOT auto-open.\n await expect(page.locator('[data-ai-launcher-trigger]')).toBeVisible({ timeout: 10_000 });\n await expect(page.locator('[data-ai-launcher-sheet]')).not.toBeVisible();\n });\n});\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,119 @@
1
+ import { test, expect } from "@playwright/test";
2
+ import { login } from "@open-mercato/core/modules/core/__integration__/helpers/auth";
3
+ test.describe("TC-AI-sharing-07: viewer sees Owner label on owner messages", () => {
4
+ const STUB_CONV_ID = "test-shared-conv-02";
5
+ const STUB_AGENT_ID = "customers.account_assistant";
6
+ const agentsStub = {
7
+ agents: [
8
+ {
9
+ id: STUB_AGENT_ID,
10
+ moduleId: "customers",
11
+ label: "Account assistant",
12
+ description: "Helps with customer accounts.",
13
+ executionMode: "chat",
14
+ mutationPolicy: "read-only",
15
+ allowedTools: [],
16
+ requiredFeatures: [],
17
+ acceptedMediaTypes: [],
18
+ hasOutputSchema: false
19
+ }
20
+ ],
21
+ total: 1,
22
+ aiConfigured: true
23
+ };
24
+ const conversationStub = {
25
+ conversation: {
26
+ conversationId: STUB_CONV_ID,
27
+ agentId: STUB_AGENT_ID,
28
+ title: "Shared conversation",
29
+ status: "open",
30
+ visibility: "shared",
31
+ pageContext: null,
32
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
33
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
34
+ lastMessageAt: null,
35
+ importedFromLocalAt: (/* @__PURE__ */ new Date()).toISOString(),
36
+ isOwner: false
37
+ },
38
+ messages: [
39
+ {
40
+ id: "msg-u1",
41
+ clientMessageId: "cmsg-u1",
42
+ role: "user",
43
+ content: "What are the open deals for Acme?",
44
+ uiParts: [],
45
+ attachmentIds: [],
46
+ files: [],
47
+ model: null,
48
+ metadata: null,
49
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
50
+ // Deliberately null — imported messages never have senderUserId.
51
+ senderUserId: null
52
+ },
53
+ {
54
+ id: "msg-a1",
55
+ clientMessageId: "cmsg-a1",
56
+ role: "assistant",
57
+ content: "I found 3 open deals for Acme Corp.",
58
+ uiParts: [],
59
+ attachmentIds: [],
60
+ files: [],
61
+ model: "claude-haiku-4-5",
62
+ metadata: null,
63
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
64
+ senderUserId: null
65
+ }
66
+ ],
67
+ nextCursor: null
68
+ };
69
+ test("viewer sees Owner/Assistant labels and read-only notice", async ({ page }) => {
70
+ await login(page, "superadmin");
71
+ await page.route(
72
+ "**/api/ai_assistant/health",
73
+ (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ healthy: true }) })
74
+ );
75
+ await page.route(
76
+ "**/api/ai_assistant/ai/agents",
77
+ (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(agentsStub) })
78
+ );
79
+ await page.route(
80
+ `**/api/ai_assistant/ai/conversations/${STUB_CONV_ID}**`,
81
+ (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(conversationStub) })
82
+ );
83
+ await page.goto(`/backend?openAiConversation=${STUB_CONV_ID}`, { waitUntil: "domcontentloaded" });
84
+ const sheet = page.locator("[data-ai-launcher-sheet]");
85
+ await expect(sheet).toBeVisible({ timeout: 1e4 });
86
+ await expect(sheet.locator("[data-ai-chat-read-only-notice]")).toBeVisible();
87
+ const userMessages = sheet.locator('[data-role="user"]');
88
+ await expect(userMessages.first()).toBeVisible();
89
+ const userLabel = userMessages.first().locator("div.text-xs").first();
90
+ await expect(userLabel).toHaveText(/owner/i);
91
+ await expect(userLabel).not.toHaveText(/^you$/i);
92
+ const assistantMessages = sheet.locator('[data-role="assistant"]');
93
+ await expect(assistantMessages.first()).toBeVisible();
94
+ const assistantLabel = assistantMessages.first().locator("div.text-xs").first();
95
+ await expect(assistantLabel).toHaveText(/assistant/i);
96
+ });
97
+ test("viewer composer is hidden (cannot send messages)", async ({ page }) => {
98
+ await login(page, "superadmin");
99
+ await page.route(
100
+ "**/api/ai_assistant/health",
101
+ (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ healthy: true }) })
102
+ );
103
+ await page.route(
104
+ "**/api/ai_assistant/ai/agents",
105
+ (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(agentsStub) })
106
+ );
107
+ await page.route(
108
+ `**/api/ai_assistant/ai/conversations/${STUB_CONV_ID}**`,
109
+ (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(conversationStub) })
110
+ );
111
+ await page.goto(`/backend?openAiConversation=${STUB_CONV_ID}`, { waitUntil: "domcontentloaded" });
112
+ const sheet = page.locator("[data-ai-launcher-sheet]");
113
+ await expect(sheet).toBeVisible({ timeout: 1e4 });
114
+ const composer = sheet.locator("form[data-ai-chat-composer], [data-ai-chat-composer]");
115
+ const isComposerVisible = await composer.isVisible().catch(() => false);
116
+ expect(isComposerVisible).toBe(false);
117
+ });
118
+ });
119
+ //# sourceMappingURL=TC-AI-sharing-07-owner-label.spec.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/ai_assistant/__integration__/TC-AI-sharing-07-owner-label.spec.ts"],
4
+ "sourcesContent": ["import { test, expect } from '@playwright/test';\nimport { login } from '@open-mercato/core/modules/core/__integration__/helpers/auth';\n\n/**\n * TC-AI-sharing-07: Owner message label in shared-conversation viewer mode\n *\n * Regression test for the bug where user-role messages in a shared\n * conversation always rendered as \"You\" for the viewer, even when they were\n * written by the conversation owner and had no senderUserId (common for\n * conversations imported from local storage).\n *\n * Root cause: `isOtherUsersMessage` included `&& message.senderUserId != null`,\n * but imported messages don't carry senderUserId. Since viewers can never send\n * messages (the composer is hidden), ALL user-role messages when isOwner===false\n * belong to the owner.\n *\n * Fix: removed the senderUserId guard from MessageRow in AiChat.tsx.\n *\n * Assertions:\n * - [data-ai-chat-read-only-notice] is visible (confirms isOwner===false path)\n * - user-role messages show \"Owner\" label, not \"You\" / \"Ty\"\n * - assistant messages show \"Assistant\"\n * - composer is hidden (read-only viewer mode)\n */\ntest.describe('TC-AI-sharing-07: viewer sees Owner label on owner messages', () => {\n const STUB_CONV_ID = 'test-shared-conv-02';\n const STUB_AGENT_ID = 'customers.account_assistant';\n\n const agentsStub = {\n agents: [\n {\n id: STUB_AGENT_ID,\n moduleId: 'customers',\n label: 'Account assistant',\n description: 'Helps with customer accounts.',\n executionMode: 'chat',\n mutationPolicy: 'read-only',\n allowedTools: [],\n requiredFeatures: [],\n acceptedMediaTypes: [],\n hasOutputSchema: false,\n },\n ],\n total: 1,\n aiConfigured: true,\n };\n\n // Conversation where isOwner===false (viewer). Messages have senderUserId:null\n // because they were imported from local storage \u2014 this was the triggering\n // condition of the bug.\n const conversationStub = {\n conversation: {\n conversationId: STUB_CONV_ID,\n agentId: STUB_AGENT_ID,\n title: 'Shared conversation',\n status: 'open',\n visibility: 'shared',\n pageContext: null,\n createdAt: new Date().toISOString(),\n updatedAt: new Date().toISOString(),\n lastMessageAt: null,\n importedFromLocalAt: new Date().toISOString(),\n isOwner: false,\n },\n messages: [\n {\n id: 'msg-u1',\n clientMessageId: 'cmsg-u1',\n role: 'user',\n content: 'What are the open deals for Acme?',\n uiParts: [],\n attachmentIds: [],\n files: [],\n model: null,\n metadata: null,\n createdAt: new Date().toISOString(),\n // Deliberately null \u2014 imported messages never have senderUserId.\n senderUserId: null,\n },\n {\n id: 'msg-a1',\n clientMessageId: 'cmsg-a1',\n role: 'assistant',\n content: 'I found 3 open deals for Acme Corp.',\n uiParts: [],\n attachmentIds: [],\n files: [],\n model: 'claude-haiku-4-5',\n metadata: null,\n createdAt: new Date().toISOString(),\n senderUserId: null,\n },\n ],\n nextCursor: null,\n };\n\n test('viewer sees Owner/Assistant labels and read-only notice', async ({ page }) => {\n await login(page, 'superadmin');\n\n await page.route('**/api/ai_assistant/health', (route) =>\n route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ healthy: true }) }),\n );\n await page.route('**/api/ai_assistant/ai/agents', (route) =>\n route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(agentsStub) }),\n );\n await page.route(`**/api/ai_assistant/ai/conversations/${STUB_CONV_ID}**`, (route) =>\n route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(conversationStub) }),\n );\n\n await page.goto(`/backend?openAiConversation=${STUB_CONV_ID}`, { waitUntil: 'domcontentloaded' });\n\n const sheet = page.locator('[data-ai-launcher-sheet]');\n await expect(sheet).toBeVisible({ timeout: 10_000 });\n\n // Read-only notice must be present for a viewer.\n await expect(sheet.locator('[data-ai-chat-read-only-notice]')).toBeVisible();\n\n // User-role messages (from owner, senderUserId=null) must show \"Owner\", not \"You\".\n const userMessages = sheet.locator('[data-role=\"user\"]');\n await expect(userMessages.first()).toBeVisible();\n // The label is the first text-xs div inside the message header.\n const userLabel = userMessages.first().locator('div.text-xs').first();\n await expect(userLabel).toHaveText(/owner/i);\n // Regression guard: must NOT show \"You\" or \"Ty\".\n await expect(userLabel).not.toHaveText(/^you$/i);\n\n // Assistant messages must still show \"Assistant\".\n const assistantMessages = sheet.locator('[data-role=\"assistant\"]');\n await expect(assistantMessages.first()).toBeVisible();\n const assistantLabel = assistantMessages.first().locator('div.text-xs').first();\n await expect(assistantLabel).toHaveText(/assistant/i);\n });\n\n test('viewer composer is hidden (cannot send messages)', async ({ page }) => {\n await login(page, 'superadmin');\n\n await page.route('**/api/ai_assistant/health', (route) =>\n route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ healthy: true }) }),\n );\n await page.route('**/api/ai_assistant/ai/agents', (route) =>\n route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(agentsStub) }),\n );\n await page.route(`**/api/ai_assistant/ai/conversations/${STUB_CONV_ID}**`, (route) =>\n route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(conversationStub) }),\n );\n\n await page.goto(`/backend?openAiConversation=${STUB_CONV_ID}`, { waitUntil: 'domcontentloaded' });\n\n const sheet = page.locator('[data-ai-launcher-sheet]');\n await expect(sheet).toBeVisible({ timeout: 10_000 });\n\n // The composer form is hidden via className when isOwner===false.\n // It should not be visible to the user.\n const composer = sheet.locator('form[data-ai-chat-composer], [data-ai-chat-composer]');\n // Either not present or hidden.\n const isComposerVisible = await composer.isVisible().catch(() => false);\n expect(isComposerVisible).toBe(false);\n });\n});\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -2,6 +2,7 @@ const features = [
2
2
  { id: "ai_assistant.view", title: "View AI Assistant Settings", module: "ai_assistant" },
3
3
  { id: "ai_assistant.settings.manage", title: "Manage AI Assistant Settings", module: "ai_assistant" },
4
4
  { id: "ai_assistant.conversations.manage", title: "Manage AI Assistant Conversations", module: "ai_assistant" },
5
+ { id: "ai_assistant.conversations.share", title: "Share AI Assistant Conversations", module: "ai_assistant" },
5
6
  { id: "ai_assistant.mcp.serve", title: "Start MCP Server", module: "ai_assistant" },
6
7
  { id: "ai_assistant.tools.list", title: "List MCP Tools", module: "ai_assistant" },
7
8
  { id: "ai_assistant.mcp_servers.view", title: "View MCP Server Configurations", module: "ai_assistant" },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/modules/ai_assistant/acl.ts"],
4
- "sourcesContent": ["export const features = [\n { id: 'ai_assistant.view', title: 'View AI Assistant Settings', module: 'ai_assistant' },\n { id: 'ai_assistant.settings.manage', title: 'Manage AI Assistant Settings', module: 'ai_assistant' },\n { id: 'ai_assistant.conversations.manage', title: 'Manage AI Assistant Conversations', module: 'ai_assistant' },\n { id: 'ai_assistant.mcp.serve', title: 'Start MCP Server', module: 'ai_assistant' },\n { id: 'ai_assistant.tools.list', title: 'List MCP Tools', module: 'ai_assistant' },\n { id: 'ai_assistant.mcp_servers.view', title: 'View MCP Server Configurations', module: 'ai_assistant' },\n { id: 'ai_assistant.mcp_servers.manage', title: 'Manage MCP Server Configurations', module: 'ai_assistant' },\n]\n\nexport default features\n"],
5
- "mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,qBAAqB,OAAO,8BAA8B,QAAQ,eAAe;AAAA,EACvF,EAAE,IAAI,gCAAgC,OAAO,gCAAgC,QAAQ,eAAe;AAAA,EACpG,EAAE,IAAI,qCAAqC,OAAO,qCAAqC,QAAQ,eAAe;AAAA,EAC9G,EAAE,IAAI,0BAA0B,OAAO,oBAAoB,QAAQ,eAAe;AAAA,EAClF,EAAE,IAAI,2BAA2B,OAAO,kBAAkB,QAAQ,eAAe;AAAA,EACjF,EAAE,IAAI,iCAAiC,OAAO,kCAAkC,QAAQ,eAAe;AAAA,EACvG,EAAE,IAAI,mCAAmC,OAAO,oCAAoC,QAAQ,eAAe;AAC7G;AAEA,IAAO,cAAQ;",
4
+ "sourcesContent": ["export const features = [\n { id: 'ai_assistant.view', title: 'View AI Assistant Settings', module: 'ai_assistant' },\n { id: 'ai_assistant.settings.manage', title: 'Manage AI Assistant Settings', module: 'ai_assistant' },\n { id: 'ai_assistant.conversations.manage', title: 'Manage AI Assistant Conversations', module: 'ai_assistant' },\n { id: 'ai_assistant.conversations.share', title: 'Share AI Assistant Conversations', module: 'ai_assistant' },\n { id: 'ai_assistant.mcp.serve', title: 'Start MCP Server', module: 'ai_assistant' },\n { id: 'ai_assistant.tools.list', title: 'List MCP Tools', module: 'ai_assistant' },\n { id: 'ai_assistant.mcp_servers.view', title: 'View MCP Server Configurations', module: 'ai_assistant' },\n { id: 'ai_assistant.mcp_servers.manage', title: 'Manage MCP Server Configurations', module: 'ai_assistant' },\n]\n\nexport default features\n"],
5
+ "mappings": "AAAO,MAAM,WAAW;AAAA,EACtB,EAAE,IAAI,qBAAqB,OAAO,8BAA8B,QAAQ,eAAe;AAAA,EACvF,EAAE,IAAI,gCAAgC,OAAO,gCAAgC,QAAQ,eAAe;AAAA,EACpG,EAAE,IAAI,qCAAqC,OAAO,qCAAqC,QAAQ,eAAe;AAAA,EAC9G,EAAE,IAAI,oCAAoC,OAAO,oCAAoC,QAAQ,eAAe;AAAA,EAC5G,EAAE,IAAI,0BAA0B,OAAO,oBAAoB,QAAQ,eAAe;AAAA,EAClF,EAAE,IAAI,2BAA2B,OAAO,kBAAkB,QAAQ,eAAe;AAAA,EACjF,EAAE,IAAI,iCAAiC,OAAO,kCAAkC,QAAQ,eAAe;AAAA,EACvG,EAAE,IAAI,mCAAmC,OAAO,oCAAoC,QAAQ,eAAe;AAC7G;AAEA,IAAO,cAAQ;",
6
6
  "names": []
7
7
  }
@@ -503,6 +503,9 @@ async function POST(req) {
503
503
  attachmentIds: bodyResult.data.attachmentIds
504
504
  });
505
505
  } catch (error) {
506
+ if (error instanceof Error && error.name === "AiChatConversationOrgNotFoundError") {
507
+ return jsonError(400, error.message, "organization_not_found");
508
+ }
506
509
  console.error("[AI Chat Agent] Failed to persist user message:", error);
507
510
  }
508
511
  const response = await runAiAgentText({
@@ -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 { NextResponse, type NextRequest } from 'next/server'\nimport type { UIMessage } from 'ai'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport { loadAgentRegistry } from '../../../lib/agent-registry'\nimport { checkAgentPolicy, type AgentPolicyDenyCode } from '../../../lib/agent-policy'\nimport {\n runAiAgentText,\n resolveLoopBudgetPreset,\n type AiAgentLoopBudgetPreset,\n} from '../../../lib/agent-runtime'\nimport { AgentPolicyError } from '../../../lib/agent-tools'\nimport { readBaseurlAllowlist, isBaseurlAllowlisted } from '../../../lib/baseurl-allowlist'\nimport {\n canonicalProviderId,\n hasAllowlistSnapshotRestrictions,\n intersectEffectiveAllowlistWithSnapshot,\n intersectAllowlists,\n isModelAllowedForProviderInEffective,\n isProviderAllowedInEffective,\n modelAllowlistEnvVarName,\n readAgentRuntimeOverrideAllowlist,\n type TenantAllowlistSnapshot,\n} from '../../../lib/model-allowlist'\nimport { AiTenantModelAllowlistRepository } from '../../../data/repositories/AiTenantModelAllowlistRepository'\nimport { AiAgentRuntimeOverrideRepository } from '../../../data/repositories/AiAgentRuntimeOverrideRepository'\nimport { createConversationStorage } from '../../../lib/conversation-storage'\nimport type { EntityManager } from '@mikro-orm/postgresql'\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 console.error('[AI Chat Agent] Conversation persistence failure:', 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 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 console.error(\n '[AI Chat Agent] Tenant allowlist lookup failed; refusing to dispatch:',\n snapshotError,\n )\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 console.error('[AI Chat Agent] Failed to persist user message:', 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 console.error('[AI Chat Agent] Dispatch failure:', 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 { NextResponse, type NextRequest } from 'next/server'\nimport type { UIMessage } from 'ai'\nimport { z } from 'zod'\nimport type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'\nimport { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'\nimport { createRequestContainer } from '@open-mercato/shared/lib/di/container'\nimport type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'\nimport { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'\nimport { loadAgentRegistry } from '../../../lib/agent-registry'\nimport { checkAgentPolicy, type AgentPolicyDenyCode } from '../../../lib/agent-policy'\nimport {\n runAiAgentText,\n resolveLoopBudgetPreset,\n type AiAgentLoopBudgetPreset,\n} from '../../../lib/agent-runtime'\nimport { AgentPolicyError } from '../../../lib/agent-tools'\nimport { readBaseurlAllowlist, isBaseurlAllowlisted } from '../../../lib/baseurl-allowlist'\nimport {\n canonicalProviderId,\n hasAllowlistSnapshotRestrictions,\n intersectEffectiveAllowlistWithSnapshot,\n intersectAllowlists,\n isModelAllowedForProviderInEffective,\n isProviderAllowedInEffective,\n modelAllowlistEnvVarName,\n readAgentRuntimeOverrideAllowlist,\n type TenantAllowlistSnapshot,\n} from '../../../lib/model-allowlist'\nimport { AiTenantModelAllowlistRepository } from '../../../data/repositories/AiTenantModelAllowlistRepository'\nimport { AiAgentRuntimeOverrideRepository } from '../../../data/repositories/AiAgentRuntimeOverrideRepository'\nimport { createConversationStorage } from '../../../lib/conversation-storage'\nimport type { EntityManager } from '@mikro-orm/postgresql'\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 console.error('[AI Chat Agent] Conversation persistence failure:', 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 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 console.error(\n '[AI Chat Agent] Tenant allowlist lookup failed; refusing to dispatch:',\n snapshotError,\n )\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 console.error('[AI Chat Agent] Failed to persist user message:', 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 console.error('[AI Chat Agent] Dispatch failure:', 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
  }
@@ -0,0 +1,128 @@
1
+ import { NextResponse } from "next/server";
2
+ import { z } from "zod";
3
+ import { getAuthFromRequest } from "@open-mercato/shared/lib/auth/server";
4
+ import { createRequestContainer } from "@open-mercato/shared/lib/di/container";
5
+ import { hasRequiredFeatures } from "../../../../../../lib/auth.js";
6
+ import {
7
+ AiChatConversationAccessError,
8
+ AiChatParticipantNotFoundError,
9
+ createConversationStorage
10
+ } from "../../../../../../lib/conversation-storage.js";
11
+ import { emitAiAssistantEvent } from "../../../../../../events.js";
12
+ const REQUIRED_FEATURE = "ai_assistant.view";
13
+ const MANAGE_CONVERSATIONS_FEATURE = "ai_assistant.conversations.manage";
14
+ const SHARE_CONVERSATIONS_FEATURE = "ai_assistant.conversations.share";
15
+ const participantParamsSchema = z.object({
16
+ conversationId: z.string().trim().min(1, "conversationId must be a non-empty string").max(128, "conversationId exceeds the maximum length of 128 characters"),
17
+ userId: z.string().uuid("userId must be a valid UUID")
18
+ });
19
+ const openApi = {
20
+ tag: "AI Assistant",
21
+ summary: "Revoke a conversation participant",
22
+ methods: {
23
+ DELETE: {
24
+ operationId: "aiAssistantRevokeConversationParticipant",
25
+ summary: "Revoke a participant from a conversation (soft-delete).",
26
+ description: 'Soft-deletes the participant row. If no active non-owner participants remain, the conversation visibility is reset to "private". Only the conversation owner or a manager may revoke participants.',
27
+ responses: [
28
+ {
29
+ status: 204,
30
+ description: "Participant revoked."
31
+ }
32
+ ],
33
+ errors: [
34
+ { status: 400, description: "Invalid path parameters." },
35
+ { status: 401, description: "Unauthenticated caller." },
36
+ { status: 403, description: "Caller lacks required features or is not the owner." },
37
+ { status: 404, description: "Conversation not found." }
38
+ ]
39
+ }
40
+ }
41
+ };
42
+ const metadata = {
43
+ DELETE: { requireAuth: true, requireFeatures: [REQUIRED_FEATURE] }
44
+ };
45
+ function jsonError(status, message, code, extra) {
46
+ return NextResponse.json({ error: message, code, ...extra ?? {} }, { status });
47
+ }
48
+ async function DELETE(req, context) {
49
+ const auth = await getAuthFromRequest(req);
50
+ if (!auth) return jsonError(401, "Unauthorized", "unauthenticated");
51
+ const rawParams = await context.params;
52
+ const parseResult = participantParamsSchema.safeParse(rawParams);
53
+ if (!parseResult.success) {
54
+ return jsonError(400, "Invalid path parameters.", "validation_error", {
55
+ issues: parseResult.error.issues
56
+ });
57
+ }
58
+ if (!auth.tenantId) return jsonError(404, "Conversation not found.", "conversation_not_found");
59
+ const container = await createRequestContainer();
60
+ const rbacService = container.resolve("rbacService");
61
+ const acl = await rbacService.loadAcl(auth.sub, {
62
+ tenantId: auth.tenantId,
63
+ organizationId: auth.orgId
64
+ });
65
+ if (!hasRequiredFeatures([REQUIRED_FEATURE], acl.features, acl.isSuperAdmin, rbacService)) {
66
+ return jsonError(403, `Caller lacks required feature "${REQUIRED_FEATURE}".`, "forbidden");
67
+ }
68
+ const canShare = hasRequiredFeatures(
69
+ [SHARE_CONVERSATIONS_FEATURE],
70
+ acl.features,
71
+ acl.isSuperAdmin,
72
+ rbacService
73
+ );
74
+ if (!canShare) {
75
+ return jsonError(
76
+ 403,
77
+ `Caller lacks required feature "${SHARE_CONVERSATIONS_FEATURE}".`,
78
+ "forbidden"
79
+ );
80
+ }
81
+ try {
82
+ const repo = createConversationStorage(container);
83
+ await repo.revokeParticipant(
84
+ parseResult.data.conversationId,
85
+ parseResult.data.userId,
86
+ {
87
+ tenantId: auth.tenantId,
88
+ organizationId: auth.orgId ?? null,
89
+ userId: auth.sub,
90
+ canManageConversations: hasRequiredFeatures(
91
+ [MANAGE_CONVERSATIONS_FEATURE],
92
+ acl.features,
93
+ acl.isSuperAdmin,
94
+ rbacService
95
+ )
96
+ }
97
+ );
98
+ try {
99
+ await emitAiAssistantEvent(
100
+ "ai_assistant.conversation.unshared",
101
+ {
102
+ conversationId: parseResult.data.conversationId,
103
+ tenantId: auth.tenantId,
104
+ organizationId: auth.orgId ?? null,
105
+ ownerUserId: auth.sub,
106
+ participantUserId: parseResult.data.userId
107
+ },
108
+ { persistent: false }
109
+ );
110
+ } catch {
111
+ }
112
+ return new NextResponse(null, { status: 204 });
113
+ } catch (err) {
114
+ if (err instanceof AiChatParticipantNotFoundError) {
115
+ return jsonError(404, err.message || "Participant not found or already revoked.", "participant_not_found");
116
+ }
117
+ if (err instanceof AiChatConversationAccessError) {
118
+ return jsonError(403, err.message || "Access denied.", "forbidden");
119
+ }
120
+ return jsonError(500, "Internal server error.", "internal_error");
121
+ }
122
+ }
123
+ export {
124
+ DELETE,
125
+ metadata,
126
+ openApi
127
+ };
128
+ //# sourceMappingURL=route.js.map