Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .changeset/23839-settings-warning-banners.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'@objectstack/service-settings': patch
---

Clause-②: no

fix(service-settings): the File Storage, Knowledge and AI settings pages show their warnings as `info_banner` callouts with `bannerSeverity: 'warning'`, not as a sentence inside a subtitle or a field's help text

Four warnings were written into ordinary strings with a hand-typed warning glyph, so the admin read them as one more sentence of the page subtitle or of a toggle's help. Each one is now an `info_banner` row in the section it concerns, directly under the field it warns about:

- `storage` — `adapter_switch_warning`, under the Adapter select: switching adapter does not migrate existing files.
- `knowledge` — `adapter_switch_warning`, under the Adapter select: switching adapter does not migrate existing indices.
- `knowledge` — `enforce_rls_warning`, under the Enforce RLS on search toggle: disabling it skips the vector-store leakage safeguard.
- `ai` — `log_prompts_warning`, under the Log full prompts toggle: full prompts may leak PII.

The namespace description or field help of each keeps its non-warning text. The translations moved with them: in the `en`, `zh-CN`, `ja-JP` and `es-ES` bundles each warning sentence left the `description` / `help` copy and sits under `settings.NAMESPACE.banners.BANNER_ID.{title,text}`, with a translated title per banner. The existing Feature Flags `beta_notice` banner gained the same copies, so no shipped locale reads it in English any more.

No setting key, value, default or save behaviour changes. A host that renders `info_banner` rows from the manifest literals draws the English title and text; translated copy is read through `resolveSettingsBannerTitle` / `resolveSettingsBannerText` from `@objectstack/spec/system`.
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,29 @@ describe('aiSettingsManifest — temperature declares a window, not a grid (#655
});
});

describe('aiSettingsManifest — the log_prompts PII warning is an info_banner', () => {
const specs = aiSettingsManifest.specifiers as any[];
const sectionOf = (i: number) => specs.slice(0, i + 1).reverse().find((s) => s.type === 'group')?.id;

it('declares it as a warning-severity info_banner with an id, a title and a body', () => {
const banners = specs.filter((s) => s.type === 'info_banner');
expect(banners.map((b) => [b.id, b.bannerSeverity])).toEqual([['log_prompts_warning', 'warning']]);
expect(typeof banners[0].label).toBe('string');
expect(banners[0].label.length).toBeGreaterThan(0);
expect(banners[0].bannerText.length).toBeGreaterThan(0);
});

it('places it in the Observability section, directly under the log_prompts toggle', () => {
const i = specs.findIndex((s) => s.id === 'log_prompts_warning');
expect(sectionOf(i)).toBe('observability');
expect(specs[i - 1].key).toBe('log_prompts');
});

it('carries the warning glyph nowhere — the banner severity is the only warning marker', () => {
expect(JSON.stringify(aiSettingsManifest)).not.toContain('⚠');
});
});

describe('aiSettingsManifest — embedder section', () => {
it('exposes embedder_provider select with 10 options incl. none + 5 Chinese providers', () => {
const f = (aiSettingsManifest.specifiers as any[]).find(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,13 @@ const manifest = {
description: 'Persist prompt/response traces to sys_ai_trace for debugging and replay.' },
{ type: 'toggle', key: 'log_prompts', label: 'Log full prompts',
required: false, default: false,
description: 'Include rendered prompts (not just metadata) in trace rows. ⚠ May leak PII — disable in regulated environments.' },
description: 'Include rendered prompts (not just metadata) in trace rows.' },
// The PII warning is an `info_banner` row (`bannerSeverity: 'warning'`), not
// a sentence in the toggle's help; its copy lives under
// `settings.ai.banners.log_prompts_warning` in the locale bundles.
{ type: 'info_banner', id: 'log_prompts_warning', label: 'Prompts may contain PII',
required: false, bannerSeverity: 'warning',
bannerText: 'May leak PII — disable in regulated environments.' },

// ── Probe ────────────────────────────────────────────────────
{ type: 'action_button', id: 'test', label: 'Test connection',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,36 @@ describe('knowledgeSettingsManifest', () => {
});
});

describe('knowledgeSettingsManifest — both warnings are info_banner rows', () => {
const specs = knowledgeSettingsManifest.specifiers as any[];
const sectionOf = (i: number) => specs.slice(0, i + 1).reverse().find((s) => s.type === 'group')?.id;

it('declares exactly the two warning-severity banners, each with an id, a title and a body', () => {
const banners = specs.filter((s) => s.type === 'info_banner');
expect(banners.map((b) => [b.id, b.bannerSeverity])).toEqual([
['adapter_switch_warning', 'warning'],
['enforce_rls_warning', 'warning'],
]);
for (const b of banners) {
expect(typeof b.label, `${b.id} label`).toBe('string');
expect(b.label.length, `${b.id} label`).toBeGreaterThan(0);
expect(b.bannerText.length, `${b.id} bannerText`).toBeGreaterThan(0);
}
});

it('places each banner in the section it concerns, directly under the field it warns about', () => {
const at = (id: string) => specs.findIndex((s) => s.id === id);
expect(sectionOf(at('adapter_switch_warning'))).toBe('adapter');
expect(specs[at('adapter_switch_warning') - 1].key).toBe('adapter');
expect(sectionOf(at('enforce_rls_warning'))).toBe('permissions');
expect(specs[at('enforce_rls_warning') - 1].key).toBe('enforce_rls');
});

it('carries the warning glyph nowhere — the banner severity is the only warning marker', () => {
expect(JSON.stringify(knowledgeSettingsManifest)).not.toContain('⚠');
});
});

describe('knowledgeTestActionHandler', () => {
it('warns for memory adapter (no service to probe)', async () => {
const r = await knowledgeTestActionHandler({ values: { adapter: 'memory' } } as any);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,7 @@ const manifest = {
icon: 'BookOpen',
description:
'Vector-store backend for RAG / knowledge sources. ' +
'⚠ Switching adapter does NOT migrate existing indices — documents ' +
'indexed under the previous adapter become unreachable until ' +
're-indexed. The embedder is configured separately under AI.',
'The embedder is configured separately under AI.',
scope: 'global',
readPermission: 'manage_platform_settings',
writePermission: 'manage_platform_settings',
Expand All @@ -61,6 +59,14 @@ const manifest = {
{ value: 'ragflow', label: 'RAGFlow (external)' },
],
},
// Warnings are `info_banner` rows with `bannerSeverity: 'warning'`, never a
// sentence inside a description; each `id` addresses its copy under
// `settings.knowledge.banners.<id>` in the locale bundles.
{ type: 'info_banner', id: 'adapter_switch_warning', label: 'Existing indices are not migrated',
required: false, bannerSeverity: 'warning',
bannerText:
'Switching adapter does NOT migrate existing indices — documents indexed under the ' +
'previous adapter become unreachable until re-indexed.' },

// ── Turso / libSQL ────────────────────────────────────────────
{ type: 'group', id: 'turso', label: 'Turso / libSQL', required: false,
Expand Down Expand Up @@ -113,9 +119,12 @@ const manifest = {
{ type: 'group', id: 'permissions', label: 'Permissions', required: false },
{ type: 'toggle', key: 'enforce_rls', label: 'Enforce RLS on search',
required: false, default: true,
description:
'Re-check every hit against the caller\'s record-level permissions via IDataEngine. ' +
'⚠ Disabling skips the platform\'s unique safeguard against vector-store data leakage — leave on in production.' },
description: 'Re-check every hit against the caller\'s record-level permissions via IDataEngine.' },
{ type: 'info_banner', id: 'enforce_rls_warning', label: 'Leave RLS on in production',
required: false, bannerSeverity: 'warning',
bannerText:
'Disabling skips the platform\'s unique safeguard against vector-store data leakage — ' +
'leave on in production.' },

// ── Probe ─────────────────────────────────────────────────────
{ type: 'action_button', id: 'test', label: 'Test connection',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,29 @@ describe('storageSettingsManifest', () => {
});
});

describe('storageSettingsManifest — the adapter-switch warning is an info_banner', () => {
const specs = storageSettingsManifest.specifiers as any[];
const sectionOf = (i: number) => specs.slice(0, i + 1).reverse().find((s) => s.type === 'group')?.id;

it('declares it as a warning-severity info_banner with an id, a title and a body', () => {
const banners = specs.filter((s) => s.type === 'info_banner');
expect(banners.map((b) => [b.id, b.bannerSeverity])).toEqual([['adapter_switch_warning', 'warning']]);
expect(typeof banners[0].label).toBe('string');
expect(banners[0].label.length).toBeGreaterThan(0);
expect(banners[0].bannerText.length).toBeGreaterThan(0);
});

it('places it in the Backend section, directly under the adapter select it concerns', () => {
const i = specs.findIndex((s) => s.id === 'adapter_switch_warning');
expect(sectionOf(i)).toBe('adapter');
expect(specs[i - 1].key).toBe('adapter');
});

it('carries the warning glyph nowhere — the banner severity is the only warning marker', () => {
expect(JSON.stringify(storageSettingsManifest)).not.toContain('⚠');
});
});

describe('storageTestActionHandler (fallback)', () => {
it('rejects local adapter without local_root', async () => {
const r = await storageTestActionHandler({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@ const manifest = {
version: 1,
label: 'File Storage',
icon: 'HardDrive',
description:
'Backend used for attachments, exports, and user uploads. ' +
'⚠ Switching adapter does not migrate existing files — files ' +
'uploaded under the previous adapter become unreachable through ' +
'the new one.',
description: 'Backend used for attachments, exports, and user uploads.',
scope: 'global',
readPermission: 'manage_platform_settings',
writePermission: 'manage_platform_settings',
Expand All @@ -31,6 +27,15 @@ const manifest = {
{ value: 's3', label: 'S3 / S3-compatible' },
],
},
// The adapter-switch warning is a structured callout, not a sentence in
// the namespace description: `bannerSeverity` carries the severity an
// inline warning glyph used to spell by hand, and `id` addresses its copy
// under `settings.storage.banners.adapter_switch_warning` in each bundle.
{ type: 'info_banner', id: 'adapter_switch_warning', label: 'Existing files are not migrated',
required: false, bannerSeverity: 'warning',
bannerText:
'Switching adapter does not migrate existing files — files uploaded under the ' +
'previous adapter become unreachable through the new one.' },

{ type: 'group', id: 'local', label: 'Local', required: false,
visible: "${data.adapter === 'local'}" },
Expand Down
35 changes: 25 additions & 10 deletions packages/services/service-settings/src/translations/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,15 +216,15 @@ export const en: PlatformTranslationData = {
realtime_cursors: { label: 'Realtime cursors' },
inline_comments: { label: 'Inline comments' },
},
banners: {
beta_notice: { title: 'Heads up',
text: 'Beta features may change without notice. Pin via env vars (e.g. `OS_FEATURE_FLAGS_AI_ENABLED=true`) to lock for the whole deployment.' },
},
},

storage: {
title: 'File Storage',
description:
'Backend used for attachments, exports, and user uploads. ' +
'⚠ Switching adapter does not migrate existing files — files ' +
'uploaded under the previous adapter become unreachable through ' +
'the new one.',
description: 'Backend used for attachments, exports, and user uploads.',
groups: {
adapter: { title: 'Backend', description: 'Choose where uploaded files are stored.' },
local: { title: 'Local' },
Expand Down Expand Up @@ -255,6 +255,11 @@ export const en: PlatformTranslationData = {
actions: {
test: { label: 'Test connection' },
},
banners: {
adapter_switch_warning: { title: 'Existing files are not migrated',
text: 'Switching adapter does not migrate existing files — files uploaded under the '
+ 'previous adapter become unreachable through the new one.' },
},
},

ai: {
Expand Down Expand Up @@ -313,7 +318,7 @@ export const en: PlatformTranslationData = {
trace_enabled: { label: 'Record traces',
help: 'Persist prompt/response traces to sys_ai_trace for debugging and replay.' },
log_prompts: { label: 'Log full prompts',
help: 'Include rendered prompts (not just metadata) in trace rows. ⚠ May leak PII — disable in regulated environments.' },
help: 'Include rendered prompts (not just metadata) in trace rows.' },
embedder_provider: {
label: 'Provider',
options: {
Expand Down Expand Up @@ -344,13 +349,15 @@ export const en: PlatformTranslationData = {
test: { label: 'Test connection' },
test_embedder: { label: 'Test embedder' },
},
banners: {
log_prompts_warning: { title: 'Prompts may contain PII',
text: 'May leak PII — disable in regulated environments.' },
},
},

knowledge: {
title: 'Knowledge',
description:
'Vector-store backend for RAG / knowledge sources. ' +
'⚠ Switching adapter does NOT migrate existing indices.',
description: 'Vector-store backend for RAG / knowledge sources.',
groups: {
adapter: { title: 'Backend',
description: 'Choose where document chunks and their vectors are stored.' },
Expand Down Expand Up @@ -386,11 +393,19 @@ export const en: PlatformTranslationData = {
over_fetch: { label: 'Over-fetch multiplier',
help: 'Internal topK × overFetch candidates fetched so JS-side metadata filtering still has rows.' },
enforce_rls: { label: 'Enforce RLS on search',
help: 'Re-check every hit against the caller\'s record-level permissions. ⚠ Disabling skips the platform\'s unique safeguard.' },
help: 'Re-check every hit against the caller\'s record-level permissions.' },
},
actions: {
test: { label: 'Test connection' },
},
banners: {
adapter_switch_warning: { title: 'Existing indices are not migrated',
text: 'Switching adapter does NOT migrate existing indices — documents indexed under the '
+ 'previous adapter become unreachable until re-indexed.' },
enforce_rls_warning: { title: 'Leave RLS on in production',
text: 'Disabling skips the platform\'s unique safeguard against vector-store data leakage — '
+ 'leave on in production.' },
},
},
},
};
33 changes: 23 additions & 10 deletions packages/services/service-settings/src/translations/es-ES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,15 +340,15 @@ export const esES: PlatformTranslationData = {
realtime_cursors: { label: 'Cursores en tiempo real' },
inline_comments: { label: 'Comentarios en línea' },
},
banners: {
beta_notice: { title: 'Atención',
text: 'Las funciones beta pueden cambiar sin previo aviso. Fíjalas con variables de entorno (p. ej., `OS_FEATURE_FLAGS_AI_ENABLED=true`) para bloquearlas en todo el despliegue.' },
},
},

storage: {
title: 'Almacenamiento de archivos',
description:
'Backend usado para adjuntos, exportaciones y subidas de usuarios. ' +
'⚠ Cambiar de adaptador no migra los archivos existentes: los archivos ' +
'subidos con el adaptador anterior dejan de ser accesibles a través ' +
'del nuevo.',
description: 'Backend usado para adjuntos, exportaciones y subidas de usuarios.',
groups: {
adapter: { title: 'Backend', description: 'Elige dónde se almacenan los archivos subidos.' },
local: { title: 'Local' },
Expand Down Expand Up @@ -379,6 +379,11 @@ export const esES: PlatformTranslationData = {
actions: {
test: { label: 'Probar conexión' },
},
banners: {
adapter_switch_warning: { title: 'Los archivos existentes no se migran',
text: 'Cambiar de adaptador no migra los archivos existentes: los archivos '
+ 'subidos con el adaptador anterior dejan de ser accesibles a través del nuevo.' },
},
},

ai: {
Expand Down Expand Up @@ -462,7 +467,7 @@ export const esES: PlatformTranslationData = {
trace_enabled: { label: 'Registrar trazas',
help: 'Persiste las trazas de prompt/respuesta en sys_ai_trace para depuración y reproducción.' },
log_prompts: { label: 'Registrar prompts completos',
help: 'Incluye los prompts renderizados (no solo metadatos) en las filas de traza. ⚠ Puede filtrar PII: desactívalo en entornos regulados.' },
help: 'Incluye los prompts renderizados (no solo metadatos) en las filas de traza.' },
embedder_provider: {
label: 'Proveedor',
options: {
Expand Down Expand Up @@ -493,13 +498,15 @@ export const esES: PlatformTranslationData = {
test: { label: 'Probar conexión' },
test_embedder: { label: 'Probar embedder' },
},
banners: {
log_prompts_warning: { title: 'Los prompts pueden contener PII',
text: 'Puede filtrar PII: desactívalo en entornos regulados.' },
},
},

knowledge: {
title: 'Conocimiento',
description:
'Backend de almacén de vectores para RAG / fuentes de conocimiento. ' +
'⚠ Cambiar de adaptador NO migra los índices existentes.',
description: 'Backend de almacén de vectores para RAG / fuentes de conocimiento.',
groups: {
adapter: { title: 'Backend',
description: 'Elige dónde se almacenan los fragmentos de documento y sus vectores.' },
Expand Down Expand Up @@ -535,11 +542,17 @@ export const esES: PlatformTranslationData = {
over_fetch: { label: 'Multiplicador de sobre-obtención',
help: 'Se obtienen topK × overFetch candidatos internos para que el filtrado de metadatos en JS siga teniendo filas.' },
enforce_rls: { label: 'Aplicar RLS en la búsqueda',
help: 'Vuelve a comprobar cada resultado contra los permisos a nivel de registro del solicitante. ⚠ Desactivarlo omite la salvaguarda exclusiva de la plataforma.' },
help: 'Vuelve a comprobar cada resultado contra los permisos a nivel de registro del solicitante.' },
},
actions: {
test: { label: 'Probar conexión' },
},
banners: {
adapter_switch_warning: { title: 'Los índices existentes no se migran',
text: 'Cambiar de adaptador NO migra los índices existentes.' },
enforce_rls_warning: { title: 'Mantenlo activado en producción',
text: 'Desactivarlo omite la salvaguarda exclusiva de la plataforma.' },
},
},
},
};
Loading
Loading