Skip to content

fix(ai): fence standalone AI prompts and mask profiler min/max by default - #1950

Open
koraysrn wants to merge 3 commits into
libredb:mainfrom
koraysrn:ai-prompt-fence-and-minmax-masking
Open

koraysrn wants to merge 3 commits into
libredb:mainfrom
koraysrn:ai-prompt-fence-and-minmax-masking

Conversation

@koraysrn

Copy link
Copy Markdown
Contributor

Description

The three standalone AI routes (describe-schema, explain, query-safety) put
database-derived text into the model prompt without any boundary, so a table
name, column comment or engine plan written by whoever can write to the
database was read by the model as the server's own instruction. This change
wraps that content in the same untrusted-content fence the agent runtime
already uses, and closes backlog entry B109.

Separately, the Data Profiler's AI summary sent a column's real min and max
values, masked only where the results grid happened to mask them. The summary
now masks every column a masking pattern matches by default, decoupled from
the grid's runtime toggle, with MaskingConfig.aiSummaryMinMax as the explicit
opt-out.

Type of Change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Code refactoring
  • Performance improvement
  • Test addition or update

Related Issue

Closes # (none filed; this closes backlog entry B109 and adds B129)

Changes Made

  • Added src/lib/ai/prompt-fence.ts with fenceForPrompt and fenceExplainPlan, reusing fenceUntrustedContent from src/lib/agent/untrusted-content.ts
  • Fenced the schema context in POST /api/ai/describe-schema
  • Fenced the query, EXPLAIN plan and schema context in POST /api/ai/explain
  • Fenced the query and schema context in POST /api/ai/query-safety
  • Added maskAiSummaryValue and the AiSummaryMinMax type plus MaskingConfig.aiSummaryMinMax, default "masked"
  • Made the DataProfiler AI summary mask min/max by default, independent of the grid toggle
  • Added tests/security/ai-prompt-fence.test.ts as a closed-list guard over src/app/api/ai
  • Registered security control 0.10 in docs/SECURITY.md and scripts/security-check.mjs
  • Deleted backlog entry B109, added B129 for the remaining browser-side masking gap, updated docs/AGENT.md citations and the D85 auth-mock count

Testing

  • I have tested this locally
  • I have added/updated tests
  • All existing tests pass

Test Environment

  • LibreDB Studio Version: 1.0.0 (working tree)
  • Browser: N/A (server and component tests only)
  • OS: Linux x64
  • Node.js/Bun Version: bun 1.4.2
  • Database Type: N/A (route and unit tests are mocked)

Results

  • bun run typecheck: pass
  • bun run lint: pass, 0 errors
  • bun run security:check: OK, 48 controls documented, 37 security tests accounted for
  • bun run test:unit: 1346 files, 48009 tests, 48006 pass, 3 skip
  • bun run test:api: 60 files, 1682 tests, all pass
  • bun run test:components: 160 files, 5993 tests, 5992 pass, 1 skip
  • bun run test:security: 37 files, 554 tests, all pass
  • bun run test:coverage: 1718 LCOV files merged
  • bun run coverage:check: 182143/182143 lines (100%)

Screenshots (if applicable)

N/A

Checklist

  • My code follows the project's code style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have updated the documentation accordingly
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • The required CI test job passes the 100% line-coverage gate (bun run test:coverage and bun run coverage:check)
  • If I changed src/lib/db/providers/, I updated the matching docs/providers/ documentation and tests/integration/db/ tests in the same PR (provider triad)
  • If the type-id has a non-null AGENT_STATEMENT_LANGUAGE row, I kept its agent leg in step
  • Any dependent changes have been merged and published

Additional Notes

Design choices and alternatives:

  1. Fence instead of sanitise. Sanitising would change meaning; the fence only labels where the server stopped talking, matching the agent boundary exactly.

  2. Default min/max masking is "masked", not "revealed". min and max are real column values leaving the machine, so the safe default is to mask, with aiSummaryMinMax "revealed" as the explicit operator opt-out.

  3. Server-side masking is left out of this PR. The profile route still returns raw values, so a host rendering its own profiler is not covered. That gap is recorded as backlog entry B129 rather than expanding this change.

Module size impact: the new shared module src/lib/ai/prompt-fence.ts is 2,162 bytes and pulls only src/lib/agent/untrusted-content.ts (7,863 bytes, zero imports, zero new npm dependencies).

@koraysrn
koraysrn force-pushed the ai-prompt-fence-and-minmax-masking branch from 0d6d737 to d4f006e Compare October 24, 2026 12:26
@koraysrn koraysrn changed the title Fence the standalone AI prompts and mask profiler min/max by default fix(ai): fence standalone AI prompts and mask profiler min/max by default Oct 24, 2026
…ault

The three standalone AI routes (describe-schema, explain, query-safety)
placed database-derived text directly into the model prompt, so a table
name, column comment or engine plan written by whoever can write to the
database was read as the server's own instruction. Add a shared
src/lib/ai/prompt-fence.ts that reuses the agent's fenceUntrustedContent
envelope, and wrap the schema context, query and EXPLAIN plan of all three
routes in it.

The Data Profiler's AI summary sent a column's real min and max values,
masked only where the results grid happened to mask them. Make the AI
summary mask every column a masking pattern matches by default, decoupled
from the grid's runtime toggle, and add MaskingConfig.aiSummaryMinMax so
an operator can restore the grid's decision with "revealed".
Add unit tests for fenceForPrompt and fenceExplainPlan (surface labels,
operation ids, marker neutralisation, plan serialisation), unit tests for
maskAiSummaryValue (default-on masking, revealed policy, repeated column
names, missing values, disabled patterns), and route-level tests that
assert the three AI routes fence their prompt content and neutralise a
marker written into the input.

Add tests/security/ai-prompt-fence.test.ts as a closed-list guard: it
enumerates src/app/api/ai and requires every AI route to call the shared
fence, and proves the B109 scenario at runtime. Update the DataProfiler
component tests so the AI summary masks min/max while the grid stays
unmasked, and so aiSummaryMinMax "revealed" restores the grid decision.
Close B109 by deleting its backlog entry and adding B129 for the remaining
browser-side masking gap. Update docs/AGENT.md citations, the
AGENT_DATA_FLOW.md egress table and the "other AI surfaces" section, and
register security control 0.10 in both docs/SECURITY.md and the
PROGRAMME_CONTROL_IDS witness in scripts/security-check.mjs, moving the
control-count assertion in tests/unit/security-check.test.ts from 47 to
48. Update the D85 auth-mock grep count to 55 for the new security test
file so the backlog structure guard stays in step.
@koraysrn
koraysrn force-pushed the ai-prompt-fence-and-minmax-masking branch from d4f006e to b3d3f66 Compare October 24, 2026 12:54
@codecov

codecov Bot commented Oct 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant