@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.
- package/.turbo/turbo-build.log +1 -1
- package/AGENTS.md +29 -12
- package/dist/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.js +0 -30
- package/dist/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/chat/route.js +0 -7
- package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/settings/allowlist/route.js +10 -10
- package/dist/modules/ai_assistant/api/settings/allowlist/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/settings/route.js +13 -34
- package/dist/modules/ai_assistant/api/settings/route.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js +0 -106
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/data/entities.js +0 -42
- package/dist/modules/ai_assistant/data/entities.js.map +2 -2
- package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js +1 -3
- package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js.map +2 -2
- package/dist/modules/ai_assistant/di.js +2 -6
- package/dist/modules/ai_assistant/di.js.map +2 -2
- package/dist/modules/ai_assistant/events.js +0 -6
- package/dist/modules/ai_assistant/events.js.map +2 -2
- package/dist/modules/ai_assistant/i18n/de.json +0 -42
- package/dist/modules/ai_assistant/i18n/en.json +0 -42
- package/dist/modules/ai_assistant/i18n/es.json +0 -42
- package/dist/modules/ai_assistant/i18n/pl.json +0 -42
- package/dist/modules/ai_assistant/lib/agent-runtime.js +5 -158
- package/dist/modules/ai_assistant/lib/agent-runtime.js.map +2 -2
- package/dist/modules/ai_assistant/lib/ai-agent-definition.js.map +2 -2
- package/dist/modules/ai_assistant/lib/auth.js +7 -6
- package/dist/modules/ai_assistant/lib/auth.js.map +2 -2
- package/dist/modules/ai_assistant/lib/entity-graph.js +3 -3
- package/dist/modules/ai_assistant/lib/entity-graph.js.map +2 -2
- package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js +0 -3
- package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js.map +2 -2
- package/dist/modules/ai_assistant/lib/llm-adapters/openai.js +1 -8
- package/dist/modules/ai_assistant/lib/llm-adapters/openai.js.map +2 -2
- package/dist/modules/ai_assistant/lib/model-factory.js +1 -6
- package/dist/modules/ai_assistant/lib/model-factory.js.map +2 -2
- package/dist/modules/ai_assistant/lib/openai-compatible-presets.js +0 -2
- package/dist/modules/ai_assistant/lib/openai-compatible-presets.js.map +2 -2
- package/dist/modules/ai_assistant/lib/tool-search.js +12 -4
- package/dist/modules/ai_assistant/lib/tool-search.js.map +2 -2
- package/generated/entities/ai_agent_runtime_override/index.ts +0 -1
- package/generated/entities.ids.generated.ts +1 -2
- package/generated/entity-fields-registry.ts +1 -13
- package/package.json +12 -13
- package/src/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.ts +0 -48
- package/src/modules/ai_assistant/api/ai/chat/route.ts +0 -10
- package/src/modules/ai_assistant/api/settings/__tests__/route.test.ts +8 -58
- package/src/modules/ai_assistant/api/settings/allowlist/route.ts +12 -10
- package/src/modules/ai_assistant/api/settings/route.ts +10 -33
- package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx +0 -146
- package/src/modules/ai_assistant/data/entities.ts +0 -56
- package/src/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.ts +0 -4
- package/src/modules/ai_assistant/di.ts +1 -5
- package/src/modules/ai_assistant/events.ts +0 -21
- package/src/modules/ai_assistant/i18n/de.json +0 -42
- package/src/modules/ai_assistant/i18n/en.json +0 -42
- package/src/modules/ai_assistant/i18n/es.json +0 -42
- package/src/modules/ai_assistant/i18n/pl.json +0 -42
- package/src/modules/ai_assistant/lib/__tests__/auth.test.ts +5 -2
- package/src/modules/ai_assistant/lib/__tests__/llm-adapters-anthropic.test.ts +0 -13
- package/src/modules/ai_assistant/lib/__tests__/llm-adapters-openai.test.ts +0 -41
- package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +0 -60
- package/src/modules/ai_assistant/lib/agent-runtime.ts +0 -259
- package/src/modules/ai_assistant/lib/ai-agent-definition.ts +0 -10
- package/src/modules/ai_assistant/lib/auth.ts +15 -6
- package/src/modules/ai_assistant/lib/entity-graph.ts +3 -3
- package/src/modules/ai_assistant/lib/llm-adapters/anthropic.ts +0 -10
- package/src/modules/ai_assistant/lib/llm-adapters/openai.ts +1 -32
- package/src/modules/ai_assistant/lib/model-factory.ts +0 -33
- package/src/modules/ai_assistant/lib/openai-compatible-presets.ts +0 -2
- package/src/modules/ai_assistant/lib/tool-search.ts +17 -4
- package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +16 -388
- package/dist/modules/ai_assistant/__integration__/TC-AI-MODERATION-008-settings-and-audit.spec.js +0 -80
- package/dist/modules/ai_assistant/__integration__/TC-AI-MODERATION-008-settings-and-audit.spec.js.map +0 -7
- package/dist/modules/ai_assistant/__integration__/TC-AI-MODERATION-009-ui.spec.js +0 -124
- package/dist/modules/ai_assistant/__integration__/TC-AI-MODERATION-009-ui.spec.js.map +0 -7
- package/dist/modules/ai_assistant/api/moderation-flags/route.js +0 -121
- package/dist/modules/ai_assistant/api/moderation-flags/route.js.map +0 -7
- package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.js +0 -139
- package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.js.map +0 -7
- package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.js +0 -10
- package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.js.map +0 -7
- package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.meta.js +0 -26
- package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.meta.js.map +0 -7
- package/dist/modules/ai_assistant/data/repositories/AiModerationFlagRepository.js +0 -44
- package/dist/modules/ai_assistant/data/repositories/AiModerationFlagRepository.js.map +0 -7
- package/dist/modules/ai_assistant/i18n/ko.json +0 -527
- package/dist/modules/ai_assistant/lib/moderation-flag-recorder.js +0 -43
- package/dist/modules/ai_assistant/lib/moderation-flag-recorder.js.map +0 -7
- package/dist/modules/ai_assistant/lib/moderation-policy.js +0 -24
- package/dist/modules/ai_assistant/lib/moderation-policy.js.map +0 -7
- package/dist/modules/ai_assistant/lib/moderation.js +0 -109
- package/dist/modules/ai_assistant/lib/moderation.js.map +0 -7
- package/dist/modules/ai_assistant/migrations/Migration20260610134045_ai_assistant.js +0 -19
- package/dist/modules/ai_assistant/migrations/Migration20260610134045_ai_assistant.js.map +0 -7
- package/generated/entities/ai_moderation_flag/index.ts +0 -9
- package/src/modules/ai_assistant/__integration__/TC-AI-MODERATION-008-settings-and-audit.spec.ts +0 -126
- package/src/modules/ai_assistant/__integration__/TC-AI-MODERATION-009-ui.spec.ts +0 -161
- package/src/modules/ai_assistant/api/moderation-flags/__tests__/route.test.ts +0 -122
- package/src/modules/ai_assistant/api/moderation-flags/route.ts +0 -137
- package/src/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.tsx +0 -186
- package/src/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.meta.ts +0 -24
- package/src/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.tsx +0 -12
- package/src/modules/ai_assistant/data/repositories/AiModerationFlagRepository.ts +0 -79
- package/src/modules/ai_assistant/i18n/ko.json +0 -527
- package/src/modules/ai_assistant/lib/__tests__/input-moderation-gate.test.ts +0 -121
- package/src/modules/ai_assistant/lib/__tests__/moderation-flag-recorder.test.ts +0 -76
- package/src/modules/ai_assistant/lib/__tests__/moderation-policy.test.ts +0 -86
- package/src/modules/ai_assistant/lib/__tests__/moderation.test.ts +0 -114
- package/src/modules/ai_assistant/lib/moderation-flag-recorder.ts +0 -75
- package/src/modules/ai_assistant/lib/moderation-policy.ts +0 -77
- package/src/modules/ai_assistant/lib/moderation.ts +0 -179
- 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
|
|
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
|
-
|
|
99
|
+
loadAclMock.mockResolvedValue({ features: ['ai_assistant.settings.manage'], isSuperAdmin: false })
|
|
100
100
|
createRequestContainerMock.mockResolvedValue({
|
|
101
101
|
resolve: (name: string) => {
|
|
102
|
-
if (name === 'rbacService') return {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
340
|
+
loadAclMock.mockResolvedValue({ features: ['ai_assistant.settings.manage'], isSuperAdmin: false })
|
|
391
341
|
createRequestContainerMock.mockResolvedValue({
|
|
392
342
|
resolve: (name: string) => {
|
|
393
|
-
if (name === 'rbacService') return {
|
|
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
|
-
|
|
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
|
|
155
|
-
auth.
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
200
|
-
auth.
|
|
201
|
-
|
|
202
|
-
|
|
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
|
|
714
|
-
auth.
|
|
715
|
-
|
|
716
|
-
|
|
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
|
|
804
|
-
auth.
|
|
805
|
-
|
|
806
|
-
|
|
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
|
}
|
package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx
CHANGED
|
@@ -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 {
|
|
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
|