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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +29 -12
  3. package/dist/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.js +0 -30
  4. package/dist/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.js.map +2 -2
  5. package/dist/modules/ai_assistant/api/ai/chat/route.js +0 -7
  6. package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
  7. package/dist/modules/ai_assistant/api/settings/allowlist/route.js +10 -10
  8. package/dist/modules/ai_assistant/api/settings/allowlist/route.js.map +2 -2
  9. package/dist/modules/ai_assistant/api/settings/route.js +13 -34
  10. package/dist/modules/ai_assistant/api/settings/route.js.map +2 -2
  11. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js +0 -106
  12. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js.map +2 -2
  13. package/dist/modules/ai_assistant/data/entities.js +0 -42
  14. package/dist/modules/ai_assistant/data/entities.js.map +2 -2
  15. package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js +1 -3
  16. package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js.map +2 -2
  17. package/dist/modules/ai_assistant/di.js +2 -6
  18. package/dist/modules/ai_assistant/di.js.map +2 -2
  19. package/dist/modules/ai_assistant/events.js +0 -6
  20. package/dist/modules/ai_assistant/events.js.map +2 -2
  21. package/dist/modules/ai_assistant/i18n/de.json +0 -42
  22. package/dist/modules/ai_assistant/i18n/en.json +0 -42
  23. package/dist/modules/ai_assistant/i18n/es.json +0 -42
  24. package/dist/modules/ai_assistant/i18n/pl.json +0 -42
  25. package/dist/modules/ai_assistant/lib/agent-runtime.js +5 -158
  26. package/dist/modules/ai_assistant/lib/agent-runtime.js.map +2 -2
  27. package/dist/modules/ai_assistant/lib/ai-agent-definition.js.map +2 -2
  28. package/dist/modules/ai_assistant/lib/auth.js +7 -6
  29. package/dist/modules/ai_assistant/lib/auth.js.map +2 -2
  30. package/dist/modules/ai_assistant/lib/entity-graph.js +3 -3
  31. package/dist/modules/ai_assistant/lib/entity-graph.js.map +2 -2
  32. package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js +0 -3
  33. package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js.map +2 -2
  34. package/dist/modules/ai_assistant/lib/llm-adapters/openai.js +1 -8
  35. package/dist/modules/ai_assistant/lib/llm-adapters/openai.js.map +2 -2
  36. package/dist/modules/ai_assistant/lib/model-factory.js +1 -6
  37. package/dist/modules/ai_assistant/lib/model-factory.js.map +2 -2
  38. package/dist/modules/ai_assistant/lib/openai-compatible-presets.js +0 -2
  39. package/dist/modules/ai_assistant/lib/openai-compatible-presets.js.map +2 -2
  40. package/dist/modules/ai_assistant/lib/tool-search.js +12 -4
  41. package/dist/modules/ai_assistant/lib/tool-search.js.map +2 -2
  42. package/generated/entities/ai_agent_runtime_override/index.ts +0 -1
  43. package/generated/entities.ids.generated.ts +1 -2
  44. package/generated/entity-fields-registry.ts +1 -13
  45. package/package.json +12 -13
  46. package/src/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.ts +0 -48
  47. package/src/modules/ai_assistant/api/ai/chat/route.ts +0 -10
  48. package/src/modules/ai_assistant/api/settings/__tests__/route.test.ts +8 -58
  49. package/src/modules/ai_assistant/api/settings/allowlist/route.ts +12 -10
  50. package/src/modules/ai_assistant/api/settings/route.ts +10 -33
  51. package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx +0 -146
  52. package/src/modules/ai_assistant/data/entities.ts +0 -56
  53. package/src/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.ts +0 -4
  54. package/src/modules/ai_assistant/di.ts +1 -5
  55. package/src/modules/ai_assistant/events.ts +0 -21
  56. package/src/modules/ai_assistant/i18n/de.json +0 -42
  57. package/src/modules/ai_assistant/i18n/en.json +0 -42
  58. package/src/modules/ai_assistant/i18n/es.json +0 -42
  59. package/src/modules/ai_assistant/i18n/pl.json +0 -42
  60. package/src/modules/ai_assistant/lib/__tests__/auth.test.ts +5 -2
  61. package/src/modules/ai_assistant/lib/__tests__/llm-adapters-anthropic.test.ts +0 -13
  62. package/src/modules/ai_assistant/lib/__tests__/llm-adapters-openai.test.ts +0 -41
  63. package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +0 -60
  64. package/src/modules/ai_assistant/lib/agent-runtime.ts +0 -259
  65. package/src/modules/ai_assistant/lib/ai-agent-definition.ts +0 -10
  66. package/src/modules/ai_assistant/lib/auth.ts +15 -6
  67. package/src/modules/ai_assistant/lib/entity-graph.ts +3 -3
  68. package/src/modules/ai_assistant/lib/llm-adapters/anthropic.ts +0 -10
  69. package/src/modules/ai_assistant/lib/llm-adapters/openai.ts +1 -32
  70. package/src/modules/ai_assistant/lib/model-factory.ts +0 -33
  71. package/src/modules/ai_assistant/lib/openai-compatible-presets.ts +0 -2
  72. package/src/modules/ai_assistant/lib/tool-search.ts +17 -4
  73. package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +16 -388
  74. package/dist/modules/ai_assistant/__integration__/TC-AI-MODERATION-008-settings-and-audit.spec.js +0 -80
  75. package/dist/modules/ai_assistant/__integration__/TC-AI-MODERATION-008-settings-and-audit.spec.js.map +0 -7
  76. package/dist/modules/ai_assistant/__integration__/TC-AI-MODERATION-009-ui.spec.js +0 -124
  77. package/dist/modules/ai_assistant/__integration__/TC-AI-MODERATION-009-ui.spec.js.map +0 -7
  78. package/dist/modules/ai_assistant/api/moderation-flags/route.js +0 -121
  79. package/dist/modules/ai_assistant/api/moderation-flags/route.js.map +0 -7
  80. package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.js +0 -139
  81. package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.js.map +0 -7
  82. package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.js +0 -10
  83. package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.js.map +0 -7
  84. package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.meta.js +0 -26
  85. package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.meta.js.map +0 -7
  86. package/dist/modules/ai_assistant/data/repositories/AiModerationFlagRepository.js +0 -44
  87. package/dist/modules/ai_assistant/data/repositories/AiModerationFlagRepository.js.map +0 -7
  88. package/dist/modules/ai_assistant/i18n/ko.json +0 -527
  89. package/dist/modules/ai_assistant/lib/moderation-flag-recorder.js +0 -43
  90. package/dist/modules/ai_assistant/lib/moderation-flag-recorder.js.map +0 -7
  91. package/dist/modules/ai_assistant/lib/moderation-policy.js +0 -24
  92. package/dist/modules/ai_assistant/lib/moderation-policy.js.map +0 -7
  93. package/dist/modules/ai_assistant/lib/moderation.js +0 -109
  94. package/dist/modules/ai_assistant/lib/moderation.js.map +0 -7
  95. package/dist/modules/ai_assistant/migrations/Migration20260610134045_ai_assistant.js +0 -19
  96. package/dist/modules/ai_assistant/migrations/Migration20260610134045_ai_assistant.js.map +0 -7
  97. package/generated/entities/ai_moderation_flag/index.ts +0 -9
  98. package/src/modules/ai_assistant/__integration__/TC-AI-MODERATION-008-settings-and-audit.spec.ts +0 -126
  99. package/src/modules/ai_assistant/__integration__/TC-AI-MODERATION-009-ui.spec.ts +0 -161
  100. package/src/modules/ai_assistant/api/moderation-flags/__tests__/route.test.ts +0 -122
  101. package/src/modules/ai_assistant/api/moderation-flags/route.ts +0 -137
  102. package/src/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.tsx +0 -186
  103. package/src/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.meta.ts +0 -24
  104. package/src/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.tsx +0 -12
  105. package/src/modules/ai_assistant/data/repositories/AiModerationFlagRepository.ts +0 -79
  106. package/src/modules/ai_assistant/i18n/ko.json +0 -527
  107. package/src/modules/ai_assistant/lib/__tests__/input-moderation-gate.test.ts +0 -121
  108. package/src/modules/ai_assistant/lib/__tests__/moderation-flag-recorder.test.ts +0 -76
  109. package/src/modules/ai_assistant/lib/__tests__/moderation-policy.test.ts +0 -86
  110. package/src/modules/ai_assistant/lib/__tests__/moderation.test.ts +0 -114
  111. package/src/modules/ai_assistant/lib/moderation-flag-recorder.ts +0 -75
  112. package/src/modules/ai_assistant/lib/moderation-policy.ts +0 -77
  113. package/src/modules/ai_assistant/lib/moderation.ts +0 -179
  114. package/src/modules/ai_assistant/migrations/Migration20260610134045_ai_assistant.ts +0 -21
@@ -27,20 +27,6 @@ import {
27
27
  import type { StopCondition } from 'ai'
28
28
  import type { ZodTypeAny } from 'zod'
29
29
  import { createModelFactory, resolveAllowRuntimeOverride } from './model-factory'
30
- import { computeEndUserIdentifier } from '@open-mercato/shared/lib/ai/safety-identifier'
31
- import { llmProviderRegistry } from '@open-mercato/shared/lib/ai/llm-provider-registry'
32
- import type { EnvLookup } from '@open-mercato/shared/lib/ai/llm-provider'
33
- import {
34
- AiModerationBlockedError,
35
- AiModerationUnavailableError,
36
- type ModerationService,
37
- } from './moderation'
38
- import {
39
- isModerationActive,
40
- resolveModerationPolicy,
41
- shouldFailClosed,
42
- } from './moderation-policy'
43
- import { recordModerationFlag } from './moderation-flag-recorder'
44
30
  import type {
45
31
  AiAgentDefinition,
46
32
  AiAgentLoopConfig,
@@ -356,16 +342,6 @@ export interface PreparedAiSdkOptions {
356
342
  * Phase 5 of spec `2026-04-28-ai-agents-agentic-loop-controls`.
357
343
  */
358
344
  toolLoopAgent?: ToolLoopAgent<never, ToolSet>
359
- /**
360
- * Provider-specific `providerOptions` fragment carrying the mapped end-user
361
- * safety identifier (e.g. OpenAI `safety_identifier`). Present only when an
362
- * identifier was derived and the resolved provider supports the mapping.
363
- * Callers using the `generateText` escape-hatch MUST forward it to the SDK
364
- * call so the identifier reaches the provider.
365
- *
366
- * Spec `2026-06-04-ai-input-moderation-and-safety-identifiers`.
367
- */
368
- providerOptions?: Record<string, unknown>
369
345
  }
370
346
 
371
347
  /**
@@ -992,15 +968,6 @@ interface ResolvedAgentModel {
992
968
  model: LanguageModel
993
969
  modelId: string
994
970
  providerId: string
995
- /**
996
- * Provider-specific `providerOptions` fragment carrying the mapped end-user
997
- * safety identifier (undefined when no identifier or no provider mapping).
998
- */
999
- providerOptions?: Record<string, unknown>
1000
- /** Whether the resolved chat provider supports input moderation. */
1001
- supportsInputModeration: boolean
1002
- /** Resolved base URL (if any), reused by the moderation endpoint call. */
1003
- baseURL?: string
1004
971
  }
1005
972
 
1006
973
  function resolveAgentModel(
@@ -1012,7 +979,6 @@ function resolveAgentModel(
1012
979
  tenantOverride?: { providerId?: string | null; modelId?: string | null; baseURL?: string | null } | null,
1013
980
  requestOverride?: { providerId?: string | null; modelId?: string | null; baseURL?: string | null } | null,
1014
981
  tenantAllowlist?: TenantAllowlistSnapshot | null,
1015
- endUserIdentifier?: string,
1016
982
  ): ResolvedAgentModel {
1017
983
  const effectiveContainer = container ?? createContainer()
1018
984
  const resolution = createModelFactory(effectiveContainer).resolveModel({
@@ -1027,145 +993,11 @@ function resolveAgentModel(
1027
993
  tenantOverride: tenantOverride ?? undefined,
1028
994
  requestOverride: requestOverride ?? undefined,
1029
995
  tenantAllowlist: tenantAllowlist ?? null,
1030
- endUserIdentifier,
1031
996
  })
1032
997
  return {
1033
998
  model: resolution.model as LanguageModel,
1034
999
  modelId: resolution.modelId,
1035
1000
  providerId: resolution.providerId,
1036
- providerOptions: resolution.providerOptions,
1037
- supportsInputModeration: resolution.supportsInputModeration === true,
1038
- baseURL: resolution.baseURL,
1039
- }
1040
- }
1041
-
1042
- /**
1043
- * Concatenates the text parts of the most recent user message. Only the new
1044
- * user turn is moderated (prior history was screened on its own turn). Returns
1045
- * an empty string when there is no user message or no text content.
1046
- */
1047
- export function extractLatestUserText(messages: UIMessage[]): string {
1048
- for (let index = messages.length - 1; index >= 0; index -= 1) {
1049
- const raw = messages[index] as unknown as {
1050
- role?: string
1051
- content?: string
1052
- parts?: Array<{ type?: string; text?: string }>
1053
- }
1054
- if (raw.role !== 'user') continue
1055
- if (Array.isArray(raw.parts)) {
1056
- return raw.parts
1057
- .filter((part) => part?.type === 'text' && typeof part.text === 'string')
1058
- .map((part) => part.text as string)
1059
- .join('\n')
1060
- }
1061
- return typeof raw.content === 'string' ? raw.content : ''
1062
- }
1063
- return ''
1064
- }
1065
-
1066
- export interface InputModerationGateParams {
1067
- untrustedInput?: boolean
1068
- supportsInputModeration: boolean
1069
- userText: string
1070
- service: ModerationService | null
1071
- /**
1072
- * Lazy resolver for the moderation provider's API key. Invoked only when the
1073
- * gate actually proceeds (policy active, provider supports moderation, text
1074
- * non-empty, service present) — so callers never pay for (or fail on) key
1075
- * resolution on the common skip paths.
1076
- */
1077
- resolveApiKey: () => string | null
1078
- baseURL?: string
1079
- moderationModel?: string
1080
- perAgentOverride?: boolean | null
1081
- tenantWideOverride?: boolean | null
1082
- env?: EnvLookup
1083
- /**
1084
- * Best-effort side effect invoked with the flagged categories right before
1085
- * the gate throws {@link AiModerationBlockedError}. Phase 3 wires the audit
1086
- * insert + event emit here; it MUST NOT throw (failures are swallowed by the
1087
- * caller) and MUST NOT block the rejection.
1088
- */
1089
- onFlagged?: (categories: Record<string, { flagged: boolean; score: number }>) => void | Promise<void>
1090
- }
1091
-
1092
- /**
1093
- * Pre-loop input moderation gate. Runs the moderation check for the current
1094
- * user turn before the model call. Decoupled from the registry/container so it
1095
- * is unit-testable: the caller resolves the {@link ModerationService} and API
1096
- * key. Throws {@link AiModerationBlockedError} when flagged; honors
1097
- * fail-closed (enforced) vs fail-open (opt-in) on endpoint unavailability.
1098
- */
1099
- export async function runInputModerationGate(params: InputModerationGateParams): Promise<void> {
1100
- const policy = resolveModerationPolicy({
1101
- untrustedInput: params.untrustedInput,
1102
- perAgentOverride: params.perAgentOverride,
1103
- tenantWideOverride: params.tenantWideOverride,
1104
- env: params.env,
1105
- })
1106
- if (!isModerationActive(policy)) return
1107
- // The resolved chat provider has no moderation endpoint — rely on its own
1108
- // server-side filtering and skip the gate.
1109
- if (!params.supportsInputModeration) return
1110
- const text = params.userText.trim()
1111
- if (!text) return
1112
-
1113
- const failClosed = shouldFailClosed(policy)
1114
- const failOpenOrThrow = (reason: string, error?: unknown): void => {
1115
- if (failClosed) {
1116
- if (error instanceof AiModerationUnavailableError) throw error
1117
- throw new AiModerationUnavailableError(reason, error)
1118
- }
1119
- logger.warn('Input moderation unavailable; failing open for opt-in surface', { reason })
1120
- }
1121
-
1122
- if (!params.service) {
1123
- return failOpenOrThrow('moderation service is not available in the container')
1124
- }
1125
- const apiKey = params.resolveApiKey()
1126
- if (!apiKey) {
1127
- return failOpenOrThrow('no API key available for the moderation provider')
1128
- }
1129
-
1130
- let result
1131
- try {
1132
- result = await params.service.checkInput({
1133
- text,
1134
- apiKey,
1135
- baseURL: params.baseURL,
1136
- model: params.moderationModel,
1137
- })
1138
- } catch (error) {
1139
- if (error instanceof AiModerationUnavailableError) {
1140
- return failOpenOrThrow(error.message, error)
1141
- }
1142
- throw error
1143
- }
1144
-
1145
- if (result.flagged) {
1146
- if (params.onFlagged) {
1147
- try {
1148
- await params.onFlagged(result.categories)
1149
- } catch (error) {
1150
- logger.error('Moderation flag side effect failed (rejection still applies)', { err: error })
1151
- }
1152
- }
1153
- throw new AiModerationBlockedError(result.categories)
1154
- }
1155
- }
1156
-
1157
- /**
1158
- * Compute the tenant-salted end-user safety identifier for a chat turn,
1159
- * best-effort. Returns undefined (never throws) so a derivation failure — e.g.
1160
- * a missing base secret in a misconfigured environment — degrades to "no
1161
- * identifier attached" instead of breaking the chat. Failures are logged.
1162
- */
1163
- function resolveEndUserIdentifier(authContext: AiChatRequestContext): string | undefined {
1164
- try {
1165
- return computeEndUserIdentifier(authContext.tenantId, authContext.userId)
1166
- } catch (error) {
1167
- logger.warn('Failed to derive end-user safety identifier', { err: error })
1168
- return undefined
1169
1001
  }
1170
1002
  }
1171
1003
 
@@ -1366,44 +1198,6 @@ async function resolveRuntimeModelOverride(
1366
1198
  }
1367
1199
  }
1368
1200
 
1369
- /**
1370
- * Resolves the per-agent and tenant-wide `input_moderation` override values for
1371
- * the moderation policy precedence. Best-effort and fail-open (returns nulls on
1372
- * any error so the gate falls back to the env default). Returns the two values
1373
- * separately because the precedence treats per-agent and tenant-wide as
1374
- * distinct steps.
1375
- */
1376
- async function resolveModerationOverrideValues(
1377
- container: AwilixContainer | undefined,
1378
- tenantId: string | null,
1379
- organizationId: string | null,
1380
- agentId: string,
1381
- ): Promise<{ perAgentOverride: boolean | null; tenantWideOverride: boolean | null }> {
1382
- const empty = { perAgentOverride: null, tenantWideOverride: null }
1383
- if (!tenantId || !container) return empty
1384
- let em: EntityManager | null = null
1385
- try {
1386
- em = container.resolve<EntityManager>('em')
1387
- } catch {
1388
- em = null
1389
- }
1390
- if (!em) return empty
1391
- try {
1392
- const repo = new AiAgentRuntimeOverrideRepository(em)
1393
- const [agentRow, tenantRow] = await Promise.all([
1394
- repo.getExact({ tenantId, organizationId: organizationId ?? null, agentId }),
1395
- repo.getDefault({ tenantId, organizationId: organizationId ?? null, agentId: null }),
1396
- ])
1397
- return {
1398
- perAgentOverride: agentRow?.inputModeration ?? null,
1399
- tenantWideOverride: tenantRow?.inputModeration ?? null,
1400
- }
1401
- } catch (error) {
1402
- logger.warn('Moderation override lookup failed; falling back to env default', { agentId, err: error })
1403
- return empty
1404
- }
1405
- }
1406
-
1407
1201
  /**
1408
1202
  * Normalizes simple `{ role, content }` chat messages into the AI SDK
1409
1203
  * `UIMessage` shape that `convertToModelMessages` requires. When the
@@ -1690,59 +1484,9 @@ export async function runAiAgentText(input: RunAiAgentTextInput): Promise<Respon
1690
1484
  tenantRuntimeOverride,
1691
1485
  input.requestOverride,
1692
1486
  tenantAllowlistSnapshot,
1693
- resolveEndUserIdentifier(input.authContext),
1694
1487
  )
1695
1488
  const { model } = resolvedModel
1696
- const modelProviderOptions = resolvedModel.providerOptions
1697
1489
  const normalizedMessages = ensureUiMessageShape(input.messages)
1698
-
1699
- // Pre-loop input moderation gate (spec 2026-06-04). Runs before the model
1700
- // call on enabled/enforced surfaces where the provider supports moderation.
1701
- // Throws AiModerationBlockedError (flagged) or AiModerationUnavailableError
1702
- // (enforced + outage); both are surfaced by the SSE error path.
1703
- let moderationService: ModerationService | null = null
1704
- try {
1705
- moderationService = input.container?.resolve<ModerationService>('moderationService') ?? null
1706
- } catch {
1707
- moderationService = null
1708
- }
1709
- // Read tenant overrides only when the provider can moderate and the agent is
1710
- // not already enforced (untrustedInput short-circuits to enforced) — avoids a
1711
- // DB round-trip on the common skip paths.
1712
- const moderationOverrides =
1713
- resolvedModel.supportsInputModeration && agent.untrustedInput !== true
1714
- ? await resolveModerationOverrideValues(
1715
- input.container,
1716
- input.authContext.tenantId,
1717
- input.authContext.organizationId,
1718
- agent.id,
1719
- )
1720
- : { perAgentOverride: null, tenantWideOverride: null }
1721
- await runInputModerationGate({
1722
- untrustedInput: agent.untrustedInput,
1723
- supportsInputModeration: resolvedModel.supportsInputModeration,
1724
- perAgentOverride: moderationOverrides.perAgentOverride,
1725
- tenantWideOverride: moderationOverrides.tenantWideOverride,
1726
- userText: extractLatestUserText(normalizedMessages),
1727
- service: moderationService,
1728
- resolveApiKey: () => llmProviderRegistry.get(resolvedModel.providerId)?.resolveApiKey() ?? null,
1729
- baseURL: resolvedModel.baseURL,
1730
- moderationModel: process.env.OM_AI_MODERATION_MODEL,
1731
- onFlagged: (categories) =>
1732
- recordModerationFlag(
1733
- {
1734
- tenantId: input.authContext.tenantId,
1735
- organizationId: input.authContext.organizationId,
1736
- agentId: agent.id,
1737
- userId: input.authContext.userId,
1738
- providerId: resolvedModel.providerId,
1739
- modelId: resolvedModel.modelId,
1740
- categories,
1741
- },
1742
- input.container,
1743
- ),
1744
- })
1745
-
1746
1490
  const hydratedMessages = attachAttachmentsToMessages(normalizedMessages, resolvedAttachments)
1747
1491
  const modelMessages = await convertToModelMessages(hydratedMessages)
1748
1492
 
@@ -1835,7 +1579,6 @@ export async function runAiAgentText(input: RunAiAgentTextInput): Promise<Respon
1835
1579
  : {}),
1836
1580
  ...(sdkActiveTools !== undefined ? { activeTools: sdkActiveTools } : {}),
1837
1581
  ...(sdkToolChoice !== undefined ? { toolChoice: sdkToolChoice } : {}),
1838
- ...(modelProviderOptions !== undefined ? { providerOptions: modelProviderOptions as never } : {}),
1839
1582
  }
1840
1583
  builtToolLoopAgent = new ToolLoopAgent(agentSettings)
1841
1584
  }
@@ -1857,7 +1600,6 @@ export async function runAiAgentText(input: RunAiAgentTextInput): Promise<Respon
1857
1600
  toolChoice: sdkToolChoice,
1858
1601
  abortSignal: abortController.signal,
1859
1602
  finalizeLoopTrace: () => loopTraceCollector.finalize(budgetEnforcer.abortReason),
1860
- ...(modelProviderOptions !== undefined ? { providerOptions: modelProviderOptions } : {}),
1861
1603
  ...(builtToolLoopAgent !== undefined ? { toolLoopAgent: builtToolLoopAgent } : {}),
1862
1604
  }
1863
1605
 
@@ -1930,7 +1672,6 @@ export async function runAiAgentText(input: RunAiAgentTextInput): Promise<Respon
1930
1672
  experimental_repairToolCall: effectiveLoop.repairToolCall as never,
1931
1673
  ...(sdkActiveTools !== undefined ? { activeTools: sdkActiveTools } : {}),
1932
1674
  ...(sdkToolChoice !== undefined ? { toolChoice: sdkToolChoice } : {}),
1933
- ...(modelProviderOptions !== undefined ? { providerOptions: modelProviderOptions as never } : {}),
1934
1675
  abortSignal: abortController.signal,
1935
1676
  })
1936
1677
  if (wallClockTimer !== undefined) {
@@ -384,16 +384,6 @@ export interface AiAgentDefinition {
384
384
  uiParts?: string[]
385
385
  readOnly?: boolean
386
386
  mutationPolicy?: AiAgentMutationPolicy
387
- /**
388
- * Marks this agent as accepting untrusted end-user input (e.g. a customer
389
- * portal or public-widget surface). When `true`, input pre-moderation is
390
- * ENFORCED on every turn wherever the resolved chat provider supports it —
391
- * tenants cannot disable it. Other agents default to off and are tenant
392
- * opt-in. Additive + optional; omitting it preserves today's behavior.
393
- *
394
- * Spec `2026-06-04-ai-input-moderation-and-safety-identifiers`.
395
- */
396
- untrustedInput?: boolean
397
387
  /**
398
388
  * @deprecated Use `loop.maxSteps` instead. Honored as alias when `loop` is
399
389
  * omitted. When both `maxSteps` and `loop.maxSteps` are specified, `loop.maxSteps`
@@ -2,7 +2,7 @@ import { createLogger } from '@open-mercato/shared/lib/logger'
2
2
  import type { AwilixContainer } from 'awilix'
3
3
  import type { EntityManager } from '@mikro-orm/postgresql'
4
4
  import type { RbacService } from '@open-mercato/core/modules/auth/services/rbacService'
5
- import { authorizeFeatures } from '@open-mercato/shared/security/featurePolicy'
5
+ import { hasAllFeatures } from '@open-mercato/shared/lib/auth/featureMatch'
6
6
 
7
7
  const logger = createLogger('ai_assistant')
8
8
 
@@ -137,11 +137,20 @@ export function hasRequiredFeatures(
137
137
  isSuperAdmin: boolean,
138
138
  rbacService?: RbacService
139
139
  ): boolean {
140
- void rbacService
141
- return authorizeFeatures(requiredFeatures ?? [], {
142
- grantedFeatures: userFeatures,
143
- unrestricted: isSuperAdmin,
144
- })
140
+ if (isSuperAdmin) return true
141
+ if (!requiredFeatures?.length) return true
142
+
143
+ // Delegate to RbacService if provided
144
+ if (rbacService) {
145
+ return rbacService.hasAllFeatures(requiredFeatures, userFeatures)
146
+ }
147
+
148
+ // Fallback for cases without rbacService: delegate to the canonical
149
+ // wildcard-aware matcher so this path stays consistent with
150
+ // RbacService.hasAllFeatures (which uses the same helper). The previous
151
+ // bespoke loop rejected a bare-segment requirement (e.g. 'entities')
152
+ // against an 'entities.*' grant, diverging from the canonical matcher.
153
+ return hasAllFeatures(requiredFeatures, userFeatures)
145
154
  }
146
155
 
147
156
  /**
@@ -7,7 +7,6 @@
7
7
 
8
8
  import { ReferenceKind, type MikroORM } from '@mikro-orm/core'
9
9
  import type { PostgreSqlDriver } from '@mikro-orm/postgresql'
10
- import { listEntityMetadata } from '@open-mercato/shared/lib/db/entityMetadata'
11
10
 
12
11
  /**
13
12
  * Relationship types mapped from MikroORM reference kinds.
@@ -87,12 +86,13 @@ function getSimpleTypeName(type: string | ((...args: unknown[]) => unknown) | un
87
86
  * Extract the entity graph from MikroORM metadata.
88
87
  */
89
88
  export async function extractEntityGraph(orm: MikroORM<PostgreSqlDriver>): Promise<EntityGraph> {
90
- const allMetadata = listEntityMetadata(orm)
89
+ const metadata = orm.getMetadata()
90
+ const allMetadata = metadata.getAll()
91
91
 
92
92
  const nodes: EntityNode[] = []
93
93
  const edges: EntityTriple[] = []
94
94
 
95
- for (const entityMeta of allMetadata) {
95
+ for (const [, entityMeta] of Object.entries(allMetadata)) {
96
96
  // Skip abstract entities and embeddables
97
97
  if (entityMeta.abstract || entityMeta.embeddable) continue
98
98
 
@@ -90,16 +90,6 @@ export function createAnthropicAdapter(): LlmProvider {
90
90
  return envKeys[0]
91
91
  },
92
92
 
93
- mapEndUserIdentifier(identifier: string): Record<string, unknown> {
94
- // Anthropic Messages API accepts `metadata.user_id` so abuse enforcement
95
- // can target a single hashed end user instead of the org key. The key MUST
96
- // be the AI SDK provider-option name `userId` (camelCase) — the SDK
97
- // translates it to the `metadata.user_id` request-body field and strips
98
- // unknown providerOptions keys, so a snake_case key would be dropped and
99
- // never reach Anthropic.
100
- return { anthropic: { metadata: { userId: identifier } } }
101
- },
102
-
103
93
  createModel(options: LlmCreateModelOptions): unknown {
104
94
  const anthropic = createAnthropic({
105
95
  apiKey: options.apiKey,
@@ -62,23 +62,6 @@ export interface OpenAICompatiblePreset {
62
62
  * The first non-empty value wins and overrides {@link baseURL}.
63
63
  */
64
64
  baseURLEnvKeys?: readonly string[]
65
- /**
66
- * When `true`, the provider exposes OpenAI's `/v1/moderations` endpoint and
67
- * the runtime may run input pre-moderation through it. Only the native
68
- * `openai` preset sets this — OpenAI-compatible backends (Groq, DeepInfra,
69
- * Together, …) do not implement the moderations endpoint, so they leave it
70
- * unset and rely on their own server-side filtering.
71
- */
72
- supportsInputModeration?: boolean
73
- /**
74
- * When `true`, the provider maps the runtime end-user safety identifier into
75
- * OpenAI's per-request `safetyIdentifier` provider option. Only the native
76
- * `openai` preset sets this — `safety_identifier` is an OpenAI-specific body
77
- * field, and OpenAI-compatible/self-hosted backends (Groq, DeepInfra, Azure,
78
- * Ollama, LM Studio, …) may reject the unknown parameter, so they leave it
79
- * unset and send no identifier (matching pre-feature behavior).
80
- */
81
- supportsEndUserSafetyIdentifier?: boolean
82
65
  /**
83
66
  * True when this gateway's model ids are `vendor/model` strings (OpenRouter,
84
67
  * Requesty, LiteLLM). Surfaced on the resolved `LlmProvider` so the model
@@ -133,13 +116,12 @@ export function createOpenAICompatibleProvider(
133
116
  return preset.baseURL
134
117
  }
135
118
 
136
- const provider: LlmProvider = {
119
+ return {
137
120
  id: preset.id,
138
121
  name: preset.name,
139
122
  envKeys: preset.envKeys,
140
123
  defaultModel: preset.defaultModel,
141
124
  defaultModels: preset.defaultModels,
142
- supportsInputModeration: preset.supportsInputModeration ?? false,
143
125
  usesVendorPrefixedModelIds: preset.usesVendorPrefixedModelIds ?? false,
144
126
 
145
127
  isConfigured(env?: EnvLookup): boolean {
@@ -169,17 +151,4 @@ export function createOpenAICompatibleProvider(
169
151
  return openai(options.modelId)
170
152
  },
171
153
  }
172
-
173
- // Only the native OpenAI preset maps the end-user safety identifier. The key
174
- // MUST be the AI SDK provider-option name `safetyIdentifier` (camelCase) — the
175
- // SDK translates it to the `safety_identifier` request-body field and strips
176
- // any unknown providerOptions keys, so a snake_case key would be silently
177
- // dropped and never reach OpenAI.
178
- if (preset.supportsEndUserSafetyIdentifier) {
179
- provider.mapEndUserIdentifier = (identifier: string): Record<string, unknown> => ({
180
- openai: { safetyIdentifier: identifier },
181
- })
182
- }
183
-
184
- return provider
185
154
  }
@@ -217,16 +217,6 @@ export interface AiModelFactoryInput {
217
217
  * intersecting at resolution time.
218
218
  */
219
219
  tenantAllowlist?: TenantAllowlistSnapshot | null
220
- /**
221
- * Optional opaque end-user identifier (tenant-salted HMAC). When set and the
222
- * resolved provider implements {@link LlmProvider.mapEndUserIdentifier}, the
223
- * factory returns the provider-specific `providerOptions` fragment on the
224
- * resolution so the runtime can attach it to the model call. No-op for
225
- * providers without a mapping or when no identifier is supplied.
226
- *
227
- * @see .ai/specs/2026-06-04-ai-input-moderation-and-safety-identifiers.md
228
- */
229
- endUserIdentifier?: string
230
220
  }
231
221
 
232
222
  /**
@@ -278,20 +268,6 @@ export interface AiModelResolution {
278
268
  originalModelId: string
279
269
  reason: string
280
270
  }
281
- /**
282
- * Provider-specific `providerOptions` fragment produced by mapping the input
283
- * {@link AiModelFactoryInput.endUserIdentifier} through the resolved
284
- * provider's {@link LlmProvider.mapEndUserIdentifier}. Undefined when no
285
- * identifier was supplied or the provider has no mapping. The runtime merges
286
- * this into the AI SDK `providerOptions` for the model call.
287
- */
288
- providerOptions?: Record<string, unknown>
289
- /**
290
- * Mirror of the resolved provider's {@link LlmProvider.supportsInputModeration}
291
- * capability flag. The runtime uses this to decide whether to run the input
292
- * pre-moderation gate before the model call.
293
- */
294
- supportsInputModeration?: boolean
295
271
  }
296
272
 
297
273
  /**
@@ -800,26 +776,17 @@ export function createModelFactory(
800
776
  )
801
777
  }
802
778
 
803
- const endUserIdentifier = normalizeOverride(input.endUserIdentifier)
804
- const providerOptions =
805
- endUserIdentifier && finalProvider.mapEndUserIdentifier
806
- ? finalProvider.mapEndUserIdentifier(endUserIdentifier)
807
- : undefined
808
-
809
779
  const model = finalProvider.createModel({
810
780
  modelId: finalModelId,
811
781
  apiKey: finalApiKey,
812
782
  baseURL: resolvedBaseURL,
813
- ...(endUserIdentifier ? { endUserIdentifier } : {}),
814
783
  })
815
784
  return {
816
785
  model,
817
786
  modelId: finalModelId,
818
787
  providerId: finalProvider.id,
819
788
  source: finalSource,
820
- supportsInputModeration: finalProvider.supportsInputModeration === true,
821
789
  ...(resolvedBaseURL !== undefined ? { baseURL: resolvedBaseURL } : {}),
822
- ...(providerOptions !== undefined ? { providerOptions } : {}),
823
790
  ...(allowlistResult.fallback
824
791
  ? {
825
792
  allowlistFallback: {
@@ -26,8 +26,6 @@ const OPENAI_PRESET: OpenAICompatiblePreset = {
26
26
  baseURL: undefined,
27
27
  baseURLEnvKeys: ['OPENAI_BASE_URL'],
28
28
  envKeys: ['OPENAI_API_KEY', 'OPENCODE_OPENAI_API_KEY'],
29
- supportsInputModeration: true,
30
- supportsEndUserSafetyIdentifier: true,
31
29
  defaultModel: 'gpt-5-mini',
32
30
  defaultModels: [
33
31
  {
@@ -1,6 +1,5 @@
1
1
  import type { SearchService } from '@open-mercato/search/service'
2
2
  import type { SearchStrategyId, IndexableRecord } from '@open-mercato/search/types'
3
- import { authorizeFeatures } from '@open-mercato/shared/security/featurePolicy'
4
3
  import type { McpToolRegistry, McpToolDefinition } from './types'
5
4
  import {
6
5
  TOOL_ENTITY_ID,
@@ -295,9 +294,23 @@ export class ToolSearchService {
295
294
  userFeatures: string[],
296
295
  isSuperAdmin: boolean
297
296
  ): boolean {
298
- return authorizeFeatures(requiredFeatures ?? [], {
299
- grantedFeatures: userFeatures,
300
- unrestricted: isSuperAdmin,
297
+ if (isSuperAdmin) return true
298
+ if (!requiredFeatures?.length) return true
299
+
300
+ return requiredFeatures.every((required) => {
301
+ // Direct match
302
+ if (userFeatures.includes(required)) return true
303
+ // Wildcard match
304
+ if (userFeatures.includes('*')) return true
305
+
306
+ // Prefix wildcard match (e.g., 'customers.*' matches 'customers.people.view')
307
+ return userFeatures.some((feature) => {
308
+ if (feature.endsWith('.*')) {
309
+ const prefix = feature.slice(0, -2)
310
+ return required.startsWith(prefix + '.')
311
+ }
312
+ return false
313
+ })
301
314
  })
302
315
  }
303
316
  }