feat(workspace): route warehouse tools through the bound workspace's engine

Draft
RA
2mo ago
+2807-314

Issue for this PR

Closes #1155

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Stacked on #1154 — review that first; this PR is the commits on top.

When a bound workspace's engine attaches, the model gets two ways to do the same thing: the native warehouse tools over local keychain connections, and the engine's MCP tools over the workspace's SaaS connection. Nothing chose between them, so the model picked whichever description read better — and that pick decided which credentials ran the query and whether it was audited. Engine calls are audited server-side; native ones aren't.

This adds a per-session decision: shadow only what materialised and is attributable to the bound workspace; anything undetermined runs locally and says why; nothing is silent. Why it works:

  • Materialised, not declared — it reads the engine tool keys actually present, so a declared-but-absent integration shadows nothing.
  • Attributed — an IDE writes its entry unpinned, and such an engine serves whichever teammate is active there, changing at runtime. #1154 makes attach guarantee the pin; this re-checks it and refuses to shadow otherwise, reusing that PR's parser rather than writing a second one.
  • Per capability, not per warehouse type — the integrations aren't symmetric. Snowflake has execute, explain and table stats; BigQuery and Postgres have execute only. Keying on the type would send an explain on a BigQuery connection to a tool that doesn't exist, and there is deliberately no fallback.
  • Calls naming no warehouse are judged against the target they'd really reach, mirroring each handler's own resolution — and the registry connection behind dbt is weighed too, since dbt falls back there whenever it yields nothing.
  • The redirect names the exact engine tool, executes nothing, and is marked in metadata so telemetry can tell it from a real execution.
  • It runs after every native safety check — the hard deny on destructive statements and the write confirmation both guard things the engine side has no equivalent for.
  • --integrations=local turns it off for a session.

Two deliberate deviations: the guard needs a companion call to attach the fail-open notice, which a pre-execution check can't do; and I left an adjacent warehouse-type reporting bug alone, since fixing it changes a shipped telemetry field.

How did you verify your code works?

bun run typecheck and the workspace test suites pass; lint adds no new errors. 250 unit tests, including each decision rather than just the happy path.

End-to-end against a live workspace, engine serving 12 tools:

ScenarioOutcome
Shadow markingThe served connection is marked per capability; local DuckDB rows unmarked
Redirect, nothing executedRedirect returned, and no matching row in query history — proven with a positive control: the marker from a run that followed the redirect appears, the one that stopped at it does not
Control untouchedDuckDB connection runs normally
Default targetNo warehouse named → resolved the default and redirected, naming it
Model behaviourUnprompted, the model followed the redirect and ran the query through the engine tool
Write confirmationAn INSERT against the served connection stops at the permission gate, with no redirect
Escape hatch--integrations=local → plain listing, local execution

Not exercised: the asymmetry case needs a workspace serving only some capabilities (unit-tested instead); the dbt default path needs a bound dbt project; de-materialisation is held until other testing here finishes.

Automated review: four rounds plus one routed in, six findings, all real, all fixed — details in the Codex review log.

GitGuardian flags a masked placeholder in a help-text snapshot that only moved columns; it is present unchanged on the base commit and is not a credential.

Screenshots / recordings

n/a — CLI change, no UI.

Residuals

Shadowing turns itself off and the call runs locally. All fail open, all in the attach
machinery rather than in this change:

  • Attach re-validation window — no attribution while a session's memo re-validates.
    Being addressed in the attach layer.
  • Session-cache eviction — beyond 256 tracked sessions the oldest is dropped and
    re-derived on the next call. Bounded and deliberate.
  • Cross-session teardown — a redirect can name a client another session has removed.
    A tool-layer failure, not a query sent to the wrong warehouse.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR