@open-mercato/ai-assistant 0.6.7-develop.6775.1.c2313bb8a3 → 0.6.7-develop.6785.1.1dd7cfac55
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 +12 -29
- package/dist/modules/ai_assistant/__integration__/TC-AI-MODERATION-008-settings-and-audit.spec.js +80 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-MODERATION-008-settings-and-audit.spec.js.map +7 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-MODERATION-009-ui.spec.js +124 -0
- package/dist/modules/ai_assistant/__integration__/TC-AI-MODERATION-009-ui.spec.js.map +7 -0
- package/dist/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.js +30 -0
- package/dist/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.js.map +2 -2
- package/dist/modules/ai_assistant/api/ai/chat/route.js +7 -0
- package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
- package/dist/modules/ai_assistant/api/moderation-flags/route.js +121 -0
- package/dist/modules/ai_assistant/api/moderation-flags/route.js.map +7 -0
- 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 +34 -13
- package/dist/modules/ai_assistant/api/settings/route.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js +106 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.js +139 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.js +10 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.js.map +7 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.meta.js +26 -0
- package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.meta.js.map +7 -0
- package/dist/modules/ai_assistant/data/entities.js +42 -0
- package/dist/modules/ai_assistant/data/entities.js.map +2 -2
- package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js +3 -1
- package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js.map +2 -2
- package/dist/modules/ai_assistant/data/repositories/AiModerationFlagRepository.js +44 -0
- package/dist/modules/ai_assistant/data/repositories/AiModerationFlagRepository.js.map +7 -0
- package/dist/modules/ai_assistant/di.js +6 -2
- package/dist/modules/ai_assistant/di.js.map +2 -2
- package/dist/modules/ai_assistant/events.js +6 -0
- package/dist/modules/ai_assistant/events.js.map +2 -2
- package/dist/modules/ai_assistant/i18n/de.json +26 -0
- package/dist/modules/ai_assistant/i18n/en.json +26 -0
- package/dist/modules/ai_assistant/i18n/es.json +26 -0
- package/dist/modules/ai_assistant/i18n/pl.json +26 -0
- package/dist/modules/ai_assistant/lib/agent-runtime.js +158 -5
- 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 +6 -7
- package/dist/modules/ai_assistant/lib/auth.js.map +2 -2
- package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js +3 -0
- package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js.map +2 -2
- package/dist/modules/ai_assistant/lib/llm-adapters/openai.js +8 -1
- package/dist/modules/ai_assistant/lib/llm-adapters/openai.js.map +2 -2
- package/dist/modules/ai_assistant/lib/model-factory.js +6 -1
- package/dist/modules/ai_assistant/lib/model-factory.js.map +2 -2
- package/dist/modules/ai_assistant/lib/moderation-flag-recorder.js +43 -0
- package/dist/modules/ai_assistant/lib/moderation-flag-recorder.js.map +7 -0
- package/dist/modules/ai_assistant/lib/moderation-policy.js +24 -0
- package/dist/modules/ai_assistant/lib/moderation-policy.js.map +7 -0
- package/dist/modules/ai_assistant/lib/moderation.js +109 -0
- package/dist/modules/ai_assistant/lib/moderation.js.map +7 -0
- package/dist/modules/ai_assistant/lib/openai-compatible-presets.js +2 -0
- package/dist/modules/ai_assistant/lib/openai-compatible-presets.js.map +2 -2
- package/dist/modules/ai_assistant/lib/tool-search.js +4 -12
- package/dist/modules/ai_assistant/lib/tool-search.js.map +2 -2
- package/dist/modules/ai_assistant/migrations/Migration20260610134045_ai_assistant.js +19 -0
- package/dist/modules/ai_assistant/migrations/Migration20260610134045_ai_assistant.js.map +7 -0
- package/generated/entities/ai_agent_runtime_override/index.ts +1 -0
- package/generated/entities/ai_moderation_flag/index.ts +9 -0
- package/generated/entities.ids.generated.ts +2 -1
- package/generated/entity-fields-registry.ts +13 -1
- package/package.json +6 -6
- package/src/modules/ai_assistant/__integration__/TC-AI-MODERATION-008-settings-and-audit.spec.ts +126 -0
- package/src/modules/ai_assistant/__integration__/TC-AI-MODERATION-009-ui.spec.ts +161 -0
- package/src/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.ts +48 -0
- package/src/modules/ai_assistant/api/ai/chat/route.ts +10 -0
- package/src/modules/ai_assistant/api/moderation-flags/__tests__/route.test.ts +122 -0
- package/src/modules/ai_assistant/api/moderation-flags/route.ts +137 -0
- package/src/modules/ai_assistant/api/settings/__tests__/route.test.ts +58 -8
- package/src/modules/ai_assistant/api/settings/allowlist/route.ts +10 -12
- package/src/modules/ai_assistant/api/settings/route.ts +33 -10
- package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx +146 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.tsx +186 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.meta.ts +24 -0
- package/src/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.tsx +12 -0
- package/src/modules/ai_assistant/data/entities.ts +56 -0
- package/src/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.ts +4 -0
- package/src/modules/ai_assistant/data/repositories/AiModerationFlagRepository.ts +79 -0
- package/src/modules/ai_assistant/di.ts +5 -1
- package/src/modules/ai_assistant/events.ts +21 -0
- package/src/modules/ai_assistant/i18n/de.json +26 -0
- package/src/modules/ai_assistant/i18n/en.json +26 -0
- package/src/modules/ai_assistant/i18n/es.json +26 -0
- package/src/modules/ai_assistant/i18n/pl.json +26 -0
- package/src/modules/ai_assistant/lib/__tests__/auth.test.ts +2 -5
- package/src/modules/ai_assistant/lib/__tests__/input-moderation-gate.test.ts +121 -0
- package/src/modules/ai_assistant/lib/__tests__/llm-adapters-anthropic.test.ts +13 -0
- package/src/modules/ai_assistant/lib/__tests__/llm-adapters-openai.test.ts +41 -0
- package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +60 -0
- package/src/modules/ai_assistant/lib/__tests__/moderation-flag-recorder.test.ts +76 -0
- package/src/modules/ai_assistant/lib/__tests__/moderation-policy.test.ts +86 -0
- package/src/modules/ai_assistant/lib/__tests__/moderation.test.ts +114 -0
- package/src/modules/ai_assistant/lib/agent-runtime.ts +259 -0
- package/src/modules/ai_assistant/lib/ai-agent-definition.ts +10 -0
- package/src/modules/ai_assistant/lib/auth.ts +6 -15
- package/src/modules/ai_assistant/lib/llm-adapters/anthropic.ts +10 -0
- package/src/modules/ai_assistant/lib/llm-adapters/openai.ts +32 -1
- package/src/modules/ai_assistant/lib/model-factory.ts +33 -0
- package/src/modules/ai_assistant/lib/moderation-flag-recorder.ts +75 -0
- package/src/modules/ai_assistant/lib/moderation-policy.ts +77 -0
- package/src/modules/ai_assistant/lib/moderation.ts +179 -0
- package/src/modules/ai_assistant/lib/openai-compatible-presets.ts +2 -0
- package/src/modules/ai_assistant/lib/tool-search.ts +4 -17
- package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +388 -16
- package/src/modules/ai_assistant/migrations/Migration20260610134045_ai_assistant.ts +21 -0
|
@@ -17,6 +17,7 @@ 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'
|
|
20
21
|
import { AiTenantModelAllowlistRepository } from '../../data/repositories/AiTenantModelAllowlistRepository'
|
|
21
22
|
import { isBaseurlAllowlisted, readBaseurlAllowlist } from '../../lib/baseurl-allowlist'
|
|
22
23
|
import { loadAgentRegistry, listAgents } from '../../lib/agent-registry'
|
|
@@ -59,6 +60,9 @@ const runtimeOverrideUpsertSchema = z.object({
|
|
|
59
60
|
allowedOverrideModelsByProvider: z
|
|
60
61
|
.record(z.string().min(1).max(64), z.array(z.string().min(1).max(256)))
|
|
61
62
|
.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(),
|
|
62
66
|
})
|
|
63
67
|
|
|
64
68
|
const runtimeOverrideClearSchema = z.object({
|
|
@@ -248,6 +252,7 @@ export async function GET(req: NextRequest) {
|
|
|
248
252
|
const em = container.resolve<EntityManager>('em')
|
|
249
253
|
const repo = new AiAgentRuntimeOverrideRepository(em)
|
|
250
254
|
const overrideRow = await repo.getDefault({ tenantId, organizationId, agentId: null })
|
|
255
|
+
const tenantWideInputModeration = overrideRow?.inputModeration ?? null
|
|
251
256
|
if (overrideRow) {
|
|
252
257
|
tenantOverride = {
|
|
253
258
|
providerId: overrideRow.providerId ?? null,
|
|
@@ -363,6 +368,20 @@ export async function GET(req: NextRequest) {
|
|
|
363
368
|
modelId: agentResolution.modelId,
|
|
364
369
|
baseURL: agentResolution.baseURL ?? null,
|
|
365
370
|
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
|
+
},
|
|
366
385
|
}
|
|
367
386
|
})
|
|
368
387
|
agentResolutions = await Promise.all(agentResolutionPromises)
|
|
@@ -691,11 +710,11 @@ export async function PUT(req: NextRequest) {
|
|
|
691
710
|
try {
|
|
692
711
|
const container = await createRequestContainer()
|
|
693
712
|
const rbacService = container.resolve<RbacService>('rbacService')
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
713
|
+
const canManage = await rbacService.userHasAllFeatures(
|
|
714
|
+
auth.sub,
|
|
715
|
+
['ai_assistant.settings.manage'],
|
|
716
|
+
{ tenantId: auth.tenantId, organizationId: auth.orgId },
|
|
717
|
+
)
|
|
699
718
|
if (!canManage) {
|
|
700
719
|
return NextResponse.json({ error: 'Forbidden', code: 'forbidden' }, { status: 403 })
|
|
701
720
|
}
|
|
@@ -719,6 +738,9 @@ export async function PUT(req: NextRequest) {
|
|
|
719
738
|
...(allowedOverrideModelsByProvider !== undefined
|
|
720
739
|
? { allowedOverrideModelsByProvider }
|
|
721
740
|
: {}),
|
|
741
|
+
...(Object.prototype.hasOwnProperty.call(bodyResult.data, 'inputModeration')
|
|
742
|
+
? { inputModeration: bodyResult.data.inputModeration ?? null }
|
|
743
|
+
: {}),
|
|
722
744
|
}
|
|
723
745
|
const row = await repo.upsertDefault(
|
|
724
746
|
upsertInput,
|
|
@@ -734,6 +756,7 @@ export async function PUT(req: NextRequest) {
|
|
|
734
756
|
baseURL: row.baseUrl,
|
|
735
757
|
allowedOverrideProviders: row.allowedOverrideProviders ?? null,
|
|
736
758
|
allowedOverrideModelsByProvider: row.allowedOverrideModelsByProvider ?? {},
|
|
759
|
+
inputModeration: row.inputModeration ?? null,
|
|
737
760
|
updatedAt: row.updatedAt,
|
|
738
761
|
})
|
|
739
762
|
} catch (error) {
|
|
@@ -777,11 +800,11 @@ export async function DELETE(req: NextRequest) {
|
|
|
777
800
|
try {
|
|
778
801
|
const container = await createRequestContainer()
|
|
779
802
|
const rbacService = container.resolve<RbacService>('rbacService')
|
|
780
|
-
const
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
803
|
+
const canManage = await rbacService.userHasAllFeatures(
|
|
804
|
+
auth.sub,
|
|
805
|
+
['ai_assistant.settings.manage'],
|
|
806
|
+
{ tenantId: auth.tenantId, organizationId: auth.orgId },
|
|
807
|
+
)
|
|
785
808
|
if (!canManage) {
|
|
786
809
|
return NextResponse.json({ error: 'Forbidden', code: 'forbidden' }, { status: 403 })
|
|
787
810
|
}
|
package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx
CHANGED
|
@@ -121,6 +121,14 @@ 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
|
+
}
|
|
124
132
|
}
|
|
125
133
|
|
|
126
134
|
type TenantAllowlist = {
|
|
@@ -875,6 +883,142 @@ function MutationPolicySection({ agent }: { agent: AgentSettings }) {
|
|
|
875
883
|
)
|
|
876
884
|
}
|
|
877
885
|
|
|
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
|
+
|
|
878
1022
|
function AgentModelOverrideSection({ agent }: { agent: AgentSettings }) {
|
|
879
1023
|
const t = useT()
|
|
880
1024
|
const queryClient = useQueryClient()
|
|
@@ -2063,6 +2207,8 @@ function AgentDetailPanel({ agent }: { agent: AgentSettings }) {
|
|
|
2063
2207
|
|
|
2064
2208
|
<AgentModelOverrideSection agent={agent} />
|
|
2065
2209
|
|
|
2210
|
+
<ModerationSection agent={agent} />
|
|
2211
|
+
|
|
2066
2212
|
<MutationPolicySection agent={agent} />
|
|
2067
2213
|
|
|
2068
2214
|
<LoopPolicySection agent={agent} />
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { useQuery } from '@tanstack/react-query'
|
|
5
|
+
import type { ColumnDef } from '@tanstack/react-table'
|
|
6
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
7
|
+
import { DataTable } from '@open-mercato/ui/backend/DataTable'
|
|
8
|
+
import { Button } from '@open-mercato/ui/primitives/button'
|
|
9
|
+
import { Input } from '@open-mercato/ui/primitives/input'
|
|
10
|
+
import { Label } from '@open-mercato/ui/primitives/label'
|
|
11
|
+
import { StatusBadge } from '@open-mercato/ui/primitives/status-badge'
|
|
12
|
+
import { apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
13
|
+
|
|
14
|
+
const PAGE_SIZE = 50
|
|
15
|
+
|
|
16
|
+
type ModerationCategory = { flagged: boolean; score: number }
|
|
17
|
+
|
|
18
|
+
type ModerationFlagRow = {
|
|
19
|
+
id: string
|
|
20
|
+
tenantId: string
|
|
21
|
+
organizationId: string | null
|
|
22
|
+
agentId: string
|
|
23
|
+
userId: string
|
|
24
|
+
providerId: string
|
|
25
|
+
modelId: string
|
|
26
|
+
categories: Record<string, ModerationCategory>
|
|
27
|
+
createdAt: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type ModerationFlagsResponse = {
|
|
31
|
+
items: ModerationFlagRow[]
|
|
32
|
+
total: number
|
|
33
|
+
page: number
|
|
34
|
+
pageSize: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function fetchModerationFlags(params: {
|
|
38
|
+
page: number
|
|
39
|
+
from: string
|
|
40
|
+
to: string
|
|
41
|
+
agentId: string
|
|
42
|
+
userId: string
|
|
43
|
+
}): Promise<ModerationFlagsResponse> {
|
|
44
|
+
const search = new URLSearchParams()
|
|
45
|
+
search.set('page', String(params.page))
|
|
46
|
+
search.set('pageSize', String(PAGE_SIZE))
|
|
47
|
+
if (params.from) search.set('from', params.from)
|
|
48
|
+
if (params.to) search.set('to', params.to)
|
|
49
|
+
if (params.agentId) search.set('agentId', params.agentId)
|
|
50
|
+
if (params.userId) search.set('userId', params.userId)
|
|
51
|
+
const { result, status } = await apiCallOrThrow<ModerationFlagsResponse>(
|
|
52
|
+
`/api/ai_assistant/moderation-flags?${search.toString()}`,
|
|
53
|
+
undefined,
|
|
54
|
+
{ errorMessage: 'Failed to load moderation flags' },
|
|
55
|
+
)
|
|
56
|
+
if (!result) throw new Error(`Failed to load moderation flags (${status})`)
|
|
57
|
+
return result
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function flaggedCategoryNames(categories: Record<string, ModerationCategory>): string[] {
|
|
61
|
+
return Object.entries(categories)
|
|
62
|
+
.filter(([, value]) => value.flagged)
|
|
63
|
+
.map(([name]) => name)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function AiModerationFlagsPageClient() {
|
|
67
|
+
const t = useT()
|
|
68
|
+
const [page, setPage] = React.useState(1)
|
|
69
|
+
const [from, setFrom] = React.useState('')
|
|
70
|
+
const [to, setTo] = React.useState('')
|
|
71
|
+
const [agentId, setAgentId] = React.useState('')
|
|
72
|
+
const [userId, setUserId] = React.useState('')
|
|
73
|
+
const [applied, setApplied] = React.useState({ from: '', to: '', agentId: '', userId: '' })
|
|
74
|
+
|
|
75
|
+
const query = useQuery<ModerationFlagsResponse>({
|
|
76
|
+
queryKey: ['ai_assistant', 'moderation_flags', page, applied],
|
|
77
|
+
queryFn: () => fetchModerationFlags({ page, ...applied }),
|
|
78
|
+
retry: false,
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const applyFilters = React.useCallback(() => {
|
|
82
|
+
setPage(1)
|
|
83
|
+
setApplied({ from, to, agentId, userId })
|
|
84
|
+
}, [from, to, agentId, userId])
|
|
85
|
+
|
|
86
|
+
const columns = React.useMemo<ColumnDef<ModerationFlagRow, unknown>[]>(
|
|
87
|
+
() => [
|
|
88
|
+
{
|
|
89
|
+
accessorKey: 'agentId',
|
|
90
|
+
header: t('ai_assistant.moderationFlags.columns.agent', 'Agent'),
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
accessorKey: 'userId',
|
|
94
|
+
header: t('ai_assistant.moderationFlags.columns.user', 'User'),
|
|
95
|
+
meta: { truncate: true, maxWidth: 220 },
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
accessorKey: 'categories',
|
|
99
|
+
header: t('ai_assistant.moderationFlags.columns.categories', 'Categories'),
|
|
100
|
+
cell: ({ row }) => {
|
|
101
|
+
const names = flaggedCategoryNames(row.original.categories)
|
|
102
|
+
if (names.length === 0) {
|
|
103
|
+
return <span className="text-muted-foreground">—</span>
|
|
104
|
+
}
|
|
105
|
+
return (
|
|
106
|
+
<div className="flex flex-wrap gap-1">
|
|
107
|
+
{names.map((name) => (
|
|
108
|
+
<StatusBadge key={name} variant="error" dot>
|
|
109
|
+
{name}
|
|
110
|
+
</StatusBadge>
|
|
111
|
+
))}
|
|
112
|
+
</div>
|
|
113
|
+
)
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
accessorKey: 'createdAt',
|
|
118
|
+
header: t('ai_assistant.moderationFlags.columns.createdAt', 'Flagged at'),
|
|
119
|
+
cell: ({ row }) => new Date(row.original.createdAt).toLocaleString(),
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
[t],
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
const total = query.data?.total ?? 0
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div className="flex flex-col gap-4">
|
|
129
|
+
<div>
|
|
130
|
+
<h2 className="text-base font-semibold">
|
|
131
|
+
{t('ai_assistant.moderationFlags.title', 'Moderation flags')}
|
|
132
|
+
</h2>
|
|
133
|
+
<p className="text-xs text-muted-foreground">
|
|
134
|
+
{t(
|
|
135
|
+
'ai_assistant.moderationFlags.subtitle',
|
|
136
|
+
'Inputs blocked by the content safety filter. Category flags and scores only — no prompt content is stored.',
|
|
137
|
+
)}
|
|
138
|
+
</p>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div className="flex flex-wrap items-end gap-3">
|
|
142
|
+
<div className="flex flex-col gap-1">
|
|
143
|
+
<Label htmlFor="moderation-from">{t('ai_assistant.moderationFlags.filters.from', 'From')}</Label>
|
|
144
|
+
<Input id="moderation-from" type="date" value={from} onChange={(e) => setFrom(e.target.value)} />
|
|
145
|
+
</div>
|
|
146
|
+
<div className="flex flex-col gap-1">
|
|
147
|
+
<Label htmlFor="moderation-to">{t('ai_assistant.moderationFlags.filters.to', 'To')}</Label>
|
|
148
|
+
<Input id="moderation-to" type="date" value={to} onChange={(e) => setTo(e.target.value)} />
|
|
149
|
+
</div>
|
|
150
|
+
<div className="flex flex-col gap-1">
|
|
151
|
+
<Label htmlFor="moderation-agent">{t('ai_assistant.moderationFlags.columns.agent', 'Agent')}</Label>
|
|
152
|
+
<Input
|
|
153
|
+
id="moderation-agent"
|
|
154
|
+
value={agentId}
|
|
155
|
+
onChange={(e) => setAgentId(e.target.value)}
|
|
156
|
+
placeholder={t('ai_assistant.moderationFlags.filters.agentPlaceholder', 'module.agent')}
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
<div className="flex flex-col gap-1">
|
|
160
|
+
<Label htmlFor="moderation-user">{t('ai_assistant.moderationFlags.columns.user', 'User')}</Label>
|
|
161
|
+
<Input id="moderation-user" value={userId} onChange={(e) => setUserId(e.target.value)} />
|
|
162
|
+
</div>
|
|
163
|
+
<Button type="button" variant="outline" onClick={applyFilters}>
|
|
164
|
+
{t('ai_assistant.moderationFlags.filters.apply', 'Apply')}
|
|
165
|
+
</Button>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<DataTable<ModerationFlagRow>
|
|
169
|
+
columns={columns}
|
|
170
|
+
data={query.data?.items ?? []}
|
|
171
|
+
isLoading={query.isLoading}
|
|
172
|
+
error={query.error ? (query.error as Error).message : undefined}
|
|
173
|
+
emptyState={t('ai_assistant.moderationFlags.empty', 'No flagged messages.')}
|
|
174
|
+
pagination={{
|
|
175
|
+
page,
|
|
176
|
+
pageSize: PAGE_SIZE,
|
|
177
|
+
total,
|
|
178
|
+
totalPages: Math.max(1, Math.ceil(total / PAGE_SIZE)),
|
|
179
|
+
onPageChange: setPage,
|
|
180
|
+
}}
|
|
181
|
+
/>
|
|
182
|
+
</div>
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export default AiModerationFlagsPageClient
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
|
|
3
|
+
const moderationIcon = React.createElement(
|
|
4
|
+
'svg',
|
|
5
|
+
{ width: 16, height: 16, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' },
|
|
6
|
+
React.createElement('path', { d: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z' }),
|
|
7
|
+
React.createElement('path', { d: 'M12 8v4' }),
|
|
8
|
+
React.createElement('path', { d: 'M12 16h.01' }),
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
export const metadata = {
|
|
12
|
+
requireAuth: true,
|
|
13
|
+
requireFeatures: ['ai_assistant.settings.manage'],
|
|
14
|
+
pageTitle: 'Moderation flags',
|
|
15
|
+
pageTitleKey: 'ai_assistant.moderationFlags.navTitle',
|
|
16
|
+
pageGroup: 'Module Configs',
|
|
17
|
+
pageGroupKey: 'settings.sections.moduleConfigs',
|
|
18
|
+
pageOrder: 432,
|
|
19
|
+
icon: moderationIcon,
|
|
20
|
+
pageContext: 'settings' as const,
|
|
21
|
+
breadcrumb: [
|
|
22
|
+
{ label: 'Moderation flags', labelKey: 'ai_assistant.moderationFlags.navTitle' },
|
|
23
|
+
],
|
|
24
|
+
} as const
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Page, PageBody } from '@open-mercato/ui/backend/Page'
|
|
2
|
+
import { AiModerationFlagsPageClient } from './AiModerationFlagsPageClient'
|
|
3
|
+
|
|
4
|
+
export default async function AiModerationFlagsPage() {
|
|
5
|
+
return (
|
|
6
|
+
<Page>
|
|
7
|
+
<PageBody>
|
|
8
|
+
<AiModerationFlagsPageClient />
|
|
9
|
+
</PageBody>
|
|
10
|
+
</Page>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
@@ -361,6 +361,17 @@ 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
|
|
364
375
|
}
|
|
365
376
|
|
|
366
377
|
/**
|
|
@@ -933,3 +944,48 @@ export class AiChatMessage {
|
|
|
933
944
|
@Property({ name: 'deleted_at', type: Date, nullable: true })
|
|
934
945
|
deletedAt?: Date | null
|
|
935
946
|
}
|
|
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,6 +35,8 @@ 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
|
|
38
40
|
/**
|
|
39
41
|
* Optional: the agent's declared allowedTools. When provided, loopActiveToolsJson
|
|
40
42
|
* is validated to be a subset. When omitted, allowlist validation is skipped
|
|
@@ -270,6 +272,7 @@ export class AiAgentRuntimeOverrideRepository {
|
|
|
270
272
|
if ('loopMaxTokens' in input) existing.loopMaxTokens = input.loopMaxTokens ?? null
|
|
271
273
|
if ('loopStopWhenJson' in input) existing.loopStopWhenJson = input.loopStopWhenJson ?? null
|
|
272
274
|
if ('loopActiveToolsJson' in input) existing.loopActiveToolsJson = input.loopActiveToolsJson ?? null
|
|
275
|
+
if ('inputModeration' in input) existing.inputModeration = input.inputModeration ?? null
|
|
273
276
|
await tx.persist(existing).flush()
|
|
274
277
|
return existing
|
|
275
278
|
}
|
|
@@ -295,6 +298,7 @@ export class AiAgentRuntimeOverrideRepository {
|
|
|
295
298
|
loopMaxTokens: input.loopMaxTokens ?? null,
|
|
296
299
|
loopStopWhenJson: input.loopStopWhenJson ?? null,
|
|
297
300
|
loopActiveToolsJson: input.loopActiveToolsJson ?? null,
|
|
301
|
+
inputModeration: input.inputModeration ?? null,
|
|
298
302
|
} as unknown as AiAgentRuntimeOverride)
|
|
299
303
|
await tx.persist(row).flush()
|
|
300
304
|
return row
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
2
|
+
import { AiModerationFlag } from '../entities'
|
|
3
|
+
import type { ModerationCategoryResult } from '../../lib/moderation'
|
|
4
|
+
|
|
5
|
+
export interface CreateModerationFlagInput {
|
|
6
|
+
tenantId: string
|
|
7
|
+
organizationId?: string | null
|
|
8
|
+
agentId: string
|
|
9
|
+
userId: string
|
|
10
|
+
providerId: string
|
|
11
|
+
modelId: string
|
|
12
|
+
categories: Record<string, ModerationCategoryResult>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ListModerationFlagsFilter {
|
|
16
|
+
tenantId: string
|
|
17
|
+
organizationId?: string | null
|
|
18
|
+
agentId?: string
|
|
19
|
+
userId?: string
|
|
20
|
+
/** Inclusive lower bound on `created_at`. */
|
|
21
|
+
from?: Date
|
|
22
|
+
/** Inclusive upper bound on `created_at`. */
|
|
23
|
+
to?: Date
|
|
24
|
+
page: number
|
|
25
|
+
pageSize: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ModerationFlagListResult {
|
|
29
|
+
items: AiModerationFlag[]
|
|
30
|
+
total: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Repository for the append-only `ai_moderation_flags` audit table. Every read
|
|
35
|
+
* is tenant-scoped — cross-tenant access is structurally impossible because
|
|
36
|
+
* `tenantId` is always part of the filter.
|
|
37
|
+
*
|
|
38
|
+
* Spec `2026-06-04-ai-input-moderation-and-safety-identifiers`.
|
|
39
|
+
*/
|
|
40
|
+
export class AiModerationFlagRepository {
|
|
41
|
+
constructor(private readonly em: EntityManager) {}
|
|
42
|
+
|
|
43
|
+
async create(input: CreateModerationFlagInput): Promise<AiModerationFlag> {
|
|
44
|
+
const flag = this.em.create(AiModerationFlag, {
|
|
45
|
+
tenantId: input.tenantId,
|
|
46
|
+
organizationId: input.organizationId ?? null,
|
|
47
|
+
agentId: input.agentId,
|
|
48
|
+
userId: input.userId,
|
|
49
|
+
providerId: input.providerId,
|
|
50
|
+
modelId: input.modelId,
|
|
51
|
+
categories: input.categories,
|
|
52
|
+
})
|
|
53
|
+
this.em.persist(flag)
|
|
54
|
+
await this.em.flush()
|
|
55
|
+
return flag
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async list(filter: ListModerationFlagsFilter): Promise<ModerationFlagListResult> {
|
|
59
|
+
const where: Record<string, unknown> = { tenantId: filter.tenantId }
|
|
60
|
+
if (filter.organizationId !== undefined && filter.organizationId !== null) {
|
|
61
|
+
where.organizationId = filter.organizationId
|
|
62
|
+
}
|
|
63
|
+
if (filter.agentId) where.agentId = filter.agentId
|
|
64
|
+
if (filter.userId) where.userId = filter.userId
|
|
65
|
+
if (filter.from || filter.to) {
|
|
66
|
+
const createdAt: Record<string, Date> = {}
|
|
67
|
+
if (filter.from) createdAt.$gte = filter.from
|
|
68
|
+
if (filter.to) createdAt.$lte = filter.to
|
|
69
|
+
where.createdAt = createdAt
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const [items, total] = await this.em.findAndCount(AiModerationFlag, where, {
|
|
73
|
+
orderBy: { createdAt: 'desc' },
|
|
74
|
+
limit: filter.pageSize,
|
|
75
|
+
offset: (filter.page - 1) * filter.pageSize,
|
|
76
|
+
})
|
|
77
|
+
return { items, total }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { asValue } from 'awilix'
|
|
1
|
+
import { asFunction, asValue } from 'awilix'
|
|
2
2
|
import type { AwilixContainer } from 'awilix'
|
|
3
3
|
import { toolRegistry } from './lib/tool-registry'
|
|
4
|
+
import { createModerationService } from './lib/moderation'
|
|
4
5
|
|
|
5
6
|
export function register(container: AwilixContainer): void {
|
|
6
7
|
container.register({
|
|
7
8
|
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(),
|
|
8
12
|
})
|
|
9
13
|
}
|