@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
@@ -15,7 +15,6 @@ import {
15
15
  type AiAgentLoopBudgetPreset,
16
16
  } from '../../../lib/agent-runtime'
17
17
  import { AgentPolicyError } from '../../../lib/agent-tools'
18
- import { AiModerationBlockedError, AiModerationUnavailableError } from '../../../lib/moderation'
19
18
  import { readBaseurlAllowlist, isBaseurlAllowlisted } from '../../../lib/baseurl-allowlist'
20
19
  import {
21
20
  canonicalProviderId,
@@ -701,15 +700,6 @@ export async function POST(req: NextRequest): Promise<Response> {
701
700
  if (error instanceof AgentPolicyError) {
702
701
  return jsonError(statusForDenyCode(error.code), error.message, error.code)
703
702
  }
704
- // Input pre-moderation outcomes (spec 2026-06-04). The blocked categories
705
- // are NEVER sent to the client — only the typed code, which the chat UI
706
- // maps to a generic translated message.
707
- if (error instanceof AiModerationBlockedError) {
708
- return jsonError(400, 'Input rejected by the content safety filter.', 'moderation_blocked')
709
- }
710
- if (error instanceof AiModerationUnavailableError) {
711
- return jsonError(503, 'Content safety check temporarily unavailable.', 'moderation_unavailable')
712
- }
713
703
  logger.error('AI Chat Agent — Dispatch failure', { err: error })
714
704
  return jsonError(
715
705
  500,
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  const authMock = jest.fn()
11
- const userHasAllFeaturesMock = jest.fn()
11
+ const loadAclMock = jest.fn()
12
12
  const createRequestContainerMock = jest.fn()
13
13
  const upsertDefaultMock = jest.fn()
14
14
  const clearDefaultMock = jest.fn()
@@ -96,10 +96,10 @@ describe('PUT /api/ai_assistant/settings', () => {
96
96
  jest.clearAllMocks()
97
97
  consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
98
98
  authMock.mockResolvedValue({ sub: 'user-1', tenantId: 'tenant-1', orgId: 'org-1' })
99
- userHasAllFeaturesMock.mockResolvedValue(true)
99
+ loadAclMock.mockResolvedValue({ features: ['ai_assistant.settings.manage'], isSuperAdmin: false })
100
100
  createRequestContainerMock.mockResolvedValue({
101
101
  resolve: (name: string) => {
102
- if (name === 'rbacService') return { userHasAllFeatures: userHasAllFeaturesMock }
102
+ if (name === 'rbacService') return { loadAcl: loadAclMock }
103
103
  if (name === 'em') return {}
104
104
  return null
105
105
  },
@@ -131,7 +131,7 @@ describe('PUT /api/ai_assistant/settings', () => {
131
131
  })
132
132
 
133
133
  it('returns 403 when caller lacks ai_assistant.settings.manage', async () => {
134
- userHasAllFeaturesMock.mockResolvedValueOnce(false)
134
+ loadAclMock.mockResolvedValueOnce({ features: ['ai_assistant.view'], isSuperAdmin: false })
135
135
 
136
136
  const response = await PUT(buildRequest('PUT', { providerId: 'openai' }) as any)
137
137
 
@@ -161,56 +161,6 @@ describe('PUT /api/ai_assistant/settings', () => {
161
161
  )
162
162
  })
163
163
 
164
- it('forwards inputModeration to the override upsert and echoes it back', async () => {
165
- upsertDefaultMock.mockResolvedValueOnce({
166
- id: 'row-1',
167
- tenantId: 'tenant-1',
168
- organizationId: 'org-1',
169
- agentId: 'catalog.catalog_assistant',
170
- providerId: null,
171
- modelId: null,
172
- baseUrl: null,
173
- inputModeration: true,
174
- updatedAt: new Date('2026-06-10T00:00:00Z'),
175
- })
176
-
177
- const response = await PUT(
178
- buildRequest('PUT', { agentId: 'catalog.catalog_assistant', inputModeration: true }) as any,
179
- )
180
-
181
- expect(response.status).toBe(200)
182
- const json = await response.json()
183
- expect(json.inputModeration).toBe(true)
184
- expect(upsertDefaultMock).toHaveBeenCalledWith(
185
- expect.objectContaining({ agentId: 'catalog.catalog_assistant', inputModeration: true }),
186
- expect.objectContaining({ tenantId: 'tenant-1' }),
187
- )
188
- })
189
-
190
- it('passes inputModeration: null (inherit) through to the upsert', async () => {
191
- upsertDefaultMock.mockResolvedValueOnce({
192
- id: 'row-1',
193
- tenantId: 'tenant-1',
194
- organizationId: 'org-1',
195
- agentId: 'catalog.catalog_assistant',
196
- providerId: null,
197
- modelId: null,
198
- baseUrl: null,
199
- inputModeration: null,
200
- updatedAt: new Date('2026-06-10T00:00:00Z'),
201
- })
202
-
203
- const response = await PUT(
204
- buildRequest('PUT', { agentId: 'catalog.catalog_assistant', inputModeration: null }) as any,
205
- )
206
-
207
- expect(response.status).toBe(200)
208
- expect(upsertDefaultMock).toHaveBeenCalledWith(
209
- expect.objectContaining({ inputModeration: null }),
210
- expect.anything(),
211
- )
212
- })
213
-
214
164
  it('saves per-agent chat override allowlist without requiring a model override', async () => {
215
165
  upsertDefaultMock.mockResolvedValueOnce({
216
166
  id: 'row-1',
@@ -318,7 +268,7 @@ describe('PUT /api/ai_assistant/settings', () => {
318
268
  })
319
269
 
320
270
  it('allows superAdmin even without the manage feature', async () => {
321
- userHasAllFeaturesMock.mockResolvedValueOnce(true)
271
+ loadAclMock.mockResolvedValueOnce({ features: [], isSuperAdmin: true })
322
272
 
323
273
  const response = await PUT(buildRequest('PUT', { providerId: 'openai' }) as any)
324
274
 
@@ -387,10 +337,10 @@ describe('DELETE /api/ai_assistant/settings', () => {
387
337
  jest.clearAllMocks()
388
338
  consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
389
339
  authMock.mockResolvedValue({ sub: 'user-1', tenantId: 'tenant-1', orgId: 'org-1' })
390
- userHasAllFeaturesMock.mockResolvedValue(true)
340
+ loadAclMock.mockResolvedValue({ features: ['ai_assistant.settings.manage'], isSuperAdmin: false })
391
341
  createRequestContainerMock.mockResolvedValue({
392
342
  resolve: (name: string) => {
393
- if (name === 'rbacService') return { userHasAllFeatures: userHasAllFeaturesMock }
343
+ if (name === 'rbacService') return { loadAcl: loadAclMock }
394
344
  if (name === 'em') return {}
395
345
  return null
396
346
  },
@@ -411,7 +361,7 @@ describe('DELETE /api/ai_assistant/settings', () => {
411
361
  })
412
362
 
413
363
  it('returns 403 when caller lacks ai_assistant.settings.manage', async () => {
414
- userHasAllFeaturesMock.mockResolvedValueOnce(false)
364
+ loadAclMock.mockResolvedValueOnce({ features: ['ai_assistant.view'], isSuperAdmin: false })
415
365
 
416
366
  const response = await DELETE(buildRequest('DELETE', {}) as any)
417
367
 
@@ -151,11 +151,12 @@ export async function PUT(req: NextRequest) {
151
151
  try {
152
152
  const container = await createRequestContainer()
153
153
  const rbacService = container.resolve<RbacService>('rbacService')
154
- const canManage = await rbacService.userHasAllFeatures(
155
- auth.sub,
156
- ['ai_assistant.settings.manage'],
157
- { tenantId: auth.tenantId, organizationId: auth.orgId },
158
- )
154
+ const acl = await rbacService.loadAcl(auth.sub, {
155
+ tenantId: auth.tenantId,
156
+ organizationId: auth.orgId,
157
+ })
158
+ const canManage =
159
+ acl.isSuperAdmin || acl.features.includes('ai_assistant.settings.manage')
159
160
  if (!canManage) {
160
161
  return NextResponse.json({ error: 'Forbidden', code: 'forbidden' }, { status: 403 })
161
162
  }
@@ -196,11 +197,12 @@ export async function DELETE(req: NextRequest) {
196
197
  try {
197
198
  const container = await createRequestContainer()
198
199
  const rbacService = container.resolve<RbacService>('rbacService')
199
- const canManage = await rbacService.userHasAllFeatures(
200
- auth.sub,
201
- ['ai_assistant.settings.manage'],
202
- { tenantId: auth.tenantId, organizationId: auth.orgId },
203
- )
200
+ const acl = await rbacService.loadAcl(auth.sub, {
201
+ tenantId: auth.tenantId,
202
+ organizationId: auth.orgId,
203
+ })
204
+ const canManage =
205
+ acl.isSuperAdmin || acl.features.includes('ai_assistant.settings.manage')
204
206
  if (!canManage) {
205
207
  return NextResponse.json({ error: 'Forbidden', code: 'forbidden' }, { status: 403 })
206
208
  }
@@ -17,7 +17,6 @@ import {
17
17
  isOpenCodeProviderConfigured,
18
18
  } from '@open-mercato/shared/lib/ai/opencode-provider'
19
19
  import { AiAgentRuntimeOverrideRepository, AiAgentRuntimeOverrideValidationError } from '../../data/repositories/AiAgentRuntimeOverrideRepository'
20
- import { resolveModerationPolicy } from '../../lib/moderation-policy'
21
20
  import { AiTenantModelAllowlistRepository } from '../../data/repositories/AiTenantModelAllowlistRepository'
22
21
  import { isBaseurlAllowlisted, readBaseurlAllowlist } from '../../lib/baseurl-allowlist'
23
22
  import { loadAgentRegistry, listAgents } from '../../lib/agent-registry'
@@ -60,9 +59,6 @@ const runtimeOverrideUpsertSchema = z.object({
60
59
  allowedOverrideModelsByProvider: z
61
60
  .record(z.string().min(1).max(64), z.array(z.string().min(1).max(256)))
62
61
  .optional(),
63
- // Input moderation override: true = on, false = off, null = inherit.
64
- // Spec 2026-06-04-ai-input-moderation-and-safety-identifiers.
65
- inputModeration: z.boolean().nullable().optional(),
66
62
  })
67
63
 
68
64
  const runtimeOverrideClearSchema = z.object({
@@ -252,7 +248,6 @@ export async function GET(req: NextRequest) {
252
248
  const em = container.resolve<EntityManager>('em')
253
249
  const repo = new AiAgentRuntimeOverrideRepository(em)
254
250
  const overrideRow = await repo.getDefault({ tenantId, organizationId, agentId: null })
255
- const tenantWideInputModeration = overrideRow?.inputModeration ?? null
256
251
  if (overrideRow) {
257
252
  tenantOverride = {
258
253
  providerId: overrideRow.providerId ?? null,
@@ -368,20 +363,6 @@ export async function GET(req: NextRequest) {
368
363
  modelId: agentResolution.modelId,
369
364
  baseURL: agentResolution.baseURL ?? null,
370
365
  source: agentResolution.source,
371
- moderation: {
372
- // `untrustedInput` agents enforce moderation; the UI renders a
373
- // non-editable "Enforced" badge for them.
374
- enforced: agent.untrustedInput === true,
375
- // Per-agent control value: null = inherit, true = on, false = off.
376
- override: agentOverrideRow?.inputModeration ?? null,
377
- // Resolved runtime policy (enforced | on | off) after precedence.
378
- effective: resolveModerationPolicy({
379
- untrustedInput: agent.untrustedInput,
380
- perAgentOverride: agentOverrideRow?.inputModeration ?? null,
381
- tenantWideOverride: tenantWideInputModeration,
382
- env,
383
- }),
384
- },
385
366
  }
386
367
  })
387
368
  agentResolutions = await Promise.all(agentResolutionPromises)
@@ -710,11 +691,11 @@ export async function PUT(req: NextRequest) {
710
691
  try {
711
692
  const container = await createRequestContainer()
712
693
  const rbacService = container.resolve<RbacService>('rbacService')
713
- const canManage = await rbacService.userHasAllFeatures(
714
- auth.sub,
715
- ['ai_assistant.settings.manage'],
716
- { tenantId: auth.tenantId, organizationId: auth.orgId },
717
- )
694
+ const acl = await rbacService.loadAcl(auth.sub, {
695
+ tenantId: auth.tenantId,
696
+ organizationId: auth.orgId,
697
+ })
698
+ const canManage = acl.isSuperAdmin || acl.features.includes('ai_assistant.settings.manage')
718
699
  if (!canManage) {
719
700
  return NextResponse.json({ error: 'Forbidden', code: 'forbidden' }, { status: 403 })
720
701
  }
@@ -738,9 +719,6 @@ export async function PUT(req: NextRequest) {
738
719
  ...(allowedOverrideModelsByProvider !== undefined
739
720
  ? { allowedOverrideModelsByProvider }
740
721
  : {}),
741
- ...(Object.prototype.hasOwnProperty.call(bodyResult.data, 'inputModeration')
742
- ? { inputModeration: bodyResult.data.inputModeration ?? null }
743
- : {}),
744
722
  }
745
723
  const row = await repo.upsertDefault(
746
724
  upsertInput,
@@ -756,7 +734,6 @@ export async function PUT(req: NextRequest) {
756
734
  baseURL: row.baseUrl,
757
735
  allowedOverrideProviders: row.allowedOverrideProviders ?? null,
758
736
  allowedOverrideModelsByProvider: row.allowedOverrideModelsByProvider ?? {},
759
- inputModeration: row.inputModeration ?? null,
760
737
  updatedAt: row.updatedAt,
761
738
  })
762
739
  } catch (error) {
@@ -800,11 +777,11 @@ export async function DELETE(req: NextRequest) {
800
777
  try {
801
778
  const container = await createRequestContainer()
802
779
  const rbacService = container.resolve<RbacService>('rbacService')
803
- const canManage = await rbacService.userHasAllFeatures(
804
- auth.sub,
805
- ['ai_assistant.settings.manage'],
806
- { tenantId: auth.tenantId, organizationId: auth.orgId },
807
- )
780
+ const acl = await rbacService.loadAcl(auth.sub, {
781
+ tenantId: auth.tenantId,
782
+ organizationId: auth.orgId,
783
+ })
784
+ const canManage = acl.isSuperAdmin || acl.features.includes('ai_assistant.settings.manage')
808
785
  if (!canManage) {
809
786
  return NextResponse.json({ error: 'Forbidden', code: 'forbidden' }, { status: 403 })
810
787
  }
@@ -121,14 +121,6 @@ type AgentResolution = {
121
121
  modelId: string
122
122
  baseURL: string | null
123
123
  source: string
124
- moderation?: {
125
- /** `untrustedInput` agents enforce moderation (non-editable). */
126
- enforced: boolean
127
- /** Per-agent control value: null = inherit, true = on, false = off. */
128
- override: boolean | null
129
- /** Resolved runtime policy after the precedence chain. */
130
- effective: 'enforced' | 'on' | 'off'
131
- }
132
124
  }
133
125
 
134
126
  type TenantAllowlist = {
@@ -883,142 +875,6 @@ function MutationPolicySection({ agent }: { agent: AgentSettings }) {
883
875
  )
884
876
  }
885
877
 
886
- type ModerationControlValue = 'inherit' | 'on' | 'off'
887
-
888
- function ModerationSection({ agent }: { agent: AgentSettings }) {
889
- const t = useT()
890
- const queryClient = useQueryClient()
891
- const settingsQuery = useQuery<RuntimeSettingsResponse>({
892
- queryKey: ['ai_assistant', 'agent_settings', 'runtime_settings'],
893
- queryFn: fetchRuntimeSettings,
894
- retry: false,
895
- })
896
- const agentResolution = settingsQuery.data?.agents.find((entry) => entry.agentId === agent.id) ?? null
897
- const moderation = agentResolution?.moderation ?? null
898
- const enforced = moderation?.enforced === true
899
- const currentValue: ModerationControlValue =
900
- moderation?.override === true ? 'on' : moderation?.override === false ? 'off' : 'inherit'
901
- const [isSaving, setIsSaving] = React.useState(false)
902
- const { runMutation } = useGuardedMutation({ contextId: `ai-agent-moderation-${agent.id}` })
903
-
904
- const save = React.useCallback(
905
- async (value: ModerationControlValue) => {
906
- if (isSaving || enforced) return
907
- setIsSaving(true)
908
- try {
909
- const inputModeration = value === 'inherit' ? null : value === 'on'
910
- await runMutation({
911
- operation: async () => {
912
- const { ok, status, result } = await apiCall<{ error?: string }>(
913
- '/api/ai_assistant/settings',
914
- {
915
- method: 'PUT',
916
- headers: { 'content-type': 'application/json' },
917
- credentials: 'include',
918
- body: JSON.stringify({ agentId: agent.id, inputModeration }),
919
- },
920
- )
921
- if (!ok) {
922
- throw new Error(result?.error ?? `[internal] Failed to save moderation setting (${status}).`)
923
- }
924
- },
925
- context: {},
926
- })
927
- flash(t('ai_assistant.agents.moderation.saved', 'Input moderation setting saved.'), 'success')
928
- await queryClient.invalidateQueries({
929
- queryKey: ['ai_assistant', 'agent_settings', 'runtime_settings'],
930
- })
931
- } catch (err) {
932
- flash(err instanceof Error ? err.message : String(err), 'error')
933
- } finally {
934
- setIsSaving(false)
935
- }
936
- },
937
- [agent.id, enforced, isSaving, queryClient, runMutation, t],
938
- )
939
-
940
- return (
941
- <section
942
- className="rounded-lg border border-border bg-background p-4"
943
- data-ai-agent-moderation={agent.id}
944
- >
945
- <header className="flex flex-wrap items-start justify-between gap-3 border-b border-border pb-3">
946
- <div className="flex min-w-0 items-start gap-2">
947
- <ShieldAlert className="size-4 text-muted-foreground" aria-hidden />
948
- <div className="min-w-0">
949
- <h3 className="text-sm font-semibold">
950
- {t('ai_assistant.agents.moderation.title', 'Input moderation')}
951
- </h3>
952
- <p className="text-xs text-muted-foreground">
953
- {t(
954
- 'ai_assistant.agents.moderation.subtitle',
955
- 'Screen user input for unsafe content before the model runs. Applies only when the resolved provider supports moderation.',
956
- )}
957
- </p>
958
- </div>
959
- </div>
960
- {moderation ? (
961
- <StatusBadge
962
- variant={moderation.effective === 'off' ? 'neutral' : 'info'}
963
- dot
964
- data-ai-agent-moderation-effective
965
- >
966
- {moderation.effective === 'enforced'
967
- ? t('ai_assistant.agents.moderation.effectiveEnforced', 'Enforced')
968
- : moderation.effective === 'on'
969
- ? t('ai_assistant.agents.moderation.effectiveOn', 'On')
970
- : t('ai_assistant.agents.moderation.effectiveOff', 'Off')}
971
- </StatusBadge>
972
- ) : null}
973
- </header>
974
-
975
- <div className="mt-3">
976
- {enforced ? (
977
- <div className="flex flex-wrap items-center gap-2" data-ai-agent-moderation-enforced>
978
- <StatusBadge variant="warning" dot>
979
- {t('ai_assistant.agents.moderation.enforcedBadge', 'Enforced')}
980
- </StatusBadge>
981
- <p className="text-xs text-muted-foreground">
982
- {t(
983
- 'ai_assistant.agents.moderation.enforcedHint',
984
- 'This agent accepts untrusted input, so moderation is always on and cannot be disabled here.',
985
- )}
986
- </p>
987
- </div>
988
- ) : (
989
- <div className="max-w-xs">
990
- <span className="text-overline font-semibold uppercase tracking-wider text-muted-foreground">
991
- {t('ai_assistant.agents.moderation.controlLabel', 'Moderation')}
992
- </span>
993
- <div className="mt-1">
994
- <Select
995
- value={currentValue}
996
- onValueChange={(value) => save(value as ModerationControlValue)}
997
- disabled={isSaving || !agentResolution}
998
- >
999
- <SelectTrigger aria-label={t('ai_assistant.agents.moderation.controlLabel', 'Moderation')}>
1000
- <SelectValue />
1001
- </SelectTrigger>
1002
- <SelectContent>
1003
- <SelectItem value="inherit">
1004
- {t('ai_assistant.agents.moderation.inheritLabel', 'Inherit')}
1005
- </SelectItem>
1006
- <SelectItem value="on">
1007
- {t('ai_assistant.agents.moderation.onLabel', 'On')}
1008
- </SelectItem>
1009
- <SelectItem value="off">
1010
- {t('ai_assistant.agents.moderation.offLabel', 'Off')}
1011
- </SelectItem>
1012
- </SelectContent>
1013
- </Select>
1014
- </div>
1015
- </div>
1016
- )}
1017
- </div>
1018
- </section>
1019
- )
1020
- }
1021
-
1022
878
  function AgentModelOverrideSection({ agent }: { agent: AgentSettings }) {
1023
879
  const t = useT()
1024
880
  const queryClient = useQueryClient()
@@ -2207,8 +2063,6 @@ function AgentDetailPanel({ agent }: { agent: AgentSettings }) {
2207
2063
 
2208
2064
  <AgentModelOverrideSection agent={agent} />
2209
2065
 
2210
- <ModerationSection agent={agent} />
2211
-
2212
2066
  <MutationPolicySection agent={agent} />
2213
2067
 
2214
2068
  <LoopPolicySection agent={agent} />
@@ -361,17 +361,6 @@ export class AiAgentRuntimeOverride {
361
361
  */
362
362
  @Property({ name: 'loop_active_tools_json', type: 'jsonb', nullable: true })
363
363
  loopActiveToolsJson?: unknown | null
364
-
365
- /**
366
- * Per-agent (or tenant-wide when `agent_id` is null) input-moderation
367
- * override. `true` = on, `false` = off, `null` = inherit (env default →
368
- * off). Additive nullable column; existing rows inherit. The `untrustedInput`
369
- * agent flag still forces moderation regardless of this value.
370
- *
371
- * Spec `2026-06-04-ai-input-moderation-and-safety-identifiers`.
372
- */
373
- @Property({ name: 'input_moderation', type: 'boolean', nullable: true })
374
- inputModeration?: boolean | null
375
364
  }
376
365
 
377
366
  /**
@@ -944,48 +933,3 @@ export class AiChatMessage {
944
933
  @Property({ name: 'deleted_at', type: Date, nullable: true })
945
934
  deletedAt?: Date | null
946
935
  }
947
-
948
- /**
949
- * Append-only audit record of an input that the moderation gate blocked.
950
- *
951
- * Stores ONLY category flags + numeric scores — never the prompt content or
952
- * any PII (Q4 data-minimization decision). Enough to identify repeat abusers
953
- * and tune policy. No `updated_at` / `deleted_at`: rows are immutable. Two
954
- * indexes back the audit listing (`tenant_id, created_at`) and the per-user
955
- * abuse review (`tenant_id, user_id`); every read filters `tenant_id`.
956
- *
957
- * Spec `2026-06-04-ai-input-moderation-and-safety-identifiers`.
958
- */
959
- @Entity({ tableName: 'ai_moderation_flags' })
960
- @Index({ name: 'ai_moderation_flags_tenant_created_idx', properties: ['tenantId', 'createdAt'] })
961
- @Index({ name: 'ai_moderation_flags_tenant_user_idx', properties: ['tenantId', 'userId'] })
962
- export class AiModerationFlag {
963
- [OptionalProps]?: 'id' | 'createdAt' | 'organizationId'
964
-
965
- @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
966
- id!: string
967
-
968
- @Property({ name: 'tenant_id', type: 'uuid' })
969
- tenantId!: string
970
-
971
- @Property({ name: 'organization_id', type: 'uuid', nullable: true })
972
- organizationId?: string | null
973
-
974
- @Property({ name: 'agent_id', type: 'text' })
975
- agentId!: string
976
-
977
- @Property({ name: 'user_id', type: 'text' })
978
- userId!: string
979
-
980
- @Property({ name: 'provider_id', type: 'text' })
981
- providerId!: string
982
-
983
- @Property({ name: 'model_id', type: 'text' })
984
- modelId!: string
985
-
986
- @Property({ name: 'categories', type: 'jsonb' })
987
- categories!: Record<string, { flagged: boolean; score: number }>
988
-
989
- @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
990
- createdAt: Date = new Date()
991
- }
@@ -35,8 +35,6 @@ export interface AiAgentRuntimeOverrideInput extends AiAgentRuntimeOverrideLoopI
35
35
  baseURL?: string | null
36
36
  allowedOverrideProviders?: string[] | null
37
37
  allowedOverrideModelsByProvider?: Record<string, string[]>
38
- /** Input-moderation override: true = on, false = off, null = inherit. */
39
- inputModeration?: boolean | null
40
38
  /**
41
39
  * Optional: the agent's declared allowedTools. When provided, loopActiveToolsJson
42
40
  * is validated to be a subset. When omitted, allowlist validation is skipped
@@ -272,7 +270,6 @@ export class AiAgentRuntimeOverrideRepository {
272
270
  if ('loopMaxTokens' in input) existing.loopMaxTokens = input.loopMaxTokens ?? null
273
271
  if ('loopStopWhenJson' in input) existing.loopStopWhenJson = input.loopStopWhenJson ?? null
274
272
  if ('loopActiveToolsJson' in input) existing.loopActiveToolsJson = input.loopActiveToolsJson ?? null
275
- if ('inputModeration' in input) existing.inputModeration = input.inputModeration ?? null
276
273
  await tx.persist(existing).flush()
277
274
  return existing
278
275
  }
@@ -298,7 +295,6 @@ export class AiAgentRuntimeOverrideRepository {
298
295
  loopMaxTokens: input.loopMaxTokens ?? null,
299
296
  loopStopWhenJson: input.loopStopWhenJson ?? null,
300
297
  loopActiveToolsJson: input.loopActiveToolsJson ?? null,
301
- inputModeration: input.inputModeration ?? null,
302
298
  } as unknown as AiAgentRuntimeOverride)
303
299
  await tx.persist(row).flush()
304
300
  return row
@@ -1,13 +1,9 @@
1
- import { asFunction, asValue } from 'awilix'
1
+ import { asValue } from 'awilix'
2
2
  import type { AwilixContainer } from 'awilix'
3
3
  import { toolRegistry } from './lib/tool-registry'
4
- import { createModerationService } from './lib/moderation'
5
4
 
6
5
  export function register(container: AwilixContainer): void {
7
6
  container.register({
8
7
  mcpToolRegistry: asValue(toolRegistry),
9
- // Input pre-moderation service (OpenAI /v1/moderations). Singleton +
10
- // overridable by downstream apps via the module overrides DI seam.
11
- moderationService: asFunction(() => createModerationService()).singleton(),
12
8
  })
13
9
  }
@@ -61,12 +61,6 @@ const events = [
61
61
  category: 'lifecycle' as const,
62
62
  clientBroadcast: true,
63
63
  },
64
- {
65
- id: 'ai_assistant.moderation_flag.created',
66
- label: 'AI Moderation Flag Created',
67
- entity: 'ai_moderation_flag',
68
- category: 'system' as const,
69
- },
70
64
  ] as const
71
65
 
72
66
  export const eventsConfig = createModuleEvents({
@@ -154,19 +148,4 @@ export interface AiConversationUnsharedPayload {
154
148
  participantUserId: string
155
149
  }
156
150
 
157
- /**
158
- * Emitted (best-effort) when the input moderation gate blocks a turn and the
159
- * audit row is persisted. Carries category flags only — never prompt content.
160
- *
161
- * Spec `2026-06-04-ai-input-moderation-and-safety-identifiers`.
162
- */
163
- export interface AiModerationFlagCreatedPayload {
164
- id: string
165
- tenantId: string
166
- organizationId: string | null
167
- agentId: string
168
- userId: string
169
- categories: string[]
170
- }
171
-
172
151
  export default eventsConfig