@open-mercato/ai-assistant 0.6.7 → 0.6.8-develop.6874.1.982d6097d8
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 +121 -15
- package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js.map +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js +2 -2
- package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.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/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js +4 -4
- package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js.map +2 -2
- 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 +42 -0
- package/dist/modules/ai_assistant/i18n/en.json +42 -0
- package/dist/modules/ai_assistant/i18n/es.json +42 -0
- package/dist/modules/ai_assistant/i18n/ko.json +527 -0
- package/dist/modules/ai_assistant/i18n/pl.json +42 -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/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 +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 +13 -12
- 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 +161 -15
- package/src/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.tsx +2 -2
- 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/backend/config/ai-assistant/playground/AiPlaygroundPageClient.tsx +4 -4
- 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 +42 -0
- package/src/modules/ai_assistant/i18n/en.json +42 -0
- package/src/modules/ai_assistant/i18n/es.json +42 -0
- package/src/modules/ai_assistant/i18n/ko.json +527 -0
- package/src/modules/ai_assistant/i18n/pl.json +42 -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/entity-graph.ts +3 -3
- 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
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input-moderation policy resolution.
|
|
3
|
+
*
|
|
4
|
+
* Resolves the effective moderation policy for an agent turn using a fixed
|
|
5
|
+
* precedence (first match wins):
|
|
6
|
+
*
|
|
7
|
+
* 1. `agentDef.untrustedInput === true` → `enforced` (tenant cannot disable).
|
|
8
|
+
* 2. Per-agent tenant override (`ai_agent_runtime_overrides` row with an
|
|
9
|
+
* `agent_id`) `input_moderation` → `on` / `off`.
|
|
10
|
+
* 3. Tenant-wide override (row with `agent_id = NULL`) `input_moderation` →
|
|
11
|
+
* `on` / `off`.
|
|
12
|
+
* 4. Env default `OM_AI_INPUT_MODERATION` (parsed via `parseBooleanWithDefault`)
|
|
13
|
+
* → `on` / `off`.
|
|
14
|
+
* 5. Default `off`.
|
|
15
|
+
*
|
|
16
|
+
* Steps 4 and 5 collapse into `parseBooleanWithDefault(env, false)`.
|
|
17
|
+
*
|
|
18
|
+
* @see .ai/specs/2026-06-04-ai-input-moderation-and-safety-identifiers.md
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { EnvLookup } from '@open-mercato/shared/lib/ai/llm-provider'
|
|
22
|
+
import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
|
|
23
|
+
|
|
24
|
+
/** Effective runtime moderation policy for a turn. */
|
|
25
|
+
export type ModerationPolicy = 'enforced' | 'on' | 'off'
|
|
26
|
+
|
|
27
|
+
/** The per-agent display state shown in settings (adds `inherit`). */
|
|
28
|
+
export type ModerationPolicyDisplay = 'enforced' | 'on' | 'off' | 'inherit'
|
|
29
|
+
|
|
30
|
+
export interface ResolveModerationPolicyInput {
|
|
31
|
+
/** Whether the agent definition marks this surface as untrusted. */
|
|
32
|
+
untrustedInput?: boolean
|
|
33
|
+
/**
|
|
34
|
+
* `input_moderation` from the agent-scoped tenant override row, or
|
|
35
|
+
* `null`/`undefined` when the row is absent or leaves it unset (inherit).
|
|
36
|
+
*/
|
|
37
|
+
perAgentOverride?: boolean | null
|
|
38
|
+
/**
|
|
39
|
+
* `input_moderation` from the tenant-wide override row (`agent_id = NULL`),
|
|
40
|
+
* or `null`/`undefined` when absent/unset (inherit).
|
|
41
|
+
*/
|
|
42
|
+
tenantWideOverride?: boolean | null
|
|
43
|
+
/** Env lookup; reads `OM_AI_INPUT_MODERATION`. Defaults to `process.env`. */
|
|
44
|
+
env?: EnvLookup
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolves the effective moderation policy following the 5-step precedence.
|
|
49
|
+
* Pure and synchronous — the runtime hydrates the override values from the DB
|
|
50
|
+
* before calling this.
|
|
51
|
+
*/
|
|
52
|
+
export function resolveModerationPolicy(input: ResolveModerationPolicyInput): ModerationPolicy {
|
|
53
|
+
if (input.untrustedInput === true) return 'enforced'
|
|
54
|
+
if (input.perAgentOverride !== null && input.perAgentOverride !== undefined) {
|
|
55
|
+
return input.perAgentOverride ? 'on' : 'off'
|
|
56
|
+
}
|
|
57
|
+
if (input.tenantWideOverride !== null && input.tenantWideOverride !== undefined) {
|
|
58
|
+
return input.tenantWideOverride ? 'on' : 'off'
|
|
59
|
+
}
|
|
60
|
+
const env = input.env ?? process.env
|
|
61
|
+
return parseBooleanWithDefault(env.OM_AI_INPUT_MODERATION, false) ? 'on' : 'off'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Whether a resolved policy should run the moderation gate for the turn. */
|
|
65
|
+
export function isModerationActive(policy: ModerationPolicy): boolean {
|
|
66
|
+
return policy === 'enforced' || policy === 'on'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Whether an *unavailable* moderation check (endpoint outage) should fail
|
|
71
|
+
* closed (reject the turn) rather than fail open (allow it through). Per spec:
|
|
72
|
+
* only `enforced` (untrusted) surfaces fail closed; opt-in `on` surfaces prefer
|
|
73
|
+
* availability and fail open. Both paths log. (`off` never reaches the gate.)
|
|
74
|
+
*/
|
|
75
|
+
export function shouldFailClosed(policy: ModerationPolicy): boolean {
|
|
76
|
+
return policy === 'enforced'
|
|
77
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input pre-moderation service.
|
|
3
|
+
*
|
|
4
|
+
* Wraps OpenAI's free `/v1/moderations` endpoint so the agent runtime can
|
|
5
|
+
* screen the current user turn BEFORE the model call on surfaces that opt in
|
|
6
|
+
* (or are forced on via `untrustedInput`). The service is intentionally pure:
|
|
7
|
+
* it performs the HTTP check and returns category flags + scores, or throws
|
|
8
|
+
* {@link AiModerationUnavailableError} when the endpoint is unreachable. It does
|
|
9
|
+
* NOT persist audit records, emit events, or throw the user-facing block — the
|
|
10
|
+
* runtime gate orchestrates those (see `agent-runtime.ts`).
|
|
11
|
+
*
|
|
12
|
+
* @see .ai/specs/2026-06-04-ai-input-moderation-and-safety-identifiers.md
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { z } from 'zod'
|
|
16
|
+
|
|
17
|
+
/** Default moderation model; override with `OM_AI_MODERATION_MODEL`. */
|
|
18
|
+
export const DEFAULT_MODERATION_MODEL = 'omni-moderation-latest'
|
|
19
|
+
|
|
20
|
+
const DEFAULT_OPENAI_BASE_URL = 'https://api.openai.com/v1'
|
|
21
|
+
const REQUEST_TIMEOUT_MS = 3000
|
|
22
|
+
|
|
23
|
+
/** Per-category moderation outcome: whether it tripped and the model's score. */
|
|
24
|
+
export interface ModerationCategoryResult {
|
|
25
|
+
flagged: boolean
|
|
26
|
+
score: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Result of an input moderation check — flags + scores only, never content. */
|
|
30
|
+
export interface ModerationResult {
|
|
31
|
+
flagged: boolean
|
|
32
|
+
categories: Record<string, ModerationCategoryResult>
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Arguments for {@link ModerationService.checkInput}. */
|
|
36
|
+
export interface ModerationCheckInput {
|
|
37
|
+
/** The current user turn text to screen. */
|
|
38
|
+
text: string
|
|
39
|
+
/** Resolved OpenAI(-compatible) API key for the moderations endpoint. */
|
|
40
|
+
apiKey: string
|
|
41
|
+
/** Optional base URL override (defaults to the OpenAI public endpoint). */
|
|
42
|
+
baseURL?: string
|
|
43
|
+
/** Optional model override (defaults to {@link DEFAULT_MODERATION_MODEL}). */
|
|
44
|
+
model?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Port resolved from the DI container by the agent runtime. */
|
|
48
|
+
export interface ModerationService {
|
|
49
|
+
checkInput(input: ModerationCheckInput): Promise<ModerationResult>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Thrown by the runtime gate (NOT the service) when flagged input is rejected
|
|
54
|
+
* on an enabled/enforced surface. The SSE error path maps it to the
|
|
55
|
+
* `moderation_blocked` code; the raw categories are never sent to the client.
|
|
56
|
+
*/
|
|
57
|
+
export class AiModerationBlockedError extends Error {
|
|
58
|
+
readonly categories: Record<string, ModerationCategoryResult>
|
|
59
|
+
|
|
60
|
+
constructor(categories: Record<string, ModerationCategoryResult>) {
|
|
61
|
+
super('[internal] Input rejected by the content safety filter')
|
|
62
|
+
this.name = 'AiModerationBlockedError'
|
|
63
|
+
this.categories = categories
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Thrown by {@link ModerationService.checkInput} when the moderation endpoint
|
|
69
|
+
* is unreachable (timeout, network error, or non-2xx after one retry). The gate
|
|
70
|
+
* decides fail-closed (enforced surfaces) vs fail-open (opt-in surfaces).
|
|
71
|
+
*/
|
|
72
|
+
export class AiModerationUnavailableError extends Error {
|
|
73
|
+
readonly cause?: unknown
|
|
74
|
+
|
|
75
|
+
constructor(message: string, cause?: unknown) {
|
|
76
|
+
super(`[internal] ${message}`)
|
|
77
|
+
this.name = 'AiModerationUnavailableError'
|
|
78
|
+
this.cause = cause
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const moderationResponseSchema = z.object({
|
|
83
|
+
results: z
|
|
84
|
+
.array(
|
|
85
|
+
z.object({
|
|
86
|
+
flagged: z.boolean(),
|
|
87
|
+
categories: z.record(z.string(), z.boolean()),
|
|
88
|
+
category_scores: z.record(z.string(), z.number()),
|
|
89
|
+
}),
|
|
90
|
+
)
|
|
91
|
+
.min(1),
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
function mergeCategories(
|
|
95
|
+
categories: Record<string, boolean>,
|
|
96
|
+
scores: Record<string, number>,
|
|
97
|
+
): Record<string, ModerationCategoryResult> {
|
|
98
|
+
const merged: Record<string, ModerationCategoryResult> = {}
|
|
99
|
+
for (const [name, flagged] of Object.entries(categories)) {
|
|
100
|
+
merged[name] = { flagged, score: scores[name] ?? 0 }
|
|
101
|
+
}
|
|
102
|
+
return merged
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function postModeration(input: ModerationCheckInput): Promise<Response> {
|
|
106
|
+
const baseURL = (input.baseURL ?? DEFAULT_OPENAI_BASE_URL).replace(/\/$/, '')
|
|
107
|
+
const controller = new AbortController()
|
|
108
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS)
|
|
109
|
+
try {
|
|
110
|
+
return await fetch(`${baseURL}/moderations`, {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers: {
|
|
113
|
+
'Content-Type': 'application/json',
|
|
114
|
+
Authorization: `Bearer ${input.apiKey}`,
|
|
115
|
+
},
|
|
116
|
+
body: JSON.stringify({
|
|
117
|
+
model: input.model ?? DEFAULT_MODERATION_MODEL,
|
|
118
|
+
input: input.text,
|
|
119
|
+
}),
|
|
120
|
+
signal: controller.signal,
|
|
121
|
+
})
|
|
122
|
+
} finally {
|
|
123
|
+
clearTimeout(timer)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Builds the default {@link ModerationService}. Stateless and DI-friendly —
|
|
129
|
+
* registered in `di.ts` and overridable by downstream apps.
|
|
130
|
+
*/
|
|
131
|
+
export function createModerationService(): ModerationService {
|
|
132
|
+
return {
|
|
133
|
+
async checkInput(input: ModerationCheckInput): Promise<ModerationResult> {
|
|
134
|
+
let lastError: unknown
|
|
135
|
+
// One short-timeout attempt + one retry (R: outage mitigation).
|
|
136
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
137
|
+
let response: Response
|
|
138
|
+
try {
|
|
139
|
+
response = await postModeration(input)
|
|
140
|
+
} catch (error) {
|
|
141
|
+
lastError = error
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
if (response.status >= 500) {
|
|
145
|
+
lastError = new Error(`moderation endpoint returned ${response.status}`)
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
if (!response.ok) {
|
|
149
|
+
// 4xx is a configuration/credential error — not transient; do not retry.
|
|
150
|
+
throw new AiModerationUnavailableError(
|
|
151
|
+
`moderation endpoint returned ${response.status}`,
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
let json: unknown
|
|
155
|
+
try {
|
|
156
|
+
json = await response.json()
|
|
157
|
+
} catch (error) {
|
|
158
|
+
throw new AiModerationUnavailableError('moderation response was not valid JSON', error)
|
|
159
|
+
}
|
|
160
|
+
const parsed = moderationResponseSchema.safeParse(json)
|
|
161
|
+
if (!parsed.success) {
|
|
162
|
+
throw new AiModerationUnavailableError(
|
|
163
|
+
'moderation response did not match the expected schema',
|
|
164
|
+
parsed.error,
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
const result = parsed.data.results[0]
|
|
168
|
+
return {
|
|
169
|
+
flagged: result.flagged,
|
|
170
|
+
categories: mergeCategories(result.categories, result.category_scores),
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
throw new AiModerationUnavailableError(
|
|
174
|
+
'moderation endpoint unreachable after retry',
|
|
175
|
+
lastError,
|
|
176
|
+
)
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -26,6 +26,8 @@ const OPENAI_PRESET: OpenAICompatiblePreset = {
|
|
|
26
26
|
baseURL: undefined,
|
|
27
27
|
baseURLEnvKeys: ['OPENAI_BASE_URL'],
|
|
28
28
|
envKeys: ['OPENAI_API_KEY', 'OPENCODE_OPENAI_API_KEY'],
|
|
29
|
+
supportsInputModeration: true,
|
|
30
|
+
supportsEndUserSafetyIdentifier: true,
|
|
29
31
|
defaultModel: 'gpt-5-mini',
|
|
30
32
|
defaultModels: [
|
|
31
33
|
{
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SearchService } from '@open-mercato/search/service'
|
|
2
2
|
import type { SearchStrategyId, IndexableRecord } from '@open-mercato/search/types'
|
|
3
|
+
import { authorizeFeatures } from '@open-mercato/shared/security/featurePolicy'
|
|
3
4
|
import type { McpToolRegistry, McpToolDefinition } from './types'
|
|
4
5
|
import {
|
|
5
6
|
TOOL_ENTITY_ID,
|
|
@@ -294,23 +295,9 @@ export class ToolSearchService {
|
|
|
294
295
|
userFeatures: string[],
|
|
295
296
|
isSuperAdmin: boolean
|
|
296
297
|
): boolean {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
return requiredFeatures.every((required) => {
|
|
301
|
-
// Direct match
|
|
302
|
-
if (userFeatures.includes(required)) return true
|
|
303
|
-
// Wildcard match
|
|
304
|
-
if (userFeatures.includes('*')) return true
|
|
305
|
-
|
|
306
|
-
// Prefix wildcard match (e.g., 'customers.*' matches 'customers.people.view')
|
|
307
|
-
return userFeatures.some((feature) => {
|
|
308
|
-
if (feature.endsWith('.*')) {
|
|
309
|
-
const prefix = feature.slice(0, -2)
|
|
310
|
-
return required.startsWith(prefix + '.')
|
|
311
|
-
}
|
|
312
|
-
return false
|
|
313
|
-
})
|
|
298
|
+
return authorizeFeatures(requiredFeatures ?? [], {
|
|
299
|
+
grantedFeatures: userFeatures,
|
|
300
|
+
unrestricted: isSuperAdmin,
|
|
314
301
|
})
|
|
315
302
|
}
|
|
316
303
|
}
|