@open-mercato/ai-assistant 0.7.0 → 0.7.1-develop.7103.1.41ff100d93

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 (41) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +1 -1
  3. package/dist/modules/ai_assistant/ai-tools/search-pack.js +93 -3
  4. package/dist/modules/ai_assistant/ai-tools/search-pack.js.map +3 -3
  5. package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.js +2 -0
  6. package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.js.map +2 -2
  7. package/dist/modules/ai_assistant/lib/codemode-tools.js +14 -6
  8. package/dist/modules/ai_assistant/lib/codemode-tools.js.map +2 -2
  9. package/dist/modules/ai_assistant/lib/generated-registry-loader.js +10 -2
  10. package/dist/modules/ai_assistant/lib/generated-registry-loader.js.map +2 -2
  11. package/dist/modules/ai_assistant/lib/http-server.js +3 -1
  12. package/dist/modules/ai_assistant/lib/http-server.js.map +2 -2
  13. package/dist/modules/ai_assistant/lib/in-process-client.js +3 -1
  14. package/dist/modules/ai_assistant/lib/in-process-client.js.map +2 -2
  15. package/dist/modules/ai_assistant/lib/mcp-client.js +2 -1
  16. package/dist/modules/ai_assistant/lib/mcp-client.js.map +2 -2
  17. package/dist/modules/ai_assistant/lib/mcp-dev-server.js +3 -1
  18. package/dist/modules/ai_assistant/lib/mcp-dev-server.js.map +2 -2
  19. package/dist/modules/ai_assistant/lib/mcp-server.js +3 -1
  20. package/dist/modules/ai_assistant/lib/mcp-server.js.map +2 -2
  21. package/dist/modules/ai_assistant/lib/mcp-tool-annotations.js +18 -0
  22. package/dist/modules/ai_assistant/lib/mcp-tool-annotations.js.map +7 -0
  23. package/package.json +8 -7
  24. package/src/modules/ai_assistant/__tests__/integration/ws-c-tool-pack-coverage.test.ts +5 -0
  25. package/src/modules/ai_assistant/ai-tools/__tests__/search-pack.test.ts +211 -5
  26. package/src/modules/ai_assistant/ai-tools/search-pack.ts +110 -4
  27. package/src/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.tsx +3 -0
  28. package/src/modules/ai_assistant/lib/__tests__/codemode-tool-annotations.test.ts +58 -0
  29. package/src/modules/ai_assistant/lib/__tests__/generated-registry-loader.test.ts +16 -0
  30. package/src/modules/ai_assistant/lib/__tests__/mcp-client.test.ts +30 -0
  31. package/src/modules/ai_assistant/lib/__tests__/mcp-server-tool-annotations.test.ts +120 -0
  32. package/src/modules/ai_assistant/lib/__tests__/mcp-tool-annotations.test.ts +57 -0
  33. package/src/modules/ai_assistant/lib/codemode-tools.ts +21 -7
  34. package/src/modules/ai_assistant/lib/generated-registry-loader.ts +10 -2
  35. package/src/modules/ai_assistant/lib/http-server.ts +2 -0
  36. package/src/modules/ai_assistant/lib/in-process-client.ts +2 -0
  37. package/src/modules/ai_assistant/lib/mcp-client.ts +1 -0
  38. package/src/modules/ai_assistant/lib/mcp-dev-server.ts +2 -0
  39. package/src/modules/ai_assistant/lib/mcp-server.ts +2 -0
  40. package/src/modules/ai_assistant/lib/mcp-tool-annotations.ts +35 -0
  41. package/src/modules/ai_assistant/lib/types.ts +11 -0
@@ -1,2 +1,2 @@
1
- [build:ai-assistant] found 232 entry points
1
+ [build:ai-assistant] found 233 entry points
2
2
  [build:ai-assistant] built successfully
package/AGENTS.md CHANGED
@@ -166,7 +166,7 @@ const listPeopleTool = defineAiTool({
166
166
  MUST rules:
167
167
  - MUST set `requiredFeatures` for any tool that reads or writes tenant data. The wildcard-aware ACL matcher is applied before the handler runs.
168
168
  - MUST use Zod for `inputSchema` — never raw JSON Schema.
169
- - MUST set `isMutation: true` on write tools. The policy gate strips these from read-only agents and from read-only tenant overrides.
169
+ - MUST set `isMutation: true` on write tools. It gates read-only agents/overrides and drives the MCP `readOnlyHint` in `tools/list`.
170
170
  - MUST route every mutation tool through `prepareMutation(...)` (see the Mutation Approvals guide at `/framework/ai-assistant/mutation-approvals`). Writing directly inside the handler bypasses the approval gate — the runtime fails closed and refuses to return a result to the operator.
171
171
  - Mutation preview resolvers SHOULD return a normalized `after` snapshot and display hints when tool inputs contain dictionary IDs or other opaque values. Use `display.fieldLabels`, `display.before`, and `display.after` so `field-diff-card` shows operator-friendly names while `field`, `before`, and `after` keep the raw execution values.
172
172
  - MUST expose tools to an agent by listing the tool name in the agent's `allowedTools`. Tools not on the whitelist never reach the model.
@@ -1,5 +1,49 @@
1
1
  import { z } from "zod";
2
+ import {
3
+ canReadSearchEntity,
4
+ filterSearchResultsByEntityAccess,
5
+ resolveReadableEntityTypes
6
+ } from "@open-mercato/shared/lib/search/entityAccess";
2
7
  import { defineAiTool } from "../lib/ai-tool-definition.js";
8
+ class SearchToolAuthorizationError extends Error {
9
+ constructor(message) {
10
+ super(message);
11
+ this.name = "SearchToolAuthorizationError";
12
+ }
13
+ }
14
+ function resolveSearchIndexer(ctx) {
15
+ try {
16
+ const indexer = ctx.container.resolve("searchIndexer");
17
+ if (indexer && typeof indexer.getEntityConfig === "function" && typeof indexer.getAllEntityConfigs === "function") {
18
+ return indexer;
19
+ }
20
+ } catch {
21
+ }
22
+ throw new SearchToolAuthorizationError("[internal] Search entity registry unavailable");
23
+ }
24
+ function authorizeEntityAccess(entityType, lookup, subject) {
25
+ if (subject.isSuperAdmin) return;
26
+ let denial;
27
+ const allowed = canReadSearchEntity(entityType, lookup, subject, {
28
+ onDeny: (_, reason) => {
29
+ denial = reason;
30
+ }
31
+ });
32
+ if (allowed) return;
33
+ if (denial === "unconfigured") {
34
+ throw new SearchToolAuthorizationError(`[internal] Entity type "${entityType}" is not configured for search`);
35
+ }
36
+ if (denial === "no-acl-features") {
37
+ throw new SearchToolAuthorizationError(
38
+ `[internal] Entity type "${entityType}" does not declare aclFeatures; access denied`
39
+ );
40
+ }
41
+ const config = lookup.getEntityConfig(entityType);
42
+ const required = config?.aclFeatures ?? [];
43
+ throw new SearchToolAuthorizationError(
44
+ `[internal] Insufficient permissions for entity "${entityType}". Required: ${required.join(", ")}`
45
+ );
46
+ }
3
47
  const hybridSearchInput = z.object({
4
48
  q: z.string().min(1).describe("Search query text."),
5
49
  limit: z.number().int().min(1).max(100).optional().describe("Maximum results (default 20, max 100)."),
@@ -21,13 +65,49 @@ const hybridSearchTool = defineAiTool({
21
65
  const service = ctx.container.resolve("searchService");
22
66
  const limit = input.limit ?? 20;
23
67
  const started = Date.now();
24
- const results = await service.search(input.q, {
68
+ const subject = {
69
+ grantedFeatures: ctx.userFeatures,
70
+ isSuperAdmin: ctx.isSuperAdmin
71
+ };
72
+ if (ctx.isSuperAdmin) {
73
+ const results2 = await service.search(input.q, {
74
+ tenantId: ctx.tenantId,
75
+ organizationId: ctx.organizationId,
76
+ limit,
77
+ strategies: input.strategies,
78
+ entityTypes: input.entityTypes
79
+ });
80
+ const timingMs2 = Date.now() - started;
81
+ const strategiesUsed2 = Array.from(
82
+ new Set(results2.map((result) => result.source).filter((id) => typeof id === "string"))
83
+ );
84
+ return {
85
+ query: input.q,
86
+ totalResults: results2.length,
87
+ results: results2,
88
+ strategiesUsed: strategiesUsed2,
89
+ timing: { ms: timingMs2 }
90
+ };
91
+ }
92
+ const lookup = resolveSearchIndexer(ctx);
93
+ const readableEntityTypes = resolveReadableEntityTypes(lookup, subject, input.entityTypes);
94
+ if (readableEntityTypes && readableEntityTypes.length === 0) {
95
+ return {
96
+ query: input.q,
97
+ totalResults: 0,
98
+ results: [],
99
+ strategiesUsed: [],
100
+ timing: { ms: Date.now() - started }
101
+ };
102
+ }
103
+ const rawResults = await service.search(input.q, {
25
104
  tenantId: ctx.tenantId,
26
105
  organizationId: ctx.organizationId,
27
106
  limit,
28
107
  strategies: input.strategies,
29
- entityTypes: input.entityTypes
108
+ entityTypes: readableEntityTypes
30
109
  });
110
+ const results = filterSearchResultsByEntityAccess(rawResults, lookup, subject);
31
111
  const timingMs = Date.now() - started;
32
112
  const strategiesUsed = Array.from(
33
113
  new Set(results.map((result) => result.source).filter((id) => typeof id === "string"))
@@ -57,13 +137,23 @@ const getRecordContextTool = defineAiTool({
57
137
  throw new Error("Tenant context is required for search.get_record_context");
58
138
  }
59
139
  const input = getRecordContextInput.parse(rawInput);
140
+ const subject = {
141
+ grantedFeatures: ctx.userFeatures,
142
+ isSuperAdmin: ctx.isSuperAdmin
143
+ };
144
+ let lookup;
145
+ if (!ctx.isSuperAdmin) {
146
+ lookup = resolveSearchIndexer(ctx);
147
+ authorizeEntityAccess(input.entityId, lookup, subject);
148
+ }
60
149
  const service = ctx.container.resolve("searchService");
61
- const results = await service.search(input.recordId, {
150
+ const rawResults = await service.search(input.recordId, {
62
151
  tenantId: ctx.tenantId,
63
152
  organizationId: ctx.organizationId,
64
153
  limit: 5,
65
154
  entityTypes: [input.entityId]
66
155
  });
156
+ const results = lookup ? filterSearchResultsByEntityAccess(rawResults, lookup, subject) : rawResults;
67
157
  const match = results.find((result) => result.recordId === input.recordId);
68
158
  if (!match) {
69
159
  return {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/ai_assistant/ai-tools/search-pack.ts"],
4
- "sourcesContent": ["/**\n * General-purpose `search.*` tool pack (Phase 1 WS-C, Step 3.8).\n *\n * These tools are discovered by the generator alongside any other module\n * `ai-tools.ts`; they expose the existing `@open-mercato/search` runtime to\n * agents that whitelist them via `allowedTools`.\n */\nimport { z } from 'zod'\nimport type { SearchOptions, SearchResult, SearchStrategyId } from '@open-mercato/shared/modules/search'\nimport { defineAiTool } from '../lib/ai-tool-definition'\nimport type { AiToolDefinition } from '../lib/types'\n\ntype SearchServiceLike = {\n search: (query: string, options: SearchOptions) => Promise<SearchResult[]>\n}\n\nconst hybridSearchInput = z.object({\n q: z.string().min(1).describe('Search query text.'),\n limit: z\n .number()\n .int()\n .min(1)\n .max(100)\n .optional()\n .describe('Maximum results (default 20, max 100).'),\n strategies: z\n .array(z.enum(['fulltext', 'vector', 'tokens']))\n .optional()\n .describe('Subset of strategies to run; defaults to the module defaults.'),\n entityTypes: z\n .array(z.string())\n .optional()\n .describe('Filter to specific entity ids (e.g. \"catalog:product\").'),\n})\n\nconst hybridSearchTool = defineAiTool({\n name: 'search.hybrid_search',\n displayName: 'Hybrid search',\n description:\n 'Run a global fulltext + vector + token search across enabled entities for the current tenant/organization.',\n inputSchema: hybridSearchInput,\n requiredFeatures: ['search.view'],\n tags: ['read', 'search'],\n handler: async (rawInput, ctx) => {\n if (!ctx.tenantId) {\n throw new Error('Tenant context is required for search.hybrid_search')\n }\n const input = hybridSearchInput.parse(rawInput)\n const service = ctx.container.resolve<SearchServiceLike>('searchService')\n const limit = input.limit ?? 20\n const started = Date.now()\n const results = await service.search(input.q, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n limit,\n strategies: input.strategies as SearchStrategyId[] | undefined,\n entityTypes: input.entityTypes,\n })\n const timingMs = Date.now() - started\n const strategiesUsed = Array.from(\n new Set(results.map((result) => result.source).filter((id): id is SearchStrategyId => typeof id === 'string')),\n )\n return {\n query: input.q,\n totalResults: results.length,\n results,\n strategiesUsed,\n timing: { ms: timingMs },\n }\n },\n})\n\nconst getRecordContextInput = z.object({\n entityId: z.string().min(1).describe('Entity identifier (e.g. \"customers:customer_person_profile\").'),\n recordId: z.string().min(1).describe('Record primary key (UUID).'),\n})\n\nconst getRecordContextTool = defineAiTool({\n name: 'search.get_record_context',\n displayName: 'Get record context',\n description:\n 'Resolve presenter, links, and URL for a specific record by re-querying the search index. Returns { found: false } when no hit matches the recordId.',\n inputSchema: getRecordContextInput,\n requiredFeatures: ['search.view'],\n tags: ['read', 'search'],\n handler: async (rawInput, ctx) => {\n if (!ctx.tenantId) {\n throw new Error('Tenant context is required for search.get_record_context')\n }\n const input = getRecordContextInput.parse(rawInput)\n const service = ctx.container.resolve<SearchServiceLike>('searchService')\n const results = await service.search(input.recordId, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n limit: 5,\n entityTypes: [input.entityId],\n })\n const match = results.find((result) => result.recordId === input.recordId)\n if (!match) {\n return {\n found: false as const,\n entityId: input.entityId,\n recordId: input.recordId,\n }\n }\n return {\n found: true as const,\n entityId: match.entityId,\n recordId: match.recordId,\n presenter: match.presenter,\n url: match.url,\n links: match.links,\n metadata: match.metadata,\n source: match.source,\n score: match.score,\n }\n },\n})\n\nexport const searchAiTools: AiToolDefinition<any, any>[] = [hybridSearchTool, getRecordContextTool]\n\nexport default searchAiTools\n"],
5
- "mappings": "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",
6
- "names": []
4
+ "sourcesContent": ["/**\n * General-purpose `search.*` tool pack (Phase 1 WS-C, Step 3.8).\n *\n * These tools are discovered by the generator alongside any other module\n * `ai-tools.ts`; they expose the existing `@open-mercato/search` runtime to\n * agents that whitelist them via `allowedTools`.\n */\nimport { z } from 'zod'\nimport type { SearchOptions, SearchResult, SearchStrategyId } from '@open-mercato/shared/modules/search'\nimport {\n canReadSearchEntity,\n filterSearchResultsByEntityAccess,\n resolveReadableEntityTypes,\n type SearchEntityAccessSubject,\n type SearchEntityConfigLookup,\n type SearchEntityDenyReason,\n} from '@open-mercato/shared/lib/search/entityAccess'\nimport { defineAiTool } from '../lib/ai-tool-definition'\nimport type { AiToolDefinition, McpToolContext } from '../lib/types'\n\ntype SearchServiceLike = {\n search: (query: string, options: SearchOptions) => Promise<SearchResult[]>\n}\n\nclass SearchToolAuthorizationError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'SearchToolAuthorizationError'\n }\n}\n\nfunction resolveSearchIndexer(ctx: McpToolContext): SearchEntityConfigLookup {\n try {\n const indexer = ctx.container.resolve('searchIndexer') as SearchEntityConfigLookup | undefined\n if (indexer && typeof indexer.getEntityConfig === 'function' && typeof indexer.getAllEntityConfigs === 'function') {\n return indexer\n }\n } catch {\n // fall through to throw\n }\n throw new SearchToolAuthorizationError('[internal] Search entity registry unavailable')\n}\n\nfunction authorizeEntityAccess(\n entityType: string,\n lookup: SearchEntityConfigLookup,\n subject: SearchEntityAccessSubject,\n): void {\n if (subject.isSuperAdmin) return\n let denial: SearchEntityDenyReason | undefined\n const allowed = canReadSearchEntity(entityType, lookup, subject, {\n onDeny: (_, reason) => {\n denial = reason\n },\n })\n if (allowed) return\n if (denial === 'unconfigured') {\n throw new SearchToolAuthorizationError(`[internal] Entity type \"${entityType}\" is not configured for search`)\n }\n if (denial === 'no-acl-features') {\n throw new SearchToolAuthorizationError(\n `[internal] Entity type \"${entityType}\" does not declare aclFeatures; access denied`,\n )\n }\n const config = lookup.getEntityConfig(entityType)\n const required = config?.aclFeatures ?? []\n throw new SearchToolAuthorizationError(\n `[internal] Insufficient permissions for entity \"${entityType}\". Required: ${required.join(', ')}`,\n )\n}\n\nconst hybridSearchInput = z.object({\n q: z.string().min(1).describe('Search query text.'),\n limit: z\n .number()\n .int()\n .min(1)\n .max(100)\n .optional()\n .describe('Maximum results (default 20, max 100).'),\n strategies: z\n .array(z.enum(['fulltext', 'vector', 'tokens']))\n .optional()\n .describe('Subset of strategies to run; defaults to the module defaults.'),\n entityTypes: z\n .array(z.string())\n .optional()\n .describe('Filter to specific entity ids (e.g. \"catalog:product\").'),\n})\n\nconst hybridSearchTool = defineAiTool({\n name: 'search.hybrid_search',\n displayName: 'Hybrid search',\n description:\n 'Run a global fulltext + vector + token search across enabled entities for the current tenant/organization.',\n inputSchema: hybridSearchInput,\n requiredFeatures: ['search.view'],\n tags: ['read', 'search'],\n handler: async (rawInput, ctx) => {\n if (!ctx.tenantId) {\n throw new Error('Tenant context is required for search.hybrid_search')\n }\n const input = hybridSearchInput.parse(rawInput)\n const service = ctx.container.resolve<SearchServiceLike>('searchService')\n const limit = input.limit ?? 20\n const started = Date.now()\n const subject: SearchEntityAccessSubject = {\n grantedFeatures: ctx.userFeatures,\n isSuperAdmin: ctx.isSuperAdmin,\n }\n\n if (ctx.isSuperAdmin) {\n const results = await service.search(input.q, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n limit,\n strategies: input.strategies as SearchStrategyId[] | undefined,\n entityTypes: input.entityTypes,\n })\n const timingMs = Date.now() - started\n const strategiesUsed = Array.from(\n new Set(results.map((result) => result.source).filter((id): id is SearchStrategyId => typeof id === 'string')),\n )\n return {\n query: input.q,\n totalResults: results.length,\n results,\n strategiesUsed,\n timing: { ms: timingMs },\n }\n }\n\n const lookup = resolveSearchIndexer(ctx)\n const readableEntityTypes = resolveReadableEntityTypes(lookup, subject, input.entityTypes)\n if (readableEntityTypes && readableEntityTypes.length === 0) {\n return {\n query: input.q,\n totalResults: 0,\n results: [],\n strategiesUsed: [],\n timing: { ms: Date.now() - started },\n }\n }\n\n const rawResults = await service.search(input.q, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n limit,\n strategies: input.strategies as SearchStrategyId[] | undefined,\n entityTypes: readableEntityTypes,\n })\n const results = filterSearchResultsByEntityAccess(rawResults, lookup, subject)\n const timingMs = Date.now() - started\n const strategiesUsed = Array.from(\n new Set(results.map((result) => result.source).filter((id): id is SearchStrategyId => typeof id === 'string')),\n )\n return {\n query: input.q,\n totalResults: results.length,\n results,\n strategiesUsed,\n timing: { ms: timingMs },\n }\n },\n})\n\nconst getRecordContextInput = z.object({\n entityId: z.string().min(1).describe('Entity identifier (e.g. \"customers:customer_person_profile\").'),\n recordId: z.string().min(1).describe('Record primary key (UUID).'),\n})\n\nconst getRecordContextTool = defineAiTool({\n name: 'search.get_record_context',\n displayName: 'Get record context',\n description:\n 'Resolve presenter, links, and URL for a specific record by re-querying the search index. Returns { found: false } when no hit matches the recordId.',\n inputSchema: getRecordContextInput,\n requiredFeatures: ['search.view'],\n tags: ['read', 'search'],\n handler: async (rawInput, ctx) => {\n if (!ctx.tenantId) {\n throw new Error('Tenant context is required for search.get_record_context')\n }\n const input = getRecordContextInput.parse(rawInput)\n const subject: SearchEntityAccessSubject = {\n grantedFeatures: ctx.userFeatures,\n isSuperAdmin: ctx.isSuperAdmin,\n }\n\n let lookup: SearchEntityConfigLookup | undefined\n if (!ctx.isSuperAdmin) {\n lookup = resolveSearchIndexer(ctx)\n authorizeEntityAccess(input.entityId, lookup, subject)\n }\n\n const service = ctx.container.resolve<SearchServiceLike>('searchService')\n const rawResults = await service.search(input.recordId, {\n tenantId: ctx.tenantId,\n organizationId: ctx.organizationId,\n limit: 5,\n entityTypes: [input.entityId],\n })\n const results = lookup ? filterSearchResultsByEntityAccess(rawResults, lookup, subject) : rawResults\n const match = results.find((result) => result.recordId === input.recordId)\n if (!match) {\n return {\n found: false as const,\n entityId: input.entityId,\n recordId: input.recordId,\n }\n }\n return {\n found: true as const,\n entityId: match.entityId,\n recordId: match.recordId,\n presenter: match.presenter,\n url: match.url,\n links: match.links,\n metadata: match.metadata,\n source: match.source,\n score: match.score,\n }\n },\n})\n\nexport const searchAiTools: AiToolDefinition<any, any>[] = [hybridSearchTool, getRecordContextTool]\n\nexport default searchAiTools\n"],
5
+ "mappings": "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",
6
+ "names": ["results", "timingMs", "strategiesUsed"]
7
7
  }
@@ -77,6 +77,7 @@ function AiModerationFlagsPageClient() {
77
77
  [t]
78
78
  );
79
79
  const total = query.data?.total ?? 0;
80
+ const totalIsCapped = query.data?.totalIsCapped === true;
80
81
  return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4", children: [
81
82
  /* @__PURE__ */ jsxs("div", { children: [
82
83
  /* @__PURE__ */ jsx("h2", { className: "text-base font-semibold", children: t("ai_assistant.moderationFlags.title", "Moderation flags") }),
@@ -125,6 +126,7 @@ function AiModerationFlagsPageClient() {
125
126
  pageSize: PAGE_SIZE,
126
127
  total,
127
128
  totalPages: Math.max(1, Math.ceil(total / PAGE_SIZE)),
129
+ totalIsCapped,
128
130
  onPageChange: setPage
129
131
  }
130
132
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../../../src/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useQuery } from '@tanstack/react-query'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { StatusBadge } from '@open-mercato/ui/primitives/status-badge'\nimport { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\n\nconst PAGE_SIZE = 50\n\ntype ModerationCategory = { flagged: boolean; score: number }\n\ntype ModerationFlagRow = {\n id: string\n tenantId: string\n organizationId: string | null\n agentId: string\n userId: string\n providerId: string\n modelId: string\n categories: Record<string, ModerationCategory>\n createdAt: string\n}\n\ntype ModerationFlagsResponse = {\n items: ModerationFlagRow[]\n total: number\n page: number\n pageSize: number\n}\n\nasync function fetchModerationFlags(params: {\n page: number\n from: string\n to: string\n agentId: string\n userId: string\n}): Promise<ModerationFlagsResponse> {\n const search = new URLSearchParams()\n search.set('page', String(params.page))\n search.set('pageSize', String(PAGE_SIZE))\n if (params.from) search.set('from', params.from)\n if (params.to) search.set('to', params.to)\n if (params.agentId) search.set('agentId', params.agentId)\n if (params.userId) search.set('userId', params.userId)\n const { result, status } = await apiCallOrThrow<ModerationFlagsResponse>(\n `/api/ai_assistant/moderation-flags?${search.toString()}`,\n undefined,\n { errorMessage: 'Failed to load moderation flags' },\n )\n if (!result) throw new Error(`Failed to load moderation flags (${status})`)\n return result\n}\n\nfunction flaggedCategoryNames(categories: Record<string, ModerationCategory>): string[] {\n return Object.entries(categories)\n .filter(([, value]) => value.flagged)\n .map(([name]) => name)\n}\n\nexport function AiModerationFlagsPageClient() {\n const t = useT()\n const [page, setPage] = React.useState(1)\n const [from, setFrom] = React.useState('')\n const [to, setTo] = React.useState('')\n const [agentId, setAgentId] = React.useState('')\n const [userId, setUserId] = React.useState('')\n const [applied, setApplied] = React.useState({ from: '', to: '', agentId: '', userId: '' })\n\n const query = useQuery<ModerationFlagsResponse>({\n queryKey: ['ai_assistant', 'moderation_flags', page, applied],\n queryFn: () => fetchModerationFlags({ page, ...applied }),\n retry: false,\n })\n\n const applyFilters = React.useCallback(() => {\n setPage(1)\n setApplied({ from, to, agentId, userId })\n }, [from, to, agentId, userId])\n\n const columns = React.useMemo<ColumnDef<ModerationFlagRow, unknown>[]>(\n () => [\n {\n accessorKey: 'agentId',\n header: t('ai_assistant.moderationFlags.columns.agent', 'Agent'),\n },\n {\n accessorKey: 'userId',\n header: t('ai_assistant.moderationFlags.columns.user', 'User'),\n meta: { truncate: true, maxWidth: 220 },\n },\n {\n accessorKey: 'categories',\n header: t('ai_assistant.moderationFlags.columns.categories', 'Categories'),\n cell: ({ row }) => {\n const names = flaggedCategoryNames(row.original.categories)\n if (names.length === 0) {\n return <span className=\"text-muted-foreground\">\u2014</span>\n }\n return (\n <div className=\"flex flex-wrap gap-1\">\n {names.map((name) => (\n <StatusBadge key={name} variant=\"error\" dot>\n {name}\n </StatusBadge>\n ))}\n </div>\n )\n },\n },\n {\n accessorKey: 'createdAt',\n header: t('ai_assistant.moderationFlags.columns.createdAt', 'Flagged at'),\n cell: ({ row }) => new Date(row.original.createdAt).toLocaleString(),\n },\n ],\n [t],\n )\n\n const total = query.data?.total ?? 0\n\n return (\n <div className=\"flex flex-col gap-4\">\n <div>\n <h2 className=\"text-base font-semibold\">\n {t('ai_assistant.moderationFlags.title', 'Moderation flags')}\n </h2>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'ai_assistant.moderationFlags.subtitle',\n 'Inputs blocked by the content safety filter. Category flags and scores only \u2014 no prompt content is stored.',\n )}\n </p>\n </div>\n\n <div className=\"flex flex-wrap items-end gap-3\">\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"moderation-from\">{t('ai_assistant.moderationFlags.filters.from', 'From')}</Label>\n <Input id=\"moderation-from\" type=\"date\" value={from} onChange={(e) => setFrom(e.target.value)} />\n </div>\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"moderation-to\">{t('ai_assistant.moderationFlags.filters.to', 'To')}</Label>\n <Input id=\"moderation-to\" type=\"date\" value={to} onChange={(e) => setTo(e.target.value)} />\n </div>\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"moderation-agent\">{t('ai_assistant.moderationFlags.columns.agent', 'Agent')}</Label>\n <Input\n id=\"moderation-agent\"\n value={agentId}\n onChange={(e) => setAgentId(e.target.value)}\n placeholder={t('ai_assistant.moderationFlags.filters.agentPlaceholder', 'module.agent')}\n />\n </div>\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"moderation-user\">{t('ai_assistant.moderationFlags.columns.user', 'User')}</Label>\n <Input id=\"moderation-user\" value={userId} onChange={(e) => setUserId(e.target.value)} />\n </div>\n <Button type=\"button\" variant=\"outline\" onClick={applyFilters}>\n {t('ai_assistant.moderationFlags.filters.apply', 'Apply')}\n </Button>\n </div>\n\n <DataTable<ModerationFlagRow>\n columns={columns}\n data={query.data?.items ?? []}\n isLoading={query.isLoading}\n error={query.error ? (query.error as Error).message : undefined}\n emptyState={t('ai_assistant.moderationFlags.empty', 'No flagged messages.')}\n pagination={{\n page,\n pageSize: PAGE_SIZE,\n total,\n totalPages: Math.max(1, Math.ceil(total / PAGE_SIZE)),\n onPageChange: setPage,\n }}\n />\n </div>\n )\n}\n\nexport default AiModerationFlagsPageClient\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useQuery } from '@tanstack/react-query'\nimport type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport { DataTable } from '@open-mercato/ui/backend/DataTable'\nimport { Button } from '@open-mercato/ui/primitives/button'\nimport { Input } from '@open-mercato/ui/primitives/input'\nimport { Label } from '@open-mercato/ui/primitives/label'\nimport { StatusBadge } from '@open-mercato/ui/primitives/status-badge'\nimport { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\n\nconst PAGE_SIZE = 50\n\ntype ModerationCategory = { flagged: boolean; score: number }\n\ntype ModerationFlagRow = {\n id: string\n tenantId: string\n organizationId: string | null\n agentId: string\n userId: string\n providerId: string\n modelId: string\n categories: Record<string, ModerationCategory>\n createdAt: string\n}\n\ntype ModerationFlagsResponse = {\n items: ModerationFlagRow[]\n total: number\n page: number\n pageSize: number\n totalIsCapped?: boolean\n}\n\nasync function fetchModerationFlags(params: {\n page: number\n from: string\n to: string\n agentId: string\n userId: string\n}): Promise<ModerationFlagsResponse> {\n const search = new URLSearchParams()\n search.set('page', String(params.page))\n search.set('pageSize', String(PAGE_SIZE))\n if (params.from) search.set('from', params.from)\n if (params.to) search.set('to', params.to)\n if (params.agentId) search.set('agentId', params.agentId)\n if (params.userId) search.set('userId', params.userId)\n const { result, status } = await apiCallOrThrow<ModerationFlagsResponse>(\n `/api/ai_assistant/moderation-flags?${search.toString()}`,\n undefined,\n { errorMessage: 'Failed to load moderation flags' },\n )\n if (!result) throw new Error(`Failed to load moderation flags (${status})`)\n return result\n}\n\nfunction flaggedCategoryNames(categories: Record<string, ModerationCategory>): string[] {\n return Object.entries(categories)\n .filter(([, value]) => value.flagged)\n .map(([name]) => name)\n}\n\nexport function AiModerationFlagsPageClient() {\n const t = useT()\n const [page, setPage] = React.useState(1)\n const [from, setFrom] = React.useState('')\n const [to, setTo] = React.useState('')\n const [agentId, setAgentId] = React.useState('')\n const [userId, setUserId] = React.useState('')\n const [applied, setApplied] = React.useState({ from: '', to: '', agentId: '', userId: '' })\n\n const query = useQuery<ModerationFlagsResponse>({\n queryKey: ['ai_assistant', 'moderation_flags', page, applied],\n queryFn: () => fetchModerationFlags({ page, ...applied }),\n retry: false,\n })\n\n const applyFilters = React.useCallback(() => {\n setPage(1)\n setApplied({ from, to, agentId, userId })\n }, [from, to, agentId, userId])\n\n const columns = React.useMemo<ColumnDef<ModerationFlagRow, unknown>[]>(\n () => [\n {\n accessorKey: 'agentId',\n header: t('ai_assistant.moderationFlags.columns.agent', 'Agent'),\n },\n {\n accessorKey: 'userId',\n header: t('ai_assistant.moderationFlags.columns.user', 'User'),\n meta: { truncate: true, maxWidth: 220 },\n },\n {\n accessorKey: 'categories',\n header: t('ai_assistant.moderationFlags.columns.categories', 'Categories'),\n cell: ({ row }) => {\n const names = flaggedCategoryNames(row.original.categories)\n if (names.length === 0) {\n return <span className=\"text-muted-foreground\">\u2014</span>\n }\n return (\n <div className=\"flex flex-wrap gap-1\">\n {names.map((name) => (\n <StatusBadge key={name} variant=\"error\" dot>\n {name}\n </StatusBadge>\n ))}\n </div>\n )\n },\n },\n {\n accessorKey: 'createdAt',\n header: t('ai_assistant.moderationFlags.columns.createdAt', 'Flagged at'),\n cell: ({ row }) => new Date(row.original.createdAt).toLocaleString(),\n },\n ],\n [t],\n )\n\n const total = query.data?.total ?? 0\n const totalIsCapped = query.data?.totalIsCapped === true\n\n return (\n <div className=\"flex flex-col gap-4\">\n <div>\n <h2 className=\"text-base font-semibold\">\n {t('ai_assistant.moderationFlags.title', 'Moderation flags')}\n </h2>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'ai_assistant.moderationFlags.subtitle',\n 'Inputs blocked by the content safety filter. Category flags and scores only \u2014 no prompt content is stored.',\n )}\n </p>\n </div>\n\n <div className=\"flex flex-wrap items-end gap-3\">\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"moderation-from\">{t('ai_assistant.moderationFlags.filters.from', 'From')}</Label>\n <Input id=\"moderation-from\" type=\"date\" value={from} onChange={(e) => setFrom(e.target.value)} />\n </div>\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"moderation-to\">{t('ai_assistant.moderationFlags.filters.to', 'To')}</Label>\n <Input id=\"moderation-to\" type=\"date\" value={to} onChange={(e) => setTo(e.target.value)} />\n </div>\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"moderation-agent\">{t('ai_assistant.moderationFlags.columns.agent', 'Agent')}</Label>\n <Input\n id=\"moderation-agent\"\n value={agentId}\n onChange={(e) => setAgentId(e.target.value)}\n placeholder={t('ai_assistant.moderationFlags.filters.agentPlaceholder', 'module.agent')}\n />\n </div>\n <div className=\"flex flex-col gap-1\">\n <Label htmlFor=\"moderation-user\">{t('ai_assistant.moderationFlags.columns.user', 'User')}</Label>\n <Input id=\"moderation-user\" value={userId} onChange={(e) => setUserId(e.target.value)} />\n </div>\n <Button type=\"button\" variant=\"outline\" onClick={applyFilters}>\n {t('ai_assistant.moderationFlags.filters.apply', 'Apply')}\n </Button>\n </div>\n\n <DataTable<ModerationFlagRow>\n columns={columns}\n data={query.data?.items ?? []}\n isLoading={query.isLoading}\n error={query.error ? (query.error as Error).message : undefined}\n emptyState={t('ai_assistant.moderationFlags.empty', 'No flagged messages.')}\n pagination={{\n page,\n pageSize: PAGE_SIZE,\n total,\n totalPages: Math.max(1, Math.ceil(total / PAGE_SIZE)),\n totalIsCapped,\n onPageChange: setPage,\n }}\n />\n </div>\n )\n}\n\nexport default AiModerationFlagsPageClient\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -378,6 +378,9 @@ function buildEntitySchemas(graph) {
378
378
  }
379
379
  const CODE_MODE_MAX_API_CALLS = 50;
380
380
  const CODE_MODE_MAX_MUTATION_CALLS = 20;
381
+ function registerCodeModeTool(tool) {
382
+ registerMcpTool(tool, { moduleId: "codemode" });
383
+ }
381
384
  async function loadCodeModeTools() {
382
385
  const commonTypes = await generateCommonTypes();
383
386
  registerSearchTool();
@@ -385,9 +388,10 @@ async function loadCodeModeTools() {
385
388
  return 2;
386
389
  }
387
390
  function registerSearchTool() {
388
- registerMcpTool(
391
+ registerCodeModeTool(
389
392
  {
390
393
  name: "search",
394
+ isMutation: false,
391
395
  description: `Query the OpenAPI spec and entity schemas. READ-ONLY, no side effects.
392
396
  Globals: spec.findEndpoints(keyword), spec.describeEndpoint(path, method), spec.describeEntity(keyword), spec.paths, spec.entitySchemas.
393
397
  Use BEFORE execute to learn endpoint schemas for CREATE/UPDATE. Skip for common paths (companies, people, orders, quotes, products).`,
@@ -447,17 +451,22 @@ Use BEFORE execute to learn endpoint schemas for CREATE/UPDATE. Skip for common
447
451
  _memoryContext: memoryContext
448
452
  };
449
453
  }
450
- },
451
- { moduleId: "codemode" }
454
+ }
452
455
  );
453
456
  }
454
457
  function registerExecuteTool(commonTypes) {
455
458
  const typesBlock = commonTypes ? `
456
459
 
457
460
  ${commonTypes}` : "";
458
- registerMcpTool(
461
+ registerCodeModeTool(
459
462
  {
460
463
  name: "execute",
464
+ // api.request() reaches every documented endpoint, including POST/PUT/DELETE,
465
+ // so the tool is neither read-only nor guaranteed non-destructive. It is
466
+ // intentionally exempt from prepareMutation: arbitrary sandbox code cannot
467
+ // provide the structured before/after preview that approval flow requires.
468
+ isMutation: true,
469
+ isDestructive: true,
461
470
  description: `Make API calls. Returns JSON.
462
471
  Globals: api.request({ method, path, query?, body? }) \u2192 { success, statusCode, data }, context { tenantId, organizationId, userId }.
463
472
  RULES: For FIND/LIST \u2192 GET only (1 call). For UPDATE \u2192 PUT to collection path with id in BODY. NEVER PUT/POST/DELETE unless user explicitly asked to change data. Before ANY write operation (POST/PUT/DELETE), you MUST use the AskUserQuestion tool to get explicit user confirmation. Do NOT just ask in text \u2014 use the tool so execution pauses until the user responds.${typesBlock}`,
@@ -526,8 +535,7 @@ RULES: For FIND/LIST \u2192 GET only (1 call). For UPDATE \u2192 PUT to collecti
526
535
  _memoryContext: memoryContext
527
536
  };
528
537
  }
529
- },
530
- { moduleId: "codemode" }
538
+ }
531
539
  );
532
540
  }
533
541
  function createApiRequestFn(ctx, onCall) {