Skip to content
Open
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
2 changes: 1 addition & 1 deletion docs/AGENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -3018,7 +3018,7 @@ the role's own grants are the whole boundary (A3).
- Kubernetes namespace names reach the model as container names; this is accepted by decision.
- **B107**: Firestore is served by plan mode only: MCP is not offered for it (`MCP_EXPOSABLE.firestore` is false), and its provider implements no `queryReadOnly`, so agent execution and MCP `run_read_query` refuse it, because every read is billed and no read contract for the query chain under an agent's budget is designed yet; its plan-mode walk reads collection names and no document (`docs/providers/firestore.md` section 3.5).
- **B108**: RabbitMQ is served by plan mode and the MCP metadata tools only: its provider implements no `queryReadOnly`, so agent execution of a workflow that sends statements and MCP `run_read_query` refuse it; its plan-mode walk reads the virtual host `/` alone and counts the others it did not read (`docs/providers/rabbitmq.md` section 8).
- **B109**: the schema docs route puts table, collection and stream names in the prompt without the untrusted-content fence, for every provider; the route has no tools, so the exposure is a spoofed document on the user's screen.
- **B129**: the AI summary's min/max masking is still a browser-side control — the Data Profiler masks a column a pattern matches by default, but the profile route still returns the raw values, and a host rendering its own profiler is not covered; the three AI routes fence the text but do not redact it.
- **B110**: the answer card's inventory chip counts every captured object under the engine's entity noun, so a RabbitMQ capture of 17 streams, 6 queues and 18 exchanges reads "41 streams read".
- **B111**: DuckDB is offered `profile_table` by the database-assessment and data-analysis workflows, and DuckDB's engine overlay (`src/lib/agent/languages/sql/engines/duckdb.ts`) declares its `profileComposition` `"unsupported"`, so every `composeTableProfile` call answers `UNSUPPORTED_DIALECT`; it was kept as it is when the agent layer moved to workflow variants, so the SQL surface stayed byte-identical.
- **B112**: a run's persisted workflow type is not checked when its ledger is read, so a ledger naming a workflow Studio does not know reaches five workflow-keyed tables as `undefined`, and a budget read on it fails as a `TypeError` rather than a named refusal.
Expand Down
25 changes: 14 additions & 11 deletions docs/AGENT_DATA_FLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ if it only lists the good news.
| An **agent run** | The above, plus the rows of each read the model performed, up to 200 per read; engine error text; server-written refusals; server-minted ids | Same fence |
| Any **run that continues a conversation**, in either mode, on any engine | **In addition to everything above**: the earlier steps' objectives — *your own earlier questions*, capped at 200 characters each — and the most recent step's report, which is a model's claims about your data. No row of any result, and no earlier step's report but the newest. Sent only when the rail attaches a previous run's id, which it does for a follow-up on the same connection; a run that starts its own conversation sends no such message. Bounded to 4000 characters by default and switchable off with `LIBREDB_AGENT_THREAD_CONTEXT=false`. See [the conversation](#2a-the-conversation-when-a-run-continues-one--fenced) | Same fence, identified as `operation agent/thread`: it is prose a user and a model wrote, and none of it is the server's voice |
| An **agent run opened as Operate** | Your objective; a **schema inventory reduced to its own names and index names** (no column names, no column types, no relations graph), read whichever of the two ways that engine is read and named with whichever noun that engine's provider declares — tables, collections, datasources, key patterns; in Plan mode the engine's **row-count estimates** for those same tables where the engine holds any — PostgreSQL and SQLite — and nothing per column; and the rows of each curated reading — which, for the `sessions` and `slow-queries` kinds, include **other database users' in-flight statement text and their database usernames**. See [the operations workflow](#5a-the-operations-workflow-what-a-curated-reading-sends) | Same fence: the inventory and every reading's rows are database content and are fenced |
| `POST /api/ai/explain` | Your statement, the EXPLAIN plan, the schema context the browser holds, the engine type | No |
| `POST /api/ai/query-safety` | Your statement, a filtered schema context, the engine type. Never an etcd, Milvus, Qdrant, InfluxDB (InfluxQL), Oxia, S3-compatible object storage or Kubernetes statement: the dialog posts nothing for an engine whose destructive vocabulary declares `safetyAnalysis: false`, which those seven do, because an etcd `put` carries its value in the statement (#1089), a Milvus or Qdrant request carries vectors, filters and values, an InfluxQL statement carries tag values and filters, an Oxia command names keys whose paths name Pulsar tenants and namespaces, an S3 command names buckets and object keys, and a kubectl statement names namespaces and objects | No |
| `POST /api/ai/describe-schema` | A schema context. From the **Data Profiler** that context includes a per-column `min=` and `max=`, which are **real column values**, except on a column the data masking configuration masks for you, which is sent masked | No |
| `POST /api/ai/explain` | Your statement, the EXPLAIN plan, the schema context the browser holds, the engine type | Yes — the server fences the query, plan and schema before the prompt (`src/lib/ai/prompt-fence.ts`) |
| `POST /api/ai/query-safety` | Your statement, a filtered schema context, the engine type. Never an etcd, Milvus, Qdrant, InfluxDB (InfluxQL), Oxia, S3-compatible object storage or Kubernetes statement: the dialog posts nothing for an engine whose destructive vocabulary declares `safetyAnalysis: false`, which those seven do, because an etcd `put` carries its value in the statement (#1089), a Milvus or Qdrant request carries vectors, filters and values, an InfluxQL statement carries tag values and filters, an Oxia command names keys whose paths name Pulsar tenants and namespaces, an S3 command names buckets and object keys, and a kubectl statement names namespaces and objects | Yes — the server fences the query and schema before the prompt (`src/lib/ai/prompt-fence.ts`) |
| `POST /api/ai/describe-schema` | A schema context. From the **Data Profiler** that context includes a per-column `min=` and `max=`, which are **real column values**; they are sent masked for every column a masking pattern matches, regardless of the grid's toggle, unless `aiSummaryMinMax: "revealed"` restores the grid's decision | Yes — the server fences the schema before the prompt (`src/lib/ai/prompt-fence.ts`) |
| Everything else in the **running server** | Every outbound connection goes to a host **you** configured — your databases, your OIDC issuer when `NEXT_PUBLIC_AUTH_PROVIDER=oidc` (`src/lib/oidc.ts`), and the model provider above. The one host `src/` names itself is OpenAI's default base URL, reached only if you set `LLM_PROVIDER=openai` (`src/lib/llm/utils/config.ts:23`) | — |
| The **`npx` launcher**, before the server exists | The release tarball and `SHA256SUMS` from a hard-coded `github.com` URL (`bin/lib/launcher-utils.mjs:87`, from `bin/studio.js:218-219`). Once per version, cached in `~/.libredb-studio/`; no other install path uses it | — |

Expand Down Expand Up @@ -657,26 +657,29 @@ output cap, and what it sends is one objective — see
## The other AI surfaces

The agent is not the only thing that can reach a model. **Four surfaces remain, over three routes** —
`describe-schema` is reached from two of them — each on an explicit user action, and none of them
fences what it sends:
`describe-schema` is reached from two of them — each on an explicit user action. Each of them is
now fenced **on the server** (`src/lib/ai/prompt-fence.ts`, which reuses the agent's
`src/lib/agent/untrusted-content.ts`), so a name written into the database cannot be read as the
server's own instruction:

| Feature | Route | What the browser sends | Call site |
| --- | --- | --- | --- |
| Visual EXPLAIN's AI explanation | `POST /api/ai/explain` | `query`, `explainPlan`, `schemaContext`, `databaseType` | `src/components/VisualExplain.tsx:486-497` |
| Query safety dialog | `POST /api/ai/query-safety` | `query`, a filtered `schemaContext`, `databaseType`; nothing at all for an etcd, Milvus, Qdrant, InfluxDB (InfluxQL), Oxia, S3-compatible object storage or Kubernetes statement (`vocabularySendsToModel`, `src/lib/db/destructive-commands.ts`) | `src/components/QuerySafetyDialog.tsx:221-231` |
| Database documentation | `POST /api/ai/describe-schema` | A schema string built from table names, row counts and column definitions | `src/components/DatabaseDocs.tsx:61-68` |
| Data Profiler's AI summary | `POST /api/ai/describe-schema` | Per column: null percent, distinct count, **`min=` and `max=`**, masked where the grid would mask the column | `src/components/DataProfiler.tsx:141-176` |
| Data Profiler's AI summary | `POST /api/ai/describe-schema` | Per column: null percent, distinct count, **`min=` and `max=`**, masked for every column a masking pattern matches regardless of the grid's toggle, unless `aiSummaryMinMax: "revealed"` restores the grid's decision | `src/components/DataProfiler.tsx:141-176` |

**That last row is the one to read carefully.** `/api/db/profile` computes `MIN(col)` and
`MAX(col)` per column (`src/app/api/db/profile/route.ts:66`), and the Data Profiler puts
both into the context it sends for an AI summary. Those are **real values out of your columns**:
the smallest and largest of each profiled column, in the column's own type. It is the sharpest difference between the
two profiling surfaces in this product: the agent's `profile_table` was built so that no value can
leave, and the legacy profiler's AI summary sends two per column. The one exception is a column the
data masking configuration masks: in the standalone app the profiler decides that exactly as the
results grid does, for the same role and switch, and sends that column's two values masked (#1421);
the embedded workspace's profiler masks the built-in patterns (email, password, card, token and the
rest), in the summary it hands a host's `onDescribeSchema` too. Masking is applied in the
leave, and the legacy profiler's AI summary sends two per column. The summary now masks those two by
default (`maskAiSummaryValue` in `src/lib/data-masking.ts`): a column a masking pattern matches is
sent masked even while the grid shows it in clear, and only `aiSummaryMinMax: "revealed"` restores
the grid's own decision for the summary. The embedded workspace's profiler masks the built-in
patterns (email, password, card, token and the rest), in the summary it hands a host's
`onDescribeSchema` too. Masking is applied in the
browser, so it narrows what a model is shown; it is not a server-side control over what the profile
route returns.

Expand Down
25 changes: 12 additions & 13 deletions docs/BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ None of it is a GitHub issue.
- [Security Phase 3 deferrals](#security-phase-3-deferrals) — K4-K8 · 5
- [Security scanner triage](#security-scanner-triage) — SCAN1-SCAN2 · 2
- [Agent M1 deferrals (#328)](#agent-m1-deferrals-328) — A2–A8 · 5
- [Agent M2 deferrals (#329)](#agent-m2-deferrals-329) — B2-B128 · 61
- [Agent M2 deferrals (#329)](#agent-m2-deferrals-329) — B2-B129 · 61
- [Passkey deferrals (#785)](#passkey-deferrals-785) — PK1-PK9 · 8
- [MCP server deferrals (#246)](#mcp-server-deferrals-246) — MCP1-MCP3 · 3
- [Governance follow-ups](#governance-follow-ups) — GOV1-GOV30 · 30
Expand Down Expand Up @@ -1174,11 +1174,11 @@ test pins the behaviour that was chosen.

### D85. The `@/lib/auth` mock is hand-copied across a layer, untyped, and already misses four exports

`grep -rl 'mock.module("@/lib/auth"' tests/` returns exactly 54 hits, re-measured 2026-10-17.
`grep -rl 'mock.module("@/lib/auth"' tests/` returns exactly 55 hits, re-measured 2026-10-17.
Twenty of them spread the real module and replace one function (`{ ...realAuth, getSession: mockGetSession }`, the agent routes' pattern).
Thirty-one write out the same five-key object - `getSession`, `signJWT`, `verifyJWT`, `login`, `logout` - down to the same `mock(async () => "mock-token")` for a token nothing reads, and one of those thirty-one is `tests/helpers/object-edit-route-harness.ts`, a shared harness that could have been the factory and copied the stub instead.
Thirty-two write out the same five-key object - `getSession`, `signJWT`, `verifyJWT`, `login`, `logout` - down to the same `mock(async () => "mock-token")` for a token nothing reads, and one of those thirty-two is `tests/helpers/object-edit-route-harness.ts`, a shared harness that could have been the factory and copied the stub instead.
The remaining three write a shorter stub of their own, two with two keys and one with a single `getSession`.
The count moved from 53 to 54 between 2026-10-12 and 2026-10-14: `tests/api/db-test-connection-modbus.test.ts`, added by `1ab0d88ff`, is the fifty-fourth, and it is a five-key copy down to the `mock(async () => "mock-token")` for a token nothing reads. The copying this entry describes continued after the entry was written, which is the argument for the factory rather than against it.
The count moved from 53 to 54 between 2026-10-12 and 2026-10-14: `tests/api/db-test-connection-modbus.test.ts`, added by `1ab0d88ff`, is the fifty-fourth, and it is a five-key copy down to the `mock(async () => "mock-token")` for a token nothing reads. `tests/security/ai-prompt-fence.test.ts`, added with the AI route fence work, is the fifty-fifth and another five-key copy. The copying this entry describes continued after the entry was written, which is the argument for the factory rather than against it.

`src/lib/auth.ts` exports nine names. The four that all but one hand-written stub lack are
`shouldMarkCookieSecure`, `resetCookieSecurityWarning`, `readCookieSecureOverride` and
Expand Down Expand Up @@ -7739,15 +7739,6 @@ Found 2026-10-12 while designing the RabbitMQ provider.

**Done when:** a stream read request is designed as an agent statement with its own read policy and bounds, `queryReadOnly` serves it, agent execution runs it and MCP runs it through the non-SQL arm B117 records, with tests; cited in `docs/AGENT.md` beside B105.

### B109. The schema docs route puts database-written names in the prompt without the untrusted-content fence

`POST /api/ai/describe-schema` (`src/app/api/ai/describe-schema/route.ts`) places the schema text the client built, which holds names anyone with write access to the database or broker chose, in the prompt with no untrusted-content fence, for every provider; the Docs tab posts it.
The route has no tools, so the exposure is a spoofed document on the user's own screen, not an action.

Found 2026-10-12 while designing the RabbitMQ provider.

**Done when:** the route fences the names as plan mode and MCP do (`src/lib/agent/untrusted-content.ts`), with a test that a table named as an instruction is found only inside the fence.

### B110. The answer card counts every inventoried object under the engine's entity noun

The inventory chip of `src/components/agent/AnswerCard.tsx` prints the capture's object count with `capture.noun`, the engine's entity noun, so on an engine whose inventory holds several kinds the chip names them all by one kind.
Expand Down Expand Up @@ -7932,6 +7923,14 @@ Found 2026-10-19 while widening the engine overlays (#1821).

**Done when:** the audited call and the drafted statement carry the connection's engine, every reader of the adapter and of the catalog advice resolves them through the run resolver, `adapter` and `catalogAdvice` join `ENGINE_OVERRIDABLE_SERVER_MEMBERS`, and a test proves an engine override of each reaches the fingerprint, the draft and the refusal while a ledger written before it keeps its step ids; cited in `docs/AGENT.md`.

### B129. The AI summary's min/max masking is still a browser-side control

The Data Profiler now masks the min/max it sends for an AI summary by default (`maskAiSummaryValue` in `src/lib/data-masking.ts`, applied in `src/components/DataProfiler.tsx`), independent of the grid's runtime toggle, unless `aiSummaryMinMax: "revealed"` is set. That masking is still applied in the browser, exactly as the grid's is: the profile route returns the raw values, and a host that renders its own profiler, or calls `onDescribeSchema` outside the component, is not covered. The three AI routes fence the text but do not redact it.

Found 2026-10-24 while closing B109.

**Done when:** the profile route or the AI route masks min/max server-side for the standalone app and the embedded workspace's `onDescribeSchema` path, or a written reason it cannot is recorded here; cited in `docs/AGENT_DATA_FLOW.md`.

## Passkey deferrals (#785)

Each was decided when passkey sign-in was designed, not overlooked.
Expand Down
Loading
Loading