Skip to content

fix(agent): exclude engine and extension objects from grounding capture - #625

Merged
cevheri merged 3 commits into
libredb:mainfrom
koraysrn:fix/b76-agent-grounding-exclusion
Sep 8, 2026
Merged

cevheri merged 3 commits into
libredb:mainfrom
koraysrn:fix/b76-agent-grounding-exclusion

Conversation

@koraysrn

@koraysrn koraysrn commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The aggregated grounding capture that fixed the wide-catalog row-cap refusal
(B52) then admitted the image's own extension objects. This closes B76 by
composing the PostgreSQL column read with the same exclusions the provider's
object browser already applies, plus a relation-level ownership test.

What changed

  • src/lib/agent/composed-sql.ts
    • Adds POSTGRES_SYSTEM_SCHEMAS (copied from the provider's SYSTEM_SCHEMAS),
      POSTGRES_EXTENSION_OWNED_SCHEMAS_SQL and
      POSTGRES_EXTENSION_OWNED_RELATIONS_SQL.
    • composePostgresCatalog now filters by the full engine-builtin schema list,
      every schema an extension created (pg_depend on pg_namespace,
      deptype = 'e'), and every relation an extension created (pg_depend on
      pg_class, deptype = 'e').
    • The relation/index/statistics reads are aligned to the full schema list; the
      column read stays the single source of object identity, so the others need
      no relation test.
  • tests/unit/lib/agent/composed-sql.test.ts — pins the SQL shape, guard
    acceptance, and that the agent's schema list cannot drift from the provider's.
  • tests/unit/lib/agent/context-snapshot.test.ts — one test per shape
    (TimescaleDB, Cloudberry, AlloyDB Omni).
  • docs/BACKLOG.md — B76 removed (B2–B75 · 21).
  • docs/AGENT.md — B76 deferral record removed.
  • docs/providers/postgres.md — agent grounding description now documents the
    three-layer exclusion.

Verification

Measured live on 2026-09-07 against the three compat images with two user
tables seeded:

Image Before After
TimescaleDB 46 2
Cloudberry 67 2
AlloyDB Omni 70 2

AlloyDB is the sharp case: the schema half alone leaves 51 objects (2 user
tables plus the 49 extension views installed into public); pg_depend reports
68 extension-owned relations, and the relation ownership test is what removes
them. A user's own views are never extension-owned, so they survive.

Checks

  • composed-sql.test.ts + context-snapshot.test.ts + backlog-structure.test.ts: 282 pass, 0 fail
  • bun run typecheck: pass
  • biome format (changed files): pass
  • bun run lint: 0 errors

@koraysrn koraysrn closed this Sep 7, 2026
@koraysrn
koraysrn deleted the fix/b76-agent-grounding-exclusion branch September 7, 2026 12:01
@koraysrn
koraysrn restored the fix/b76-agent-grounding-exclusion branch September 7, 2026 12:03
@koraysrn koraysrn reopened this Sep 7, 2026
@codecov

codecov Bot commented Sep 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@cevheri cevheri added documentation Improvements or additions to documentation enhancement New feature or request labels Sep 7, 2026

@cevheri cevheri left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the diff and ran the two touched test files locally: 189 pass, 0 fail.

Verified correct: the NOT IN composition has no AND/OR precedence hazard in any of the four statements, the copied POSTGRES_SYSTEM_SCHEMAS list is byte-identical to the provider's, the drift guard is non-vacuous, the B2-B75 - 21 count matches the 21 remaining ### B entries, and no stale B76 references are left.

Five comments below. The two in composed-sql.ts are behaviour: a missing fallback that breaks the agent path on Materialize, and an exclusion applied to one of the three catalog reads. The other three are a doc claim, a markdown break and a nominal test.

* question the name was standing in for, and `pg_depend` answers it directly.
* Same query the provider composes (`EXTENSION_OWNED_SCHEMAS_SQL`).
*/
const POSTGRES_EXTENSION_OWNED_SCHEMAS_SQL =

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The provider does not read pg_depend / pg_extension unguarded. It carries a fallback: isMissingExtensionCatalogError -> withoutExtensionOwnershipTest (src/lib/db/providers/sql/postgres.ts:458 and :484), because Materialize raises on those catalogs.

The agent path has no equivalent. composeCatalogRead returns one composed statement and there is no rewrite chain around it, so on a connection typed postgres that is actually Materialize (and postgres is in AGENT_EXECUTION_ENGINES) all four catalog reads fail: the context capture goes silently unavailable and inspect_schema returns a DB failure.

The live verification covered TimescaleDB, Cloudberry and AlloyDB Omni, all of which do have pg_depend, so it could not see this. Either mirror the provider's fallback or state the Materialize exclusion explicitly.

Comment thread src/lib/agent/composed-sql.ts Outdated
"FROM information_schema.columns " +
"WHERE table_schema NOT IN ('pg_catalog', 'information_schema')" +
`WHERE ${postgresSchemaExclusion("table_schema")}` +
` AND (table_schema, table_name) NOT IN (${POSTGRES_EXTENSION_OWNED_RELATIONS_SQL})` +

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The relation-ownership test is applied on the columns read only, which leaves the exclusion inconsistent across the reads a run makes:

  • kind=indexes and kind=statistics filter by schema alone and go to the model through readCatalog, not through buildPostgresTables
  • the object browser has no pg_class ownership test at all

Concretely, with PostGIS installed, public.spatial_ref_sys stays visible in the browser and in the index and statistics reads, but disappears from the column inventory. Same object, three different answers in one run. Worth either extending the test to the other kinds or recording why columns is the only place it belongs.

Comment thread docs/providers/postgres.md Outdated
escape, so `'a\'` would read as an unterminated literal.
schema/table selector and the server writes the `columns` statement itself, executed as
`sql.query.read` like any other statement. The model never supplies that SQL. The statement's
`WHERE` excludes the engine's own objects three ways, all copied from this provider's object

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"all copied from this provider's object browser rather than invented on the agent path" is not accurate for the third way. The browser has no pg_class extension-ownership test; that one is new on the agent path. As written the sentence also hides the divergence: the relation test covers the columns read and not indexes / statistics or the browser.

Comment thread docs/AGENT.md
noise; the object set was the same under the flat projection, which refused before any of it reached
a run.

**Settled as limits rather than as work.** The seven below have no entry in `docs/BACKLOG.md`, and

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The B76 removal took the blank line before this paragraph with it, so "Settled as limits rather than as work." now renders as a continuation of the B65 bullet instead of a new paragraph. One blank line above this line fixes it.

});

describe("captureContextSnapshot — the capture excludes each image's own extension objects (B76)", () => {
const SHAPES = [

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These three "per shape" cases pass no fixture rows and assert substrings of the same composed string, so nothing shape-specific is actually exercised. They pass identically if the shapes are swapped. If the intent is a test per shape, each needs rows that only that shape's exclusion removes.

The aggregated column read that fixed the wide-catalog row-cap refusal (B52) then admitted the image's own extension objects, so a grounded run on TimescaleDB, Cloudberry or AlloyDB Omni reasoned over an inventory that was mostly internal noise.

Compose the PostgreSQL column read with the same exclusions the provider's object browser already applies, plus a relation-level ownership test that reaches AlloyDB's public-installed extension views: the full engine-builtin schema list, every schema an extension created (pg_depend on pg_namespace, deptype = 'e'), and every relation an extension created (pg_depend on pg_class, deptype = 'e').

The relation, index and statistics reads are aligned to the full schema list; the column read stays the single source of object identity, so the others need no relation test (their rows attach to the column inventory).

Measured live on 2026-09-07 against the three compat images with two user tables seeded: 46 -> 2 object rows on TimescaleDB, 67 -> 2 on Cloudberry, 70 -> 2 on AlloyDB Omni; on AlloyDB the schema half alone leaves 51 and pg_depend reports 68 extension-owned relations.

Closes B76.
…consistency

Apply the relation ownership test to all four catalog reads, not only columns, so inspect_schema answers the same object set whatever kind it is asked for (PostGIS spatial_ref_sys stays out of indexes and statistics too).

Mirror the provider's Materialize fallback: a postgres-typed connection whose engine lacks pg_depend/pg_extension retries without the ownership tests, keeping the fixed schema list (withoutExtensionOwnershipTest).

Correct the provider doc (the pg_class ownership test is new on the agent path), restore the blank line before "Settled as limits" in docs/AGENT.md, and give the per-shape tests shape-specific rows rather than shared substrings.
@koraysrn
koraysrn force-pushed the fix/b76-agent-grounding-exclusion branch from 90a04da to 32b620f Compare September 8, 2026 10:27
@koraysrn

koraysrn commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor Author

Addressed all five review comments.

  1. Materialize fallback mirrored the provider's behaviour: withoutExtensionOwnershipTest strips both ownership tests, and readCatalog retries without them when a database error names pg_depend/pg_extension. A postgres-typed Materialize no longer loses all four reads.
  2. Cross-kind consistency the relation ownership test now applies to columns, relations, indexes and statistics, so inspect_schema answers the same object set whatever kind is asked; a PostGIS-owned spatial_ref_sys no longer splits across inventories.
  3. Provider doc corrected to say the pg_class ownership test is new on the agent path (the first two layers are the ones copied from the browser).
  4. docs/AGENT.md restored the blank line before "Settled as limits".
  5. Per-shape tests each shape now carries its own fixture rows and asserts that only that shape's exclusion removes them, instead of asserting shared substrings.

Verified: composed-sql + context-snapshot + backlog-structure tests 285 pass / 0 fail, typecheck clean, lint 0 errors. Re-verified live against TimescaleDB, Cloudberry and AlloyDB Omni - all four reads run on each (columns/indexes/statistics: 2 rows; relations: 0), including Cloudberry's MPP planner.

@koraysrn
koraysrn requested a review from cevheri September 8, 2026 10:40
@cevheri

cevheri commented Sep 8, 2026

Copy link
Copy Markdown
Member

All five addressed, and the two behaviour ones are the right shape: the retry sits in readCatalog, which both inspect_schema and the grounding capture funnel through, and the strip is pinned for all four kinds.

Two things before merge, the first one mine.

  1. Materialize does not raise on those catalogs. postgres.ts:454, four lines above the code I cited, lists it among the engines that accept the ownership test; the provider's fallback is there for engines nobody has run. My review note said otherwise, and it is now in three code comments and a test name. Rewording is enough. The fallback itself is worth having.

  2. The retry is untested. I replaced its body with a throw and all 8503 unit tests stayed green.
    The 100% gate and codecov/patch pass anyway because bun's lcov reports hits (56 and 29) on lines that never execute. One test that drives a database-error refusal naming pg_depend through readCatalog and asserts a second statement was sent would pin the trigger condition, which is the half composed-sql.test.ts cannot reach.

One aside, not a blocker: the live table reports 0 rows for relations on all three images, which is also what an unseeded foreign-key read returns, so that read's new exclusion is not demonstrated there

Drop the incorrect Materialize-raises claim: the provider lists Materialize among engines that accept the ownership test, and its fallback exists for PostgreSQL-wire engines nobody has run. Reword the two code comments and the test name to say that instead.

Add a readCatalog test that drives a pg_depend database error and asserts the retry sends a second statement without the ownership tests, pinning the trigger condition composed-sql.test.ts cannot reach.
@koraysrn

koraysrn commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

Both fixed.

  1. Wording — removed the incorrect "Materialize raises" claim from the two code comments and the test name. The fallback is now described for what it is: a safety net for PostgreSQL-wire engines the driver serves but nobody here has run, one without pg_depend/pg_extension. The fallback code is unchanged.
  2. Retry test — added readCatalog — the extension-ownership fallback retry: it drives a pg_depend database error through readCatalogForGrounding and asserts a second statement is sent without the ownership tests, plus a negative case showing an unrelated error is not retried. The trigger condition is now pinned at the tools layer.

Checks: 603 pass / 0 fail across the five touched suites, typecheck clean, lint 0 errors.

@cevheri

cevheri commented Sep 8, 2026

Copy link
Copy Markdown
Member

Both closed, and the retry is pinned now. Three mutations, each caught by the new test: removing the retry body fails the positive case, widening the trigger to fire on any database error fails the negative case, and making withoutExtensionOwnershipTest a no-op fails the positive case again.
That is the half composed-sql.test.ts could not reach.

The wording is right too. No attribution to Materialize survives anywhere under src/lib/agent/, and what replaced it matches the provider's own measured comment. Local gates clean here, all checks green.

One thing 👍 I am leaving as it is: the live table still reports 0 rows for relations on all three images, which is also what an unseeded foreign-key read returns, so that read's new exclusion is pinned by the unit test rather than by the measurement. Fine for B76.

Merging. Thanks for the three fast rounds.

@cevheri
cevheri merged commit 830d68f into libredb:main Sep 8, 2026
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants