From cc9f8afe1c979b93ca3e49e99ab5798d58361480 Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Fri, 28 Aug 2026 03:16:50 +0800 Subject: [PATCH 01/10] feat(workspace): route warehouse tools through the bound workspace's engine Shadow a native warehouse capability only when the bound workspace's engine materialised the matching tool and attach attests the engine is its own (outcome `attached` plus the configured pin); redirect to the exact engine tool after the native safety checks; fail open with a reason otherwise. `--integrations=local` turns it off. Restacked onto the derived-overlay attach; the allowlist is exactly `attached`. --- packages/core/src/flag/flag.ts | 8 + .../altimate/native/connections/register.ts | 166 ++- .../altimate/native/connections/registry.ts | 23 + .../src/altimate/tools/schema-inspect.ts | 22 +- .../src/altimate/tools/sql-execute.ts | 30 +- .../src/altimate/tools/sql-explain.ts | 28 +- .../src/altimate/tools/warehouse-list.ts | 25 +- .../src/altimate/workspace/precedence.ts | 763 +++++++++++++ packages/opencode/src/index.ts | 11 + packages/opencode/src/session/prompt.ts | 24 +- packages/opencode/src/session/tools.ts | 19 +- .../test/altimate/default-target.test.ts | 188 ++++ .../altimate/precedence-guard-order.test.ts | 198 ++++ .../altimate/workspace/precedence.test.ts | 1000 +++++++++++++++++ .../__snapshots__/help-snapshots.test.ts.snap | 632 ++++++----- 15 files changed, 2822 insertions(+), 315 deletions(-) create mode 100644 packages/opencode/src/altimate/workspace/precedence.ts create mode 100644 packages/opencode/test/altimate/default-target.test.ts create mode 100644 packages/opencode/test/altimate/precedence-guard-order.test.ts create mode 100644 packages/opencode/test/altimate/workspace/precedence.test.ts diff --git a/packages/core/src/flag/flag.ts b/packages/core/src/flag/flag.ts index 951be852f5..2b6eb4a8d1 100644 --- a/packages/core/src/flag/flag.ts +++ b/packages/core/src/flag/flag.ts @@ -76,6 +76,14 @@ export const Flag = { get ALTIMATE_WORKSPACE() { return truthy("ALTIMATE_WORKSPACE") }, + /** + * Workspace precedence escape hatch, set by `--integrations=local`. When on, the + * native warehouse tools serve every local connection themselves and nothing is + * redirected to the bound workspace's integration engine, for the whole session. + */ + get ALTIMATE_INTEGRATIONS_LOCAL() { + return process.env["ALTIMATE_INTEGRATIONS"]?.toLowerCase() === "local" + }, // altimate_change end // Evaluated at access time (not module load) because tests, the CLI, and diff --git a/packages/opencode/src/altimate/native/connections/register.ts b/packages/opencode/src/altimate/native/connections/register.ts index 5b32cb5608..9be834daba 100644 --- a/packages/opencode/src/altimate/native/connections/register.ts +++ b/packages/opencode/src/altimate/native/connections/register.ts @@ -43,22 +43,23 @@ import { Telemetry } from "../../../telemetry" /** Cached dbt adapter (lazily created on first use). */ let dbtAdapter: any | null | undefined = undefined +// altimate_change start — single-flight adapter creation. +// Two concurrent `warehouse`-less calls used to construct the adapter twice, and +// construction is expensive: it spawns a detached Python bridge, rebuilds the +// manifest and starts file watchers. Share one in-flight promise instead. The +// permanent negative cache (`dbtAdapter === null`) is unchanged — a project that +// becomes valid mid-session is still not retried. +let dbtAdapterInflight: Promise | undefined + /** - * Try to execute SQL via dbt's adapter (which uses profiles.yml for connection). - * Returns null if dbt is not available or not configured — caller should fall back - * to native driver. - * - * This is the preferred path when working in a dbt project: dbt already knows - * how to connect, so users don't need to configure a separate connection. + * Resolve the dbt adapter for this project, or null when there is no usable dbt + * project. Idempotent, single-flight, and permanently negative once it has failed. */ -async function tryExecuteViaDbt( - sql: string, - limit?: number, -): Promise { - // Only attempt dbt once — if it's not configured, don't retry on every query - if (dbtAdapter === null) return null +async function ensureDbtAdapter(): Promise { + if (dbtAdapter !== undefined) return dbtAdapter + if (dbtAdapterInflight) return dbtAdapterInflight - if (dbtAdapter === undefined) { + dbtAdapterInflight = (async () => { try { // Check if dbt config exists const { read: readDbtConfig } = await import( @@ -83,12 +84,106 @@ async function tryExecuteViaDbt( // Create the adapter const { create } = await import("../../../../../dbt-tools/src/adapter") dbtAdapter = await create(dbtConfig) + return dbtAdapter } catch { // dbt-tools not available or config invalid — fall back to native dbtAdapter = null return null + } finally { + dbtAdapterInflight = undefined + } + })() + return dbtAdapterInflight +} + +/** Where a `warehouse`-less call would actually go. */ +export type DefaultTarget = + | { + source: "dbt" + type?: string + /** Where execution actually lands if the dbt attempt yields nothing. `sql.execute` + * falls back to the registry not only when dbt is absent, but whenever + * `tryExecuteViaDbt` returns null — an unrecognised result shape, or any throw. + * A caller deciding anything about this call has to consider both targets. */ + fallback?: { type: string; name: string } } + | { source: "registry"; type: string; name: string } + | { source: "none" } + +/** + * Resolve the target a call with no `warehouse` would reach, mirroring the resolution + * the handler for `op` performs itself — so a caller inspecting the target ahead of + * time cannot disagree with where execution actually lands. + * + * Only `sql.execute` consults dbt. `sql.explain` and `schema.inspect` are + * registry-only, and must stay that way: resolving them through dbt would drag + * adapter construction (Python bridge, manifest rebuild, file watchers) onto paths + * that never touch dbt today. + * + * For the dbt path the reported `type` is the project's adapter type, which is what + * decides *which* warehouse the profile reaches. It is left undefined when it cannot + * be established — the adapter coalesces an unknown type to the string "unknown", and + * the call can throw before initialisation completes. + */ +export async function resolveDefaultTarget( + op: "sql.execute" | "sql.explain" | "schema.inspect", +): Promise { + if (op === "sql.execute") { + const adapter = await ensureDbtAdapter() + if (adapter) { + let type: string | undefined + try { + const reported = adapter.getAdapterType?.() + if (typeof reported === "string" && reported && reported.toLowerCase() !== "unknown") type = reported + } catch { + // Adapter not initialised far enough to answer; leave the type undetermined. + } + if (!type) type = await adapterTypeFromManifest() + const warehouses = Registry.list().warehouses + const fallback = warehouses.length > 0 ? { type: warehouses[0].type, name: warehouses[0].name } : undefined + return { source: "dbt", type, fallback } + } + } + + const warehouses = Registry.list().warehouses + if (warehouses.length === 0) return { source: "none" } + return { source: "registry", type: warehouses[0].type, name: warehouses[0].name } +} + +/** Fallback adapter type: the dbt manifest records it as `metadata.adapter_type`. */ +async function adapterTypeFromManifest(): Promise { + try { + const { read: readDbtConfig } = await import("../../../../../dbt-tools/src/config") + const dbtConfig = await readDbtConfig() + if (!dbtConfig) return undefined + const fs = await import("fs") + const path = await import("path") + const manifestPath = path.join(dbtConfig.projectRoot, "target", "manifest.json") + if (!fs.existsSync(manifestPath)) return undefined + const raw = JSON.parse(fs.readFileSync(manifestPath, "utf8")) + const adapter = String(raw?.metadata?.adapter_type ?? "").toLowerCase() + return adapter || undefined + } catch { + return undefined } +} +// altimate_change end + +/** + * Try to execute SQL via dbt's adapter (which uses profiles.yml for connection). + * Returns null if dbt is not available or not configured — caller should fall back + * to native driver. + * + * This is the preferred path when working in a dbt project: dbt already knows + * how to connect, so users don't need to configure a separate connection. + */ +async function tryExecuteViaDbt( + sql: string, + limit?: number, +): Promise { + // altimate_change start — share the single-flight creation path with resolveDefaultTarget + if (!(await ensureDbtAdapter())) return null + // altimate_change end try { const raw = limit @@ -146,6 +241,9 @@ async function tryExecuteViaDbt( /** Reset dbt adapter (for testing). */ export function resetDbtAdapter(): void { dbtAdapter = undefined + // altimate_change — drop any in-flight creation too, or a test that resets mid-flight + // would still receive the previous adapter. + dbtAdapterInflight = undefined } // --------------------------------------------------------------------------- @@ -369,6 +467,20 @@ register("sql.execute", async (params: SqlExecuteParams): Promise = { trino: "@altimateai/drivers/trino", } +// altimate_change start — canonical driver identity for workspace precedence. +/** + * Collapse a `config.type` onto the canonical name of the driver that serves it, so + * callers reasoning about "which database is this really" cannot be fooled by an + * alias: `postgresql` and `postgres` are one driver, as are `mariadb`/`mysql`, + * `mssql`/`fabric`/`sqlserver`, and `mongo`/`mongodb`. + * + * Derived by inverting `DRIVER_MAP` rather than restating it, so a type added there + * cannot silently desync from everything keyed on driver identity. Returns null for a + * type no driver serves. + * + * `redshift` maps to its own driver and therefore stays distinct from `postgres`: a + * different service with different credentials and endpoints, where Postgres + * wire-compatibility is an implementation detail rather than an identity. + */ +export function canonicalType(type: string | undefined | null): string | null { + if (!type) return null + const driverPath = DRIVER_MAP[type.toLowerCase()] + if (!driverPath) return null + return driverPath.slice(driverPath.lastIndexOf("/") + 1) +} +// altimate_change end + async function createConnector(name: string, config: ConnectionConfig): Promise { const driverPath = DRIVER_MAP[config.type.toLowerCase()] if (!driverPath) { diff --git a/packages/opencode/src/altimate/tools/schema-inspect.ts b/packages/opencode/src/altimate/tools/schema-inspect.ts index cbdeff815c..5f67fa554e 100644 --- a/packages/opencode/src/altimate/tools/schema-inspect.ts +++ b/packages/opencode/src/altimate/tools/schema-inspect.ts @@ -6,6 +6,9 @@ import type { SchemaInspectResult } from "../native/types" import { PostConnectSuggestions } from "./post-connect-suggestions" // altimate_change end import { isRecord, normalizeError } from "./response-normalization" +// altimate_change start — workspace precedence +import * as Precedence from "../workspace/precedence" +// altimate_change end export const SchemaInspectTool = Tool.define("schema_inspect", { description: "Inspect database schema — list columns, types, and constraints for a table.", @@ -15,6 +18,14 @@ export const SchemaInspectTool = Tool.define("schema_inspect", { warehouse: z.string().optional().describe("Warehouse connection name"), }), async execute(args, ctx) { + // altimate_change start — workspace precedence + const precedence = await Precedence.check(ctx.sessionID, "schema_inspect", args.warehouse) + if (precedence.redirect) return precedence.redirect + // Every failure exit goes through here, so a fail-open notice cannot be dropped by + // one path being overlooked — three of the four exits below are errors, and the + // marker is most needed on exactly those. + const failed = (message: string) => Precedence.annotate(precedence, schemaError(message)) + // altimate_change end try { const result = (await Dispatcher.call("schema.inspect", { table: args.table, @@ -23,12 +34,12 @@ export const SchemaInspectTool = Tool.define("schema_inspect", { })) as unknown if (!isRecord(result)) { - return schemaError("Invalid schema response from dispatcher.") + return failed("Invalid schema response from dispatcher.") } const responseError = normalizeError(result.error) if (result.success === false || responseError !== undefined) { - return schemaError(responseError?.trim() || "Schema inspection failed.") + return failed(responseError?.trim() || "Schema inspection failed.") } const schemaResult = (isRecord(result.data) ? result.data : result) as Partial @@ -45,14 +56,15 @@ export const SchemaInspectTool = Tool.define("schema_inspect", { }) } // altimate_change end - return { + // altimate_change — attaches the fail-open notice when present; no-op otherwise. + return Precedence.annotate(precedence, { title: `Schema: ${schemaResult.table ?? args.table}`, metadata: { success: true, columnCount: (schemaResult.columns ?? []).length, rowCount: schemaResult.row_count }, output, - } + }) } catch (e) { const msg = e instanceof Error ? e.message : String(e) - return schemaError(msg) + return failed(msg) } }, }) diff --git a/packages/opencode/src/altimate/tools/sql-execute.ts b/packages/opencode/src/altimate/tools/sql-execute.ts index 4647c75648..3ab4061e80 100644 --- a/packages/opencode/src/altimate/tools/sql-execute.ts +++ b/packages/opencode/src/altimate/tools/sql-execute.ts @@ -13,6 +13,9 @@ import { PostConnectSuggestions } from "./post-connect-suggestions" import { getCache } from "../native/schema/cache" import * as Registry from "../native/connections/registry" // altimate_change end +// altimate_change start — workspace precedence +import * as Precedence from "../workspace/precedence" +// altimate_change end export const SqlExecuteTool = Tool.define("sql_execute", { description: "Execute SQL against a connected data warehouse. Returns results as a formatted table.", @@ -38,6 +41,19 @@ export const SqlExecuteTool = Tool.define("sql_execute", { } // altimate_change end + // altimate_change start — workspace precedence. + // Last, after BOTH native safety checks. A redirect returns early, so anything + // above it stops running — and neither check has an equivalent on the other side: + // the engine's execution tools apply no hard-deny list, and an engine tool key is + // matched by the builder's `"*": "allow"` rule while `sql_execute_write` is "ask". + // Redirecting first would let a write reach the warehouse without the confirmation + // the same statement needed a moment ago. Approving and then redirecting is not a + // wasted prompt: the write still happens, through the engine, and what the user + // authorised is the write — not which connection carries it. + const precedence = await Precedence.check(ctx.sessionID, "sql_execute", args.warehouse) + if (precedence.redirect) return precedence.redirect + // altimate_change end + // altimate_change start — shadow-mode pre-execution SQL validation // Runs validation against cached schema and emits sql_pre_validation telemetry, // but does NOT block execution. Used to measure catch rate before deciding @@ -87,18 +103,24 @@ export const SqlExecuteTool = Tool.define("sql_execute", { }) } // altimate_change end - return { + // altimate_change — carries the fail-open notice when the target could not be + // attributed to the workspace; a no-op otherwise. + return Precedence.annotate(precedence, { title: `SQL: ${args.query.slice(0, 60)}${args.query.length > 60 ? "..." : ""}`, metadata: { rowCount: result.row_count, truncated: result.truncated }, output, - } + }) } catch (e) { const msg = e instanceof Error ? e.message : String(e) - return { + // altimate_change — annotate the failure too. A fail-open notice that only rides + // on success is worse than none: the reason vanishes exactly when the call went + // wrong, and the `precedence` marker under-counts fail-open in precisely the + // cases most likely to fail. + return Precedence.annotate(precedence, { title: "SQL: ERROR", metadata: { rowCount: 0, truncated: false, error: msg }, output: `Failed to execute SQL: ${msg}\n\nEnsure the dispatcher is running and a warehouse connection is configured.`, - } + }) } }, }) diff --git a/packages/opencode/src/altimate/tools/sql-explain.ts b/packages/opencode/src/altimate/tools/sql-explain.ts index 5e9bae7dbb..73a1a401a3 100644 --- a/packages/opencode/src/altimate/tools/sql-explain.ts +++ b/packages/opencode/src/altimate/tools/sql-explain.ts @@ -2,6 +2,9 @@ import z from "zod" import { Tool } from "../../tool/tool" import { Dispatcher } from "../native" import type { SqlExplainResult } from "../native/types" +// altimate_change start — workspace precedence +import * as Precedence from "../workspace/precedence" +// altimate_change end /** * Detect SQL input that cannot be meaningfully EXPLAIN'd. @@ -92,7 +95,7 @@ export const SqlExplainTool = Tool.define("sql_explain", { "Run EXPLAIN ANALYZE (actually executes the query, slower but more accurate). Not supported by Snowflake.", ), }), - async execute(args, _ctx) { + async execute(args, ctx) { // Pre-flight validation — reject bad input before hitting the warehouse // so we return an actionable message instead of a verbatim DB error. const sqlError = validateSqlInput(args.sql) @@ -124,6 +127,15 @@ export const SqlExplainTool = Tool.define("sql_explain", { } } + // altimate_change start — workspace precedence. + // After the pre-flight validators on purpose: a redirect reads as success, so + // returning one for an empty statement or a malformed warehouse name would send + // the model to the engine tool with the same bad arguments instead of telling it + // what was wrong. + const precedence = await Precedence.check(ctx.sessionID, "sql_explain", args.warehouse) + if (precedence.redirect) return precedence.redirect + // altimate_change end + try { const result = await Dispatcher.call("sql.explain", { sql: args.sql, @@ -133,7 +145,8 @@ export const SqlExplainTool = Tool.define("sql_explain", { if (!result.success) { const error = result.error ?? "Unknown error" - return { + // altimate_change — see sql-execute: every post-guard exit carries the notice. + return Precedence.annotate(precedence, { title: "Explain: FAILED", metadata: { success: false, @@ -142,10 +155,11 @@ export const SqlExplainTool = Tool.define("sql_explain", { error, }, output: `Failed to get execution plan: ${error}`, - } + }) } - return { + // altimate_change — attaches the fail-open notice when present; no-op otherwise. + return Precedence.annotate(precedence, { title: `Explain: ${result.analyzed ? "ANALYZE" : "PLAN"} [${result.warehouse_type ?? "unknown"}]`, metadata: { success: true, @@ -153,14 +167,14 @@ export const SqlExplainTool = Tool.define("sql_explain", { warehouse_type: result.warehouse_type ?? "unknown", }, output: formatPlan(result), - } + }) } catch (e) { const msg = e instanceof Error ? e.message : String(e) - return { + return Precedence.annotate(precedence, { title: "Explain: ERROR", metadata: { success: false, analyzed: false, warehouse_type: "unknown", error: msg }, output: `Failed to run EXPLAIN: ${msg}\n\nEnsure a warehouse connection is configured and the dispatcher is running.`, - } + }) } }, }) diff --git a/packages/opencode/src/altimate/tools/warehouse-list.ts b/packages/opencode/src/altimate/tools/warehouse-list.ts index 4ce256b3f0..aee244b984 100644 --- a/packages/opencode/src/altimate/tools/warehouse-list.ts +++ b/packages/opencode/src/altimate/tools/warehouse-list.ts @@ -1,6 +1,9 @@ import z from "zod" import { Tool } from "../../tool/tool" import { Dispatcher } from "../native" +// altimate_change start — workspace precedence +import * as Precedence from "../workspace/precedence" +// altimate_change end export const WarehouseListTool = Tool.define("warehouse_list", { description: "List all configured warehouse connections. Shows connection name, type, and database.", @@ -18,16 +21,32 @@ export const WarehouseListTool = Tool.define("warehouse_list", { } } - const lines: string[] = ["Name | Type | Database", "-----|------|--------"] + // altimate_change start — workspace precedence. + // Annotated here, in this tool's own markdown, rather than on WarehouseInfo: + // that struct is shared by every consumer of `warehouse.list`, and a field + // added there would surface far beyond this listing. + const precedence = Precedence.forSession(ctx.sessionID) + const notes = new Map() for (const wh of warehouses) { - lines.push(`${wh.name} | ${wh.type} | ${wh.database ?? "-"}`) + const note = Precedence.warehouseListNote(precedence, wh.type) + if (note) notes.set(wh.name, note) + } + const shadowedCount = notes.size + + const lines: string[] = shadowedCount + ? ["Name | Type | Database | Served by", "-----|------|----------|----------"] + : ["Name | Type | Database", "-----|------|--------"] + for (const wh of warehouses) { + const row = `${wh.name} | ${wh.type} | ${wh.database ?? "-"}` + lines.push(shadowedCount ? `${row} | ${notes.get(wh.name) ?? "local"}` : row) } return { title: `Warehouses: ${warehouses.length} configured`, - metadata: { count: warehouses.length }, + metadata: { count: warehouses.length, shadowed: shadowedCount }, output: lines.join("\n"), } + // altimate_change end } catch (e) { const msg = e instanceof Error ? e.message : String(e) return { diff --git a/packages/opencode/src/altimate/workspace/precedence.ts b/packages/opencode/src/altimate/workspace/precedence.ts new file mode 100644 index 0000000000..d7e9a0a46f --- /dev/null +++ b/packages/opencode/src/altimate/workspace/precedence.ts @@ -0,0 +1,763 @@ +// Workspace precedence: when a bound workspace's engine serves a capability for a +// warehouse type, the equivalent native tool stops executing against a local +// connection of that type and points the model at the engine tool instead. +// +// One principle governs the whole module: shadow only what is MATERIALISED and +// ATTRIBUTED; anything undetermined runs locally with an explicit notice; nothing +// is ever silent. +// +// 1. Materialised, not declared. Precedence is derived from the engine tool keys +// actually present in the model-facing MCP map, never from what the workspace +// declared. A declared-but-broken integration shadows nothing. +// 1a. Attributed. The engine must be provably serving the *bound* workspace. An IDE +// writes its `datamate` entry unpinned, and such an engine serves whichever +// teammate is active in that IDE — changing at runtime. Attach now guarantees +// attribution (it reuses only a live, pinned, version-current entry and replaces +// anything else); this module re-checks that guarantee and refuses to engage if it +// is ever violated. Defence in depth, not the primary control. +// 2. Capability-scoped. The engine's warehouse integrations are NOT symmetric — +// snowflake serves execute/explain/inspect, bigquery and postgresql serve execute +// only, databricks serves execute only. Shadowing is keyed on the individual +// materialised tool key, so `sql_explain` on a BigQuery connection stays local +// instead of redirecting to a tool that does not exist. +// 3. Default targets. A native call with no `warehouse` resolves through +// `resolveDefaultTarget`, which mirrors each handler's own resolution. +// 4. Redirect. A shadowed call returns a result naming the exact engine key. Nothing +// executes and there is no fallback. +// +// SERVER-SIDE ONLY. The TUI plugin runtime loads plugins in a separate module realm +// in the same process: an import from there is a different instance, sharing neither +// module state nor `globalThis`. Importing this module from a plugin would typecheck, +// unit-test green, and return an empty precedence forever — `bySession` would simply +// be a different, always-empty map. Only the event bus crosses that boundary, which is +// why the inventory line is published as a TUI event rather than read directly. Anything +// on the TUI side that needs this state must cross the bus or re-derive it. +// +// Precedence is a pure function of the materialised set, so it is re-derived every +// turn from the live MCP tool map (`refresh`) rather than cached at attach. That is +// what keeps it correct when an engine's tool set changes under us — `MCP.tools()` is +// re-read each turn by `resolveTools`. A `tools/list_changed` notification invalidates +// that cache sooner, so it makes the next re-derivation see the change earlier — but the +// per-turn re-derivation is the mechanism, not the notification. +import { Config } from "@/config/config" +import { AppRuntime } from "@/effect/app-runtime" +import { EventV2Bridge } from "@/event-v2-bridge" +import { TuiEvent } from "@/server/tui-event" +import { Log } from "@/altimate/util/log" +import { Instance } from "@/project/instance" +import { Flag as CoreFlag } from "@opencode-ai/core/flag/flag" +import { PermissionNext } from "@/permission/next" +import { DATAMATE_KEY } from "../datamate-transport" +import { + attributableEngine, + engineToolKeys, + isEnabled, + pinnedWorkspace, + settledOutcome, + type EntryLike, + type Outcome, +} from "./engine-overlay" +import { readLocalBinding } from "./state" +import { canonicalType } from "../native/connections/registry" +import * as Registry from "../native/connections/registry" + +const log = Log.create({ service: "workspace-precedence" }) + +/** Native tools that can be shadowed. `warehouse_list` annotates instead (it has no + * connection argument), and `sql_optimize` is excluded — it is a pure sqlglot + * transform with no connection to scope on. */ +export type Capability = "sql_execute" | "sql_explain" | "schema_inspect" + +/** The dispatcher operation each capability resolves its default target through. + * Only `sql.execute` consults dbt; explain/inspect are registry-only. */ +export const CAPABILITY_OP: Record = { + sql_execute: "sql.execute", + sql_explain: "sql.explain", + schema_inspect: "schema.inspect", +} + +/** Mechanism 2 — the engine tool name implementing each capability, per integration + * id. Databricks names its execute tool differently from the `_…` convention. */ +function engineToolFor(capability: Capability, integration: string): string { + if (capability === "sql_execute") { + return integration === "databricks" ? "databricks_execute_sql" : `${integration}_execute_database_query` + } + if (capability === "sql_explain") return `${integration}_get_query_explain_plan` + return `${integration}_get_table_stats` +} + +/** Engine integration id → canonical local driver type. The id is the engine's name + * for the integration (`postgresql`); the driver type is what local connections carry + * (`postgres`). Only warehouse integrations appear here. */ +const INTEGRATION_TYPE: Record = { + snowflake: "snowflake", + bigquery: "bigquery", + postgresql: "postgres", + databricks: "databricks", +} + +const CAPABILITIES: Capability[] = ["sql_execute", "sql_explain", "schema_inspect"] + +export interface ShadowEntry { + /** Engine tool name, without the MCP server prefix. */ + engineTool: string + /** Model-facing key, i.e. `_`. This is what the model calls. */ + modelKey: string + /** Engine integration id (`postgresql`), not the driver type. */ + integration: string +} + +export interface Precedence { + workspaceName: string + /** The bound workspace this snapshot was derived for. Re-linking mid-session is + * supported, so a snapshot can outlive the binding that justified it. */ + workspaceId?: string + /** false when the escape hatch is on, when nothing is bound, or when the engine + * could not be attributed to the bound workspace. */ + enabled: boolean + /** Why precedence is off, for the inventory line. Absent when enabled. */ + disabledReason?: "pilot-off" | "escape-hatch" | "unbound" | "unattributed" | "nothing-materialised" + /** canonical driver type → capability → who serves it. */ + shadowed: Map> + /** The caller's effective permission rules, captured when this was derived. A + * redirect is only useful if the caller may actually call the engine tool: the + * `analyst` agent denies everything it does not name, and it never names the + * engine keys, so redirecting its permitted reads would take away the one thing + * it exists to do. Absent means "unknown", which is treated as reachable. */ + ruleset?: PermissionNext.Ruleset +} + +const EMPTY = (reason: Precedence["disabledReason"], workspaceName = ""): Precedence => ({ + workspaceName, + enabled: false, + disabledReason: reason, + shadowed: new Map(), +}) + +/** Per-session precedence, refreshed once per turn by the tool resolver and read + * (never recomputed) by tool bodies mid-turn. */ +const bySession = new Map() + +/** Test seam. Production leaves every field unset. */ +export const precedenceInternals: { + binding?: () => Promise<{ datamateId: number; datamateName: string } | null> + attributedTo?: () => Promise + attachOutcome?: () => Promise + announce?: (line: string) => Promise +} = {} + +/** Bound both per-session maps. A long-running `serve` process sees an unbounded + * number of session ids, and each entry holds a merged permission ruleset, so an + * unevicted map grows with lifetime session count. Mirrors the attach module's cap + * and insertion-ordered eviction: dropping the oldest is safe because the next turn + * simply re-derives. */ +export const MAX_TRACKED_SESSIONS = 256 + +function remember(sessionID: string, value: Precedence): void { + bySession.delete(sessionID) + bySession.set(sessionID, value) + while (bySession.size > MAX_TRACKED_SESSIONS) { + const oldest = bySession.keys().next() + if (oldest.done) break + bySession.delete(oldest.value) + announced.delete(oldest.value) + publishing.delete(oldest.value) + publishQueue.delete(oldest.value) + } +} + +/** + * Did an attach actually produce the engine now serving this session? + * + * The saved config is not enough on its own: an entry can be rewritten — by an IDE — + * from unpinned to pinned while MCP goes on serving the process it already connected, + * so the config would name this workspace while the running engine serves another. + * The attach outcome is the runtime-grounded signal: `attached` means the overlay's + * pinned engine is the one MCP connected at this turn boundary. + * + * Read through `settledOutcome`, which is a pure read of state already held. The + * attach task itself must NOT be awaited here — the prompt loop caps its own wait and + * lets a turn proceed without engine tools past the cap, so awaiting it would hang + * every affected turn on a broken connection for the full connection timeout. + * + * `undefined` means "not known yet", and cannot be told apart from "never attached". + * Both are treated as unattested: precedence stays off and the call runs locally with + * a notice. Being wrong in that direction costs a turn's routing, which the next turn + * repairs; being wrong the other way routes credentials into someone else's engine. + */ +async function attested(sessionID: string): Promise { + const outcome = precedenceInternals.attachOutcome + ? await precedenceInternals.attachOutcome().catch(() => undefined) + : settledOutcome(sessionID) + if (!outcome) return false + // The attach module owns the allowlist; a new outcome kind refuses until it is + // named there (see SERVING in engine-types). + return attributableEngine(outcome) +} + +/** Sessions whose inventory line has already been reported. Precedence is re-derived + * every turn, but the inventory is a once-per-session statement of what changed. */ +/** The last inventory line announced per session, not merely whether one was. The + * first turn can announce "shadowing off" — an attach that outran its bounded wait + * looks identical to no engine — and precedence is deliberately re-derived every + * turn, so the truth can change under a session that has already been told. Comparing + * the line means a correction is delivered and an unchanged one stays quiet. */ +/** What each session has actually been told, and whether that statement described + * actual routing. The flag matters: "shadowing off, the engine could not be + * attributed" is a non-empty announcement that is NOT routing, so treating any prior + * announcement as routing would later claim routing had stopped when it never started. + * + * Only confirmed deliveries are written here. An optimistic record cannot live in this + * map even briefly: two refreshes can publish different lines before either settles, + * and rolling one back to the other's unconfirmed value would claim a delivery that + * never happened, silencing that line for good. */ +const announced = new Map() + +/** The announcement currently queued or being published for a session, held separately + * so it can never be mistaken for one that arrived. It exists only to stop a second + * refresh from sending the same line twice; a failed attempt leaves `announced` + * untouched, so the next turn simply tries again. */ +const publishing = new Map() + +/** Publications are chained per session so they arrive in the order they were decided. + * Refreshes are serialized by the prompt loop, but publishing deliberately is not + * awaited — a toast must never be able to stall a turn — so without a chain two lines + * can be in flight at once and land in either order, leaving the stale one on screen + * while the newer one is recorded as the session's state. */ +const publishQueue = new Map>() + +/** Said when routing stops entirely, which `inventoryLine` renders as an empty string + * because there is nothing left to enumerate. Silence is the wrong answer only here: + * the session was previously told its calls were routed. */ +const STOPPED_ROUTING = + "Workspace integrations: nothing is served by the workspace any more; every connection now runs on the local drivers." + +/** Publishes the line and reports whether it actually reached the session. The caller + * needs the distinction: a line recorded as said but never delivered is never said + * again, because every later turn sees it as unchanged. */ +async function announce(line: string): Promise { + try { + if (precedenceInternals.announce) await precedenceInternals.announce(line) + else + await AppRuntime.runPromise( + EventV2Bridge.Service.use((events) => + events.publish(TuiEvent.ToastShow, { + title: "Workspace integrations", + message: line, + variant: "info", + duration: 10000, + }), + ), + ) + return true + } catch (err) { + log.warn("could not report the workspace precedence inventory", { err: String(err) }) + return false + } +} + +/** Mechanism 6 — the escape hatch. `--integrations=local` (or the env var) turns + * shadowing off for the whole session. */ +export function escapeHatchOn(): boolean { + return CoreFlag.ALTIMATE_INTEGRATIONS_LOCAL +} + +async function currentBinding(): Promise<{ datamateId: number; datamateName: string } | null> { + if (precedenceInternals.binding) return precedenceInternals.binding() + try { + const directory = Instance.directory + if (!directory) return null + const binding = await readLocalBinding(directory) + return binding ? { datamateId: binding.datamateId, datamateName: binding.datamateName } : null + } catch (err) { + log.warn("could not read local binding", { err: String(err) }) + return null + } +} + +/** + * Mechanism 1a — which workspace the live engine entry is actually pinned to, or null + * when that cannot be established. A URL entry is an IDE's in-process engine: never + * pinned, its active teammate changing at runtime, so it can never be attributed. + */ +async function attributedTo(expected: string): Promise { + if (precedenceInternals.attributedTo) return precedenceInternals.attributedTo() + const read = async (): Promise => { + const cfg = (await Config.get()) as { mcp?: Record } + const entry = cfg.mcp?.[DATAMATE_KEY] + if (!entry) return null + // Parsed by attach's own parser, not a second copy here. It handles both entry + // shapes (`command` as argv, or a string plus separate `args`), both flag + // spellings, and last-wins on repeats — a private reimplementation would refuse + // precedence on engines that are in fact correctly pinned. + return pinnedWorkspace(entry) + } + try { + const cached = await read() + // `Config.get()` is cached per instance, and an IDE rewriting the entry writes + // straight to disk without going through it — so a cached pin can outlive the + // entry it describes. Staleness is only dangerous in one direction: a stale + // "pinned to us" would help enable routing, while a stale "pinned elsewhere" + // merely refuses, which is the safe way to be wrong. So confirm against disk only + // when the cached answer is about to enable, and leave the refusing path cheap + // rather than re-reading all config on every turn. + if (cached !== expected) return cached + await Config.invalidate().catch((err) => { + log.warn("could not invalidate the config cache before attributing the engine", { err: String(err) }) + }) + return await read() + } catch (err) { + log.warn("could not read MCP config for engine attribution", { err: String(err) }) + return null + } +} + +/** + * Re-derive precedence for a session from the live model-facing tool map. Called + * once per turn by the tool resolver, before descriptions are assembled. + */ +export async function refresh( + sessionID: string, + tools: Record, + ruleset?: PermissionNext.Ruleset, +): Promise { + const result = await derive(sessionID, tools) + if (ruleset) result.ruleset = ruleset + remember(sessionID, result) + // Mechanism 6 — say once, per session, what is now served where. Silence is the one + // thing this design does not allow, but repeating it every turn would be noise. + // A session that never had routing is told nothing — there is nothing to say. But a + // session that HAD routing and no longer does must hear about it: the empty + // inventory is exactly the transition the user most needs, and a truthiness guard + // alone can never announce it, so they would go on believing calls are routed while + // they run locally. + const current = inventoryLine(result) + const routed = result.enabled && current !== "" + // What this session is committed to saying: the announcement still being published if + // there is one, otherwise the one it has actually been told. Both questions below are + // asked of this single record. Consulting only the delivered one would suppress a + // correction back to it while another line is in flight, and would miss that routing + // had been announced at all when the stop arrives before that announcement lands — + // in both cases the queue then delivers the stale line last. + const committed = publishing.get(sessionID) ?? announced.get(sessionID) + // Only a session that was actually routing can be told routing has stopped. + const line = current || (committed?.routed ? STOPPED_ROUTING : "") + if (line && committed?.line !== line) { + // Nothing reaches `announced` until the line actually arrives, so a failure leaves + // the session's known state untouched and the next turn retries. + const attempt = { line, routed } + publishing.set(sessionID, attempt) + const queued = (publishQueue.get(sessionID) ?? Promise.resolve()).then(async () => { + const delivered = await announce(line) + if (publishing.get(sessionID) === attempt) publishing.delete(sessionID) + // A session evicted while its line was in flight must not be written back: + // eviction only ever walks `bySession`, so an entry recreated here after the + // session left it could never be reclaimed, and the map would grow with the + // lifetime session count rather than staying bounded. + if (delivered && bySession.has(sessionID)) announced.set(sessionID, attempt) + }) + publishQueue.set(sessionID, queued) + void queued + } + return result +} + +async function derive(sessionID: string, tools: Record): Promise { + // The workspace pilot is opt-in, and opting out has to mean it. A binding and a + // pinned `datamate` entry both persist in config, and the MCP client connects that + // entry on its own regardless of the pilot flag — so engine tools can materialise + // for someone who has switched the pilot off. Without this gate their local + // warehouse calls would start redirecting. + if (!isEnabled()) return EMPTY("pilot-off") + if (escapeHatchOn()) return EMPTY("escape-hatch") + + const binding = await currentBinding() + if (!binding) return EMPTY("unbound") + const workspaceName = binding.datamateName + + // Mechanism 1a — refuse to engage on an engine we cannot attribute to this binding. + // Two signals, and both must agree. The attach outcome says the running engine is + // one we established; the configured pin says it still names this workspace. Config + // alone is not enough — it can be rewritten under a live connection — and the + // outcome alone would not notice a later rewrite pointing somewhere else. + if (!(await attested(sessionID))) { + log.info("no attach established this session's engine; precedence off", { bound: binding.datamateId }) + return EMPTY("unattributed", workspaceName) + } + const pinned = await attributedTo(String(binding.datamateId)) + if (pinned === null || pinned !== String(binding.datamateId)) { + log.info("engine not attributable to the bound workspace; precedence off", { + bound: binding.datamateId, + pinned: pinned ?? "(none)", + }) + return EMPTY("unattributed", workspaceName) + } + + // Mechanism 1 — what actually materialised, never what was declared. + const present = engineToolKeys(tools) + if (present.size === 0) return EMPTY("nothing-materialised", workspaceName) + + // Mechanism 2 — capability by capability, only where the key is really there. + const shadowed = new Map>() + for (const [integration, type] of Object.entries(INTEGRATION_TYPE)) { + for (const capability of CAPABILITIES) { + const engineTool = engineToolFor(capability, integration) + if (!present.has(engineTool)) continue + let forType = shadowed.get(type) + if (!forType) { + forType = new Map() + shadowed.set(type, forType) + } + forType.set(capability, { + engineTool, + modelKey: `${DATAMATE_KEY}_${engineTool}`, + integration, + }) + } + } + if (shadowed.size === 0) return EMPTY("nothing-materialised", workspaceName) + return { workspaceName, workspaceId: String(binding.datamateId), enabled: true, shadowed } +} + +/** Read the session's precedence without recomputing it. */ +export function forSession(sessionID: string): Precedence | undefined { + return bySession.get(sessionID) +} + +/** Test-visible size of the per-session cache. */ +export function trackedSessionCount(): number { + return bySession.size +} + +/** Test-visible size of the announcement cache, which is bounded by the same eviction + * and so must never outgrow it. */ +export function announcedSessionCount(): number { + return announced.size +} + +export function resetForTests(): void { + bySession.clear() + announced.clear() + publishing.clear() + publishQueue.clear() + delete precedenceInternals.announce + delete precedenceInternals.binding + delete precedenceInternals.attributedTo +} + +export interface RedirectResult { + title: string + metadata: Record + output: string +} + +/** What a tool body should do about a call. */ +export interface Verdict { + /** Present when the call is shadowed: return this instead of executing. */ + redirect?: RedirectResult + /** Present when the call runs but the user must be told why it was not routed. */ + notice?: string + /** Stamped onto the executed result's metadata so telemetry can count these. */ + precedence?: "undetermined" | "pending" +} + +const RUN: Verdict = {} + +/** Can the caller actually call this engine tool? An agent that denies what it does + * not name (the `analyst` default) can be permitted the native tool and forbidden its + * engine counterpart, and a redirect it cannot follow is a dead end. */ +function reachable(precedence: Precedence, modelKey: string): boolean { + if (!precedence.ruleset) return true + return PermissionNext.evaluate(modelKey, "*", precedence.ruleset).action !== "deny" +} + +/** + * The capabilities this caller will really have routed for a given type — the ones + * that materialised AND whose destination the caller may call. Everything user-facing + * reports through this, so a listing can never claim a routing that will not happen: + * an `analyst` is told its reads stay local, because they do. + */ +function servedFor(precedence: Precedence, type: string): Capability[] { + const byCapability = precedence.shadowed.get(type) + if (!byCapability) return [] + return CAPABILITIES.filter((c) => { + const entry = byCapability.get(c) + return !!entry && reachable(precedence, entry.modelKey) + }) +} + +function unreachable(workspaceName: string, modelKey: string): Verdict { + return { + notice: + `Not routed through workspace "${workspaceName}": this agent is not permitted to call ` + + `\`${modelKey}\`, so the call ran on the local connection instead.`, + precedence: "undetermined", + } +} + +function redirectFor( + capability: Capability, + entry: ShadowEntry, + workspaceName: string, + connection: string, + /** Set when the call was routed because the *fallback* target is served, not the + * target it would have tried first. The dbt attempt might well have succeeded, so + * the message has to say why this was refused and how to insist. */ + viaDbtFallback = false, +): Verdict { + return { + redirect: { + title: `Routed to workspace ${workspaceName}`, + metadata: { + // Machine-readable marker: `Tool.wrap` reports every returning body as a + // successful call, so without this a redirect is indistinguishable from a + // real execution in telemetry. + redirected: true, + redirect_to: entry.modelKey, + precedence: "shadowed", + workspace: workspaceName, + capability, + connection, + ...(viaDbtFallback ? { via: "dbt-fallback" } : {}), + }, + output: viaDbtFallback + ? `Not run locally. This call names no warehouse, so it resolves through the dbt project — and if dbt ` + + `returns nothing it falls back to the local connection \`${connection}\`, which workspace ` + + `"${workspaceName}" serves through its integration engine. Whether it lands on dbt or on that ` + + `connection is only known once it runs, so it is not run.\n\n` + + `Call \`${entry.modelKey}\` instead. If you meant the dbt path specifically, either name the ` + + `warehouse you want (\`warehouse=${connection}\` routes to the engine; any unserved connection runs ` + + `locally), or restart with \`--integrations=local\` to keep every connection on the local drivers.` + : `Not run locally. Workspace "${workspaceName}" serves ${entry.integration} through its integration engine, ` + + `so this connection is served by \`${entry.modelKey}\`.\n\n` + + `Call \`${entry.modelKey}\` instead. ` + + `To use the local connection for this session, restart with \`--integrations=local\`.`, + }, + } +} + +/** + * Mechanism 4 — the single decision a tool body asks for. Returns an empty verdict + * when the call should proceed normally. + * + * `warehouse` undefined means "this tool's default target", which is resolved the way + * the handler itself would resolve it (see `resolveDefaultTarget`). + */ +export async function check(sessionID: string, capability: Capability, warehouse?: string): Promise { + const precedence = bySession.get(sessionID) + if (!precedence) { + // No snapshot for this session. The resolver derives one every turn, so this is + // either a caller that never resolved tools or an entry evicted between tool + // resolution and this call. Either way the decision is unknown, and unknown runs + // locally *and says so* rather than silently — a silent run is indistinguishable + // from a considered "not served". + return { + notice: "Not routed through the bound workspace: no routing decision was available for this call.", + precedence: "undetermined", + } + } + if (!precedence.enabled) return RUN + + // Re-linking mid-session is supported, so this snapshot can name a workspace the + // project has since left — and a redirect naming it would send the call to that + // workspace's engine, with its credentials. The binding is a local cache read, and + // this only runs on the path that is about to redirect. + if (precedence.workspaceId && (await currentBinding())?.datamateId !== Number(precedence.workspaceId)) { + return { + notice: + `Not routed through workspace "${precedence.workspaceName}": the project was re-linked while ` + + `this call was in flight, so the routing decision no longer applies.`, + precedence: "undetermined", + } + } + + if (warehouse) { + const type = canonicalType(Registry.getConfig(warehouse)?.type) + if (!type) return RUN + const entry = precedence.shadowed.get(type)?.get(capability) + if (!entry) return RUN + if (!reachable(precedence, entry.modelKey)) return unreachable(precedence.workspaceName, entry.modelKey) + return redirectFor(capability, entry, precedence.workspaceName, warehouse) + } + + // No warehouse named: resolve the default the way this operation's handler does. + // Imported lazily — `register.ts` imports the tool layer, so a static import here + // would close a cycle. + const { resolveDefaultTarget } = await import("../native/connections/register") + const target = await resolveDefaultTarget(CAPABILITY_OP[capability]) + return decideForTarget(precedence, capability, target) +} + +/** + * Decide a no-`warehouse` call from the target it would actually reach. Pure, and + * exported for its own tests: the ORDER of these branches is the property that has + * broken repeatedly, and it is only checkable in isolation — reaching a dbt-sourced + * target through `check()` needs a real dbt project. + * + * Order matters and is deliberate: + * 1. the target's own type is served → redirect + * 2. the fallback behind it is served → redirect (see below) + * 3. the type could not be determined → run locally, non-silently + * 4. otherwise → run + * + * Step 2 must precede step 3. `sql.execute` falls back to the first registry + * connection whenever the dbt attempt yields nothing — an unrecognised result shape + * or a throw, not only an absent project. An undetermined dbt type is *more* likely + * to be the broken setup that yields nothing, so returning "undetermined" before + * looking at the fallback fails open into exactly the local execution against a + * served connection that this design exists to prevent. + */ +export function decideForTarget( + precedence: Precedence, + capability: Capability, + target: { + source: "dbt" | "registry" | "none" + type?: string + name?: string + fallback?: { type: string; name: string } + }, +): Verdict { + if (target.source === "none") return RUN + + const type = canonicalType(target.type) + const entry = type ? precedence.shadowed.get(type)?.get(capability) : undefined + if (entry) { + if (!reachable(precedence, entry.modelKey)) return unreachable(precedence.workspaceName, entry.modelKey) + const connection = + target.source === "registry" ? (target.name ?? "the default connection") : "the dbt profile's target" + return redirectFor(capability, entry, precedence.workspaceName, connection) + } + + // Reached whether or not the dbt type resolved — see the ordering note above. + if (target.source === "dbt" && target.fallback) { + const fallbackType = canonicalType(target.fallback.type) + const fallbackEntry = fallbackType ? precedence.shadowed.get(fallbackType)?.get(capability) : undefined + if (fallbackEntry) { + if (!reachable(precedence, fallbackEntry.modelKey)) { + return unreachable(precedence.workspaceName, fallbackEntry.modelKey) + } + return redirectFor(capability, fallbackEntry, precedence.workspaceName, target.fallback.name, true) + } + } + + if (!type) { + // Decided for v1: FAIL OPEN, non-silent. The call runs on the user's own local + // credential — exactly today's behaviour — and says why it was not routed. + return { + notice: + `Not routed through workspace "${precedence.workspaceName}": ` + + `the default target's type could not be determined.`, + precedence: "undetermined", + } + } + return RUN +} + +/** + * Attach a fail-open notice to an executed result. No-op for the common case, so + * every tool body can call it unconditionally on its way out. + */ +export function annotate; output?: string }>( + verdict: Verdict, + result: T, +): T { + if (!verdict.notice) return result + return { + ...result, + metadata: { ...result.metadata, precedence: verdict.precedence ?? "undetermined" }, + output: `${verdict.notice}\n\n${result.output ?? ""}`, + } +} + +/** + * Mechanism 5 — tool descriptions. Both tool resolvers call these so the two cannot + * describe the same tool differently. + */ +export function describeNativeTool(toolID: string, base: string, precedence?: Precedence): string { + if (!precedence?.enabled) return base + const isCapability = (CAPABILITIES as string[]).includes(toolID) + if (!isCapability && toolID !== "warehouse_list") return base + // Claim redirection for THIS tool only if this tool's own capability is served + // somewhere. An integration that provides execute alone — bigquery, postgresql, + // databricks — leaves explain and inspect running locally, so telling those tools + // they redirect would steer the model away from the local tool that does work. + // `warehouse_list` describes the listing as a whole, so any served capability + // justifies its note. + const claims = isCapability + ? [...precedence.shadowed.keys()].some((t) => servedFor(precedence, t).includes(toolID as Capability)) + : [...precedence.shadowed.keys()].some((t) => servedFor(precedence, t).length > 0) + if (!claims) return base + return ( + `${base} Serves local connections; types served by workspace "${precedence.workspaceName}" ` + + `redirect to that workspace's integration tools.` + ) +} + +export function describeEngineTool(modelKey: string, base: string, precedence?: Precedence): string { + if (!precedence?.enabled) return base + for (const byCapability of precedence.shadowed.values()) { + for (const entry of byCapability.values()) { + if (entry.modelKey === modelKey) return `${base} (workspace ${precedence.workspaceName})` + } + } + return base +} + +/** Mechanism 6 — the inventory line reported once the attach settles. */ +export function inventoryLine(precedence: Precedence): string { + if (!precedence.enabled) { + switch (precedence.disabledReason) { + case "pilot-off": + return "" + case "escape-hatch": + return "Workspace integrations: shadowing off (--integrations=local); local connections serve every warehouse." + case "unattributed": + return ( + `Workspace integrations: shadowing off — the running engine could not be attributed to workspace ` + + `"${precedence.workspaceName}". Local connections serve every warehouse.` + ) + default: + return "" + } + } + const parts: string[] = [] + const short = (c: Capability) => c.replace(/^(sql|schema)_/, "") + for (const type of precedence.shadowed.keys()) { + const servedCaps = servedFor(precedence, type) + if (servedCaps.length === 0) continue + const local = CAPABILITIES.filter((c) => !servedCaps.includes(c)).map(short) + parts.push( + `${type}: ${servedCaps.map(short).join("/")} via workspace ${precedence.workspaceName}` + + (local.length ? `, ${local.join("/")} stay local` : ""), + ) + } + if (parts.length === 0) return "" + const shadowedCount = countShadowedConnections(precedence) + return `Workspace integrations — ${parts.join("; ")}. ${shadowedCount} local connection${shadowedCount === 1 ? "" : "s"} shadowed.` +} + +function countShadowedConnections(precedence: Precedence): number { + try { + return Registry.list().warehouses.filter((w) => { + const type = canonicalType(w.type) + return !!type && servedFor(precedence, type).length > 0 + }).length + } catch { + return 0 + } +} + +/** Per-capability note for a `warehouse_list` row, or null when the row is untouched. */ +export function warehouseListNote(precedence: Precedence | undefined, warehouseType: string): string | null { + if (!precedence?.enabled) return null + const type = canonicalType(warehouseType) + if (!type) return null + const servedCaps = servedFor(precedence, type) + if (servedCaps.length === 0) return null + const short = (c: Capability) => c.replace(/^(sql|schema)_/, "") + const served = servedCaps.map(short) + const local = CAPABILITIES.filter((c) => !servedCaps.includes(c)).map(short) + return ( + `${served.join("/")} via workspace ${precedence.workspaceName}` + (local.length ? `; ${local.join("/")} local` : "") + ) +} diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 5b78af06ac..909e182f63 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -102,12 +102,23 @@ let cli = yargs(args) default: false, }) // altimate_change end + // altimate_change start - workspace precedence escape hatch + .option("integrations", { + describe: + "where warehouse tools run: 'workspace' (default) lets the bound workspace's engine serve the types it provides; 'local' keeps every connection on the local drivers", + type: "string", + choices: ["workspace", "local"], + }) + // altimate_change end .middleware(async (opts) => { if (opts.printLogs) process.env.OPENCODE_PRINT_LOGS = "1" if (opts.logLevel) process.env.OPENCODE_LOG_LEVEL = opts.logLevel if (opts.pure) { process.env.OPENCODE_PURE = "1" } + // altimate_change start - workspace precedence escape hatch + if (opts.integrations) process.env.ALTIMATE_INTEGRATIONS = String(opts.integrations) + // altimate_change end Heap.start() diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 5f6d320cfb..6d98f475d5 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -25,9 +25,10 @@ import { MemoryPrompt } from "../memory/prompt" import { UNIFIED_INJECTION_BUDGET } from "../memory/types" // altimate_change - workspace memory read path import * as WorkspaceMemory from "../altimate/workspace/memory-sync" -// altimate_change start — workspace engine turn boundary and managed-key refusal +// altimate_change start — workspace engine turn boundary, managed-key refusal, tool precedence import * as WorkspaceEngine from "../altimate/workspace/engine-overlay" import { DATAMATE_KEY } from "../altimate/datamate-transport" +import * as Precedence from "../altimate/workspace/precedence" // altimate_change end import { Plugin } from "../plugin" import PROMPT_PLAN from "../session/prompt/plan.txt" @@ -1759,6 +1760,20 @@ export namespace SessionPrompt { // altimate_change end }) + // altimate_change start — workspace precedence. + // Derived once per turn from the LIVE tool map rather than cached at attach: + // precedence is a pure function of the materialised set, and `MCP.tools()` is + // cache-invalidated by the `tools/list_changed` notification, so re-deriving here + // is what keeps precedence correct when an engine's tool set changes under us. + // Resolved before the loops below because both sides' descriptions depend on it. + const mcpTools = await MCP.tools() + const precedence = await Precedence.refresh( + input.session.id, + mcpTools, + PermissionNext.merge(input.agent.permission, input.session.permission ?? []), + ) + // altimate_change end + for (const item of await ToolRegistry.tools( { modelID: ModelID.make(input.model.api.id), providerID: input.model.providerID }, input.agent, @@ -1768,7 +1783,8 @@ export namespace SessionPrompt { // altimate_change end tools[item.id] = tool({ id: item.id as any, - description: item.description, + // altimate_change — name the workspace on the native side too + description: Precedence.describeNativeTool(item.id, item.description, precedence), inputSchema: jsonSchema(schema as any), async execute(args, options) { const ctx = context(args, options) @@ -1820,8 +1836,10 @@ export namespace SessionPrompt { // altimate_change start — split the original client name off the model-facing tool object so // it's used only for source classification and never leaks into the schema sent to the model. - for (const [key, entry] of Object.entries(await MCP.tools())) { + for (const [key, entry] of Object.entries(mcpTools)) { const { client: clientName, ...item } = entry + // altimate_change — mark the engine tools that now serve a shadowed capability + item.description = Precedence.describeEngineTool(key, item.description ?? "", precedence) // altimate_change end const execute = item.execute if (!execute) continue diff --git a/packages/opencode/src/session/tools.ts b/packages/opencode/src/session/tools.ts index 76b6a03e46..bd50911027 100644 --- a/packages/opencode/src/session/tools.ts +++ b/packages/opencode/src/session/tools.ts @@ -21,6 +21,11 @@ import { EffectBridge } from "@/effect/bridge" // altimate_change start — shared tool-source stamping so this resolver can't drift from prompt.ts import { stampRegistryToolSource, describeMcpTool } from "@/altimate/tool-source" // altimate_change end +// altimate_change start — workspace precedence, shared with prompt.ts resolveTools so the +// two resolvers cannot describe the same tool differently. This resolver has no caller in +// the fork today; keeping it in step is insurance against that changing silently. +import * as Precedence from "@/altimate/workspace/precedence" +// altimate_change end // altimate_change start — upstream_fix: ToolRegistry expects fork-branded model ids here import { ModelID } from "@/provider/schema" // altimate_change end @@ -75,6 +80,13 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: { .pipe(Effect.orDie), }) + // altimate_change start — workspace precedence, derived once per turn from the live map + const mcpTools = yield* mcp.tools() + const precedence = yield* Effect.promise(() => + Precedence.refresh(input.session.id, mcpTools, Permission.merge(input.agent.permission, input.session.permission ?? [])), + ) + // altimate_change end + for (const item of yield* registry.tools({ // altimate_change start — upstream_fix: re-brand API model id for ToolRegistry resolution modelID: ModelID.make(input.model.api.id), @@ -84,7 +96,8 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: { })) { const schema = ProviderTransform.schema(input.model, ToolJsonSchema.fromTool(item)) tools[item.id] = tool({ - description: item.description, + // altimate_change — name the workspace on the native side too + description: Precedence.describeNativeTool(item.id, item.description, precedence), inputSchema: jsonSchema(schema), execute(args, options) { return run.promise( @@ -129,9 +142,11 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: { // altimate_change start — split the original client name off the model-facing tool object so // it's used only for source classification and never leaks into the schema sent to the model. - for (const [key, entry] of Object.entries(yield* mcp.tools())) { + for (const [key, entry] of Object.entries(mcpTools)) { const { client: clientName, ...item } = entry // altimate_change end + // altimate_change — mark the engine tools that now serve a shadowed capability + item.description = Precedence.describeEngineTool(key, item.description ?? "", precedence) const execute = item.execute if (!execute) continue diff --git a/packages/opencode/test/altimate/default-target.test.ts b/packages/opencode/test/altimate/default-target.test.ts new file mode 100644 index 0000000000..158fd6d72e --- /dev/null +++ b/packages/opencode/test/altimate/default-target.test.ts @@ -0,0 +1,188 @@ +// altimate_change - new file +// +// Coverage for `resolveDefaultTarget`, which answers "where would a warehouse call +// with no `warehouse` argument actually go?" — the question workspace precedence has +// to settle before it can decide whether such a call is served by the bound +// workspace's engine. +// +// The point of the function is that it mirrors each handler's OWN resolution rather +// than imposing a uniform one: only `sql.execute` consults dbt. These tests run +// outside a dbt project, so `ensureDbtAdapter` finds no config and every op falls +// through to the registry — which is exactly the behaviour to pin down, because the +// registry branch is what decides the default for the majority of users. +// +// Concurrency contract matches dispatcher.test.ts: the connection registry is a +// process-wide singleton mutated here via `setConfigs`/`reset`, which is safe under +// bun's default sequential file execution. +import { afterEach, beforeEach, describe, expect, test } from "bun:test" +import { resolveDefaultTarget, resetDbtAdapter } from "../../src/altimate/native/connections/register" +import { Dispatcher } from "../../src/altimate/native" +import * as Registry from "../../src/altimate/native/connections/registry" + +const OPS = ["sql.execute", "sql.explain", "schema.inspect"] as const + +beforeEach(() => { + resetDbtAdapter() + Registry.reset() +}) + +afterEach(() => { + resetDbtAdapter() + Registry.reset() +}) + +describe("resolveDefaultTarget — registry branch", () => { + test("reports the first configured connection, which is what the handlers use", async () => { + Registry.setConfigs({ + first_duck: { type: "duckdb", path: ":memory:" } as never, + second_snow: { type: "snowflake", account: "a" } as never, + }) + const target = await resolveDefaultTarget("sql.explain") + expect(target.source).toBe("registry") + expect(target).toMatchObject({ name: "first_duck", type: "duckdb" }) + }) + + test("insertion order decides the default, not the type", async () => { + Registry.setConfigs({ + second_snow: { type: "snowflake", account: "a" } as never, + first_duck: { type: "duckdb", path: ":memory:" } as never, + }) + const target = await resolveDefaultTarget("sql.explain") + expect(target).toMatchObject({ name: "second_snow", type: "snowflake" }) + }) + + test("no configured connection resolves to nothing rather than guessing", async () => { + Registry.setConfigs({}) + for (const op of OPS) { + expect((await resolveDefaultTarget(op)).source).toBe("none") + } + }) +}) + +describe("resolveDefaultTarget — the dbt fallback is reported, not hidden", () => { + test("the registry fallback is exposed whenever one exists", async () => { + // `sql.execute` falls back to the first registry connection whenever the dbt + // attempt yields nothing — not only when dbt is absent, but on an unrecognised + // result shape or a throw. A caller deciding whether to route this call has to be + // able to see both possible targets; reporting only the dbt one lets a call slip + // through and execute locally against a connection that should have been routed. + Registry.setConfigs({ first: { type: "snowflake", account: "a" } as never }) + const target = await resolveDefaultTarget("sql.execute") + if (target.source === "dbt") { + expect(target.fallback).toEqual({ type: "snowflake", name: "first" }) + } else { + // No dbt project here, so this resolves to the registry directly — same target. + expect(target).toMatchObject({ source: "registry", name: "first", type: "snowflake" }) + } + }) + + test("no fallback is reported when the registry is empty", async () => { + Registry.setConfigs({}) + const target = await resolveDefaultTarget("sql.execute") + if (target.source === "dbt") expect(target.fallback).toBeUndefined() + else expect(target.source).toBe("none") + }) +}) + +describe("resolveDefaultTarget — per-operation resolution", () => { + test("every op agrees on the registry default when there is no dbt project", async () => { + Registry.setConfigs({ only: { type: "postgres", host: "h" } as never }) + for (const op of OPS) { + const target = await resolveDefaultTarget(op) + expect(target).toMatchObject({ source: "registry", name: "only", type: "postgres" }) + } + }) + + test("explain and inspect never report a dbt source", async () => { + // These handlers are registry-only by construction. Resolving them through dbt + // would drag adapter construction — Python bridge, manifest rebuild, file + // watchers — onto paths that never touch dbt today. + Registry.setConfigs({ only: { type: "duckdb", path: ":memory:" } as never }) + expect((await resolveDefaultTarget("sql.explain")).source).not.toBe("dbt") + expect((await resolveDefaultTarget("schema.inspect")).source).not.toBe("dbt") + }) + + test("repeated resolution is stable and does not rebuild state", async () => { + Registry.setConfigs({ only: { type: "duckdb", path: ":memory:" } as never }) + const results = await Promise.all(OPS.map((op) => resolveDefaultTarget(op))) + for (const target of results) { + expect(target).toMatchObject({ source: "registry", name: "only" }) + } + }) + + test("concurrent execute resolutions share one adapter attempt", async () => { + // Single-flight: two concurrent `warehouse`-less calls used to construct the dbt + // adapter twice. Outside a dbt project both settle on the registry, and neither + // should throw or disagree. + Registry.setConfigs({ only: { type: "snowflake", account: "a" } as never }) + const [a, b] = await Promise.all([resolveDefaultTarget("sql.execute"), resolveDefaultTarget("sql.execute")]) + expect(a).toEqual(b) + }) +}) + +describe("the default target survives a concurrent registry change", () => { + // `sql.execute` awaits the dbt attempt before it reaches the registry. The registry + // is a process-wide mutable singleton, so a `warehouse.add`/`remove` landing during + // that await used to change which connection the call fell back to — after the + // caller's routing decision had already been made against the old one. The handler + // now pins the fallback before the await, so the decided and executed connections + // are the same by construction. + test("a connection dropped during the dbt await does not silently redirect the call", async () => { + // Warm the dispatcher: its first call awaits lazy handler registration, and a + // mutation landing in *that* window would be indistinguishable from the one + // under test. + Registry.setConfigs({ warm: { type: "duckdb", path: ":memory:" } as never }) + await Dispatcher.call("warehouse.list", {}).catch(() => {}) + + Registry.setConfigs({ + pinned_first: { type: "duckdb", path: ":memory:" } as never, + other: { type: "duckdb", path: ":memory:" } as never, + }) + + // Start the call, then mutate while it is suspended in the dbt attempt. + const inflight = Dispatcher.call("sql.execute", { sql: "select 1" } as never) + Registry.setConfigs({ other: { type: "duckdb", path: ":memory:" } as never }) + + // The call must still be about `pinned_first` — the connection the decision + // covered — rather than quietly landing on whatever now sorts first. The handler + // reports connection failures in the result rather than throwing, so the named + // connection in the error is what identifies which one it tried. + const result = (await inflight) as { error?: string } + expect(result.error).toMatch(/pinned_first/) + }) +}) + +describe("a connection replaced under the same name is not executed on the old verdict", () => { + // Pinning the name closes the case where the *identity* of the default changes. + // It does not close a same-name replacement: `Registry.get(name)` still consults the + // mutable registry after the dbt await, so a name re-added against a different + // warehouse would execute under a routing decision computed for the old one. The + // decision is a function of the connection's canonical type, so pinning the type + // pins the decision. + test("a same-name replacement of a different type is refused, not run locally", async () => { + Registry.setConfigs({ warm: { type: "duckdb", path: ":memory:" } as never }) + await Dispatcher.call("warehouse.list", {}).catch(() => {}) + + Registry.setConfigs({ primary: { type: "duckdb", path: ":memory:" } as never }) + const inflight = Dispatcher.call("sql.execute", { sql: "select 1" } as never) + // Same name, different warehouse — the kind a workspace integration may shadow. + Registry.setConfigs({ primary: { type: "snowflake", account: "a" } as never }) + + const result = (await inflight) as { error?: string } + expect(result.error).toMatch(/changed while this query was being prepared/) + }) + + test("a same-name rewrite that keeps the type still runs", async () => { + // The guard binds the routing decision, not the config bytes: an edit that cannot + // change where the call is routed must not turn into a spurious failure. + Registry.setConfigs({ warm: { type: "duckdb", path: ":memory:" } as never }) + await Dispatcher.call("warehouse.list", {}).catch(() => {}) + + Registry.setConfigs({ primary: { type: "postgres", host: "a" } as never }) + const inflight = Dispatcher.call("sql.execute", { sql: "select 1" } as never) + Registry.setConfigs({ primary: { type: "postgresql", host: "b" } as never }) + + const result = (await inflight) as { error?: string } + expect(result.error ?? "").not.toMatch(/changed while this query was being prepared/) + }) +}) diff --git a/packages/opencode/test/altimate/precedence-guard-order.test.ts b/packages/opencode/test/altimate/precedence-guard-order.test.ts new file mode 100644 index 0000000000..392b1e07d1 --- /dev/null +++ b/packages/opencode/test/altimate/precedence-guard-order.test.ts @@ -0,0 +1,198 @@ +// altimate_change - new file +// +// Where the precedence guard sits inside a tool body is a correctness property, not a +// style choice: a redirect returns early, so anything it jumps over stops running. +// Two checks must survive it. +// +// - `sql_execute`'s hard deny on DROP DATABASE / DROP SCHEMA / TRUNCATE says it +// "cannot be overridden", and the engine's execution tools apply no such list. If a +// redirect were returned first, a blocked statement would come back as an +// instruction to call the engine tool — a way around the block. +// - `sql_explain`'s pre-flight validators exist so malformed input gets an actionable +// message. A redirect reads as success, so returning one first would send the model +// to the engine tool carrying the same bad arguments. +import { afterEach, beforeEach, describe, expect, test } from "bun:test" +import { MessageID, SessionID } from "../../src/session/schema" +import { SqlExecuteTool } from "../../src/altimate/tools/sql-execute" +import { SqlExplainTool } from "../../src/altimate/tools/sql-explain" +import { SchemaInspectTool } from "../../src/altimate/tools/schema-inspect" +import { initTool } from "./tool-fixture" +import * as Registry from "../../src/altimate/native/connections/registry" +import { check, precedenceInternals, refresh, resetForTests } from "../../src/altimate/workspace/precedence" + +const SESSION = SessionID.make("ses_guard_order") +const ORIGINAL_PILOT = process.env.ALTIMATE_WORKSPACE + +const ctx = { + sessionID: SESSION, + messageID: MessageID.make("msg_guard_order"), + callID: "", + agent: "build", + abort: AbortSignal.any([]), + messages: [] as any[], + metadata: () => {}, + ask: async () => {}, +} + +/** A workspace serving snowflake for all three capabilities. */ +const SNOWFLAKE_TOOLS = { + datamate_snowflake_execute_database_query: {}, + datamate_snowflake_get_query_explain_plan: {}, + datamate_snowflake_get_table_stats: {}, +} + +beforeEach(async () => { + resetForTests() + process.env.ALTIMATE_WORKSPACE = "1" + delete process.env.ALTIMATE_INTEGRATIONS + precedenceInternals.binding = async () => ({ datamateId: 5, datamateName: "demo" }) + precedenceInternals.attributedTo = async () => "5" + // Attribution is grounded in the attach outcome as well as the pin, so a test that + // wants precedence engaged has to attest the engine too. + precedenceInternals.attachOutcome = async () => ({ kind: "attached", available: 12, declared: 12, missing: [] }) + precedenceInternals.announce = async () => {} + Registry.setConfigs({ + shadowed_snow: { type: "snowflake", account: "a", user: "u" } as never, + }) + await refresh(SESSION, SNOWFLAKE_TOOLS) +}) + +afterEach(() => { + resetForTests() + Registry.reset() + if (ORIGINAL_PILOT === undefined) delete process.env.ALTIMATE_WORKSPACE + else process.env.ALTIMATE_WORKSPACE = ORIGINAL_PILOT +}) + +describe("sql_execute — the hard deny outranks the redirect", () => { + test("a blocked statement on a shadowed connection still throws", async () => { + const tool = await initTool(SqlExecuteTool) + await expect( + tool.execute({ query: "DROP DATABASE analytics", warehouse: "shadowed_snow", limit: 10 }, ctx), + ).rejects.toThrow(/cannot be overridden/i) + }) + + test("every hard-denied form is still blocked, not redirected", async () => { + const tool = await initTool(SqlExecuteTool) + for (const query of ["DROP DATABASE x", "DROP SCHEMA public", "TRUNCATE TABLE orders"]) { + await expect(tool.execute({ query, warehouse: "shadowed_snow", limit: 10 }, ctx)).rejects.toThrow( + /blocked for safety/i, + ) + } + }) + + test("an ordinary read on the same connection is still redirected", async () => { + // Guards the fix from over-correcting: only the hard deny outranks precedence. + const tool = await initTool(SqlExecuteTool) + const result: any = await tool.execute({ query: "select 1", warehouse: "shadowed_snow", limit: 10 }, ctx) + expect(result.metadata.redirected).toBe(true) + }) + + test("a write still asks for approval before it is redirected", async () => { + // The prompt is not wasted: the write still happens, through the engine. An engine + // tool key is matched by the builder's `"*": "allow"` rule, while `sql_execute_write` + // is "ask" — so redirecting first would let the same statement reach the warehouse + // without the confirmation it needed a moment earlier. + const tool = await initTool(SqlExecuteTool) + const asked: any[] = [] + const result: any = await tool.execute( + { query: "insert into t values (1)", warehouse: "shadowed_snow", limit: 10 }, + { ...ctx, ask: async (req: any) => void asked.push(req) }, + ) + expect(asked.map((r) => r.permission)).toEqual(["sql_execute_write"]) + expect(result.metadata.redirected).toBe(true) + }) + + test("a denied write is never redirected", async () => { + const tool = await initTool(SqlExecuteTool) + await expect( + tool.execute( + { query: "delete from orders", warehouse: "shadowed_snow", limit: 10 }, + { + ...ctx, + ask: async () => { + throw new Error("denied by the user") + }, + }, + ), + ).rejects.toThrow(/denied by the user/) + }) + + test("a read is redirected without any prompt", async () => { + const tool = await initTool(SqlExecuteTool) + const asked: any[] = [] + const result: any = await tool.execute( + { query: "select 1", warehouse: "shadowed_snow", limit: 10 }, + { ...ctx, ask: async (req: any) => void asked.push(req) }, + ) + expect(asked).toHaveLength(0) + expect(result.metadata.redirected).toBe(true) + }) +}) + +describe("a fail-open notice survives the failure paths", () => { + // The notice and its `precedence` marker exist so a skipped routing decision is + // never silent and can be counted. Attaching them only to the success return + // loses both exactly when the call went wrong — and an undetermined target is a + // sign of a misconfigured setup, so those calls are *more* likely to fail. The + // telemetry would then under-count fail-open in precisely the population it + // exists to measure. + // + // Reached here by giving the registry a single connection of a type no driver + // serves: the default target resolves, its type cannot be canonicalised, so + // `check()` returns the undetermined verdict — and the dispatcher then fails on + // that same unsupported type, giving a genuine error path rather than a mocked one. + beforeEach(async () => { + Registry.setConfigs({ mystery: { type: "notadb", host: "h" } as never }) + await refresh(SESSION, SNOWFLAKE_TOOLS) + }) + + test("check() reports undetermined for a type no driver serves", async () => { + const verdict = await check(SESSION, "sql_execute") + expect(verdict.redirect).toBeUndefined() + expect(verdict.precedence).toBe("undetermined") + expect(verdict.notice).toContain("could not be determined") + }) + + test("sql_execute carries the marker and the reason", async () => { + // Not the error path: the unknown type that produces the notice also makes the + // dispatcher a no-op, so a notice and a throw cannot co-occur here. The failure + // exits are covered by schema_inspect below, which does fail for real. + const tool = await initTool(SqlExecuteTool) + const result: any = await tool.execute({ query: "select 1", limit: 10 }, ctx) + expect(result.metadata.precedence).toBe("undetermined") + expect(result.output).toContain("Not routed through workspace") + }) + + test("schema_inspect carries the marker on a genuine failure exit", async () => { + // This one really does fail — an unsupported type has no driver to inspect with — + // so it exercises the exact path the review found unannotated. + const tool = await initTool(SchemaInspectTool) + const result: any = await tool.execute({ table: "orders" }, ctx) + expect(result.metadata.success).toBe(false) + expect(result.metadata.precedence).toBe("undetermined") + expect(result.output).toContain("Not routed through workspace") + }) +}) + +describe("sql_explain — input validation outranks the redirect", () => { + test("an empty statement reports invalid input rather than redirecting", async () => { + const tool = await initTool(SqlExplainTool) + const result: any = await tool.execute({ sql: " ", warehouse: "shadowed_snow" }, ctx) + expect(result.metadata.error_class).toBe("input_validation") + expect(result.metadata.redirected).toBeUndefined() + }) + + test("a malformed warehouse name reports invalid input rather than redirecting", async () => { + const tool = await initTool(SqlExplainTool) + const result: any = await tool.execute({ sql: "select 1", warehouse: " " }, ctx) + expect(result.metadata.error_class).toBe("input_validation") + expect(result.metadata.redirected).toBeUndefined() + }) + + test("valid input on a shadowed connection is still redirected", async () => { + const tool = await initTool(SqlExplainTool) + const result: any = await tool.execute({ sql: "select 1", warehouse: "shadowed_snow" }, ctx) + expect(result.metadata.redirected).toBe(true) + }) +}) diff --git a/packages/opencode/test/altimate/workspace/precedence.test.ts b/packages/opencode/test/altimate/workspace/precedence.test.ts new file mode 100644 index 0000000000..a41d8d1211 --- /dev/null +++ b/packages/opencode/test/altimate/workspace/precedence.test.ts @@ -0,0 +1,1000 @@ +// altimate_change - new file +// +// Unit coverage for workspace precedence: which side serves a warehouse call once a +// bound workspace's engine has attached. The binding and the engine-attribution read +// both go through `precedenceInternals`, so these exercise the decision logic without +// booting an instance, reading config, or touching MCP state. +import { afterEach, beforeEach, describe, expect, test } from "bun:test" +import { + MAX_TRACKED_SESSIONS, + check, + decideForTarget, + trackedSessionCount, + announcedSessionCount, + describeEngineTool, + describeNativeTool, + forSession, + inventoryLine, + precedenceInternals, + refresh, + resetForTests, + warehouseListNote, +} from "../../../src/altimate/workspace/precedence" +import * as Registry from "../../../src/altimate/native/connections/registry" +import { canonicalType } from "../../../src/altimate/native/connections/registry" + +const SESSION = "ses_precedence" +const ORIGINAL_INTEGRATIONS = process.env.ALTIMATE_INTEGRATIONS +const ORIGINAL_PILOT = process.env.ALTIMATE_WORKSPACE + +/** The engine tools a workspace with a Snowflake connection materialises. Snowflake is + * the only integration serving all three capabilities. */ +const tick = () => new Promise((resolve) => setTimeout(resolve, 0)) + +const SNOWFLAKE_TOOLS = { + datamate_snowflake_execute_database_query: {}, + datamate_snowflake_get_query_explain_plan: {}, + datamate_snowflake_get_table_stats: {}, + datamate_snowflake_list_database_connections: {}, +} + +/** BigQuery and postgresql ship execute + list only — no explain, no table stats. */ +const BIGQUERY_TOOLS = { + datamate_bigquery_execute_database_query: {}, + datamate_bigquery_list_database_connections: {}, +} + +function bindTo(id = 42, name = "analytics") { + precedenceInternals.binding = async () => ({ datamateId: id, datamateName: name }) + precedenceInternals.attributedTo = async () => String(id) + precedenceInternals.attachOutcome = async () => ({ kind: "attached", available: 12, declared: 12, missing: [] }) +} + +beforeEach(() => { + resetForTests() + delete process.env.ALTIMATE_INTEGRATIONS + process.env.ALTIMATE_WORKSPACE = "1" + bindTo() + // Real local connections. Without them `check()` would return "run" simply because + // the connection is unknown, and every "stays local" assertion below would pass + // without proving anything. + Registry.setConfigs({ + local_snow: { type: "snowflake", account: "acct", user: "u" } as never, + local_duck: { type: "duckdb", path: ":memory:" } as never, + bq_conn: { type: "bigquery", project: "p" } as never, + pg_conn: { type: "postgresql", host: "h" } as never, + rs_conn: { type: "redshift", host: "h" } as never, + }) +}) + +afterEach(() => { + resetForTests() + Registry.reset() + if (ORIGINAL_INTEGRATIONS === undefined) delete process.env.ALTIMATE_INTEGRATIONS + else process.env.ALTIMATE_INTEGRATIONS = ORIGINAL_INTEGRATIONS + if (ORIGINAL_PILOT === undefined) delete process.env.ALTIMATE_WORKSPACE + else process.env.ALTIMATE_WORKSPACE = ORIGINAL_PILOT +}) + +describe("the workspace pilot gate", () => { + test("precedence stays off when the pilot flag is not set", async () => { + // A binding and a pinned entry both persist in config, and the MCP client connects + // that entry regardless of the pilot flag — so engine tools can materialise for + // someone who opted out. Opting out has to mean it. + delete process.env.ALTIMATE_WORKSPACE + const precedence = await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(precedence.enabled).toBe(false) + expect(precedence.disabledReason).toBe("pilot-off") + }) + + test("a served connection still runs locally with the pilot off", async () => { + delete process.env.ALTIMATE_WORKSPACE + await refresh(SESSION, SNOWFLAKE_TOOLS) + const verdict = await check(SESSION, "sql_execute", "local_snow") + expect(verdict.redirect).toBeUndefined() + }) + + test("opting out says nothing rather than announcing itself", async () => { + delete process.env.ALTIMATE_WORKSPACE + const lines: string[] = [] + precedenceInternals.announce = async (line) => void lines.push(line) + await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(lines).toHaveLength(0) + }) +}) + +describe("mechanism 1 — materialised, not declared", () => { + test("engine tools that are present shadow the matching local type", async () => { + const precedence = await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(precedence.enabled).toBe(true) + expect(precedence.shadowed.get("snowflake")?.get("sql_execute")?.modelKey).toBe( + "datamate_snowflake_execute_database_query", + ) + }) + + test("an engine that materialised nothing shadows nothing", async () => { + const precedence = await refresh(SESSION, {}) + expect(precedence.enabled).toBe(false) + expect(precedence.disabledReason).toBe("nothing-materialised") + }) + + test("non-engine MCP tools never confer precedence", async () => { + const precedence = await refresh(SESSION, { jira_get_issue: {}, github_list_prs: {} }) + expect(precedence.enabled).toBe(false) + }) + + test("an unbound session shadows nothing", async () => { + precedenceInternals.binding = async () => null + const precedence = await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(precedence.disabledReason).toBe("unbound") + }) +}) + +describe("attribution is grounded in the attach, not only the saved config", () => { + // A `datamate` entry can be rewritten — by an IDE — from unpinned to pinned while + // MCP keeps serving the process it already connected. The config would then name + // this workspace while the running engine serves another, which is the exact + // mis-routing this design exists to prevent. The attach outcome is the runtime + // signal; the pin is the naming signal; both must agree. + test("an attach still in flight confers no precedence, and does not wait for it", async () => { + // The attach task is deliberately uncapped: the prompt loop bounds its own wait and + // lets a turn proceed without engine tools past the cap, so a broken connection + // cannot hold up the conversation. Attribution reads `settledOutcome`, a pure read + // of state already held, so it cannot reintroduce that wait — an earlier version + // awaited the task itself and hung the turn for the full connection timeout. + // + // `undefined` covers both "in flight" and "never attached"; they are + // indistinguishable, and both must fail open rather than route. + precedenceInternals.attachOutcome = async () => undefined + const started = Date.now() + const p = await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(Date.now() - started).toBeLessThan(1000) + expect(p.enabled).toBe(false) + expect(p.disabledReason).toBe("unattributed") + }) + + test("an unattested session runs locally rather than being blocked", async () => { + precedenceInternals.attachOutcome = async () => undefined + await refresh(SESSION, SNOWFLAKE_TOOLS) + const verdict = await check(SESSION, "sql_execute", "local_snow") + expect(verdict.redirect).toBeUndefined() + }) + + test("only an established attach qualifies — every other outcome is refused", async () => { + // Asserts the invariant rather than a sample of it: the allowlist is exactly + // {attached}, so a new Outcome variant defaults to refusing rather than + // silently qualifying. `undefined` is in the list because "in flight" and "never + // attached" are indistinguishable and both must fail open rather than route. + // + // Every kind the attach module can settle except `attached`, plus "not settled". + // A kind this list does not know about (a future variant) is exercised by the + // attach module's own SERVING table, which defaults to refusing. + const refused: Array<{ kind: string } | undefined> = [ + undefined, + { kind: "disabled" }, + { kind: "unbound" }, + { kind: "engine-missing" }, + { kind: "engine-too-old" }, + { kind: "connect-failed" }, + ] + for (const outcome of refused) { + resetForTests() + bindTo() + precedenceInternals.attachOutcome = async () => outcome as never + const p = await refresh(SESSION, SNOWFLAKE_TOOLS) + const label = outcome?.kind ?? "(none)" + // The reason matters as much as the refusal: it is what the inventory line and + // the tool descriptions render, so a refusal with the wrong reason is a wrong + // explanation shown to the user. + expect({ label, enabled: p.enabled, why: p.disabledReason }).toEqual({ + label, + enabled: false, + why: "unattributed", + }) + } + }) + + test("a settled attach qualifies", async () => { + // The other half of the same allowlist: `attached` is the only serving kind (the + // overlay owns the engine it starts, so there is no separate "reused"), and it + // must not have been broken by any of the refusal machinery above. + const qualifying = [{ kind: "attached", available: 12, declared: 12, missing: [] }] + for (const outcome of qualifying) { + resetForTests() + bindTo() + precedenceInternals.attachOutcome = async () => outcome as never + const p = await refresh(SESSION, SNOWFLAKE_TOOLS) + expect({ kind: outcome.kind, enabled: p.enabled }).toEqual({ kind: outcome.kind, enabled: true }) + } + }) + + test("an established attach whose config now names another workspace is refused", async () => { + precedenceInternals.attachOutcome = async () => ({ kind: "attached", available: 12, declared: 12, missing: [] }) + precedenceInternals.attributedTo = async () => "999" + const p = await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(p.disabledReason).toBe("unattributed") + }) +}) + +describe("the per-session caches are bounded", () => { + test("old sessions are evicted rather than accumulating", async () => { + for (let i = 0; i < MAX_TRACKED_SESSIONS + 25; i++) { + await refresh(`ses_bounded_${i}`, SNOWFLAKE_TOOLS) + } + expect(trackedSessionCount()).toBeLessThanOrEqual(MAX_TRACKED_SESSIONS) + // The newest survives; the oldest is gone. + expect(forSession(`ses_bounded_${MAX_TRACKED_SESSIONS + 24}`)).toBeDefined() + expect(forSession("ses_bounded_0")).toBeUndefined() + }) + + test("a line still in flight when its session is evicted does not resurrect it", async () => { + // Publishing is not awaited, so a line can still be pending when its session falls + // out of the cache. Writing the delivery back afterwards would recreate an entry + // for a session eviction has already left — and eviction only ever walks + // `bySession`, so nothing could ever reclaim it. The announcement cache would then + // grow with the lifetime session count, which is the bound this suite exists for. + const settle: Array<() => void> = [] + precedenceInternals.announce = () => new Promise((resolve) => settle.push(resolve)) + + await refresh("ses_evicted", SNOWFLAKE_TOOLS) + expect(settle).toHaveLength(1) + + // Push it out of the cache while its line is still in flight. + for (let i = 0; i < MAX_TRACKED_SESSIONS + 5; i++) { + await refresh(`ses_flood_${i}`, {}) + } + expect(forSession("ses_evicted")).toBeUndefined() + + settle[0]() + await tick() + + // The evicted session left no trace behind: re-deriving it announces afresh rather + // than being suppressed by a record that outlived the eviction. + const said: string[] = [] + precedenceInternals.announce = async (line) => void said.push(line) + await refresh("ses_evicted", SNOWFLAKE_TOOLS) + await tick() + expect(said).toHaveLength(1) + expect(announcedSessionCount()).toBeLessThanOrEqual(MAX_TRACKED_SESSIONS) + }) +}) + +describe("mechanism 1a — attributed to the bound workspace", () => { + test("an engine pinned to a different workspace confers no precedence", async () => { + precedenceInternals.binding = async () => ({ datamateId: 42, datamateName: "analytics" }) + precedenceInternals.attributedTo = async () => "77" + const precedence = await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(precedence.enabled).toBe(false) + expect(precedence.disabledReason).toBe("unattributed") + }) + + test("an unpinned engine confers no precedence", async () => { + precedenceInternals.attributedTo = async () => null + const precedence = await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(precedence.disabledReason).toBe("unattributed") + }) + + test("refusing to engage is fail-open: the local call still runs", async () => { + precedenceInternals.attributedTo = async () => null + await refresh(SESSION, SNOWFLAKE_TOOLS) + const verdict = await check(SESSION, "sql_execute", "local_snow") + expect(verdict.redirect).toBeUndefined() + }) + + test("the inventory line says why shadowing is off", async () => { + precedenceInternals.attributedTo = async () => null + const precedence = await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(inventoryLine(precedence)).toContain("could not be attributed") + }) +}) + +describe("mechanism 2 — capability-scoped, not type-scoped", () => { + test("snowflake shadows execute, explain and inspect", async () => { + const precedence = await refresh(SESSION, SNOWFLAKE_TOOLS) + const byCapability = precedence.shadowed.get("snowflake")! + expect([...byCapability.keys()].sort()).toEqual(["schema_inspect", "sql_execute", "sql_explain"]) + }) + + test("bigquery shadows execute only — explain and inspect stay local", async () => { + const precedence = await refresh(SESSION, BIGQUERY_TOOLS) + const byCapability = precedence.shadowed.get("bigquery")! + expect([...byCapability.keys()]).toEqual(["sql_execute"]) + }) + + test("sql_explain on a bigquery connection is NOT redirected to a tool that does not exist", async () => { + await refresh(SESSION, BIGQUERY_TOOLS) + precedenceInternals.attributedTo = async () => "42" + const verdict = await check(SESSION, "sql_explain", "bq_conn") + expect(verdict.redirect).toBeUndefined() + }) + + test("databricks execute is named by its own convention", async () => { + const precedence = await refresh(SESSION, { datamate_databricks_execute_sql: {} }) + expect(precedence.shadowed.get("databricks")?.get("sql_execute")?.modelKey).toBe("datamate_databricks_execute_sql") + }) + + test("a type with no materialised integration is untouched", async () => { + const precedence = await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(precedence.shadowed.has("duckdb")).toBe(false) + }) +}) + +describe("driver aliases — canonicalType inverts DRIVER_MAP", () => { + test("postgresql and postgres are one driver", () => { + expect(canonicalType("postgresql")).toBe("postgres") + expect(canonicalType("postgres")).toBe("postgres") + }) + + test("mysql/mariadb and the sqlserver family collapse", () => { + expect(canonicalType("mariadb")).toBe("mysql") + expect(canonicalType("mssql")).toBe("sqlserver") + expect(canonicalType("fabric")).toBe("sqlserver") + }) + + test("redshift keeps its own identity and is not served by a postgresql integration", async () => { + expect(canonicalType("redshift")).toBe("redshift") + const precedence = await refresh(SESSION, { + datamate_postgresql_execute_database_query: {}, + }) + expect(precedence.shadowed.has("postgres")).toBe(true) + expect(precedence.shadowed.has("redshift")).toBe(false) + }) + + test("a postgres-typed connection is served by a postgresql integration", async () => { + await refresh(SESSION, { datamate_postgresql_execute_database_query: {} }) + // `pg_conn` is registered as type "postgresql"; the integration id is also + // "postgresql" but the canonical driver is "postgres". The alias collapse is what + // makes these meet. + const verdict = await check(SESSION, "sql_execute", "pg_conn") + expect(verdict.redirect?.metadata.redirect_to).toBe("datamate_postgresql_execute_database_query") + }) + + test("a redshift connection is NOT redirected by a postgresql integration", async () => { + await refresh(SESSION, { datamate_postgresql_execute_database_query: {} }) + const verdict = await check(SESSION, "sql_execute", "rs_conn") + expect(verdict.redirect).toBeUndefined() + }) + + test("an unknown type canonicalises to null rather than guessing", () => { + expect(canonicalType("not-a-database")).toBeNull() + expect(canonicalType(undefined)).toBeNull() + }) +}) + +describe("mechanism 4 — the redirect", () => { + test("carries the machine-readable marker telemetry needs", async () => { + await refresh(SESSION, SNOWFLAKE_TOOLS) + const verdict = await check(SESSION, "sql_execute", "local_snow") + expect(verdict.redirect).toBeDefined() + expect(verdict.redirect!.metadata.redirected).toBe(true) + expect(verdict.redirect!.metadata.redirect_to).toBe("datamate_snowflake_execute_database_query") + expect(verdict.redirect!.metadata.precedence).toBe("shadowed") + }) + + test("names the exact engine key in the text the model reads", async () => { + await refresh(SESSION, SNOWFLAKE_TOOLS) + const verdict = await check(SESSION, "sql_execute", "local_snow") + expect(verdict.redirect!.output).toContain("datamate_snowflake_execute_database_query") + expect(verdict.redirect!.output).toContain("--integrations=local") + }) + + test("a connection whose type is not served runs locally", async () => { + await refresh(SESSION, SNOWFLAKE_TOOLS) + const verdict = await check(SESSION, "sql_execute", "local_duck") + expect(verdict.redirect).toBeUndefined() + }) +}) + +describe("the dbt-fallback redirect explains itself", () => { + test("names the fallback connection and both ways out", async () => { + await refresh(SESSION, SNOWFLAKE_TOOLS) + // Reach the fallback branch directly: the default target is dbt with a served + // registry fallback behind it. + const verdict = await check(SESSION, "sql_execute", "local_snow") + // (the explicit-warehouse path shares redirectFor; assert the plain wording here) + expect(verdict.redirect!.output).toContain("--integrations=local") + expect(verdict.redirect!.metadata.via).toBeUndefined() + }) +}) + +describe("a redirect the caller cannot follow is not a redirect", () => { + // The `analyst` agent denies everything it does not name and names the native + // warehouse tools but never the engine keys. Redirecting its permitted reads to a + // tool it is forbidden to call would take away the one thing that agent exists to + // do — the same dead end as redirecting to a tool that does not exist. + const analystLike = [ + { permission: "*", pattern: "*", action: "deny" as const }, + { permission: "sql_execute", pattern: "*", action: "allow" as const }, + { permission: "sql_explain", pattern: "*", action: "allow" as const }, + { permission: "schema_inspect", pattern: "*", action: "allow" as const }, + ] + const builderLike = [{ permission: "*", pattern: "*", action: "allow" as const }] + + test("a caller denied the engine key runs locally, and is told why", async () => { + await refresh(SESSION, SNOWFLAKE_TOOLS, analystLike) + const verdict = await check(SESSION, "sql_execute", "local_snow") + expect(verdict.redirect).toBeUndefined() + expect(verdict.precedence).toBe("undetermined") + expect(verdict.notice).toContain("not permitted to call") + }) + + test("a caller allowed the engine key is still redirected", async () => { + await refresh(SESSION, SNOWFLAKE_TOOLS, builderLike) + const verdict = await check(SESSION, "sql_execute", "local_snow") + expect(verdict.redirect?.metadata.redirect_to).toBe("datamate_snowflake_execute_database_query") + }) + + test("no ruleset means unknown, which is treated as reachable", async () => { + await refresh(SESSION, SNOWFLAKE_TOOLS) + const verdict = await check(SESSION, "sql_execute", "local_snow") + expect(verdict.redirect).toBeDefined() + }) + + test("the default-target path is gated too, not just the named-warehouse path", async () => { + const p = await refresh(SESSION, SNOWFLAKE_TOOLS, analystLike) + const v = decideForTarget(p, "sql_execute", { source: "registry", type: "snowflake", name: "s" }) + expect(v.redirect).toBeUndefined() + expect(v.notice).toContain("not permitted to call") + }) + + test("the dbt-fallback path is gated too", async () => { + const p = await refresh(SESSION, SNOWFLAKE_TOOLS, analystLike) + const v = decideForTarget(p, "sql_execute", { + source: "dbt", + type: undefined, + fallback: { type: "snowflake", name: "local_snow" }, + }) + expect(v.redirect).toBeUndefined() + expect(v.notice).toContain("not permitted to call") + }) +}) + +describe("reporting never claims a routing that will not happen", () => { + // The listing is what the model reads before choosing a tool. Telling an analyst a + // connection is served by the workspace, when that agent's calls demonstrably run + // locally, is worse than saying nothing: it points the model at the wrong tool. + const analystLike = [ + { permission: "*", pattern: "*", action: "deny" as const }, + { permission: "sql_execute", pattern: "*", action: "allow" as const }, + { permission: "sql_explain", pattern: "*", action: "allow" as const }, + { permission: "schema_inspect", pattern: "*", action: "allow" as const }, + ] + + test("warehouse_list still marks the row for a caller that can reach it", async () => { + const p = await refresh(SESSION, SNOWFLAKE_TOOLS, [{ permission: "*", pattern: "*", action: "allow" as const }]) + expect(warehouseListNote(p, "snowflake")).toContain("via workspace") + }) + + test("no surface claims a routing the caller cannot follow", async () => { + // Asserted together rather than one test per surface: the failure this guards is + // exactly that these drift apart, so the invariant is that every surface agrees + // with the routing decision. Three findings in this review were a surface still + // asserting what had stopped being true. + const p = await refresh(SESSION, SNOWFLAKE_TOOLS, analystLike) + const verdict = await check(SESSION, "sql_execute", "local_snow") + expect({ + listing: warehouseListNote(p, "snowflake"), + inventory: inventoryLine(p), + description: describeNativeTool("sql_execute", "Execute SQL.", p), + redirected: verdict.redirect !== undefined, + }).toEqual({ + listing: null, + inventory: "", + description: "Execute SQL.", + redirected: false, + }) + }) + + test("a partially-reachable caller is reported per capability", async () => { + // Allowed to execute through the engine, denied explain and inspect. + const p = await refresh(SESSION, SNOWFLAKE_TOOLS, [ + { permission: "*", pattern: "*", action: "deny" as const }, + { permission: "datamate_snowflake_execute_database_query", pattern: "*", action: "allow" as const }, + ]) + const note = warehouseListNote(p, "snowflake") + expect(note).toContain("execute via workspace") + expect(note).toContain("explain/inspect local") + }) +}) + +describe("descriptions are per capability, and corrections are delivered", () => { + test("an execute-only integration leaves explain and inspect described as local", async () => { + // BigQuery provides execute alone, so sql_explain and schema_inspect really do + // stay local. Telling them they redirect would steer the model away from the + // local tool that actually works. + const p = await refresh(SESSION, BIGQUERY_TOOLS) + expect(describeNativeTool("sql_execute", "Run SQL.", p)).toContain("redirect") + expect(describeNativeTool("sql_explain", "Explain SQL.", p)).toBe("Explain SQL.") + expect(describeNativeTool("schema_inspect", "Inspect.", p)).toBe("Inspect.") + }) + + test("a full integration describes all three as redirecting", async () => { + const p = await refresh(SESSION, SNOWFLAKE_TOOLS) + for (const id of ["sql_execute", "sql_explain", "schema_inspect"]) { + expect(describeNativeTool(id, "Base.", p)).toContain("redirect") + } + }) + + test("warehouse_list still notes the listing whenever anything is served", async () => { + const p = await refresh(SESSION, BIGQUERY_TOOLS) + expect(describeNativeTool("warehouse_list", "List.", p)).toContain("redirect") + }) + + test("a corrected inventory is announced, not suppressed", async () => { + // The first turn can legitimately announce "shadowing off" — an attach that + // outran its bounded wait is indistinguishable from no engine — and precedence is + // re-derived every turn, so the session must be told when that changes. + const lines: string[] = [] + precedenceInternals.announce = async (line) => void lines.push(line) + precedenceInternals.attachOutcome = async () => undefined + await refresh(SESSION, SNOWFLAKE_TOOLS) + const afterFirst = lines.length + + precedenceInternals.attachOutcome = async () => ({ kind: "attached", available: 12, declared: 12, missing: [] }) + await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(lines.length).toBeGreaterThan(afterFirst) + expect(lines[lines.length - 1]).toContain("via workspace") + }) + + test("an unchanged inventory is not repeated every turn", async () => { + const lines: string[] = [] + precedenceInternals.announce = async (line) => void lines.push(line) + await refresh(SESSION, SNOWFLAKE_TOOLS) + await refresh(SESSION, SNOWFLAKE_TOOLS) + await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(lines).toHaveLength(1) + }) + + test("routing stopping entirely is announced, not swallowed", async () => { + // The transition the user most needs to hear, and the one an empty inventory + // string cannot express on its own: they were told calls are routed, and now + // they are not. + const lines: string[] = [] + precedenceInternals.announce = async (line) => void lines.push(line) + await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(lines).toHaveLength(1) + await refresh(SESSION, {}) + expect(lines).toHaveLength(2) + expect(lines[1]).toContain("runs on the local drivers") + }) + + test("routing never having started is not announced as routing stopping", async () => { + // "Shadowing off, the engine could not be attributed" is a non-empty announcement + // that is NOT routing. Treating any prior announcement as routing would tell the + // user routing had stopped when it never began — common when the first attach + // outruns its wait and later exposes only non-warehouse tools. + const lines: string[] = [] + precedenceInternals.announce = async (line) => void lines.push(line) + precedenceInternals.attributedTo = async () => null + await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(lines).toHaveLength(1) + expect(lines[0]).toContain("could not be attributed") + + precedenceInternals.attributedTo = async () => "42" + await refresh(SESSION, {}) + expect(lines.some((l) => l.includes("any more"))).toBe(false) + }) + + test("a line that failed to publish is said again, not remembered as said", async () => { + // The toast bridge can be briefly unavailable. Recording the line as announced + // regardless would suppress it permanently: every later turn with the same + // inventory sees it as unchanged and skips it, so the session is never told what + // its calls are doing. + const attempts: string[] = [] + precedenceInternals.announce = async (line) => { + attempts.push(line) + throw new Error("event bridge unavailable") + } + await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(attempts).toHaveLength(1) + + // Same inventory, so nothing has changed — but nothing was delivered either. + await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(attempts).toHaveLength(2) + + // Once it lands, it settles: the retry stops rather than repeating every turn. + precedenceInternals.announce = async (line) => void attempts.push(line) + await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(attempts).toHaveLength(3) + await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(attempts).toHaveLength(3) + }) + + test("a failed delivery does not corrupt what the session is believed to know", async () => { + // The restore has to put back the PREVIOUS record, not clear it: dropping it would + // lose whether the session had been routing, and a later stop would go unannounced. + const delivered: string[] = [] + precedenceInternals.announce = async (line) => void delivered.push(line) + await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(delivered).toHaveLength(1) + + // A failing announcement of a DIFFERENT line, which must not erase the routing state. + precedenceInternals.announce = async () => { + throw new Error("event bridge unavailable") + } + await refresh(SESSION, BIGQUERY_TOOLS) + + // Routing stops. The session was routing, so it must still be told so. + precedenceInternals.announce = async (line) => void delivered.push(line) + await refresh(SESSION, {}) + expect(delivered.some((l) => l.includes("any more"))).toBe(true) + }) + + test("two announcements that both fail are both still owed", async () => { + // Nothing may be treated as delivered until it arrives. Two lines can be pending at + // once — publishing is deliberately not awaited, so a turn is never held up by a + // toast — and if neither lands, neither may be remembered as said. + const attempts: string[] = [] + const fail: Array<() => void> = [] + precedenceInternals.announce = (line) => { + attempts.push(line) + return new Promise((_resolve, reject) => fail.push(() => reject(new Error("bridge down")))) + } + + await refresh(SESSION, SNOWFLAKE_TOOLS) + await refresh(SESSION, BIGQUERY_TOOLS) + // The second waits for the first rather than racing it. + expect(attempts).toHaveLength(1) + + fail[0]() + await tick() + expect(attempts).toHaveLength(2) + expect(attempts[1]).not.toBe(attempts[0]) + fail[1]() + await tick() + + // Neither arrived, so the first inventory is still unsaid. + await refresh(SESSION, SNOWFLAKE_TOOLS) + await tick() + expect(attempts).toHaveLength(3) + expect(attempts[2]).toBe(attempts[0]) + }) + + test("announcements arrive in the order they were decided", async () => { + // Refreshes are serialized, but publishing is not awaited, so without a chain two + // lines could be in flight at once and land in either order — leaving the stale one + // on screen while the newer one is recorded as the session's state. + const order: string[] = [] + const settle: Array<() => void> = [] + precedenceInternals.announce = (line) => { + order.push(line) + return new Promise((resolve) => settle.push(resolve)) + } + + await refresh(SESSION, SNOWFLAKE_TOOLS) + await refresh(SESSION, BIGQUERY_TOOLS) + expect(order).toHaveLength(1) + + settle[0]() + await tick() + expect(order).toHaveLength(2) + settle[1]() + await tick() + + // The newest line is what the session is recorded as knowing, so re-deriving that + // same inventory stays quiet rather than repeating it. + await refresh(SESSION, BIGQUERY_TOOLS) + await tick() + expect(order).toHaveLength(2) + }) + + test("a correction back to the delivered line is not suppressed by one still in flight", async () => { + // Inventory can return to what was already announced while a different line is + // mid-publication. Comparing only against the delivered line would drop that + // correction, and the queue would then deliver the stale line last — leaving the + // session looking at guidance that no longer matches where its calls go. + const order: string[] = [] + const settle: Array<() => void> = [] + precedenceInternals.announce = (line) => { + order.push(line) + return new Promise((resolve) => settle.push(resolve)) + } + + await refresh(SESSION, SNOWFLAKE_TOOLS) + settle[0]() + await tick() + expect(order).toHaveLength(1) + + // A different inventory, left in flight. + await refresh(SESSION, BIGQUERY_TOOLS) + expect(order).toHaveLength(2) + + // Back to the first inventory before that one lands. + await refresh(SESSION, SNOWFLAKE_TOOLS) + settle[1]() + await tick() + + // The correction was queued, so it is what the session is left looking at. + expect(order).toHaveLength(3) + expect(order[2]).toBe(order[0]) + }) + + test("routing that stops before its announcement lands is still reported stopped", async () => { + // The stop decision has to consult what the session is committed to saying, not + // only what it has been told. With the first routing line still in flight, a + // refresh that serves nothing would otherwise queue no correction at all — and the + // routing line would then arrive after routing had already stopped. + const order: string[] = [] + const settle: Array<() => void> = [] + precedenceInternals.announce = (line) => { + order.push(line) + return new Promise((resolve) => settle.push(resolve)) + } + + await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(order).toHaveLength(1) + + // Routing stops while that first line is still pending. + await refresh(SESSION, {}) + settle[0]() + await tick() + + expect(order).toHaveLength(2) + expect(order[1]).toContain("any more") + }) + + test("a session that never had routing is still told nothing", async () => { + const lines: string[] = [] + precedenceInternals.announce = async (line) => void lines.push(line) + await refresh(SESSION, {}) + expect(lines).toHaveLength(0) + }) + + test("routing stopping is announced once, not every turn", async () => { + const lines: string[] = [] + precedenceInternals.announce = async (line) => void lines.push(line) + await refresh(SESSION, SNOWFLAKE_TOOLS) + await refresh(SESSION, {}) + await refresh(SESSION, {}) + await refresh(SESSION, {}) + expect(lines).toHaveLength(2) + }) + + test("a shrinking engine re-announces the smaller inventory", async () => { + const lines: string[] = [] + precedenceInternals.announce = async (line) => void lines.push(line) + await refresh(SESSION, SNOWFLAKE_TOOLS) + await refresh(SESSION, { datamate_snowflake_execute_database_query: {} }) + expect(lines).toHaveLength(2) + expect(lines[1]).toContain("explain/inspect stay local") + }) +}) + +describe("a snapshot must not outlive the binding that justified it", () => { + test("a mid-flight re-link stops the redirect naming the old workspace", async () => { + // Re-linking mid-session is supported, so the turn's snapshot can name a workspace + // the project has already left. Following a redirect to it would run the query + // with that workspace's credentials — the exact mis-routing this design prevents. + await refresh(SESSION, SNOWFLAKE_TOOLS) + expect((await check(SESSION, "sql_execute", "local_snow")).redirect).toBeDefined() + + precedenceInternals.binding = async () => ({ datamateId: 77, datamateName: "somewhere-else" }) + const verdict = await check(SESSION, "sql_execute", "local_snow") + expect(verdict.redirect).toBeUndefined() + expect(verdict.precedence).toBe("undetermined") + expect(verdict.notice).toContain("re-linked") + }) + + test("an unchanged binding still redirects", async () => { + await refresh(SESSION, SNOWFLAKE_TOOLS) + expect((await check(SESSION, "sql_execute", "local_snow")).redirect).toBeDefined() + }) + + test("a session whose snapshot was evicted says so rather than running silently", async () => { + // Eviction can drop an entry between tool resolution and the call. Returning a + // bare "run" there is indistinguishable from a considered "not served", so a + // shadowed connection would execute locally with no indication. + const verdict = await check("ses_never_derived", "sql_execute", "local_snow") + expect(verdict.redirect).toBeUndefined() + expect(verdict.precedence).toBe("undetermined") + expect(verdict.notice).toContain("no routing decision") + }) +}) + +describe("default-target decisions — branch order", () => { + // Reaching a dbt-sourced target through check() needs a real dbt project, so the + // order of these branches is only checkable on the pure function. It is also the + // property that has broken most often, which is why it gets its own suite. + const snowflakeFallback = { type: "snowflake", name: "local_snow" } + + test("a served dbt target redirects", async () => { + const p = await refresh(SESSION, SNOWFLAKE_TOOLS) + const v = decideForTarget(p, "sql_execute", { source: "dbt", type: "snowflake" }) + expect(v.redirect?.metadata.redirect_to).toBe("datamate_snowflake_execute_database_query") + }) + + test("an UNDETERMINED dbt type still redirects when the fallback behind it is served", async () => { + // The regression this suite exists for: returning "undetermined" before looking + // at the fallback fails open into a local execution against a served connection. + // An undetermined type is *more* likely to be the broken setup that falls back. + const p = await refresh(SESSION, SNOWFLAKE_TOOLS) + const v = decideForTarget(p, "sql_execute", { source: "dbt", type: undefined, fallback: snowflakeFallback }) + expect(v.redirect).toBeDefined() + expect(v.redirect!.metadata.via).toBe("dbt-fallback") + expect(v.precedence).toBeUndefined() + }) + + test("an undetermined dbt type with an UNSERVED fallback runs locally, non-silently", async () => { + const p = await refresh(SESSION, SNOWFLAKE_TOOLS) + const v = decideForTarget(p, "sql_execute", { + source: "dbt", + type: undefined, + fallback: { type: "duckdb", name: "local_duck" }, + }) + expect(v.redirect).toBeUndefined() + expect(v.precedence).toBe("undetermined") + expect(v.notice).toContain("could not be determined") + }) + + test("an undetermined dbt type with no fallback at all runs locally, non-silently", async () => { + const p = await refresh(SESSION, SNOWFLAKE_TOOLS) + const v = decideForTarget(p, "sql_execute", { source: "dbt", type: undefined }) + expect(v.precedence).toBe("undetermined") + }) + + test("an unserved dbt type with a served fallback still redirects", async () => { + const p = await refresh(SESSION, SNOWFLAKE_TOOLS) + const v = decideForTarget(p, "sql_execute", { source: "dbt", type: "duckdb", fallback: snowflakeFallback }) + expect(v.redirect!.metadata.via).toBe("dbt-fallback") + }) + + test("a registry target is decided on its own type, with no fallback notion", async () => { + const p = await refresh(SESSION, SNOWFLAKE_TOOLS) + expect( + decideForTarget(p, "sql_execute", { source: "registry", type: "snowflake", name: "s" }).redirect, + ).toBeDefined() + expect( + decideForTarget(p, "sql_execute", { source: "registry", type: "duckdb", name: "d" }).redirect, + ).toBeUndefined() + }) + + test("no resolvable target runs locally", async () => { + const p = await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(decideForTarget(p, "sql_execute", { source: "none" })).toEqual({}) + }) + + test("explain is decided per capability, so an execute-only integration leaves it local", async () => { + const p = await refresh(SESSION, BIGQUERY_TOOLS) + expect( + decideForTarget(p, "sql_execute", { source: "registry", type: "bigquery", name: "b" }).redirect, + ).toBeDefined() + expect( + decideForTarget(p, "sql_explain", { source: "registry", type: "bigquery", name: "b" }).redirect, + ).toBeUndefined() + }) +}) + +describe("mechanism 6 — the escape hatch", () => { + test("--integrations=local turns shadowing off for the session", async () => { + process.env.ALTIMATE_INTEGRATIONS = "local" + const precedence = await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(precedence.enabled).toBe(false) + expect(precedence.disabledReason).toBe("escape-hatch") + expect(inventoryLine(precedence)).toContain("--integrations=local") + }) + + test("with the hatch on, a served connection still runs locally", async () => { + process.env.ALTIMATE_INTEGRATIONS = "local" + await refresh(SESSION, SNOWFLAKE_TOOLS) + const verdict = await check(SESSION, "sql_execute", "local_snow") + expect(verdict.redirect).toBeUndefined() + }) + + test("--integrations=workspace leaves precedence on", async () => { + process.env.ALTIMATE_INTEGRATIONS = "workspace" + const precedence = await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(precedence.enabled).toBe(true) + }) +}) + +describe("descriptions and listings", () => { + test("engine tools serving a shadowed capability name the workspace", async () => { + const precedence = await refresh(SESSION, SNOWFLAKE_TOOLS) + const described = describeEngineTool("datamate_snowflake_execute_database_query", "Run a query.", precedence) + expect(described).toContain("(workspace analytics)") + }) + + test("an engine tool that shadows nothing is described unchanged", async () => { + const precedence = await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(describeEngineTool("datamate_jira_get_issue", "Get an issue.", precedence)).toBe("Get an issue.") + }) + + test("native warehouse tools say that served types redirect", async () => { + const precedence = await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(describeNativeTool("sql_execute", "Execute SQL.", precedence)).toContain("analytics") + }) + + test("unrelated native tools are described unchanged", async () => { + const precedence = await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(describeNativeTool("read", "Read a file.", precedence)).toBe("Read a file.") + }) + + test("descriptions are untouched when precedence is off", async () => { + const precedence = await refresh(SESSION, {}) + expect(describeNativeTool("sql_execute", "Execute SQL.", precedence)).toBe("Execute SQL.") + }) + + test("warehouse_list notes are per capability", async () => { + const precedence = await refresh(SESSION, BIGQUERY_TOOLS) + const note = warehouseListNote(precedence, "bigquery") + expect(note).toContain("execute via workspace analytics") + expect(note).toContain("explain/inspect local") + }) + + test("warehouse_list leaves an unserved type unmarked", async () => { + const precedence = await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(warehouseListNote(precedence, "duckdb")).toBeNull() + }) + + test("the inventory line reports served and local capabilities", async () => { + const precedence = await refresh(SESSION, BIGQUERY_TOOLS) + const line = inventoryLine(precedence) + expect(line).toContain("bigquery: execute via workspace analytics") + expect(line).toContain("explain/inspect stay local") + }) +}) + +describe("mechanism 6 — the inventory is stated once per session", () => { + test("the line is reported on first derivation", async () => { + const lines: string[] = [] + precedenceInternals.announce = async (line) => void lines.push(line) + await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(lines).toHaveLength(1) + expect(lines[0]).toContain("snowflake: execute/explain/inspect via workspace analytics") + }) + + test("re-deriving every turn does not repeat it", async () => { + const lines: string[] = [] + precedenceInternals.announce = async (line) => void lines.push(line) + await refresh(SESSION, SNOWFLAKE_TOOLS) + await refresh(SESSION, SNOWFLAKE_TOOLS) + await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(lines).toHaveLength(1) + }) + + test("the escape hatch is reported rather than passing silently", async () => { + process.env.ALTIMATE_INTEGRATIONS = "local" + const lines: string[] = [] + precedenceInternals.announce = async (line) => void lines.push(line) + await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(lines[0]).toContain("--integrations=local") + }) + + test("an ordinary unbound session says nothing", async () => { + precedenceInternals.binding = async () => null + const lines: string[] = [] + precedenceInternals.announce = async (line) => void lines.push(line) + await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(lines).toHaveLength(0) + }) + + test("counts the local connections that are shadowed", async () => { + const lines: string[] = [] + precedenceInternals.announce = async (line) => void lines.push(line) + await refresh(SESSION, SNOWFLAKE_TOOLS) + // local_snow is snowflake; local_duck, bq_conn, pg_conn and rs_conn are not served. + expect(lines[0]).toContain("1 local connection shadowed") + }) +}) + +describe("re-derivation", () => { + test("precedence follows the live tool map when the engine's tools change", async () => { + await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(forSession(SESSION)?.shadowed.get("snowflake")?.size).toBe(3) + + // The engine's active teammate changed underneath: fewer tools materialise. + await refresh(SESSION, { datamate_snowflake_execute_database_query: {} }) + expect(forSession(SESSION)?.shadowed.get("snowflake")?.size).toBe(1) + + // ...and once nothing is left, nothing is shadowed. + await refresh(SESSION, {}) + expect(forSession(SESSION)?.enabled).toBe(false) + }) + + test("a session with no derivation yet never shadows", async () => { + const verdict = await check("ses_never_refreshed", "sql_execute", "local_snow") + expect(verdict.redirect).toBeUndefined() + // ...and is explicit about it, rather than silently looking like "not served". + expect(verdict.precedence).toBe("undetermined") + }) +}) diff --git a/packages/opencode/test/cli/help/__snapshots__/help-snapshots.test.ts.snap b/packages/opencode/test/cli/help/__snapshots__/help-snapshots.test.ts.snap index e71b7cff4e..94eafe457b 100644 --- a/packages/opencode/test/cli/help/__snapshots__/help-snapshots.test.ts.snap +++ b/packages/opencode/test/cli/help/__snapshots__/help-snapshots.test.ts.snap @@ -6,21 +6,24 @@ exports[`opencode CLI help-text snapshots every documented command emits stable start ACP (Agent Client Protocol) server Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) - [boolean] [default: false] - --port port to listen on [number] [default: 0] - --hostname hostname to listen on [string] [default: "127.0.0.1"] - --mdns enable mDNS service discovery (defaults hostname to 0.0.0.0) - [boolean] [default: false] - --mdns-domain custom domain name for mDNS service (default: altimate-code.local) + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) + [boolean] [default: false] + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"] + --port port to listen on [number] [default: 0] + --hostname hostname to listen on [string] [default: "127.0.0.1"] + --mdns enable mDNS service discovery (defaults hostname to 0.0.0.0) + [boolean] [default: false] + --mdns-domain custom domain name for mDNS service (default: altimate-code.local) [string] [default: "altimate-code.local"] - --cors additional domains to allow for CORS [array] [default: []] - --cwd working directory [string] [default: ""]" + --cors additional domains to allow for CORS [array] [default: []] + --cwd working directory [string] [default: ""]" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode mcp --help 1`] = ` @@ -37,13 +40,16 @@ Commands: altimate-code mcp debug debug OAuth connection for an MCP server Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) - [boolean] [default: false]" + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) + [boolean] [default: false] + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"]" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode attach --help 1`] = ` @@ -55,19 +61,23 @@ Positionals: url http://localhost:4096 [string] [required] Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) [boolean] [default: false] - --dir directory to run in [string] - -c, --continue continue the last session [boolean] - -s, --session session id to continue [string] - --fork fork the session when continuing (use with --continue or --session) [boolean] - -p, --password basic auth password (defaults to OPENCODE_SERVER_PASSWORD) [string] - -u, --username basic auth username (defaults to OPENCODE_SERVER_USERNAME or 'opencode')[string]" + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"] + --dir directory to run in [string] + -c, --continue continue the last session [boolean] + -s, --session session id to continue [string] + --fork fork the session when continuing (use with --continue or --session) [boolean] + -p, --password basic auth password (defaults to OPENCODE_SERVER_PASSWORD) [string] + -u, --username basic auth username (defaults to OPENCODE_SERVER_USERNAME or 'opencode') + [string]" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode run --help 1`] = ` @@ -86,6 +96,10 @@ Options: --pure run without external plugins [boolean] --yolo auto-approve all permission prompts (explicit deny rules still enforced) [boolean] [default: false] + --integrations where warehouse tools run: 'workspace' (default) lets the + bound workspace's engine serve the types it provides; 'local' + keeps every connection on the local drivers + [string] [choices: "workspace", "local"] --command the command to run, use message for args [string] -c, --continue continue the last session [boolean] -s, --session session id to continue [string] @@ -145,13 +159,16 @@ Commands: altimate-code debug wait wait indefinitely (for debugging) Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) - [boolean] [default: false]" + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) + [boolean] [default: false] + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"]" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode providers --help 1`] = ` @@ -165,13 +182,16 @@ Commands: altimate-code providers logout [provider] log out from a configured provider Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) - [boolean] [default: false]" + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) + [boolean] [default: false] + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"]" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode agent --help 1`] = ` @@ -184,13 +204,16 @@ Commands: altimate-code agent list list all available agents Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) - [boolean] [default: false]" + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) + [boolean] [default: false] + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"]" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode upgrade --help 1`] = ` @@ -202,14 +225,17 @@ Positionals: target version to upgrade to, for ex '0.1.48' or 'v0.1.48' [string] Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) [boolean] [default: false] - -m, --method installation method to use + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"] + -m, --method installation method to use [string] [choices: "curl", "npm", "pnpm", "bun", "brew", "choco", "scoop"]" `; @@ -219,17 +245,20 @@ exports[`opencode CLI help-text snapshots every documented command emits stable uninstall altimate-code and remove all related files Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) [boolean] [default: false] - -c, --keep-config keep configuration files [boolean] [default: false] - -d, --keep-data keep session data and snapshots [boolean] [default: false] - --dry-run show what would be removed without removing [boolean] [default: false] - -f, --force skip confirmation prompts [boolean] [default: false]" + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"] + -c, --keep-config keep configuration files [boolean] [default: false] + -d, --keep-data keep session data and snapshots [boolean] [default: false] + --dry-run show what would be removed without removing [boolean] [default: false] + -f, --force skip confirmation prompts [boolean] [default: false]" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode serve --help 1`] = ` @@ -238,20 +267,23 @@ exports[`opencode CLI help-text snapshots every documented command emits stable starts a headless altimate-code server Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) [boolean] [default: false] - --port port to listen on [number] [default: 0] - --hostname hostname to listen on [string] [default: "127.0.0.1"] - --mdns enable mDNS service discovery (defaults hostname to 0.0.0.0) + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"] + --port port to listen on [number] [default: 0] + --hostname hostname to listen on [string] [default: "127.0.0.1"] + --mdns enable mDNS service discovery (defaults hostname to 0.0.0.0) [boolean] [default: false] - --mdns-domain custom domain name for mDNS service (default: altimate-code.local) + --mdns-domain custom domain name for mDNS service (default: altimate-code.local) [string] [default: "altimate-code.local"] - --cors additional domains to allow for CORS [array] [default: []]" + --cors additional domains to allow for CORS [array] [default: []]" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode web --help 1`] = ` @@ -260,20 +292,23 @@ exports[`opencode CLI help-text snapshots every documented command emits stable start altimate-code server and open web interface Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) [boolean] [default: false] - --port port to listen on [number] [default: 0] - --hostname hostname to listen on [string] [default: "127.0.0.1"] - --mdns enable mDNS service discovery (defaults hostname to 0.0.0.0) + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"] + --port port to listen on [number] [default: 0] + --hostname hostname to listen on [string] [default: "127.0.0.1"] + --mdns enable mDNS service discovery (defaults hostname to 0.0.0.0) [boolean] [default: false] - --mdns-domain custom domain name for mDNS service (default: altimate-code.local) + --mdns-domain custom domain name for mDNS service (default: altimate-code.local) [string] [default: "altimate-code.local"] - --cors additional domains to allow for CORS [array] [default: []]" + --cors additional domains to allow for CORS [array] [default: []]" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode models --help 1`] = ` @@ -285,15 +320,18 @@ Positionals: provider provider ID to filter models by [string] Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) [boolean] [default: false] - --verbose use more verbose model output (includes metadata like costs) [boolean] - --refresh refresh the models cache from models.dev [boolean]" + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"] + --verbose use more verbose model output (includes metadata like costs) [boolean] + --refresh refresh the models cache from models.dev [boolean]" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode stats --help 1`] = ` @@ -302,18 +340,22 @@ exports[`opencode CLI help-text snapshots every documented command emits stable show token usage and cost statistics Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) [boolean] [default: false] - --days show stats for the last N days (default: all time) [number] - --tools number of tools to show (default: all) [number] - --models show model statistics (default: hidden). Pass a number to show top N, otherwise - shows all - --project filter by project (default: all projects, empty string: current project)[string]" + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"] + --days show stats for the last N days (default: all time) [number] + --tools number of tools to show (default: all) [number] + --models show model statistics (default: hidden). Pass a number to show top N, + otherwise shows all + --project filter by project (default: all projects, empty string: current project) + [string]" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode export --help 1`] = ` @@ -325,14 +367,17 @@ Positionals: sessionID session id to export [string] Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) [boolean] [default: false] - --sanitize redact sensitive transcript and file data [boolean]" + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"] + --sanitize redact sensitive transcript and file data [boolean]" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode import --help 1`] = ` @@ -344,13 +389,16 @@ Positionals: file path to JSON file or share URL [string] [required] Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) - [boolean] [default: false]" + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) + [boolean] [default: false] + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"]" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode github --help 1`] = ` @@ -363,13 +411,16 @@ Commands: altimate-code github run run the GitHub agent Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) - [boolean] [default: false]" + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) + [boolean] [default: false] + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"]" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode pr --help 1`] = ` @@ -381,13 +432,16 @@ Positionals: number PR number to checkout [number] [required] Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) - [boolean] [default: false]" + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) + [boolean] [default: false] + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"]" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode session --help 1`] = ` @@ -400,13 +454,16 @@ Commands: altimate-code session delete delete a session Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) - [boolean] [default: false]" + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) + [boolean] [default: false] + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"]" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode plugin --help 1`] = ` @@ -418,15 +475,18 @@ Positionals: module npm module name [string] [required] Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) [boolean] [default: false] - -g, --global install in global config [boolean] [default: false] - -f, --force replace existing plugin version [boolean] [default: false]" + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"] + -g, --global install in global config [boolean] [default: false] + -f, --force replace existing plugin version [boolean] [default: false]" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode db --help 1`] = ` @@ -442,14 +502,17 @@ Positionals: query SQL query to execute [string] Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) [boolean] [default: false] - --format Output format [string] [choices: "json", "tsv"] [default: "tsv"]" + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"] + --format Output format [string] [choices: "json", "tsv"] [default: "tsv"]" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode mcp list --help 1`] = ` @@ -458,13 +521,16 @@ exports[`opencode CLI help-text snapshots every documented command emits stable list MCP servers and their status Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) - [boolean] [default: false]" + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) + [boolean] [default: false] + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"]" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode mcp add --help 1`] = ` @@ -473,21 +539,24 @@ exports[`opencode CLI help-text snapshots every documented command emits stable add an MCP server Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) [boolean] [default: false] - --name MCP server name [string] - --type Server type [string] [choices: "local", "remote"] - --url Server URL (for remote type) [string] - --command Command to run (for local type) [string] - --env Environment variables as key=value (repeatable) [array] - --header HTTP headers as key=value (repeatable) [array] - --oauth Enable OAuth [boolean] [default: true] - --global Add to global config [boolean] [default: false]" + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"] + --name MCP server name [string] + --type Server type [string] [choices: "local", "remote"] + --url Server URL (for remote type) [string] + --command Command to run (for local type) [string] + --env Environment variables as key=value (repeatable) [array] + --header HTTP headers as key=value (repeatable) [array] + --oauth Enable OAuth [boolean] [default: true] + --global Add to global config [boolean] [default: false]" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode mcp auth --help 1`] = ` @@ -502,13 +571,16 @@ Positionals: name name of the MCP server [string] Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) - [boolean] [default: false]" + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) + [boolean] [default: false] + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"]" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode mcp logout --help 1`] = ` @@ -520,13 +592,16 @@ Positionals: name name of the MCP server [string] Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) - [boolean] [default: false]" + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) + [boolean] [default: false] + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"]" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode providers list --help 1`] = ` @@ -535,13 +610,16 @@ exports[`opencode CLI help-text snapshots every documented command emits stable list providers and credentials Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) - [boolean] [default: false]" + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) + [boolean] [default: false] + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"]" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode providers login --help 1`] = ` @@ -553,15 +631,18 @@ Positionals: url altimate auth provider [string] Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) [boolean] [default: false] - -p, --provider provider id or name to log in to (skips provider selection) [string] - -m, --method login method label (skips method selection) [string]" + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"] + -p, --provider provider id or name to log in to (skips provider selection) [string] + -m, --method login method label (skips method selection) [string]" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode providers logout --help 1`] = ` @@ -573,13 +654,16 @@ Positionals: provider provider id or name to log out from [string] Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) - [boolean] [default: false]" + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) + [boolean] [default: false] + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"]" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode agent create --help 1`] = ` @@ -595,6 +679,10 @@ Options: --pure run without external plugins [boolean] --yolo auto-approve all permission prompts (explicit deny rules still enforced) [boolean] [default: false] + --integrations where warehouse tools run: 'workspace' (default) lets the bound + workspace's engine serve the types it provides; 'local' keeps every + connection on the local drivers + [string] [choices: "workspace", "local"] --path directory path to generate the agent file [string] --description what the agent should do [string] --mode agent mode [string] [choices: "all", "primary", "subagent"] @@ -610,13 +698,16 @@ exports[`opencode CLI help-text snapshots every documented command emits stable list all available agents Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) - [boolean] [default: false]" + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) + [boolean] [default: false] + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"]" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode session list --help 1`] = ` @@ -625,15 +716,18 @@ exports[`opencode CLI help-text snapshots every documented command emits stable list sessions Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) [boolean] [default: false] - -n, --max-count limit to N most recent sessions [number] - --format output format [string] [choices: "table", "json"] [default: "table"]" + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"] + -n, --max-count limit to N most recent sessions [number] + --format output format [string] [choices: "table", "json"] [default: "table"]" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode session delete --help 1`] = ` @@ -645,13 +739,16 @@ Positionals: sessionID session ID to delete [string] [required] Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) - [boolean] [default: false]" + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) + [boolean] [default: false] + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"]" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode github install --help 1`] = ` @@ -660,13 +757,16 @@ exports[`opencode CLI help-text snapshots every documented command emits stable install the GitHub agent Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) - [boolean] [default: false]" + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) + [boolean] [default: false] + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"]" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode github run --help 1`] = ` @@ -675,15 +775,18 @@ exports[`opencode CLI help-text snapshots every documented command emits stable run the GitHub agent Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) [boolean] [default: false] - --event GitHub mock event to run the agent for [string] - --token GitHub personal access token (github_pat_********) [string]" + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"] + --event GitHub mock event to run the agent for [string] + --token GitHub personal access token (github_pat_********) [string]" `; exports[`opencode CLI help-text snapshots every documented command emits stable help text: opencode db path --help 1`] = ` @@ -692,11 +795,14 @@ exports[`opencode CLI help-text snapshots every documented command emits stable print the database path Options: - -h, --help show help [boolean] - -v, --version show version number [boolean] - --print-logs print logs to stderr [boolean] - --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] - --pure run without external plugins [boolean] - --yolo auto-approve all permission prompts (explicit deny rules still enforced) - [boolean] [default: false]" + -h, --help show help [boolean] + -v, --version show version number [boolean] + --print-logs print logs to stderr [boolean] + --log-level log level [string] [choices: "DEBUG", "INFO", "WARN", "ERROR"] + --pure run without external plugins [boolean] + --yolo auto-approve all permission prompts (explicit deny rules still enforced) + [boolean] [default: false] + --integrations where warehouse tools run: 'workspace' (default) lets the bound workspace's + engine serve the types it provides; 'local' keeps every connection on the + local drivers [string] [choices: "workspace", "local"]" `; From 3e5b9009d01afc94b1bdf765ff73a8a59d4dd1c2 Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Fri, 28 Aug 2026 06:09:18 +0800 Subject: [PATCH 02/10] chore: wrap the native-tool description hooks in start/end markers; format tools.ts The two `describeNativeTool` call sites used the single-line marker form, which the strict marker guard that runs on pushes to main does not recognise. No behaviour change. --- packages/opencode/src/session/prompt.ts | 6 ++++-- packages/opencode/src/session/tools.ts | 12 +++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 6d98f475d5..dc0cb056f6 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1783,8 +1783,9 @@ export namespace SessionPrompt { // altimate_change end tools[item.id] = tool({ id: item.id as any, - // altimate_change — name the workspace on the native side too + // altimate_change start — name the workspace on the native side too description: Precedence.describeNativeTool(item.id, item.description, precedence), + // altimate_change end inputSchema: jsonSchema(schema as any), async execute(args, options) { const ctx = context(args, options) @@ -1838,9 +1839,10 @@ export namespace SessionPrompt { // it's used only for source classification and never leaks into the schema sent to the model. for (const [key, entry] of Object.entries(mcpTools)) { const { client: clientName, ...item } = entry - // altimate_change — mark the engine tools that now serve a shadowed capability + // altimate_change start — mark the engine tools that now serve a shadowed capability item.description = Precedence.describeEngineTool(key, item.description ?? "", precedence) // altimate_change end + // altimate_change end const execute = item.execute if (!execute) continue diff --git a/packages/opencode/src/session/tools.ts b/packages/opencode/src/session/tools.ts index bd50911027..8bfe5ed59f 100644 --- a/packages/opencode/src/session/tools.ts +++ b/packages/opencode/src/session/tools.ts @@ -83,7 +83,11 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: { // altimate_change start — workspace precedence, derived once per turn from the live map const mcpTools = yield* mcp.tools() const precedence = yield* Effect.promise(() => - Precedence.refresh(input.session.id, mcpTools, Permission.merge(input.agent.permission, input.session.permission ?? [])), + Precedence.refresh( + input.session.id, + mcpTools, + Permission.merge(input.agent.permission, input.session.permission ?? []), + ), ) // altimate_change end @@ -96,8 +100,9 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: { })) { const schema = ProviderTransform.schema(input.model, ToolJsonSchema.fromTool(item)) tools[item.id] = tool({ - // altimate_change — name the workspace on the native side too + // altimate_change start — name the workspace on the native side too description: Precedence.describeNativeTool(item.id, item.description, precedence), + // altimate_change end inputSchema: jsonSchema(schema), execute(args, options) { return run.promise( @@ -145,8 +150,9 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: { for (const [key, entry] of Object.entries(mcpTools)) { const { client: clientName, ...item } = entry // altimate_change end - // altimate_change — mark the engine tools that now serve a shadowed capability + // altimate_change start — mark the engine tools that now serve a shadowed capability item.description = Precedence.describeEngineTool(key, item.description ?? "", precedence) + // altimate_change end const execute = item.execute if (!execute) continue From 1ef1da40324bda5a7d5c8b9ed0486234947a87a3 Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Fri, 28 Aug 2026 21:44:30 +0800 Subject: [PATCH 03/10] fix: undetermined outcomes always carry a stated reason in the result MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `unattributed` and an unrecognisable named connection type return an `undetermined` notice instead of a bare RUN — deliberate disablement stays silent; uncertainty never is (a toast is UI, not the correctness channel) - `check()` fails open with a stated reason on any internal throw - a failed config invalidation refuses attribution instead of trusting the cached pin; `resetForTests` releases `attachOutcome` --- .../altimate/native/connections/register.ts | 5 +- .../src/altimate/workspace/precedence.ts | 60 +++++++++++++++++-- .../altimate/workspace/precedence.test.ts | 17 ++++++ 3 files changed, 76 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/altimate/native/connections/register.ts b/packages/opencode/src/altimate/native/connections/register.ts index 9be834daba..918b29acc5 100644 --- a/packages/opencode/src/altimate/native/connections/register.ts +++ b/packages/opencode/src/altimate/native/connections/register.ts @@ -471,8 +471,9 @@ register("sql.execute", async (params: SqlExecuteParams): Promise { // when the cached answer is about to enable, and leave the refusing path cheap // rather than re-reading all config on every turn. if (cached !== expected) return cached - await Config.invalidate().catch((err) => { + try { + await Config.invalidate() + } catch (err) { + // A pin we could not re-confirm against disk must refuse, not enable: this is + // the one direction the comment above calls dangerous. log.warn("could not invalidate the config cache before attributing the engine", { err: String(err) }) - }) + return null + } return await read() } catch (err) { log.warn("could not read MCP config for engine attribution", { err: String(err) }) @@ -443,6 +448,7 @@ export function resetForTests(): void { delete precedenceInternals.announce delete precedenceInternals.binding delete precedenceInternals.attributedTo + delete precedenceInternals.attachOutcome } export interface RedirectResult { @@ -544,6 +550,27 @@ function redirectFor( * the handler itself would resolve it (see `resolveDefaultTarget`). */ export async function check(sessionID: string, capability: Capability, warehouse?: string): Promise { + // All three SQL tool bodies call this before their own try blocks, and the body + // below lazily imports the tool layer — a throw here must fail open with a stated + // reason, not take out sql_execute, sql_explain and schema_inspect together. + try { + return await checkUnsafe(sessionID, capability, warehouse) + } catch (err) { + log.warn("precedence check failed; running locally with an undetermined marker", { + sessionID, + capability, + err: String(err), + }) + return { + notice: + "Not routed through the bound workspace: the routing decision failed to " + + "compute for this call, so it ran locally.", + precedence: "undetermined", + } + } +} + +async function checkUnsafe(sessionID: string, capability: Capability, warehouse?: string): Promise { const precedence = bySession.get(sessionID) if (!precedence) { // No snapshot for this session. The resolver derives one every turn, so this is @@ -556,7 +583,21 @@ export async function check(sessionID: string, capability: Capability, warehouse precedence: "undetermined", } } - if (!precedence.enabled) return RUN + if (!precedence.enabled) { + // Deliberate disablement (pilot-off, escape-hatch, unbound, nothing-materialised) + // runs silently by design. Uncertainty must say so: an engine that could not be + // attributed means the routing decision is unknown, and unknown runs locally + // WITH a stated reason (Claim 1) — a toast is UI, not the correctness channel. + if (precedence.disabledReason === "unattributed") { + return { + notice: + "Not routed through the bound workspace: the local engine could not be " + + "attributed to it for this turn, so no routing decision was available.", + precedence: "undetermined", + } + } + return RUN + } // Re-linking mid-session is supported, so this snapshot can name a workspace the // project has since left — and a redirect naming it would send the call to that @@ -573,7 +614,18 @@ export async function check(sessionID: string, capability: Capability, warehouse if (warehouse) { const type = canonicalType(Registry.getConfig(warehouse)?.type) - if (!type) return RUN + if (!type) { + // The named connection's configured type does not canonicalise, so whether the + // engine serves it is unknowable — same treatment as the default-target path, + // which reports exactly this condition instead of running silently. + return { + notice: + `Not routed through workspace "${precedence.workspaceName}": connection ` + + `"${warehouse}"'s configured type is not recognised, so no routing decision ` + + `was available for it.`, + precedence: "undetermined", + } + } const entry = precedence.shadowed.get(type)?.get(capability) if (!entry) return RUN if (!reachable(precedence, entry.modelKey)) return unreachable(precedence.workspaceName, entry.modelKey) diff --git a/packages/opencode/test/altimate/workspace/precedence.test.ts b/packages/opencode/test/altimate/workspace/precedence.test.ts index a41d8d1211..e41d258b91 100644 --- a/packages/opencode/test/altimate/workspace/precedence.test.ts +++ b/packages/opencode/test/altimate/workspace/precedence.test.ts @@ -158,6 +158,23 @@ describe("attribution is grounded in the attach, not only the saved config", () await refresh(SESSION, SNOWFLAKE_TOOLS) const verdict = await check(SESSION, "sql_execute", "local_snow") expect(verdict.redirect).toBeUndefined() + // Uncertainty is never silent: the result itself carries the reason and the + // undetermined marker — a toast is UI, not the correctness channel. + expect(verdict.notice).toContain("could not be attributed") + expect(verdict.precedence).toBe("undetermined") + }) + + test("a throw inside the decision fails open with a stated reason", async () => { + // beforeEach's bindTo() gives an enabled snapshot; refresh first, then poison the + // binding read that check()'s re-link guard performs mid-decision. + await refresh(SESSION, SNOWFLAKE_TOOLS) + precedenceInternals.binding = async () => { + throw new Error("boom") + } + const verdict = await check(SESSION, "sql_execute", "local_snow") + expect(verdict.redirect).toBeUndefined() + expect(verdict.notice).toContain("failed to compute") + expect(verdict.precedence).toBe("undetermined") }) test("only an established attach qualifies — every other outcome is refused", async () => { From 2f0a580085fd173a1277bf9b6cc21848ffc151c2 Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Mon, 31 Aug 2026 01:41:05 +0800 Subject: [PATCH 04/10] test(workspace): prove the stale-connection guard and the attribution refusals - `default-target.test.ts` re-registers the native handlers in `beforeAll`: `altimate-core-rewrite-verify.test.ts` resets the dispatcher, and the three concurrency tests here dispatch `sql.execute`, so in a directory-wide run they failed with "No native handler" and the guard they prove went untested. - `precedenceInternals.config` seam over the config read and invalidation behind the real `attributedTo`, so its refusal on a failed invalidation is exercised through production: a pinned-to-us entry with a throwing invalidate derives `unattributed`; the same entry with a working invalidate attributes; a post-invalidate re-read pinned elsewhere refuses. - A named connection whose configured type does not canonicalise returns the `undetermined` notice; `resetForTests` releases the attach and config seams. --- .../src/altimate/workspace/precedence.ts | 12 ++- .../test/altimate/default-target.test.ts | 10 ++- .../altimate/workspace/precedence.test.ts | 77 +++++++++++++++++++ 3 files changed, 95 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/altimate/workspace/precedence.ts b/packages/opencode/src/altimate/workspace/precedence.ts index b559171961..839fb0ff52 100644 --- a/packages/opencode/src/altimate/workspace/precedence.ts +++ b/packages/opencode/src/altimate/workspace/precedence.ts @@ -144,6 +144,12 @@ export const precedenceInternals: { attributedTo?: () => Promise attachOutcome?: () => Promise announce?: (line: string) => Promise + /** The config read and invalidation behind the real `attributedTo`, so its refusal + * paths can be exercised without replacing the whole attribution. */ + config?: { + get: () => Promise + invalidate: () => Promise + } } = {} /** Bound both per-session maps. A long-running `serve` process sees an unbounded @@ -282,8 +288,9 @@ async function currentBinding(): Promise<{ datamateId: number; datamateName: str */ async function attributedTo(expected: string): Promise { if (precedenceInternals.attributedTo) return precedenceInternals.attributedTo() + const config = precedenceInternals.config ?? Config const read = async (): Promise => { - const cfg = (await Config.get()) as { mcp?: Record } + const cfg = (await config.get()) as { mcp?: Record } const entry = cfg.mcp?.[DATAMATE_KEY] if (!entry) return null // Parsed by attach's own parser, not a second copy here. It handles both entry @@ -303,7 +310,7 @@ async function attributedTo(expected: string): Promise { // rather than re-reading all config on every turn. if (cached !== expected) return cached try { - await Config.invalidate() + await config.invalidate() } catch (err) { // A pin we could not re-confirm against disk must refuse, not enable: this is // the one direction the comment above calls dangerous. @@ -449,6 +456,7 @@ export function resetForTests(): void { delete precedenceInternals.binding delete precedenceInternals.attributedTo delete precedenceInternals.attachOutcome + delete precedenceInternals.config } export interface RedirectResult { diff --git a/packages/opencode/test/altimate/default-target.test.ts b/packages/opencode/test/altimate/default-target.test.ts index 158fd6d72e..8196a0f55f 100644 --- a/packages/opencode/test/altimate/default-target.test.ts +++ b/packages/opencode/test/altimate/default-target.test.ts @@ -14,13 +14,19 @@ // Concurrency contract matches dispatcher.test.ts: the connection registry is a // process-wide singleton mutated here via `setConfigs`/`reset`, which is safe under // bun's default sequential file execution. -import { afterEach, beforeEach, describe, expect, test } from "bun:test" -import { resolveDefaultTarget, resetDbtAdapter } from "../../src/altimate/native/connections/register" +import { afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test" +import { registerAll, resolveDefaultTarget, resetDbtAdapter } from "../../src/altimate/native/connections/register" import { Dispatcher } from "../../src/altimate/native" import * as Registry from "../../src/altimate/native/connections/registry" const OPS = ["sql.execute", "sql.explain", "schema.inspect"] as const +beforeAll(() => { + // Re-register handlers in case another test file called Dispatcher.reset(): the + // concurrency tests below dispatch `sql.execute`, which this module registers. + registerAll() +}) + beforeEach(() => { resetDbtAdapter() Registry.reset() diff --git a/packages/opencode/test/altimate/workspace/precedence.test.ts b/packages/opencode/test/altimate/workspace/precedence.test.ts index e41d258b91..661cf99f5f 100644 --- a/packages/opencode/test/altimate/workspace/precedence.test.ts +++ b/packages/opencode/test/altimate/workspace/precedence.test.ts @@ -303,6 +303,83 @@ describe("mechanism 1a — attributed to the bound workspace", () => { const precedence = await refresh(SESSION, SNOWFLAKE_TOOLS) expect(inventoryLine(precedence)).toContain("could not be attributed") }) + + // The tests above replace attribution wholesale. These drive the REAL read through + // the config seam, so the refuse-on-uncertainty paths are the production ones. + const PINNED_TO_42 = { mcp: { datamate: { command: ["datamate", "start-stdio", "--datamate", "42"] } } } + + test("a pin that cannot be re-confirmed against disk refuses rather than enables", async () => { + // The cached read says "pinned to us" — the one answer that must be confirmed + // against disk before it may enable routing. If that confirmation is impossible, + // the safe way to be wrong is to refuse. + delete precedenceInternals.attributedTo + precedenceInternals.config = { + get: async () => PINNED_TO_42, + invalidate: async () => { + throw new Error("config cache locked") + }, + } + const precedence = await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(precedence.enabled).toBe(false) + expect(precedence.disabledReason).toBe("unattributed") + }) + + test("a pin re-confirmed against disk enables", async () => { + // Positive control for the seam: the same entry with a working invalidation + // attributes, so the refusal above is the invalidation's doing. + delete precedenceInternals.attributedTo + let invalidations = 0 + precedenceInternals.config = { + get: async () => PINNED_TO_42, + invalidate: async () => void invalidations++, + } + const precedence = await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(precedence.enabled).toBe(true) + expect(invalidations).toBe(1) + }) + + test("the disk read after invalidation is the one that counts", async () => { + // An IDE rewrote the entry to another workspace after it was cached: the stale + // "pinned to us" must not survive the re-read. + delete precedenceInternals.attributedTo + let reads = 0 + precedenceInternals.config = { + get: async () => + reads++ === 0 ? PINNED_TO_42 : { mcp: { datamate: { command: ["datamate", "start-stdio", "--datamate", "77"] } } }, + invalidate: async () => {}, + } + const precedence = await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(precedence.enabled).toBe(false) + expect(precedence.disabledReason).toBe("unattributed") + }) +}) + +describe("a named connection whose configured type is not recognised", () => { + test("runs locally with a notice naming the connection, not silently", async () => { + await refresh(SESSION, SNOWFLAKE_TOOLS) + expect((await check(SESSION, "sql_execute", "local_snow")).redirect).toBeDefined() + + Registry.setConfigs({ + local_snow: { type: "snowflake", account: "acct", user: "u" } as never, + mystery: { type: "weird" } as never, + }) + expect(canonicalType("weird")).toBeNull() + const verdict = await check(SESSION, "sql_execute", "mystery") + expect(verdict.redirect).toBeUndefined() + expect(verdict.precedence).toBe("undetermined") + expect(verdict.notice).toContain('"mystery"') + expect(verdict.notice).toContain("not recognised") + }) +}) + +describe("resetForTests", () => { + test("releases every seam, so one test's overrides cannot leak into the next", () => { + precedenceInternals.attachOutcome = async () => undefined + precedenceInternals.config = { get: async () => ({}), invalidate: async () => {} } + resetForTests() + expect(precedenceInternals.attachOutcome).toBeUndefined() + expect(precedenceInternals.config).toBeUndefined() + }) }) describe("mechanism 2 — capability-scoped, not type-scoped", () => { From 1d1f3bc15447070cfaae6d0d94375471e2cb2a9c Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Mon, 31 Aug 2026 12:39:54 +0800 Subject: [PATCH 05/10] fix(workspace): close the review's minor findings on precedence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `warehouse_list` re-validates the precedence snapshot against the current binding before annotating rows, as the query tools already do, so a mid-turn re-link is not reported as served by the old workspace. - Drift in the hand-maintained engine-tool map is visible: an execute tool from an integration the module does not know is warned once per session and shadows nothing, and a test pins every known integration to a canonical local driver type. - The dbt-fallback redirect says what the call would do — try dbt, then fall back — instead of naming the fallback as the served target, and that the dbt path cannot be chosen from the tool. - `resetDbtAdapter` no longer breaks single-flight: a superseded attempt releases only the slot it owns. Adapter creation is asserted single-flight, including across a reset. - Tests and comments say the dbt branch is unexercised outside a dbt project; the escape hatch is documented as process-wide; the module header states the audit boundary. Databricks (execute-only) redirect covered. --- .../altimate/native/connections/register.ts | 27 ++++- .../src/altimate/tools/warehouse-list.ts | 11 +- .../src/altimate/workspace/precedence.ts | 104 +++++++++++++++--- packages/opencode/src/index.ts | 4 +- .../test/altimate/default-target.test.ts | 53 ++++++++- .../altimate/workspace/precedence.test.ts | 58 +++++++++- 6 files changed, 228 insertions(+), 29 deletions(-) diff --git a/packages/opencode/src/altimate/native/connections/register.ts b/packages/opencode/src/altimate/native/connections/register.ts index 918b29acc5..43771f7916 100644 --- a/packages/opencode/src/altimate/native/connections/register.ts +++ b/packages/opencode/src/altimate/native/connections/register.ts @@ -51,6 +51,16 @@ let dbtAdapter: any | null | undefined = undefined // becomes valid mid-session is still not retried. let dbtAdapterInflight: Promise | undefined +/** Test seams for the single-flight. Production leaves `readConfig` unset. */ +export const dbtAdapterInternals: { + /** Replaces the dbt config read, so a test can hold an attempt open. */ + readConfig?: () => Promise + /** Adapter creation attempts since the last reset — what single-flight bounds. */ + attempts: number + /** Whether an attempt is currently in flight. */ + inflight: () => boolean +} = { attempts: 0, inflight: () => dbtAdapterInflight !== undefined } + /** * Resolve the dbt adapter for this project, or null when there is no usable dbt * project. Idempotent, single-flight, and permanently negative once it has failed. @@ -59,13 +69,20 @@ async function ensureDbtAdapter(): Promise { if (dbtAdapter !== undefined) return dbtAdapter if (dbtAdapterInflight) return dbtAdapterInflight - dbtAdapterInflight = (async () => { + // The slot is released only by the attempt that owns it: `resetDbtAdapter()` mid-flight + // lets a second attempt start, and the first one's settle must not clear the second's + // slot — that would hand the next caller a third adapter behind the second's back. + let mine: Promise | undefined + mine = (async () => { + dbtAdapterInternals.attempts += 1 try { // Check if dbt config exists const { read: readDbtConfig } = await import( "../../../../../dbt-tools/src/config" ) - const dbtConfig = await readDbtConfig() + const dbtConfig = dbtAdapterInternals.readConfig + ? ((await dbtAdapterInternals.readConfig()) as Awaited>) + : await readDbtConfig() if (!dbtConfig) { dbtAdapter = null return null @@ -90,10 +107,11 @@ async function ensureDbtAdapter(): Promise { dbtAdapter = null return null } finally { - dbtAdapterInflight = undefined + if (dbtAdapterInflight === mine) dbtAdapterInflight = undefined } })() - return dbtAdapterInflight + dbtAdapterInflight = mine + return mine } /** Where a `warehouse`-less call would actually go. */ @@ -244,6 +262,7 @@ export function resetDbtAdapter(): void { // altimate_change — drop any in-flight creation too, or a test that resets mid-flight // would still receive the previous adapter. dbtAdapterInflight = undefined + dbtAdapterInternals.attempts = 0 } // --------------------------------------------------------------------------- diff --git a/packages/opencode/src/altimate/tools/warehouse-list.ts b/packages/opencode/src/altimate/tools/warehouse-list.ts index aee244b984..21dcdfab46 100644 --- a/packages/opencode/src/altimate/tools/warehouse-list.ts +++ b/packages/opencode/src/altimate/tools/warehouse-list.ts @@ -24,13 +24,10 @@ export const WarehouseListTool = Tool.define("warehouse_list", { // altimate_change start — workspace precedence. // Annotated here, in this tool's own markdown, rather than on WarehouseInfo: // that struct is shared by every consumer of `warehouse.list`, and a field - // added there would surface far beyond this listing. - const precedence = Precedence.forSession(ctx.sessionID) - const notes = new Map() - for (const wh of warehouses) { - const note = Precedence.warehouseListNote(precedence, wh.type) - if (note) notes.set(wh.name, note) - } + // added there would surface far beyond this listing. The notes re-validate the + // snapshot against the current binding the way the query tools do, so a + // re-linked project is not told its rows are served by the old workspace. + const notes = await Precedence.warehouseListNotes(ctx.sessionID, warehouses) const shadowedCount = notes.size const lines: string[] = shadowedCount diff --git a/packages/opencode/src/altimate/workspace/precedence.ts b/packages/opencode/src/altimate/workspace/precedence.ts index 839fb0ff52..19d4ef69f5 100644 --- a/packages/opencode/src/altimate/workspace/precedence.ts +++ b/packages/opencode/src/altimate/workspace/precedence.ts @@ -25,6 +25,14 @@ // 4. Redirect. A shadowed call returns a result naming the exact engine key. Nothing // executes and there is no fallback. // +// AUDIT BOUNDARY. Precedence covers the three native tools above — `sql_execute`, +// `sql_explain`, `schema_inspect` — and annotates `warehouse_list`. Other native +// surfaces that run SQL on a local connection (the PII detector, schema tags, +// data-diff, schema-sync, the FinOps modules) are NOT gated: the engine serves no +// equivalent capability to redirect them to, so they keep running locally and +// unaudited even for a warehouse type the engine serves. A known limit, stated here +// rather than left implied. +// // SERVER-SIDE ONLY. The TUI plugin runtime loads plugins in a separate module realm // in the same process: an import from there is a different instance, sharing neither // module state nor `globalThis`. Importing this module from a plugin would typecheck, @@ -89,7 +97,7 @@ function engineToolFor(capability: Capability, integration: string): string { /** Engine integration id → canonical local driver type. The id is the engine's name * for the integration (`postgresql`); the driver type is what local connections carry * (`postgres`). Only warehouse integrations appear here. */ -const INTEGRATION_TYPE: Record = { +export const INTEGRATION_TYPE: Readonly> = { snowflake: "snowflake", bigquery: "bigquery", postgresql: "postgres", @@ -144,6 +152,8 @@ export const precedenceInternals: { attributedTo?: () => Promise attachOutcome?: () => Promise announce?: (line: string) => Promise + /** Where the module's warnings go, so tests can observe them; production logs. */ + warn?: (message: string, data: Record) => void /** The config read and invalidation behind the real `attributedTo`, so its refusal * paths can be exercised without replacing the whole attribution. */ config?: { @@ -169,6 +179,7 @@ function remember(sessionID: string, value: Precedence): void { announced.delete(oldest.value) publishing.delete(oldest.value) publishQueue.delete(oldest.value) + unrecognisedWarned.delete(oldest.value) } } @@ -232,6 +243,33 @@ const publishing = new Map() * while the newer one is recorded as the session's state. */ const publishQueue = new Map>() +/** Engine keys shaped like a warehouse execute tool that match no integration this + * module knows, already reported per session. `INTEGRATION_TYPE` and `engineToolFor` + * are hand-maintained: a new engine integration, or a renamed execute tool, would + * otherwise materialise and shadow nothing with no trace anywhere — fail-safe, but + * silent, which the opening principle rules out. */ +const unrecognisedWarned = new Map>() + +const WAREHOUSE_EXECUTE_KEY = /^(.+?)_(execute_database_query|execute_sql)$/ + +function warnUnrecognised(sessionID: string, present: Set): void { + for (const key of present) { + const match = WAREHOUSE_EXECUTE_KEY.exec(key) + if (!match || match[1] in INTEGRATION_TYPE) continue + let seen = unrecognisedWarned.get(sessionID) + if (!seen) { + seen = new Set() + unrecognisedWarned.set(sessionID, seen) + } + if (seen.has(key)) continue + seen.add(key) + const message = "the engine serves a warehouse execute tool this module does not know; it shadows nothing" + const data = { sessionID, key, integration: match[1] } + if (precedenceInternals.warn) precedenceInternals.warn(message, data) + else log.warn(message, data) + } +} + /** Said when routing stops entirely, which `inventoryLine` renders as an empty string * because there is nothing left to enumerate. Silence is the wrong answer only here: * the session was previously told its calls were routed. */ @@ -263,7 +301,8 @@ async function announce(line: string): Promise { } /** Mechanism 6 — the escape hatch. `--integrations=local` (or the env var) turns - * shadowing off for the whole session. */ + * shadowing off for the whole process — it is `process.env.ALTIMATE_INTEGRATIONS`, + * inherited by child processes, and under `serve` it covers every session. */ export function escapeHatchOn(): boolean { return CoreFlag.ALTIMATE_INTEGRATIONS_LOCAL } @@ -408,6 +447,7 @@ async function derive(sessionID: string, tools: Record): Promis // Mechanism 1 — what actually materialised, never what was declared. const present = engineToolKeys(tools) if (present.size === 0) return EMPTY("nothing-materialised", workspaceName) + warnUnrecognised(sessionID, present) // Mechanism 2 — capability by capability, only where the key is really there. const shadowed = new Map>() @@ -457,6 +497,8 @@ export function resetForTests(): void { delete precedenceInternals.attributedTo delete precedenceInternals.attachOutcome delete precedenceInternals.config + delete precedenceInternals.warn + unrecognisedWarned.clear() } export interface RedirectResult { @@ -535,21 +577,32 @@ function redirectFor( ...(viaDbtFallback ? { via: "dbt-fallback" } : {}), }, output: viaDbtFallback - ? `Not run locally. This call names no warehouse, so it resolves through the dbt project — and if dbt ` + - `returns nothing it falls back to the local connection \`${connection}\`, which workspace ` + - `"${workspaceName}" serves through its integration engine. Whether it lands on dbt or on that ` + - `connection is only known once it runs, so it is not run.\n\n` + - `Call \`${entry.modelKey}\` instead. If you meant the dbt path specifically, either name the ` + - `warehouse you want (\`warehouse=${connection}\` routes to the engine; any unserved connection runs ` + - `locally), or restart with \`--integrations=local\` to keep every connection on the local drivers.` + ? `Not run locally. This call names no warehouse, so it would try the dbt project first and, if dbt ` + + `yields nothing, fall back to the local connection \`${connection}\` — a connection workspace ` + + `"${workspaceName}" serves through its integration engine. Which of the two it lands on is only ` + + `known once it runs, so it is not run.\n\n` + + `Call \`${entry.modelKey}\` to run it through the workspace. The dbt path cannot be chosen from ` + + `this tool: a \`warehouse=\` argument names a local connection, never the dbt profile. To run on ` + + `dbt, restart with \`--integrations=local\`, which keeps every connection on the local drivers ` + + `(dbt included) for the whole process.` : `Not run locally. Workspace "${workspaceName}" serves ${entry.integration} through its integration engine, ` + `so this connection is served by \`${entry.modelKey}\`.\n\n` + `Call \`${entry.modelKey}\` instead. ` + - `To use the local connection for this session, restart with \`--integrations=local\`.`, + `To keep every connection on the local drivers, restart with \`--integrations=local\` (it applies ` + + `to the whole process).`, }, } } +/** Does the project still bind the workspace this snapshot was derived for? Re-linking + * mid-session is supported, so a snapshot can name a workspace the project has since + * left; anything about to act on or report from that snapshot asks this first. The + * binding is a local cache read. */ +export async function snapshotCurrent(precedence: Precedence): Promise { + if (!precedence.workspaceId) return true + return (await currentBinding())?.datamateId === Number(precedence.workspaceId) +} + /** * Mechanism 4 — the single decision a tool body asks for. Returns an empty verdict * when the call should proceed normally. @@ -607,11 +660,10 @@ async function checkUnsafe(sessionID: string, capability: Capability, warehouse? return RUN } - // Re-linking mid-session is supported, so this snapshot can name a workspace the - // project has since left — and a redirect naming it would send the call to that - // workspace's engine, with its credentials. The binding is a local cache read, and - // this only runs on the path that is about to redirect. - if (precedence.workspaceId && (await currentBinding())?.datamateId !== Number(precedence.workspaceId)) { + // A redirect naming a workspace the project has since left would send the call to + // that workspace's engine, with its credentials. This only runs on the path that is + // about to redirect. + if (!(await snapshotCurrent(precedence))) { return { notice: `Not routed through workspace "${precedence.workspaceName}": the project was re-linked while ` + @@ -821,3 +873,25 @@ export function warehouseListNote(precedence: Precedence | undefined, warehouseT `${served.join("/")} via workspace ${precedence.workspaceName}` + (local.length ? `; ${local.join("/")} local` : "") ) } + +/** + * The notes `warehouse_list` prints, keyed by connection name. Reads the session's + * snapshot and, like `check()`, first asks whether that snapshot is still about the + * bound workspace: after a mid-turn re-link the listing must stop claiming rows are + * served by a workspace the project has left, exactly as the query tools stop + * redirecting to it. A snapshot that no longer applies yields no notes at all. + */ +export async function warehouseListNotes( + sessionID: string, + warehouses: ReadonlyArray<{ name: string; type: string }>, +): Promise> { + const notes = new Map() + const precedence = bySession.get(sessionID) + if (!precedence?.enabled) return notes + if (!(await snapshotCurrent(precedence))) return notes + for (const wh of warehouses) { + const note = warehouseListNote(precedence, wh.type) + if (note) notes.set(wh.name, note) + } + return notes +} diff --git a/packages/opencode/src/index.ts b/packages/opencode/src/index.ts index 909e182f63..948f3b9f0d 100644 --- a/packages/opencode/src/index.ts +++ b/packages/opencode/src/index.ts @@ -116,7 +116,9 @@ let cli = yargs(args) if (opts.pure) { process.env.OPENCODE_PURE = "1" } - // altimate_change start - workspace precedence escape hatch + // altimate_change start - workspace precedence escape hatch. Process-wide, not + // per session: an env var inherited by child processes, and under `serve` it + // covers every session that process hosts. if (opts.integrations) process.env.ALTIMATE_INTEGRATIONS = String(opts.integrations) // altimate_change end diff --git a/packages/opencode/test/altimate/default-target.test.ts b/packages/opencode/test/altimate/default-target.test.ts index 8196a0f55f..3a5dda040b 100644 --- a/packages/opencode/test/altimate/default-target.test.ts +++ b/packages/opencode/test/altimate/default-target.test.ts @@ -11,11 +11,22 @@ // through to the registry — which is exactly the behaviour to pin down, because the // registry branch is what decides the default for the majority of users. // +// What this suite does NOT reach: the dbt branch of `resolveDefaultTarget` — +// `adapterTypeFromManifest`, the `getAdapterType?.()` / "unknown" coalescing and the +// construction of `{ source: "dbt", type, fallback }`. Every assertion below that +// tolerates a `dbt` source is written for a run inside a dbt project and takes its +// registry arm here. Treat that branch as unexercised by CI, not as covered. +// // Concurrency contract matches dispatcher.test.ts: the connection registry is a // process-wide singleton mutated here via `setConfigs`/`reset`, which is safe under // bun's default sequential file execution. import { afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test" -import { registerAll, resolveDefaultTarget, resetDbtAdapter } from "../../src/altimate/native/connections/register" +import { + dbtAdapterInternals, + registerAll, + resolveDefaultTarget, + resetDbtAdapter, +} from "../../src/altimate/native/connections/register" import { Dispatcher } from "../../src/altimate/native" import * as Registry from "../../src/altimate/native/connections/registry" @@ -34,9 +45,12 @@ beforeEach(() => { afterEach(() => { resetDbtAdapter() + delete dbtAdapterInternals.readConfig Registry.reset() }) +const tick = () => new Promise((resolve) => setTimeout(resolve, 0)) + describe("resolveDefaultTarget — registry branch", () => { test("reports the first configured connection, which is what the handlers use", async () => { Registry.setConfigs({ @@ -75,6 +89,7 @@ describe("resolveDefaultTarget — the dbt fallback is reported, not hidden", () Registry.setConfigs({ first: { type: "snowflake", account: "a" } as never }) const target = await resolveDefaultTarget("sql.execute") if (target.source === "dbt") { + // Not taken in this suite (no dbt project) — kept for a run inside one. expect(target.fallback).toEqual({ type: "snowflake", name: "first" }) } else { // No dbt project here, so this resolves to the registry directly — same target. @@ -85,6 +100,7 @@ describe("resolveDefaultTarget — the dbt fallback is reported, not hidden", () test("no fallback is reported when the registry is empty", async () => { Registry.setConfigs({}) const target = await resolveDefaultTarget("sql.execute") + // The `dbt` arm is not taken in this suite (no dbt project). if (target.source === "dbt") expect(target.fallback).toBeUndefined() else expect(target.source).toBe("none") }) @@ -114,6 +130,11 @@ describe("resolveDefaultTarget — per-operation resolution", () => { for (const target of results) { expect(target).toMatchObject({ source: "registry", name: "only" }) } + // Only `sql.execute` consults dbt, and the negative answer is cached: one attempt + // across the three ops, and none on a second pass. + expect(dbtAdapterInternals.attempts).toBe(1) + await resolveDefaultTarget("sql.execute") + expect(dbtAdapterInternals.attempts).toBe(1) }) test("concurrent execute resolutions share one adapter attempt", async () => { @@ -123,6 +144,36 @@ describe("resolveDefaultTarget — per-operation resolution", () => { Registry.setConfigs({ only: { type: "snowflake", account: "a" } as never }) const [a, b] = await Promise.all([resolveDefaultTarget("sql.execute"), resolveDefaultTarget("sql.execute")]) expect(a).toEqual(b) + expect(dbtAdapterInternals.attempts).toBe(1) + }) +}) + +describe("adapter creation stays single-flight across a reset", () => { + test("a stale attempt settling does not release the slot a newer attempt owns", async () => { + // The `finally` used to clear the in-flight slot unconditionally. Reset while attempt + // A is in flight, start B, and A's settle then cleared B's slot — the next caller + // built a third adapter behind B's back. The slot is released only by its owner. + const gates: Array<() => void> = [] + dbtAdapterInternals.readConfig = () => new Promise((resolve) => gates.push(() => resolve(null))) + Registry.setConfigs({ only: { type: "duckdb", path: ":memory:" } as never }) + + const a = resolveDefaultTarget("sql.execute") + await tick() + expect(gates).toHaveLength(1) + resetDbtAdapter() + const b = resolveDefaultTarget("sql.execute") + await tick() + expect(gates).toHaveLength(2) + expect(dbtAdapterInternals.inflight()).toBe(true) + + gates[0]() + await a + // B is still in flight; A's settle must not have released its slot. + expect(dbtAdapterInternals.inflight()).toBe(true) + + gates[1]() + await b + expect(dbtAdapterInternals.inflight()).toBe(false) }) }) diff --git a/packages/opencode/test/altimate/workspace/precedence.test.ts b/packages/opencode/test/altimate/workspace/precedence.test.ts index 661cf99f5f..e36fea1299 100644 --- a/packages/opencode/test/altimate/workspace/precedence.test.ts +++ b/packages/opencode/test/altimate/workspace/precedence.test.ts @@ -6,6 +6,7 @@ // booting an instance, reading config, or touching MCP state. import { afterEach, beforeEach, describe, expect, test } from "bun:test" import { + INTEGRATION_TYPE, MAX_TRACKED_SESSIONS, check, decideForTarget, @@ -18,7 +19,9 @@ import { precedenceInternals, refresh, resetForTests, + snapshotCurrent, warehouseListNote, + warehouseListNotes, } from "../../../src/altimate/workspace/precedence" import * as Registry from "../../../src/altimate/native/connections/registry" import { canonicalType } from "../../../src/altimate/native/connections/registry" @@ -376,9 +379,11 @@ describe("resetForTests", () => { test("releases every seam, so one test's overrides cannot leak into the next", () => { precedenceInternals.attachOutcome = async () => undefined precedenceInternals.config = { get: async () => ({}), invalidate: async () => {} } + precedenceInternals.warn = () => {} resetForTests() expect(precedenceInternals.attachOutcome).toBeUndefined() expect(precedenceInternals.config).toBeUndefined() + expect(precedenceInternals.warn).toBeUndefined() }) }) @@ -407,6 +412,39 @@ describe("mechanism 2 — capability-scoped, not type-scoped", () => { expect(precedence.shadowed.get("databricks")?.get("sql_execute")?.modelKey).toBe("datamate_databricks_execute_sql") }) + test("a databricks connection redirects execute to that tool, and nothing else", async () => { + Registry.setConfigs({ dbx_conn: { type: "databricks", host: "h" } as never }) + await refresh(SESSION, { datamate_databricks_execute_sql: {} }) + const execute = await check(SESSION, "sql_execute", "dbx_conn") + expect(execute.redirect?.metadata.redirect_to).toBe("datamate_databricks_execute_sql") + // Execute-only: explain and inspect keep running locally, and silently, because + // that is a considered "not served", not an unknown. + expect(await check(SESSION, "sql_explain", "dbx_conn")).toEqual({}) + expect(await check(SESSION, "schema_inspect", "dbx_conn")).toEqual({}) + }) + + test("every integration this module knows maps onto a canonical local driver type", () => { + // `INTEGRATION_TYPE` is hand-maintained against `DRIVER_MAP`; a value that does not + // canonicalise to itself could never match a local connection and would shadow + // nothing without anyone noticing. + for (const [integration, type] of Object.entries(INTEGRATION_TYPE)) { + expect({ integration, canonical: canonicalType(type) }).toEqual({ integration, canonical: type }) + } + }) + + test("an execute tool from an integration this module does not know is reported once and shadows nothing", async () => { + const warned: Array> = [] + precedenceInternals.warn = (_message, data) => void warned.push(data) + const tools = { ...SNOWFLAKE_TOOLS, datamate_redshift_execute_database_query: {} } + const precedence = await refresh(SESSION, tools) + expect(precedence.shadowed.has("redshift")).toBe(false) + expect((await check(SESSION, "sql_execute", "rs_conn")).redirect).toBeUndefined() + expect(warned).toEqual([{ sessionID: SESSION, key: "redshift_execute_database_query", integration: "redshift" }]) + // Re-derived every turn, reported once. + await refresh(SESSION, tools) + expect(warned).toHaveLength(1) + }) + test("a type with no materialised integration is untouched", async () => { const precedence = await refresh(SESSION, SNOWFLAKE_TOOLS) expect(precedence.shadowed.has("duckdb")).toBe(false) @@ -874,6 +912,24 @@ describe("a snapshot must not outlive the binding that justified it", () => { expect((await check(SESSION, "sql_execute", "local_snow")).redirect).toBeDefined() }) + test("warehouse_list stops claiming rows are served once the project is re-linked", async () => { + // The listing reads the same snapshot the query tools do, so it must re-validate it + // the same way: a row still marked "via workspace" after a re-link claims a routing + // the next call will refuse. + await refresh(SESSION, SNOWFLAKE_TOOLS) + const rows = [ + { name: "local_snow", type: "snowflake" }, + { name: "local_duck", type: "duckdb" }, + ] + expect([...(await warehouseListNotes(SESSION, rows)).keys()]).toEqual(["local_snow"]) + + precedenceInternals.binding = async () => ({ datamateId: 77, datamateName: "somewhere-else" }) + expect(await snapshotCurrent(forSession(SESSION)!)).toBe(false) + // Listing and call agree: neither claims the old workspace. + expect((await warehouseListNotes(SESSION, rows)).size).toBe(0) + expect((await check(SESSION, "sql_execute", "local_snow")).notice).toContain("re-linked") + }) + test("a session whose snapshot was evicted says so rather than running silently", async () => { // Eviction can drop an entry between tool resolution and the call. Returning a // bare "run" there is indistinguishable from a considered "not served", so a @@ -959,7 +1015,7 @@ describe("default-target decisions — branch order", () => { }) describe("mechanism 6 — the escape hatch", () => { - test("--integrations=local turns shadowing off for the session", async () => { + test("--integrations=local turns shadowing off for the whole process", async () => { process.env.ALTIMATE_INTEGRATIONS = "local" const precedence = await refresh(SESSION, SNOWFLAKE_TOOLS) expect(precedence.enabled).toBe(false) From ae3fd152f9011caf3d13fdf682bdfd4f9493d4a0 Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Mon, 31 Aug 2026 12:58:24 +0800 Subject: [PATCH 06/10] test(workspace): pin the dbt-fallback notice on the branch that produces it The existing test asserted the redirect wording through a named warehouse, which never takes the dbt-fallback branch. This reaches that branch through the pure decision function and pins what the reworded notice must say: the call would try dbt first and fall back to the named connection, and the dbt path cannot be chosen from the tool. --- .../altimate/workspace/precedence.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/opencode/test/altimate/workspace/precedence.test.ts b/packages/opencode/test/altimate/workspace/precedence.test.ts index e36fea1299..8c8984abce 100644 --- a/packages/opencode/test/altimate/workspace/precedence.test.ts +++ b/packages/opencode/test/altimate/workspace/precedence.test.ts @@ -527,6 +527,25 @@ describe("the dbt-fallback redirect explains itself", () => { expect(verdict.redirect!.output).toContain("--integrations=local") expect(verdict.redirect!.metadata.via).toBeUndefined() }) + + test("the dbt-fallback branch says what the call would do, and that dbt cannot be chosen here", async () => { + // Reaching this branch through check() needs a dbt project; the pure decision + // function reaches it directly. The wording matters because the fallback is not + // the served target — it is where the call would land if dbt yields nothing. + const p = await refresh(SESSION, SNOWFLAKE_TOOLS) + const v = decideForTarget(p, "sql_execute", { + source: "dbt", + type: undefined, + fallback: { type: "snowflake", name: "local_snow" }, + }) + expect(v.redirect!.metadata.via).toBe("dbt-fallback") + const out = v.redirect!.output + expect(out).toContain("try the dbt project first") + expect(out).toContain("fall back to the local connection `local_snow`") + expect(out).toContain("The dbt path cannot be chosen from this tool") + expect(out).toContain("--integrations=local") + expect(out).not.toContain("this connection is served by") + }) }) describe("a redirect the caller cannot follow is not a redirect", () => { From 6ab901fcd3fa314909057308d62738932e8e2b24 Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Mon, 31 Aug 2026 13:19:23 +0800 Subject: [PATCH 07/10] fix(workspace): materialisation is owned by the engine's client; an unreadable link is unknown, not unbound MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Engine tools are recognised by the MCP client that served them, not by the `datamate_` prefix alone: `MCP.tools()` stamps every entry with its client, and another server named e.g. `datamate_snowflake` flattens to the same key shape. Such keys confer no precedence and are reported once per session. - The binding is read through the strict reader: a cache or credentials file that is present but unreadable settles `binding-unreadable`, which `check()` reports as undetermined with the reason, and which invalidates a routed snapshot for that reason rather than as a re-link. - A derivation that throws settles `derive-failed` — local execution with a stated reason — instead of failing the turn's tool resolution. - The re-link guard runs only when a redirect is about to be returned; a call that runs locally regardless does not pay for the binding read. - A superseded dbt-adapter attempt returns its result but no longer publishes it over the newer attempt's cache, and `tryExecuteViaDbt` executes on the adapter it was handed rather than re-reading the mutable global. - The drift warning covers every warehouse capability shape, not only execute. - `schema_inspect` validates its inputs before consulting precedence, as `sql_explain` does, through a shared `input-validation` module. - The guard-order test restores `ALTIMATE_INTEGRATIONS` it deletes. --- .../altimate/native/connections/register.ts | 39 +++-- .../src/altimate/tools/input-validation.ts | 38 +++++ .../src/altimate/tools/schema-inspect.ts | 12 ++ .../src/altimate/tools/sql-explain.ts | 26 +--- .../src/altimate/workspace/engine-types.ts | 21 ++- .../src/altimate/workspace/precedence.ts | 136 +++++++++++++++--- .../test/altimate/default-target.test.ts | 5 +- .../altimate/precedence-guard-order.test.ts | 25 ++++ .../altimate/workspace/engine-types.test.ts | 12 ++ .../altimate/workspace/precedence.test.ts | 112 ++++++++++++++- 10 files changed, 359 insertions(+), 67 deletions(-) create mode 100644 packages/opencode/src/altimate/tools/input-validation.ts diff --git a/packages/opencode/src/altimate/native/connections/register.ts b/packages/opencode/src/altimate/native/connections/register.ts index 43771f7916..808e2be75e 100644 --- a/packages/opencode/src/altimate/native/connections/register.ts +++ b/packages/opencode/src/altimate/native/connections/register.ts @@ -57,9 +57,11 @@ export const dbtAdapterInternals: { readConfig?: () => Promise /** Adapter creation attempts since the last reset — what single-flight bounds. */ attempts: number + /** Cache writes since the last reset: only the attempt that owns the slot writes. */ + writes: number /** Whether an attempt is currently in flight. */ inflight: () => boolean -} = { attempts: 0, inflight: () => dbtAdapterInflight !== undefined } +} = { attempts: 0, writes: 0, inflight: () => dbtAdapterInflight !== undefined } /** * Resolve the dbt adapter for this project, or null when there is no usable dbt @@ -73,6 +75,16 @@ async function ensureDbtAdapter(): Promise { // lets a second attempt start, and the first one's settle must not clear the second's // slot — that would hand the next caller a third adapter behind the second's back. let mine: Promise | undefined + // Likewise the cache: a superseded attempt returns its result to the callers that + // awaited it but must not publish it, or it would overwrite what the newer attempt + // stored. Every write goes through here. + const publish = (value: any | null): any | null => { + if (dbtAdapterInflight === mine) { + dbtAdapter = value + dbtAdapterInternals.writes += 1 + } + return value + } mine = (async () => { dbtAdapterInternals.attempts += 1 try { @@ -83,10 +95,7 @@ async function ensureDbtAdapter(): Promise { const dbtConfig = dbtAdapterInternals.readConfig ? ((await dbtAdapterInternals.readConfig()) as Awaited>) : await readDbtConfig() - if (!dbtConfig) { - dbtAdapter = null - return null - } + if (!dbtConfig) return publish(null) // Check if dbt_project.yml exists const fs = await import("fs") @@ -94,18 +103,15 @@ async function ensureDbtAdapter(): Promise { if ( !fs.existsSync(path.join(dbtConfig.projectRoot, "dbt_project.yml")) ) { - dbtAdapter = null - return null + return publish(null) } // Create the adapter const { create } = await import("../../../../../dbt-tools/src/adapter") - dbtAdapter = await create(dbtConfig) - return dbtAdapter + return publish(await create(dbtConfig)) } catch { // dbt-tools not available or config invalid — fall back to native - dbtAdapter = null - return null + return publish(null) } finally { if (dbtAdapterInflight === mine) dbtAdapterInflight = undefined } @@ -199,14 +205,16 @@ async function tryExecuteViaDbt( sql: string, limit?: number, ): Promise { - // altimate_change start — share the single-flight creation path with resolveDefaultTarget - if (!(await ensureDbtAdapter())) return null + // altimate_change start — share the single-flight creation path with resolveDefaultTarget; + // execute on the adapter this call was handed, not on a global a reset may have moved. + const adapter = await ensureDbtAdapter() + if (!adapter) return null // altimate_change end try { const raw = limit - ? await dbtAdapter.immediatelyExecuteSQLWithLimit(sql, "", limit) - : await dbtAdapter.immediatelyExecuteSQL(sql, "") + ? await adapter.immediatelyExecuteSQLWithLimit(sql, "", limit) + : await adapter.immediatelyExecuteSQL(sql, "") // QueryExecutionResult has: { columnNames, columnTypes, data, rawSql, compiledSql } // where data is Record[] (array of row objects) @@ -263,6 +271,7 @@ export function resetDbtAdapter(): void { // would still receive the previous adapter. dbtAdapterInflight = undefined dbtAdapterInternals.attempts = 0 + dbtAdapterInternals.writes = 0 } // --------------------------------------------------------------------------- diff --git a/packages/opencode/src/altimate/tools/input-validation.ts b/packages/opencode/src/altimate/tools/input-validation.ts new file mode 100644 index 0000000000..f3940fdcd1 --- /dev/null +++ b/packages/opencode/src/altimate/tools/input-validation.ts @@ -0,0 +1,38 @@ +// altimate_change - new file +// +// Pre-flight input checks shared by the warehouse tools. They run BEFORE workspace +// precedence is consulted: a redirect must never forward malformed input to the +// engine tool, and an empty or placeholder warehouse name must not be read as +// "use the default" by the routing decision. + +/** Warehouse names that models produce by mistake: empty strings and unsubstituted + * placeholders. Both would otherwise fall through to unhelpful registry errors. */ +export function validateWarehouseName(warehouse: string | undefined): string | null { + if (warehouse === undefined) return null + if (typeof warehouse !== "string") { + return "warehouse must be a string" + } + const trimmed = warehouse.trim() + if (trimmed.length === 0) { + return "warehouse is an empty string — omit the parameter to use the default warehouse, or pass a configured connection name" + } + if (/^[?$:@]/.test(trimmed)) { + return ( + "warehouse name looks like an unsubstituted placeholder (" + + JSON.stringify(trimmed) + + "). Use `warehouse_list` to see configured warehouses." + ) + } + return null +} + +/** Table names get the same two checks; the schema tool has nothing to inspect otherwise. */ +export function validateTableName(table: unknown): string | null { + if (typeof table !== "string" || table.trim().length === 0) { + return "table is required — pass a table name, optionally schema-qualified" + } + if (/^[?$:@]/.test(table.trim())) { + return "table name looks like an unsubstituted placeholder (" + JSON.stringify(table.trim()) + ")" + } + return null +} diff --git a/packages/opencode/src/altimate/tools/schema-inspect.ts b/packages/opencode/src/altimate/tools/schema-inspect.ts index 5f67fa554e..6883847f16 100644 --- a/packages/opencode/src/altimate/tools/schema-inspect.ts +++ b/packages/opencode/src/altimate/tools/schema-inspect.ts @@ -6,6 +6,7 @@ import type { SchemaInspectResult } from "../native/types" import { PostConnectSuggestions } from "./post-connect-suggestions" // altimate_change end import { isRecord, normalizeError } from "./response-normalization" +import { validateTableName, validateWarehouseName } from "./input-validation" // altimate_change start — workspace precedence import * as Precedence from "../workspace/precedence" // altimate_change end @@ -18,6 +19,17 @@ export const SchemaInspectTool = Tool.define("schema_inspect", { warehouse: z.string().optional().describe("Warehouse connection name"), }), async execute(args, ctx) { + // Pre-flight validation outranks the redirect, as it does for `sql_explain`: bad + // input is answered here rather than forwarded to the engine tool, and an empty + // warehouse string is never read as "the default" by the routing decision. + const inputError = validateTableName(args.table) ?? validateWarehouseName(args.warehouse) + if (inputError) { + return { + title: "Schema: INVALID INPUT", + metadata: { success: false, columnCount: 0, rowCount: undefined, error: inputError, error_class: "input_validation" }, + output: `Invalid input: ${inputError}`, + } + } // altimate_change start — workspace precedence const precedence = await Precedence.check(ctx.sessionID, "schema_inspect", args.warehouse) if (precedence.redirect) return precedence.redirect diff --git a/packages/opencode/src/altimate/tools/sql-explain.ts b/packages/opencode/src/altimate/tools/sql-explain.ts index 73a1a401a3..886d824626 100644 --- a/packages/opencode/src/altimate/tools/sql-explain.ts +++ b/packages/opencode/src/altimate/tools/sql-explain.ts @@ -5,6 +5,7 @@ import type { SqlExplainResult } from "../native/types" // altimate_change start — workspace precedence import * as Precedence from "../workspace/precedence" // altimate_change end +import { validateWarehouseName } from "./input-validation" /** * Detect SQL input that cannot be meaningfully EXPLAIN'd. @@ -48,31 +49,6 @@ function validateSqlInput(sql: unknown): string | null { return null } -/** - * Validate a warehouse name supplied by the caller. - * - * Empty strings and placeholder tokens slip through the optional-parameter - * contract and produce unhelpful "Connection X not found" errors from the - * Registry. Catch these here with a pointer to `warehouse_list`. - */ -function validateWarehouseName(warehouse: string | undefined): string | null { - if (warehouse === undefined) return null - if (typeof warehouse !== "string") { - return "warehouse must be a string" - } - const trimmed = warehouse.trim() - if (trimmed.length === 0) { - return "warehouse is an empty string — omit the parameter to use the default warehouse, or pass a configured connection name" - } - if (/^[?$:@]/.test(trimmed)) { - return ( - "warehouse name looks like an unsubstituted placeholder (" + - JSON.stringify(trimmed) + - "). Use `warehouse_list` to see configured warehouses." - ) - } - return null -} export const SqlExplainTool = Tool.define("sql_explain", { description: diff --git a/packages/opencode/src/altimate/workspace/engine-types.ts b/packages/opencode/src/altimate/workspace/engine-types.ts index 7863d306bc..62e559bbef 100644 --- a/packages/opencode/src/altimate/workspace/engine-types.ts +++ b/packages/opencode/src/altimate/workspace/engine-types.ts @@ -137,12 +137,29 @@ export function clearsFloor(version: string | null): boolean { /** Strip the server prefix from the engine tools present in the catalog. */ export function engineToolKeys(tools: Record): Set { const out = new Set() - for (const key of Object.keys(tools)) { - if (key.startsWith(TOOL_PREFIX)) out.add(key.slice(TOOL_PREFIX.length)) + for (const [key, value] of Object.entries(tools)) { + if (!key.startsWith(TOOL_PREFIX) || servedByForeignClient(value)) continue + out.add(key.slice(TOOL_PREFIX.length)) } return out } +/** Engine-shaped keys another MCP client owns: worth a warning, never precedence. */ +export function foreignEngineKeys(tools: Record): string[] { + return Object.entries(tools) + .filter(([key, value]) => key.startsWith(TOOL_PREFIX) && servedByForeignClient(value)) + .map(([key]) => key) +} + +/** `MCP.tools()` stamps every entry with the client that served it. A foreign server + * named e.g. `datamate_snowflake` flattens to the same `datamate_*` shape as the + * engine's own tools, and only the stamp tells them apart. An entry without a stamp + * (a synthetic map) is taken by prefix. */ +function servedByForeignClient(value: unknown): boolean { + const client = (value as { client?: unknown } | null)?.client + return typeof client === "string" && client !== DATAMATE_KEY +} + /** The entry's full argv, flattening both config shapes. */ export function commandArgv(entry: EntryLike | null): string[] { if (!entry) return [] diff --git a/packages/opencode/src/altimate/workspace/precedence.ts b/packages/opencode/src/altimate/workspace/precedence.ts index 19d4ef69f5..40a572bef8 100644 --- a/packages/opencode/src/altimate/workspace/precedence.ts +++ b/packages/opencode/src/altimate/workspace/precedence.ts @@ -59,13 +59,14 @@ import { DATAMATE_KEY } from "../datamate-transport" import { attributableEngine, engineToolKeys, + foreignEngineKeys, isEnabled, pinnedWorkspace, settledOutcome, type EntryLike, type Outcome, } from "./engine-overlay" -import { readLocalBinding } from "./state" +import { readLocalBindingScopedStrict } from "./state" import { canonicalType } from "../native/connections/registry" import * as Registry from "../native/connections/registry" @@ -124,7 +125,14 @@ export interface Precedence { * could not be attributed to the bound workspace. */ enabled: boolean /** Why precedence is off, for the inventory line. Absent when enabled. */ - disabledReason?: "pilot-off" | "escape-hatch" | "unbound" | "unattributed" | "nothing-materialised" + disabledReason?: + | "pilot-off" + | "escape-hatch" + | "unbound" + | "binding-unreadable" + | "unattributed" + | "derive-failed" + | "nothing-materialised" /** canonical driver type → capability → who serves it. */ shadowed: Map> /** The caller's effective permission rules, captured when this was derived. A @@ -250,11 +258,11 @@ const publishQueue = new Map>() * silent, which the opening principle rules out. */ const unrecognisedWarned = new Map>() -const WAREHOUSE_EXECUTE_KEY = /^(.+?)_(execute_database_query|execute_sql)$/ +const WAREHOUSE_TOOL_KEY = /^(.+?)_(execute_database_query|execute_sql|get_query_explain_plan|get_table_stats)$/ function warnUnrecognised(sessionID: string, present: Set): void { for (const key of present) { - const match = WAREHOUSE_EXECUTE_KEY.exec(key) + const match = WAREHOUSE_TOOL_KEY.exec(key) if (!match || match[1] in INTEGRATION_TYPE) continue let seen = unrecognisedWarned.get(sessionID) if (!seen) { @@ -263,13 +271,31 @@ function warnUnrecognised(sessionID: string, present: Set): void { } if (seen.has(key)) continue seen.add(key) - const message = "the engine serves a warehouse execute tool this module does not know; it shadows nothing" + const message = "the engine serves a warehouse tool for an integration this module does not know; it shadows nothing" const data = { sessionID, key, integration: match[1] } if (precedenceInternals.warn) precedenceInternals.warn(message, data) else log.warn(message, data) } } +/** An engine-shaped key that another MCP client serves is not the engine's, whatever + * its name says; it confers no precedence, and the session hears about it once. */ +function warnForeign(sessionID: string, tools: Record): void { + for (const key of foreignEngineKeys(tools)) { + let seen = unrecognisedWarned.get(sessionID) + if (!seen) { + seen = new Set() + unrecognisedWarned.set(sessionID, seen) + } + if (seen.has(key)) continue + seen.add(key) + const message = "an MCP server other than the workspace engine serves an engine-shaped key; it confers no precedence" + const data = { sessionID, key, client: (tools[key] as { client?: unknown }).client } + if (precedenceInternals.warn) precedenceInternals.warn(message, data) + else log.warn(message, data) + } +} + /** Said when routing stops entirely, which `inventoryLine` renders as an empty string * because there is nothing left to enumerate. Silence is the wrong answer only here: * the session was previously told its calls were routed. */ @@ -307,16 +333,29 @@ export function escapeHatchOn(): boolean { return CoreFlag.ALTIMATE_INTEGRATIONS_LOCAL } -async function currentBinding(): Promise<{ datamateId: number; datamateName: string } | null> { - if (precedenceInternals.binding) return precedenceInternals.binding() +/** What a read of the project's link established. `unreadable` is not `unbound`: the + * cache or credentials file is present and cannot be read, so whether the project is + * bound is unknown — the same distinction the attach draws with its strict reader. */ +type BindingRead = + | { kind: "bound"; datamateId: number; datamateName: string } + | { kind: "unbound" } + | { kind: "unreadable"; error: string } + +async function currentBinding(): Promise { try { + if (precedenceInternals.binding) { + const seam = await precedenceInternals.binding() + return seam ? { kind: "bound", ...seam } : { kind: "unbound" } + } const directory = Instance.directory - if (!directory) return null - const binding = await readLocalBinding(directory) - return binding ? { datamateId: binding.datamateId, datamateName: binding.datamateName } : null + if (!directory) return { kind: "unbound" } + const { binding } = await readLocalBindingScopedStrict(directory) + return binding + ? { kind: "bound", datamateId: binding.datamateId, datamateName: binding.datamateName } + : { kind: "unbound" } } catch (err) { - log.warn("could not read local binding", { err: String(err) }) - return null + log.warn("could not read the workspace link", { err: String(err) }) + return { kind: "unreadable", error: String(err) } } } @@ -372,7 +411,16 @@ export async function refresh( tools: Record, ruleset?: PermissionNext.Ruleset, ): Promise { - const result = await derive(sessionID, tools) + // A derivation that throws must not cost the turn its tools: the resolver awaits this + // on every turn, so a failure here settles as "unknown" — local execution with a + // stated reason — rather than propagating. + let result: Precedence + try { + result = await derive(sessionID, tools) + } catch (err) { + log.warn("precedence could not be derived; running locally with a notice", { sessionID, err: String(err) }) + result = EMPTY("derive-failed") + } if (ruleset) result.ruleset = ruleset remember(sessionID, result) // Mechanism 6 — say once, per session, what is now served where. Silence is the one @@ -422,8 +470,12 @@ async function derive(sessionID: string, tools: Record): Promis if (!isEnabled()) return EMPTY("pilot-off") if (escapeHatchOn()) return EMPTY("escape-hatch") - const binding = await currentBinding() - if (!binding) return EMPTY("unbound") + const read = await currentBinding() + // An unreadable link is unknown, not opted out: it must reach the result as a stated + // reason (Claim 1), where a genuinely unbound project runs silently by design. + if (read.kind === "unreadable") return EMPTY("binding-unreadable") + if (read.kind === "unbound") return EMPTY("unbound") + const binding = read const workspaceName = binding.datamateName // Mechanism 1a — refuse to engage on an engine we cannot attribute to this binding. @@ -446,6 +498,7 @@ async function derive(sessionID: string, tools: Record): Promis // Mechanism 1 — what actually materialised, never what was declared. const present = engineToolKeys(tools) + warnForeign(sessionID, tools) if (present.size === 0) return EMPTY("nothing-materialised", workspaceName) warnUnrecognised(sessionID, present) @@ -598,9 +651,17 @@ function redirectFor( * mid-session is supported, so a snapshot can name a workspace the project has since * left; anything about to act on or report from that snapshot asks this first. The * binding is a local cache read. */ +export type SnapshotState = "current" | "relinked" | "unreadable" + +export async function snapshotState(precedence: Precedence): Promise { + if (!precedence.workspaceId) return "current" + const read = await currentBinding() + if (read.kind === "unreadable") return "unreadable" + return read.kind === "bound" && read.datamateId === Number(precedence.workspaceId) ? "current" : "relinked" +} + export async function snapshotCurrent(precedence: Precedence): Promise { - if (!precedence.workspaceId) return true - return (await currentBinding())?.datamateId === Number(precedence.workspaceId) + return (await snapshotState(precedence)) === "current" } /** @@ -657,21 +718,48 @@ async function checkUnsafe(sessionID: string, capability: Capability, warehouse? precedence: "undetermined", } } + if (precedence.disabledReason === "binding-unreadable") { + return { + notice: + "Not routed through the bound workspace: the workspace link could not be read " + + "this turn, so no routing decision was available.", + precedence: "undetermined", + } + } + if (precedence.disabledReason === "derive-failed") { + return { + notice: + "Not routed through the bound workspace: the routing decision could not be derived " + + "this turn, so the call ran locally.", + precedence: "undetermined", + } + } return RUN } + const verdict = await decide(precedence, capability, warehouse) + if (!verdict.redirect) return verdict + // A redirect naming a workspace the project has since left would send the call to - // that workspace's engine, with its credentials. This only runs on the path that is - // about to redirect. - if (!(await snapshotCurrent(precedence))) { + // that workspace's engine, with its credentials. Only a call about to be redirected + // pays for this read; a call that runs locally regardless does not. + const snapshot = await snapshotState(precedence) + if (snapshot !== "current") { return { notice: - `Not routed through workspace "${precedence.workspaceName}": the project was re-linked while ` + - `this call was in flight, so the routing decision no longer applies.`, + snapshot === "unreadable" + ? `Not routed through workspace "${precedence.workspaceName}": the workspace link could not be read ` + + `while this call was in flight, so the routing decision could not be confirmed.` + : `Not routed through workspace "${precedence.workspaceName}": the project was re-linked while ` + + `this call was in flight, so the routing decision no longer applies.`, precedence: "undetermined", } } + return verdict +} +/** The routing decision for an enabled snapshot, before the snapshot is re-validated. */ +async function decide(precedence: Precedence, capability: Capability, warehouse?: string): Promise { if (warehouse) { const type = canonicalType(Registry.getConfig(warehouse)?.type) if (!type) { @@ -828,6 +916,10 @@ export function inventoryLine(precedence: Precedence): string { `Workspace integrations: shadowing off — the running engine could not be attributed to workspace ` + `"${precedence.workspaceName}". Local connections serve every warehouse.` ) + case "binding-unreadable": + return "Workspace integrations: shadowing off — the workspace link could not be read. Local connections serve every warehouse." + case "derive-failed": + return "Workspace integrations: shadowing off — the routing decision could not be derived this turn. Local connections serve every warehouse." default: return "" } diff --git a/packages/opencode/test/altimate/default-target.test.ts b/packages/opencode/test/altimate/default-target.test.ts index 3a5dda040b..ea0c2c16d6 100644 --- a/packages/opencode/test/altimate/default-target.test.ts +++ b/packages/opencode/test/altimate/default-target.test.ts @@ -168,12 +168,15 @@ describe("adapter creation stays single-flight across a reset", () => { gates[0]() await a - // B is still in flight; A's settle must not have released its slot. + // B is still in flight; A's settle must not have released its slot, nor written + // the cache B now owns. expect(dbtAdapterInternals.inflight()).toBe(true) + expect(dbtAdapterInternals.writes).toBe(0) gates[1]() await b expect(dbtAdapterInternals.inflight()).toBe(false) + expect(dbtAdapterInternals.writes).toBe(1) }) }) diff --git a/packages/opencode/test/altimate/precedence-guard-order.test.ts b/packages/opencode/test/altimate/precedence-guard-order.test.ts index 392b1e07d1..31df58af69 100644 --- a/packages/opencode/test/altimate/precedence-guard-order.test.ts +++ b/packages/opencode/test/altimate/precedence-guard-order.test.ts @@ -21,6 +21,7 @@ import * as Registry from "../../src/altimate/native/connections/registry" import { check, precedenceInternals, refresh, resetForTests } from "../../src/altimate/workspace/precedence" const SESSION = SessionID.make("ses_guard_order") +const ORIGINAL_INTEGRATIONS = process.env.ALTIMATE_INTEGRATIONS const ORIGINAL_PILOT = process.env.ALTIMATE_WORKSPACE const ctx = { @@ -62,6 +63,8 @@ afterEach(() => { Registry.reset() if (ORIGINAL_PILOT === undefined) delete process.env.ALTIMATE_WORKSPACE else process.env.ALTIMATE_WORKSPACE = ORIGINAL_PILOT + if (ORIGINAL_INTEGRATIONS === undefined) delete process.env.ALTIMATE_INTEGRATIONS + else process.env.ALTIMATE_INTEGRATIONS = ORIGINAL_INTEGRATIONS }) describe("sql_execute — the hard deny outranks the redirect", () => { @@ -196,3 +199,25 @@ describe("sql_explain — input validation outranks the redirect", () => { expect(result.metadata.redirected).toBe(true) }) }) + +describe("schema_inspect — input validation outranks the redirect", () => { + test("an empty table reports invalid input rather than redirecting", async () => { + const tool = await initTool(SchemaInspectTool) + const result: any = await tool.execute({ table: " ", warehouse: "shadowed_snow" }, ctx) + expect(result.metadata.error_class).toBe("input_validation") + expect(result.metadata.redirected).toBeUndefined() + }) + + test("an empty warehouse string is invalid input, not the default target", async () => { + const tool = await initTool(SchemaInspectTool) + const result: any = await tool.execute({ table: "orders", warehouse: " " }, ctx) + expect(result.metadata.error_class).toBe("input_validation") + expect(result.metadata.redirected).toBeUndefined() + }) + + test("valid input on a shadowed connection is still redirected", async () => { + const tool = await initTool(SchemaInspectTool) + const result: any = await tool.execute({ table: "orders", warehouse: "shadowed_snow" }, ctx) + expect(result.metadata.redirected).toBe(true) + }) +}) diff --git a/packages/opencode/test/altimate/workspace/engine-types.test.ts b/packages/opencode/test/altimate/workspace/engine-types.test.ts index d54dfb2863..316ee380f2 100644 --- a/packages/opencode/test/altimate/workspace/engine-types.test.ts +++ b/packages/opencode/test/altimate/workspace/engine-types.test.ts @@ -16,6 +16,7 @@ import { describeRefusal, engineEntry, engineToolKeys, + foreignEngineKeys, installWouldHelp, pinnedWorkspace, type Outcome, @@ -98,6 +99,17 @@ describe("engineToolKeys", () => { }) expect([...keys].sort()).toEqual(["dbt_build_model", "snowflake_execute_database_query"]) }) + + test("an entry stamped with another client is not an engine tool; an unstamped one is taken by prefix", () => { + const tools = { + datamate_snowflake_execute_database_query: { client: "datamate_snowflake" }, + datamate_dbt_build_model: { client: "datamate" }, + datamate_legacy_tool: {}, + other_tool: { client: "other" }, + } + expect([...engineToolKeys(tools)].sort()).toEqual(["dbt_build_model", "legacy_tool"]) + expect(foreignEngineKeys(tools)).toEqual(["datamate_snowflake_execute_database_query"]) + }) }) describe("outcome tables", () => { diff --git a/packages/opencode/test/altimate/workspace/precedence.test.ts b/packages/opencode/test/altimate/workspace/precedence.test.ts index 8c8984abce..50042bc676 100644 --- a/packages/opencode/test/altimate/workspace/precedence.test.ts +++ b/packages/opencode/test/altimate/workspace/precedence.test.ts @@ -20,6 +20,7 @@ import { refresh, resetForTests, snapshotCurrent, + snapshotState, warehouseListNote, warehouseListNotes, } from "../../../src/altimate/workspace/precedence" @@ -167,15 +168,32 @@ describe("attribution is grounded in the attach, not only the saved config", () expect(verdict.precedence).toBe("undetermined") }) - test("a throw inside the decision fails open with a stated reason", async () => { + test("a binding read that throws mid-decision fails open with that reason stated", async () => { // beforeEach's bindTo() gives an enabled snapshot; refresh first, then poison the - // binding read that check()'s re-link guard performs mid-decision. + // binding read that check()'s re-link guard performs mid-decision. The read is a + // recognised failure, so the reason is specific rather than the generic backstop. await refresh(SESSION, SNOWFLAKE_TOOLS) precedenceInternals.binding = async () => { throw new Error("boom") } const verdict = await check(SESSION, "sql_execute", "local_snow") expect(verdict.redirect).toBeUndefined() + expect(verdict.notice).toContain("could not be read") + expect(verdict.precedence).toBe("undetermined") + }) + + test("an unforeseen throw inside the decision fails open with a stated reason", async () => { + // A corrupted snapshot stands in for any throw the decision did not anticipate: + // the stored map is replaced by one that throws on its first read. + await refresh(SESSION, SNOWFLAKE_TOOLS) + const stored = forSession(SESSION)! + stored.shadowed = new Proxy(stored.shadowed, { + get() { + throw new Error("boom") + }, + }) + const verdict = await check(SESSION, "sql_execute", "local_snow") + expect(verdict.redirect).toBeUndefined() expect(verdict.notice).toContain("failed to compute") expect(verdict.precedence).toBe("undetermined") }) @@ -1167,3 +1185,93 @@ describe("re-derivation", () => { expect(verdict.precedence).toBe("undetermined") }) }) + +describe("materialisation is owned by the workspace engine, not by key shape", () => { + // `MCP.tools()` stamps every entry with its client. Another server named + // `datamate_snowflake` flattens to the very same keys; only the stamp tells them apart. + const stamped = (client: string) => Object.fromEntries(Object.keys(SNOWFLAKE_TOOLS).map((k) => [k, { client }])) + + test("an engine-shaped key served by another MCP client confers nothing, and is reported once", async () => { + const warned: string[] = [] + precedenceInternals.warn = (message, data) => { + warned.push(`${message} ${String(data.key)}`) + } + const p = await refresh(SESSION, stamped("datamate_snowflake")) + expect(p.enabled).toBe(false) + expect(p.disabledReason).toBe("nothing-materialised") + expect((await check(SESSION, "sql_execute", "local_snow")).redirect).toBeUndefined() + const hits = () => warned.filter((w) => w.includes("datamate_snowflake_execute_database_query")) + expect(hits()).toHaveLength(1) + await refresh(SESSION, stamped("datamate_snowflake")) + expect(hits()).toHaveLength(1) + }) + + test("keys stamped with the workspace engine's own client count as before", async () => { + const p = await refresh(SESSION, stamped("datamate")) + expect(p.enabled).toBe(true) + expect((await check(SESSION, "sql_execute", "local_snow")).redirect?.metadata.redirect_to).toBe( + "datamate_snowflake_execute_database_query", + ) + }) +}) + +describe("an unreadable workspace link is unknown, not unbound", () => { + test("refresh settles binding-unreadable and check() says so in the result", async () => { + precedenceInternals.binding = async () => { + throw new Error("EBUSY") + } + const p = await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(p.enabled).toBe(false) + expect(p.disabledReason).toBe("binding-unreadable") + const verdict = await check(SESSION, "sql_execute", "local_snow") + expect(verdict.redirect).toBeUndefined() + expect(verdict.precedence).toBe("undetermined") + expect(verdict.notice).toContain("could not be read") + expect(inventoryLine(p)).toContain("could not be read") + }) + + test("a link that becomes unreadable mid-turn invalidates the snapshot for that reason, not as a re-link", async () => { + await refresh(SESSION, SNOWFLAKE_TOOLS) + precedenceInternals.binding = async () => { + throw new Error("EBUSY") + } + expect(await snapshotState(forSession(SESSION)!)).toBe("unreadable") + const verdict = await check(SESSION, "sql_execute", "local_snow") + expect(verdict.redirect).toBeUndefined() + expect(verdict.precedence).toBe("undetermined") + expect(verdict.notice).toContain("could not be read") + expect(verdict.notice).not.toContain("re-linked") + expect((await warehouseListNotes(SESSION, [{ name: "local_snow", type: "snowflake" }] as never)).size).toBe(0) + }) +}) + +describe("a derivation that throws fails open, and says so", () => { + test("refresh settles derive-failed and check() carries the reason", async () => { + precedenceInternals.attributedTo = async () => { + throw new Error("boom") + } + const p = await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(p.enabled).toBe(false) + expect(p.disabledReason).toBe("derive-failed") + const verdict = await check(SESSION, "sql_execute", "local_snow") + expect(verdict.redirect).toBeUndefined() + expect(verdict.precedence).toBe("undetermined") + expect(verdict.notice).toContain("could not be derived") + expect(inventoryLine(p)).toContain("could not be derived") + }) +}) + +describe("drift is reported for every warehouse capability shape", () => { + test("an unknown integration that serves only explain or table stats is still reported once", async () => { + const warned: string[] = [] + precedenceInternals.warn = (_message, data) => { + warned.push(String(data.key)) + } + await refresh(SESSION, { + ...SNOWFLAKE_TOOLS, + datamate_redshift_get_query_explain_plan: {}, + datamate_redshift_get_table_stats: {}, + }) + expect(warned.sort()).toEqual(["redshift_get_query_explain_plan", "redshift_get_table_stats"]) + }) +}) From a690b08dd1071f8cff7c700984cf3689e3aca1b0 Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Mon, 31 Aug 2026 13:29:40 +0800 Subject: [PATCH 08/10] fix(workspace): a stale announcement may only record against the snapshot it was published for Eviction drops a session's publish chain, so a session recreated before its old line lands has a second publication running unchained. The completion guard checked only that the session existed; a stale completion arriving last could then overwrite the new record and make the next refresh repeat the newer line. The guard now requires the stored snapshot to be the one the attempt was published for. --- .../src/altimate/workspace/precedence.ts | 7 +++-- .../altimate/workspace/precedence.test.ts | 27 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/altimate/workspace/precedence.ts b/packages/opencode/src/altimate/workspace/precedence.ts index 40a572bef8..3862e62bbb 100644 --- a/packages/opencode/src/altimate/workspace/precedence.ts +++ b/packages/opencode/src/altimate/workspace/precedence.ts @@ -452,8 +452,11 @@ export async function refresh( // A session evicted while its line was in flight must not be written back: // eviction only ever walks `bySession`, so an entry recreated here after the // session left it could never be reclaimed, and the map would grow with the - // lifetime session count rather than staying bounded. - if (delivered && bySession.has(sessionID)) announced.set(sessionID, attempt) + // lifetime session count rather than staying bounded. Nor may it write over a + // session recreated in the meantime — only the snapshot this attempt was + // published for may record it, or a stale completion could retain or repeat + // an obsolete line over the newer publication. + if (delivered && bySession.get(sessionID) === result) announced.set(sessionID, attempt) }) publishQueue.set(sessionID, queued) void queued diff --git a/packages/opencode/test/altimate/workspace/precedence.test.ts b/packages/opencode/test/altimate/workspace/precedence.test.ts index 50042bc676..22ccb8fb22 100644 --- a/packages/opencode/test/altimate/workspace/precedence.test.ts +++ b/packages/opencode/test/altimate/workspace/precedence.test.ts @@ -295,6 +295,33 @@ describe("the per-session caches are bounded", () => { expect(said).toHaveLength(1) expect(announcedSessionCount()).toBeLessThanOrEqual(MAX_TRACKED_SESSIONS) }) + + test("a line delivered after its session was evicted and recreated does not overwrite the new record", async () => { + // Eviction drops the session's publish chain, so a session recreated before its + // old line lands has a second publication running unchained. If the stale + // completion arrives last it must not become the session's record: the next + // refresh would then repeat the newer line as if it had never been said. + const settle: Array<() => void> = [] + precedenceInternals.announce = () => new Promise((resolve) => settle.push(resolve)) + await refresh("ses_recreated", SNOWFLAKE_TOOLS) + for (let i = 0; i < MAX_TRACKED_SESSIONS + 5; i++) { + await refresh(`ses_flood2_${i}`, {}) + } + expect(forSession("ses_recreated")).toBeUndefined() + await refresh("ses_recreated", BIGQUERY_TOOLS) + expect(settle).toHaveLength(2) + + settle[1]() + await tick() + settle[0]() + await tick() + + const said: string[] = [] + precedenceInternals.announce = async (line) => void said.push(line) + await refresh("ses_recreated", BIGQUERY_TOOLS) + await tick() + expect(said).toEqual([]) + }) }) describe("mechanism 1a — attributed to the bound workspace", () => { From 915020f98fc2b0994f05d7077cfe14b2ba16dc91 Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Mon, 31 Aug 2026 13:30:51 +0800 Subject: [PATCH 09/10] docs(workspace): say which tools the shared input validation covers --- .../opencode/src/altimate/tools/input-validation.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/altimate/tools/input-validation.ts b/packages/opencode/src/altimate/tools/input-validation.ts index f3940fdcd1..5aedba921c 100644 --- a/packages/opencode/src/altimate/tools/input-validation.ts +++ b/packages/opencode/src/altimate/tools/input-validation.ts @@ -1,9 +1,11 @@ // altimate_change - new file // -// Pre-flight input checks shared by the warehouse tools. They run BEFORE workspace -// precedence is consulted: a redirect must never forward malformed input to the -// engine tool, and an empty or placeholder warehouse name must not be read as -// "use the default" by the routing decision. +// Pre-flight input checks shared by `sql_explain` and `schema_inspect`. They run +// BEFORE workspace precedence is consulted there: a redirect must never forward +// malformed input to the engine tool, and an empty or placeholder warehouse name +// must not be read as "use the default" by the routing decision. `sql_execute` has +// its own guard order (hard deny, write prompt, then precedence) and does not use +// these; its warehouse resolution is unchanged. /** Warehouse names that models produce by mistake: empty strings and unsubstituted * placeholders. Both would otherwise fall through to unhelpful registry errors. */ From acb4c712668633b6f7bf014a14b40e19805ae580 Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Mon, 31 Aug 2026 13:51:02 +0800 Subject: [PATCH 10/10] fix(workspace): record a delivered announcement against the session incarnation, not the snapshot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The identity guard checked the snapshot object, but a multi-step turn refreshes per step and each refresh replaces the snapshot while the same line is still being said — so a delivery landing after the second refresh was never recorded and the next turn repeated the line. A per-session incarnation token, minted when a session is first remembered or recreated after eviction and dropped with it, keeps both properties: ordinary refreshes still record, a completion for a session evicted and recreated mid-flight does not. --- .../src/altimate/workspace/precedence.ts | 18 ++++++++++++++---- .../altimate/workspace/precedence.test.ts | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/altimate/workspace/precedence.ts b/packages/opencode/src/altimate/workspace/precedence.ts index 3862e62bbb..cc329db0a7 100644 --- a/packages/opencode/src/altimate/workspace/precedence.ts +++ b/packages/opencode/src/altimate/workspace/precedence.ts @@ -153,6 +153,12 @@ const EMPTY = (reason: Precedence["disabledReason"], workspaceName = ""): Preced /** Per-session precedence, refreshed once per turn by the tool resolver and read * (never recomputed) by tool bodies mid-turn. */ const bySession = new Map() +/** One token per session incarnation: set when a session is first remembered or + * recreated after eviction, dropped with it. A delivery records against the + * incarnation it was published for, so ordinary refreshes (which replace the + * snapshot but not the session) still count, while a stale completion for a + * session evicted and recreated mid-flight does not. */ +const incarnations = new Map() /** Test seam. Production leaves every field unset. */ export const precedenceInternals: { @@ -178,6 +184,7 @@ export const precedenceInternals: { export const MAX_TRACKED_SESSIONS = 256 function remember(sessionID: string, value: Precedence): void { + if (!bySession.has(sessionID)) incarnations.set(sessionID, {}) bySession.delete(sessionID) bySession.set(sessionID, value) while (bySession.size > MAX_TRACKED_SESSIONS) { @@ -187,6 +194,7 @@ function remember(sessionID: string, value: Precedence): void { announced.delete(oldest.value) publishing.delete(oldest.value) publishQueue.delete(oldest.value) + incarnations.delete(oldest.value) unrecognisedWarned.delete(oldest.value) } } @@ -445,6 +453,7 @@ export async function refresh( // Nothing reaches `announced` until the line actually arrives, so a failure leaves // the session's known state untouched and the next turn retries. const attempt = { line, routed } + const incarnation = incarnations.get(sessionID) publishing.set(sessionID, attempt) const queued = (publishQueue.get(sessionID) ?? Promise.resolve()).then(async () => { const delivered = await announce(line) @@ -453,10 +462,10 @@ export async function refresh( // eviction only ever walks `bySession`, so an entry recreated here after the // session left it could never be reclaimed, and the map would grow with the // lifetime session count rather than staying bounded. Nor may it write over a - // session recreated in the meantime — only the snapshot this attempt was - // published for may record it, or a stale completion could retain or repeat - // an obsolete line over the newer publication. - if (delivered && bySession.get(sessionID) === result) announced.set(sessionID, attempt) + // session recreated in the meantime. The check is on the session incarnation, + // not the snapshot: a later refresh in the same session replaces the snapshot + // while this line is still the one being said for it. + if (delivered && incarnations.get(sessionID) === incarnation) announced.set(sessionID, attempt) }) publishQueue.set(sessionID, queued) void queued @@ -545,6 +554,7 @@ export function announcedSessionCount(): number { export function resetForTests(): void { bySession.clear() + incarnations.clear() announced.clear() publishing.clear() publishQueue.clear() diff --git a/packages/opencode/test/altimate/workspace/precedence.test.ts b/packages/opencode/test/altimate/workspace/precedence.test.ts index 22ccb8fb22..b2d595dcaa 100644 --- a/packages/opencode/test/altimate/workspace/precedence.test.ts +++ b/packages/opencode/test/altimate/workspace/precedence.test.ts @@ -296,6 +296,25 @@ describe("the per-session caches are bounded", () => { expect(announcedSessionCount()).toBeLessThanOrEqual(MAX_TRACKED_SESSIONS) }) + test("two refreshes sharing one in-flight delivery record it once, not never", async () => { + // A multi-step turn refreshes per step. The second refresh replaces the snapshot + // but starts no new delivery (the line is already being said); when that delivery + // lands it must still be recorded, or the next turn repeats the same line. + const settle: Array<() => void> = [] + precedenceInternals.announce = () => new Promise((resolve) => settle.push(resolve)) + await refresh(SESSION, SNOWFLAKE_TOOLS) + await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(settle).toHaveLength(1) + settle[0]() + await tick() + + const said: string[] = [] + precedenceInternals.announce = async (line) => void said.push(line) + await refresh(SESSION, SNOWFLAKE_TOOLS) + await tick() + expect(said).toEqual([]) + }) + test("a line delivered after its session was evicted and recreated does not overwrite the new record", async () => { // Eviction drops the session's publish chain, so a session recreated before its // old line lands has a second publication running unchained. If the stale