@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.
Files changed (120) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +12 -29
  3. package/dist/modules/ai_assistant/__integration__/TC-AI-MODERATION-008-settings-and-audit.spec.js +80 -0
  4. package/dist/modules/ai_assistant/__integration__/TC-AI-MODERATION-008-settings-and-audit.spec.js.map +7 -0
  5. package/dist/modules/ai_assistant/__integration__/TC-AI-MODERATION-009-ui.spec.js +124 -0
  6. package/dist/modules/ai_assistant/__integration__/TC-AI-MODERATION-009-ui.spec.js.map +7 -0
  7. package/dist/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.js +30 -0
  8. package/dist/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.js.map +2 -2
  9. package/dist/modules/ai_assistant/api/ai/chat/route.js +7 -0
  10. package/dist/modules/ai_assistant/api/ai/chat/route.js.map +2 -2
  11. package/dist/modules/ai_assistant/api/moderation-flags/route.js +121 -0
  12. package/dist/modules/ai_assistant/api/moderation-flags/route.js.map +7 -0
  13. package/dist/modules/ai_assistant/api/settings/allowlist/route.js +10 -10
  14. package/dist/modules/ai_assistant/api/settings/allowlist/route.js.map +2 -2
  15. package/dist/modules/ai_assistant/api/settings/route.js +34 -13
  16. package/dist/modules/ai_assistant/api/settings/route.js.map +2 -2
  17. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js +121 -15
  18. package/dist/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.js.map +2 -2
  19. package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js +2 -2
  20. package/dist/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.js.map +2 -2
  21. package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.js +139 -0
  22. package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.js.map +7 -0
  23. package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.js +10 -0
  24. package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.js.map +7 -0
  25. package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.meta.js +26 -0
  26. package/dist/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.meta.js.map +7 -0
  27. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js +4 -4
  28. package/dist/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.js.map +2 -2
  29. package/dist/modules/ai_assistant/data/entities.js +42 -0
  30. package/dist/modules/ai_assistant/data/entities.js.map +2 -2
  31. package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js +3 -1
  32. package/dist/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.js.map +2 -2
  33. package/dist/modules/ai_assistant/data/repositories/AiModerationFlagRepository.js +44 -0
  34. package/dist/modules/ai_assistant/data/repositories/AiModerationFlagRepository.js.map +7 -0
  35. package/dist/modules/ai_assistant/di.js +6 -2
  36. package/dist/modules/ai_assistant/di.js.map +2 -2
  37. package/dist/modules/ai_assistant/events.js +6 -0
  38. package/dist/modules/ai_assistant/events.js.map +2 -2
  39. package/dist/modules/ai_assistant/i18n/de.json +42 -0
  40. package/dist/modules/ai_assistant/i18n/en.json +42 -0
  41. package/dist/modules/ai_assistant/i18n/es.json +42 -0
  42. package/dist/modules/ai_assistant/i18n/ko.json +527 -0
  43. package/dist/modules/ai_assistant/i18n/pl.json +42 -0
  44. package/dist/modules/ai_assistant/lib/agent-runtime.js +158 -5
  45. package/dist/modules/ai_assistant/lib/agent-runtime.js.map +2 -2
  46. package/dist/modules/ai_assistant/lib/ai-agent-definition.js.map +2 -2
  47. package/dist/modules/ai_assistant/lib/auth.js +6 -7
  48. package/dist/modules/ai_assistant/lib/auth.js.map +2 -2
  49. package/dist/modules/ai_assistant/lib/entity-graph.js +3 -3
  50. package/dist/modules/ai_assistant/lib/entity-graph.js.map +2 -2
  51. package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js +3 -0
  52. package/dist/modules/ai_assistant/lib/llm-adapters/anthropic.js.map +2 -2
  53. package/dist/modules/ai_assistant/lib/llm-adapters/openai.js +8 -1
  54. package/dist/modules/ai_assistant/lib/llm-adapters/openai.js.map +2 -2
  55. package/dist/modules/ai_assistant/lib/model-factory.js +6 -1
  56. package/dist/modules/ai_assistant/lib/model-factory.js.map +2 -2
  57. package/dist/modules/ai_assistant/lib/moderation-flag-recorder.js +43 -0
  58. package/dist/modules/ai_assistant/lib/moderation-flag-recorder.js.map +7 -0
  59. package/dist/modules/ai_assistant/lib/moderation-policy.js +24 -0
  60. package/dist/modules/ai_assistant/lib/moderation-policy.js.map +7 -0
  61. package/dist/modules/ai_assistant/lib/moderation.js +109 -0
  62. package/dist/modules/ai_assistant/lib/moderation.js.map +7 -0
  63. package/dist/modules/ai_assistant/lib/openai-compatible-presets.js +2 -0
  64. package/dist/modules/ai_assistant/lib/openai-compatible-presets.js.map +2 -2
  65. package/dist/modules/ai_assistant/lib/tool-search.js +4 -12
  66. package/dist/modules/ai_assistant/lib/tool-search.js.map +2 -2
  67. package/dist/modules/ai_assistant/migrations/Migration20260610134045_ai_assistant.js +19 -0
  68. package/dist/modules/ai_assistant/migrations/Migration20260610134045_ai_assistant.js.map +7 -0
  69. package/generated/entities/ai_agent_runtime_override/index.ts +1 -0
  70. package/generated/entities/ai_moderation_flag/index.ts +9 -0
  71. package/generated/entities.ids.generated.ts +2 -1
  72. package/generated/entity-fields-registry.ts +13 -1
  73. package/package.json +13 -12
  74. package/src/modules/ai_assistant/__integration__/TC-AI-MODERATION-008-settings-and-audit.spec.ts +126 -0
  75. package/src/modules/ai_assistant/__integration__/TC-AI-MODERATION-009-ui.spec.ts +161 -0
  76. package/src/modules/ai_assistant/__integration__/helpers/aiAssistantFixtures.ts +48 -0
  77. package/src/modules/ai_assistant/api/ai/chat/route.ts +10 -0
  78. package/src/modules/ai_assistant/api/moderation-flags/__tests__/route.test.ts +122 -0
  79. package/src/modules/ai_assistant/api/moderation-flags/route.ts +137 -0
  80. package/src/modules/ai_assistant/api/settings/__tests__/route.test.ts +58 -8
  81. package/src/modules/ai_assistant/api/settings/allowlist/route.ts +10 -12
  82. package/src/modules/ai_assistant/api/settings/route.ts +33 -10
  83. package/src/modules/ai_assistant/backend/config/ai-assistant/agents/AiAgentSettingsPageClient.tsx +161 -15
  84. package/src/modules/ai_assistant/backend/config/ai-assistant/allowlist/AiTenantAllowlistPageClient.tsx +2 -2
  85. package/src/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/AiModerationFlagsPageClient.tsx +186 -0
  86. package/src/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.meta.ts +24 -0
  87. package/src/modules/ai_assistant/backend/config/ai-assistant/moderation-flags/page.tsx +12 -0
  88. package/src/modules/ai_assistant/backend/config/ai-assistant/playground/AiPlaygroundPageClient.tsx +4 -4
  89. package/src/modules/ai_assistant/data/entities.ts +56 -0
  90. package/src/modules/ai_assistant/data/repositories/AiAgentRuntimeOverrideRepository.ts +4 -0
  91. package/src/modules/ai_assistant/data/repositories/AiModerationFlagRepository.ts +79 -0
  92. package/src/modules/ai_assistant/di.ts +5 -1
  93. package/src/modules/ai_assistant/events.ts +21 -0
  94. package/src/modules/ai_assistant/i18n/de.json +42 -0
  95. package/src/modules/ai_assistant/i18n/en.json +42 -0
  96. package/src/modules/ai_assistant/i18n/es.json +42 -0
  97. package/src/modules/ai_assistant/i18n/ko.json +527 -0
  98. package/src/modules/ai_assistant/i18n/pl.json +42 -0
  99. package/src/modules/ai_assistant/lib/__tests__/auth.test.ts +2 -5
  100. package/src/modules/ai_assistant/lib/__tests__/input-moderation-gate.test.ts +121 -0
  101. package/src/modules/ai_assistant/lib/__tests__/llm-adapters-anthropic.test.ts +13 -0
  102. package/src/modules/ai_assistant/lib/__tests__/llm-adapters-openai.test.ts +41 -0
  103. package/src/modules/ai_assistant/lib/__tests__/model-factory.test.ts +60 -0
  104. package/src/modules/ai_assistant/lib/__tests__/moderation-flag-recorder.test.ts +76 -0
  105. package/src/modules/ai_assistant/lib/__tests__/moderation-policy.test.ts +86 -0
  106. package/src/modules/ai_assistant/lib/__tests__/moderation.test.ts +114 -0
  107. package/src/modules/ai_assistant/lib/agent-runtime.ts +259 -0
  108. package/src/modules/ai_assistant/lib/ai-agent-definition.ts +10 -0
  109. package/src/modules/ai_assistant/lib/auth.ts +6 -15
  110. package/src/modules/ai_assistant/lib/entity-graph.ts +3 -3
  111. package/src/modules/ai_assistant/lib/llm-adapters/anthropic.ts +10 -0
  112. package/src/modules/ai_assistant/lib/llm-adapters/openai.ts +32 -1
  113. package/src/modules/ai_assistant/lib/model-factory.ts +33 -0
  114. package/src/modules/ai_assistant/lib/moderation-flag-recorder.ts +75 -0
  115. package/src/modules/ai_assistant/lib/moderation-policy.ts +77 -0
  116. package/src/modules/ai_assistant/lib/moderation.ts +179 -0
  117. package/src/modules/ai_assistant/lib/openai-compatible-presets.ts +2 -0
  118. package/src/modules/ai_assistant/lib/tool-search.ts +4 -17
  119. package/src/modules/ai_assistant/migrations/.snapshot-open-mercato.json +388 -16
  120. 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
- if (isSuperAdmin) return true
298
- if (!requiredFeatures?.length) return true
299
-
300
- return requiredFeatures.every((required) => {
301
- // Direct match
302
- if (userFeatures.includes(required)) return true
303
- // Wildcard match
304
- if (userFeatures.includes('*')) return true
305
-
306
- // Prefix wildcard match (e.g., 'customers.*' matches 'customers.people.view')
307
- return userFeatures.some((feature) => {
308
- if (feature.endsWith('.*')) {
309
- const prefix = feature.slice(0, -2)
310
- return required.startsWith(prefix + '.')
311
- }
312
- return false
313
- })
298
+ return authorizeFeatures(requiredFeatures ?? [], {
299
+ grantedFeatures: userFeatures,
300
+ unrestricted: isSuperAdmin,
314
301
  })
315
302
  }
316
303
  }