From edd88e56bc96d5f2e8f6ec7054bc53bbdc49a66b Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Wed, 26 Aug 2026 22:55:31 +0800 Subject: [PATCH 01/30] feat(workspace): route warehouse tools through the bound workspace's engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a bound workspace's integration engine attaches, the model sees two implementations of the same capability with different credential sources: the native warehouse tools over local keychain connections, and the engine's MCP tools over the SaaS connection. Nothing arbitrated between them, so the model picked by description — and that choice silently decided which credentials ran the query and whether the call was audited. Add `altimate/workspace/precedence.ts`, which decides per session which side serves a call. One principle runs through it: shadow only what is materialised AND attributed; anything undetermined runs locally with an explicit notice; nothing is ever silent. - Materialised, not declared: derived from the engine tool keys actually present in the model-facing MCP map, so a declared-but-broken integration shadows nothing. - Attributed: the engine must be pinned to the bound workspace. Attach guarantees this; precedence re-checks it and refuses to engage otherwise. - Capability-scoped, not type-scoped: the engine's warehouse integrations are not symmetric — snowflake serves execute/explain/inspect, bigquery and postgresql serve execute only, databricks serves execute only. Keying on the individual materialised tool key keeps `sql_explain` on a BigQuery connection local instead of redirecting it to a tool that does not exist. - Redirect, no fallback: a shadowed call returns a result naming the exact engine key and executes nothing. The result carries `metadata.redirected` and `metadata.redirect_to`, because `Tool.wrap` reports every returning body as a successful call and a redirect would otherwise be indistinguishable from a real execution. Add `resolveDefaultTarget(op)` so a call with no `warehouse` is judged against the target it would really reach. Only `sql.execute` consults dbt; explain and inspect stay registry-only, or the guard would drag adapter construction (Python bridge, manifest rebuild, file watchers) onto paths that never touch dbt today. Adapter creation is now single-flight — two concurrent `warehouse`-less calls used to construct it twice. Add `canonicalType()` to the connection registry, derived by inverting `DRIVER_MAP` so aliases cannot desync: `postgresql`/`postgres` are one driver, as are `mariadb`/`mysql` and `mssql`/`fabric`/`sqlserver`. `redshift` keeps its own identity and is not served by a postgresql integration. Precedence is re-derived every turn from the live tool map rather than cached at attach, so it stays correct when an engine's tool set changes underneath. Both tool resolvers call the same description helpers. `--integrations=local` turns shadowing off for the session. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RM9xasTNbk2k1eqhpF1Hp5 --- packages/core/src/flag/flag.ts | 8 + .../altimate/native/connections/register.ts | 114 ++++- .../altimate/native/connections/registry.ts | 23 + .../src/altimate/tools/schema-inspect.ts | 12 +- .../src/altimate/tools/sql-execute.ts | 16 +- .../src/altimate/tools/sql-explain.ts | 15 +- .../src/altimate/tools/warehouse-list.ts | 25 +- .../src/altimate/workspace/precedence.ts | 400 ++++++++++++++++++ packages/opencode/src/index.ts | 11 + packages/opencode/src/session/prompt.ts | 18 +- packages/opencode/src/session/tools.ts | 17 +- 11 files changed, 632 insertions(+), 27 deletions(-) create mode 100644 packages/opencode/src/altimate/workspace/precedence.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..d5bcb727f8 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,13 +84,97 @@ 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 } + | { 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() + return { source: "dbt", type } } } + 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 ? await dbtAdapter.immediatelyExecuteSQLWithLimit(sql, "", limit) @@ -146,6 +231,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 } // --------------------------------------------------------------------------- diff --git a/packages/opencode/src/altimate/native/connections/registry.ts b/packages/opencode/src/altimate/native/connections/registry.ts index 8be3bfc672..f4961bed64 100644 --- a/packages/opencode/src/altimate/native/connections/registry.ts +++ b/packages/opencode/src/altimate/native/connections/registry.ts @@ -133,6 +133,29 @@ const DRIVER_MAP: Record = { 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..d82037f308 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,10 @@ 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 + // altimate_change end try { const result = (await Dispatcher.call("schema.inspect", { table: args.table, @@ -45,11 +52,12 @@ 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) diff --git a/packages/opencode/src/altimate/tools/sql-execute.ts b/packages/opencode/src/altimate/tools/sql-execute.ts index 4647c75648..8afadcdca3 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.", @@ -22,6 +25,13 @@ export const SqlExecuteTool = Tool.define("sql_execute", { limit: z.number().optional().default(100).describe("Max rows to return"), }), async execute(args, ctx) { + // altimate_change start — workspace precedence. + // Ahead of the write-permission prompt on purpose: a shadowed write should be + // redirected, not approved and then redirected. + const precedence = await Precedence.check(ctx.sessionID, "sql_execute", args.warehouse) + if (precedence.redirect) return precedence.redirect + // altimate_change end + // altimate_change start - SQL write access control // Permission checks OUTSIDE try/catch so denial errors propagate to the framework const { queryType, blocked } = classifyAndCheck(args.query) @@ -87,11 +97,13 @@ 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 { diff --git a/packages/opencode/src/altimate/tools/sql-explain.ts b/packages/opencode/src/altimate/tools/sql-explain.ts index 5e9bae7dbb..2afcca8db0 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,12 @@ 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) { + // altimate_change start — workspace precedence + const precedence = await Precedence.check(ctx.sessionID, "sql_explain", args.warehouse) + if (precedence.redirect) return precedence.redirect + // altimate_change end + // 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) @@ -145,7 +153,8 @@ export const SqlExplainTool = Tool.define("sql_explain", { } } - 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,7 +162,7 @@ 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 { 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..5cb1bc0933 --- /dev/null +++ b/packages/opencode/src/altimate/workspace/precedence.ts @@ -0,0 +1,400 @@ +// 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. Attach +// reuses any connected `datamate` entry, and an IDE writes that entry unpinned, +// so a reused engine can be serving a different workspace than the local binding +// names. Attach is being fixed to guarantee attribution; this module re-checks it +// and refuses to engage if the guarantee is ever violated. Defence in depth. +// 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. +// +// 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 +// cache-invalidated by the `tools/list_changed` notification, and `resolveTools` runs +// once per turn. +import { Config } from "@/config/config" +import { Log } from "@/altimate/util/log" +import { Instance } from "@/project/instance" +import { Flag as CoreFlag } from "@opencode-ai/core/flag/flag" +import { DATAMATE_KEY } from "../datamate-transport" +import { engineToolKeys } from "./engine-sync" +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 + /** 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?: "escape-hatch" | "unbound" | "unattributed" | "nothing-materialised" + /** canonical driver type → capability → who serves it. */ + shadowed: Map> +} + +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 +} = {} + +/** 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. Reads the merged MCP config rather than + * engine-sync's internals so this module needs no changes there. + * + * A command entry carries the pin as `--datamate `. A URL entry is an IDE's + * in-process engine, which is never pinned and whose active teammate changes at + * runtime — it can never be attributed. + */ +async function attributedTo(): Promise { + if (precedenceInternals.attributedTo) return precedenceInternals.attributedTo() + try { + const cfg = (await Config.get()) as { + mcp?: Record + } + const entry = cfg.mcp?.[DATAMATE_KEY] + if (!entry || entry.url || !entry.command) return null + const flag = entry.command.indexOf("--datamate") + if (flag === -1) return null + return entry.command[flag + 1] ?? null + } 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): Promise { + const result = await derive(tools) + bySession.set(sessionID, result) + return result +} + +async function derive(tools: Record): Promise { + 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. + const pinned = await attributedTo() + 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, enabled: true, shadowed } +} + +/** Read the session's precedence without recomputing it. */ +export function forSession(sessionID: string): Precedence | undefined { + return bySession.get(sessionID) +} + +export function resetForTests(): void { + bySession.clear() + 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 = {} + +function redirectFor(capability: Capability, entry: ShadowEntry, workspaceName: string, connection: string): 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, + }, + output: + `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 || !precedence.enabled) return RUN + + if (warehouse) { + const type = canonicalType(Registry.getConfig(warehouse)?.type) + if (!type) return RUN + const entry = precedence.shadowed.get(type)?.get(capability) + return entry ? redirectFor(capability, entry, precedence.workspaceName, warehouse) : RUN + } + + // 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]) + if (target.source === "none") return RUN + + const type = canonicalType(target.type) + 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", + } + } + const entry = precedence.shadowed.get(type)?.get(capability) + if (!entry) return RUN + const connection = target.source === "registry" ? target.name : "the dbt profile's target" + return redirectFor(capability, entry, precedence.workspaceName, connection) +} + +/** + * 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 shadowed = CAPABILITIES.includes(toolID as Capability) || toolID === "warehouse_list" + if (!shadowed) 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 "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[] = [] + for (const [type, byCapability] of precedence.shadowed) { + const served = CAPABILITIES.filter((c) => byCapability.has(c)).map((c) => c.replace(/^(sql|schema)_/, "")) + const local = CAPABILITIES.filter((c) => !byCapability.has(c)).map((c) => c.replace(/^(sql|schema)_/, "")) + parts.push( + `${type}: ${served.join("/")} via workspace ${precedence.workspaceName}` + + (local.length ? `, ${local.join("/")} stay local` : ""), + ) + } + 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 && precedence.shadowed.has(type) + }).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 byCapability = precedence.shadowed.get(type) + if (!byCapability) return null + const served = CAPABILITIES.filter((c) => byCapability.has(c)).map((c) => c.replace(/^(sql|schema)_/, "")) + const local = CAPABILITIES.filter((c) => !byCapability.has(c)).map((c) => c.replace(/^(sql|schema)_/, "")) + 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 d2fe2b3506..f3b4f323f3 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 96e092e34c..e7fc3efea0 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -26,6 +26,7 @@ import { UNIFIED_INJECTION_BUDGET } from "../memory/types" // altimate_change - workspace memory read path import * as WorkspaceMemory from "../altimate/workspace/memory-sync" import * as WorkspaceEngine from "../altimate/workspace/engine-sync" +import * as Precedence from "../altimate/workspace/precedence" import { Plugin } from "../plugin" import PROMPT_PLAN from "../session/prompt/plan.txt" import BUILD_SWITCH from "../session/prompt/build-switch.txt" @@ -1760,6 +1761,16 @@ 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) + // altimate_change end + for (const item of await ToolRegistry.tools( { modelID: ModelID.make(input.model.api.id), providerID: input.model.providerID }, input.agent, @@ -1769,7 +1780,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) @@ -1821,8 +1833,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..35005de950 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,11 @@ 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)) + // 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 +94,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 +140,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 From 2f25ec50fc625e63dc2b8f8180896d62caf11d15 Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Wed, 26 Aug 2026 23:11:55 +0800 Subject: [PATCH 02/30] test(workspace): cover precedence decisions; share attach's pin parser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unit coverage per mechanism: what materialises confers precedence and what merely declares does not; an engine pinned elsewhere (or not pinned at all) confers none and fails open so the local call still runs; capability scoping, including the case the design turns on — `sql_explain` against a BigQuery connection stays local rather than redirecting to an engine tool that does not exist; driver-alias collapse end to end, with `redshift` proven not to be served by a postgresql integration; the redirect's telemetry marker; the escape hatch; and re-derivation as an engine's tool set changes. The registry is seeded with real connections in `beforeEach`. Without that, `check()` returns "run" simply because the connection is unknown, and every "stays local" assertion passes without proving anything. Also cover `resolveDefaultTarget`: insertion order decides the registry default, an empty registry resolves to nothing rather than guessing, explain and inspect never report a dbt source, and two concurrent execute resolutions agree. Replace this module's private `--datamate` parser with attach's exported `pinnedWorkspace`. The private one read only `command` as an argv array, only the space-separated spelling, and took the first match — so it would have reported "unattributed" for engines that are correctly pinned via a string `command` plus `args`, via `--datamate=`, or via a repeated flag where last wins. Precedence fails open, so that would have quietly disabled the feature rather than breaking loudly. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RM9xasTNbk2k1eqhpF1Hp5 --- .../src/altimate/workspace/precedence.ts | 37 +-- .../test/altimate/default-target.test.ts | 95 ++++++ .../altimate/workspace/precedence.test.ts | 309 ++++++++++++++++++ 3 files changed, 421 insertions(+), 20 deletions(-) create mode 100644 packages/opencode/test/altimate/default-target.test.ts create mode 100644 packages/opencode/test/altimate/workspace/precedence.test.ts diff --git a/packages/opencode/src/altimate/workspace/precedence.ts b/packages/opencode/src/altimate/workspace/precedence.ts index 5cb1bc0933..ba8b64cb1d 100644 --- a/packages/opencode/src/altimate/workspace/precedence.ts +++ b/packages/opencode/src/altimate/workspace/precedence.ts @@ -9,11 +9,12 @@ // 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. Attach -// reuses any connected `datamate` entry, and an IDE writes that entry unpinned, -// so a reused engine can be serving a different workspace than the local binding -// names. Attach is being fixed to guarantee attribution; this module re-checks it -// and refuses to engage if the guarantee is ever violated. Defence in depth. +// 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 @@ -34,7 +35,7 @@ import { Log } from "@/altimate/util/log" import { Instance } from "@/project/instance" import { Flag as CoreFlag } from "@opencode-ai/core/flag/flag" import { DATAMATE_KEY } from "../datamate-transport" -import { engineToolKeys } from "./engine-sync" +import { engineToolKeys, pinnedWorkspace, type ExistingEntry } from "./engine-sync" import { readLocalBinding } from "./state" import { canonicalType } from "../native/connections/registry" import * as Registry from "../native/connections/registry" @@ -133,25 +134,21 @@ async function currentBinding(): Promise<{ datamateId: number; datamateName: str } /** - * Mechanism 1a — which workspace the live engine entry is actually pinned to, or - * null when that cannot be established. Reads the merged MCP config rather than - * engine-sync's internals so this module needs no changes there. - * - * A command entry carries the pin as `--datamate `. A URL entry is an IDE's - * in-process engine, which is never pinned and whose active teammate changes at - * runtime — it can never be attributed. + * 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(): Promise { if (precedenceInternals.attributedTo) return precedenceInternals.attributedTo() try { - const cfg = (await Config.get()) as { - mcp?: Record - } + const cfg = (await Config.get()) as { mcp?: Record } const entry = cfg.mcp?.[DATAMATE_KEY] - if (!entry || entry.url || !entry.command) return null - const flag = entry.command.indexOf("--datamate") - if (flag === -1) return null - return entry.command[flag + 1] ?? null + 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) } catch (err) { log.warn("could not read MCP config for engine attribution", { err: String(err) }) return null 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..bf22526d4c --- /dev/null +++ b/packages/opencode/test/altimate/default-target.test.ts @@ -0,0 +1,95 @@ +// 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 * 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 — 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) + }) +}) 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..0ce40b7876 --- /dev/null +++ b/packages/opencode/test/altimate/workspace/precedence.test.ts @@ -0,0 +1,309 @@ +// 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 { + check, + 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 + +/** The engine tools a workspace with a Snowflake connection materialises. Snowflake is + * the only integration serving all three capabilities. */ +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) +} + +beforeEach(() => { + resetForTests() + delete process.env.ALTIMATE_INTEGRATIONS + 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 +}) + +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("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("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("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() + }) +}) From bb4db1a2c34147fbd095ef652c5342105fa1f490 Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Wed, 26 Aug 2026 23:23:43 +0800 Subject: [PATCH 03/30] feat(workspace): report once what the workspace now serves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Precedence is re-derived every turn, but what changed is a once-per-session statement: which capabilities the bound workspace's engine now serves for which warehouse types, how many local connections that shadows, and — when shadowing is off — why. Silence is the one outcome this design does not allow; repeating the line every turn would be noise. The escape hatch and an unattributable engine both announce themselves. An ordinary unbound session says nothing, because nothing changed for it. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RM9xasTNbk2k1eqhpF1Hp5 --- .../src/altimate/workspace/precedence.ts | 41 +++++++++++++++++- .../altimate/workspace/precedence.test.ts | 43 +++++++++++++++++++ 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/altimate/workspace/precedence.ts b/packages/opencode/src/altimate/workspace/precedence.ts index ba8b64cb1d..e2323cabd4 100644 --- a/packages/opencode/src/altimate/workspace/precedence.ts +++ b/packages/opencode/src/altimate/workspace/precedence.ts @@ -31,6 +31,9 @@ // cache-invalidated by the `tools/list_changed` notification, and `resolveTools` runs // once per turn. 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" @@ -112,8 +115,31 @@ const bySession = new Map() export const precedenceInternals: { binding?: () => Promise<{ datamateId: number; datamateName: string } | null> attributedTo?: () => Promise + announce?: (line: string) => Promise } = {} +/** 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. */ +const announced = new Set() + +async function announce(line: string): Promise { + if (precedenceInternals.announce) return precedenceInternals.announce(line) + try { + await AppRuntime.runPromise( + EventV2Bridge.Service.use((events) => + events.publish(TuiEvent.ToastShow, { + title: "Workspace integrations", + message: line, + variant: "info", + duration: 10000, + }), + ), + ) + } catch (err) { + log.warn("could not report the workspace precedence inventory", { err: String(err) }) + } +} + /** Mechanism 6 — the escape hatch. `--integrations=local` (or the env var) turns * shadowing off for the whole session. */ export function escapeHatchOn(): boolean { @@ -162,6 +188,15 @@ async function attributedTo(): Promise { export async function refresh(sessionID: string, tools: Record): Promise { const result = await derive(tools) bySession.set(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. + if (!announced.has(sessionID)) { + const line = inventoryLine(result) + if (line) { + announced.add(sessionID) + void announce(line).catch(() => {}) + } + } return result } @@ -215,6 +250,8 @@ export function forSession(sessionID: string): Precedence | undefined { export function resetForTests(): void { bySession.clear() + announced.clear() + delete precedenceInternals.announce delete precedenceInternals.binding delete precedenceInternals.attributedTo } @@ -314,7 +351,7 @@ export function annotate; output? if (!verdict.notice) return result return { ...result, - metadata: { ...(result.metadata ?? {}), precedence: verdict.precedence ?? "undetermined" }, + metadata: { ...result.metadata, precedence: verdict.precedence ?? "undetermined" }, output: `${verdict.notice}\n\n${result.output ?? ""}`, } } @@ -325,7 +362,7 @@ export function annotate; output? */ export function describeNativeTool(toolID: string, base: string, precedence?: Precedence): string { if (!precedence?.enabled) return base - const shadowed = CAPABILITIES.includes(toolID as Capability) || toolID === "warehouse_list" + const shadowed = (CAPABILITIES as string[]).includes(toolID) || toolID === "warehouse_list" if (!shadowed) return base return ( `${base} Serves local connections; types served by workspace "${precedence.workspaceName}" ` + diff --git a/packages/opencode/test/altimate/workspace/precedence.test.ts b/packages/opencode/test/altimate/workspace/precedence.test.ts index 0ce40b7876..3cc21641a6 100644 --- a/packages/opencode/test/altimate/workspace/precedence.test.ts +++ b/packages/opencode/test/altimate/workspace/precedence.test.ts @@ -288,6 +288,49 @@ describe("descriptions and listings", () => { }) }) +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) From 04e51806eba42443b722fc08b9283a5d7aa6258f Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Wed, 26 Aug 2026 23:45:45 +0800 Subject: [PATCH 04/30] fix(workspace): refresh the CLI help snapshot; mark precedence server-side only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new `--integrations` flag is longer than any existing root option, so yargs re-wraps the whole option table — the snapshot churn is realignment plus the one new row. Verified whitespace-normalised: the only new content is that flag's own description. Also record why this module must never be imported from a TUI plugin. Plugins load in a separate module realm in the same process, so such an import is a different instance sharing neither module state nor `globalThis`: it would typecheck, unit-test green, and return an empty precedence forever. Only the event bus crosses, which is why the inventory line is published as a TUI event rather than read directly. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RM9xasTNbk2k1eqhpF1Hp5 --- .../src/altimate/workspace/precedence.ts | 8 + .../__snapshots__/help-snapshots.test.ts.snap | 632 ++++++++++-------- 2 files changed, 377 insertions(+), 263 deletions(-) diff --git a/packages/opencode/src/altimate/workspace/precedence.ts b/packages/opencode/src/altimate/workspace/precedence.ts index e2323cabd4..53955e170e 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. // +// 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 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 11b1c72c1b7f11e7c92cbe37c2f415118a701e55 Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Thu, 27 Aug 2026 01:15:15 +0800 Subject: [PATCH 05/30] fix(workspace): gate precedence on the pilot flag; route the dbt fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two correctness holes found in review, both confirmed before fixing. The workspace pilot is opt-in, but precedence checked only its own escape hatch. 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, and their local warehouse calls would start redirecting. Verified against a live workspace: with the flag unset, an engine still spawned and served its tools. Precedence now returns disabled unless the pilot is on. `sql_execute` with no `warehouse` 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 any throw inside `tryExecuteViaDbt`. Reporting only the dbt target let a call through whose execution then landed locally on a connection that should have been routed to the workspace engine: silent, and precisely what this design exists to prevent. `resolveDefaultTarget` now reports that fallback alongside the dbt target, and a shadowed fallback redirects. That trades a possible false redirect — a dbt project on an unserved type whose first registry connection is served — for never executing silently against a served connection. The redirect is visible and recoverable (name a `warehouse`, or `--integrations=local`); the silent local execution is not. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RM9xasTNbk2k1eqhpF1Hp5 --- .../altimate/native/connections/register.ts | 14 ++++++-- .../src/altimate/workspace/precedence.ts | 35 ++++++++++++++++--- .../test/altimate/default-target.test.ts | 25 +++++++++++++ .../altimate/workspace/precedence.test.ts | 31 ++++++++++++++++ 4 files changed, 98 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/altimate/native/connections/register.ts b/packages/opencode/src/altimate/native/connections/register.ts index d5bcb727f8..ab18acda15 100644 --- a/packages/opencode/src/altimate/native/connections/register.ts +++ b/packages/opencode/src/altimate/native/connections/register.ts @@ -98,7 +98,15 @@ async function ensureDbtAdapter(): Promise { /** Where a `warehouse`-less call would actually go. */ export type DefaultTarget = - | { source: "dbt"; type?: string } + | { + 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" } @@ -131,7 +139,9 @@ export async function resolveDefaultTarget( // Adapter not initialised far enough to answer; leave the type undetermined. } if (!type) type = await adapterTypeFromManifest() - return { source: "dbt", type } + const warehouses = Registry.list().warehouses + const fallback = warehouses.length > 0 ? { type: warehouses[0].type, name: warehouses[0].name } : undefined + return { source: "dbt", type, fallback } } } diff --git a/packages/opencode/src/altimate/workspace/precedence.ts b/packages/opencode/src/altimate/workspace/precedence.ts index 53955e170e..629b286194 100644 --- a/packages/opencode/src/altimate/workspace/precedence.ts +++ b/packages/opencode/src/altimate/workspace/precedence.ts @@ -46,7 +46,7 @@ import { Log } from "@/altimate/util/log" import { Instance } from "@/project/instance" import { Flag as CoreFlag } from "@opencode-ai/core/flag/flag" import { DATAMATE_KEY } from "../datamate-transport" -import { engineToolKeys, pinnedWorkspace, type ExistingEntry } from "./engine-sync" +import { engineToolKeys, isEnabled, pinnedWorkspace, type ExistingEntry } from "./engine-sync" import { readLocalBinding } from "./state" import { canonicalType } from "../native/connections/registry" import * as Registry from "../native/connections/registry" @@ -103,7 +103,7 @@ 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?: "escape-hatch" | "unbound" | "unattributed" | "nothing-materialised" + disabledReason?: "pilot-off" | "escape-hatch" | "unbound" | "unattributed" | "nothing-materialised" /** canonical driver type → capability → who serves it. */ shadowed: Map> } @@ -209,6 +209,12 @@ export async function refresh(sessionID: string, tools: Record) } async function derive(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() @@ -343,9 +349,26 @@ export async function check(sessionID: string, capability: Capability, warehouse } } const entry = precedence.shadowed.get(type)?.get(capability) - if (!entry) return RUN - const connection = target.source === "registry" ? target.name : "the dbt profile's target" - return redirectFor(capability, entry, precedence.workspaceName, connection) + if (entry) { + const connection = target.source === "registry" ? target.name : "the dbt profile's target" + return redirectFor(capability, entry, precedence.workspaceName, connection) + } + + // The dbt target itself is not served — but `sql.execute` falls back to the first + // registry connection whenever the dbt attempt yields nothing, including on an + // unrecognised result shape or a throw. If that fallback is a served connection, + // letting the call proceed would execute locally against exactly what precedence + // exists to route. Redirect rather than gamble on dbt succeeding: a redirect is + // visible and recoverable (name a `warehouse`, or `--integrations=local`), whereas + // the silent local execution is the harm this design is for. + if (target.source === "dbt" && target.fallback) { + const fallbackType = canonicalType(target.fallback.type) + const fallbackEntry = fallbackType ? precedence.shadowed.get(fallbackType)?.get(capability) : undefined + if (fallbackEntry) { + return redirectFor(capability, fallbackEntry, precedence.workspaceName, target.fallback.name) + } + } + return RUN } /** @@ -392,6 +415,8 @@ export function describeEngineTool(modelKey: string, base: string, precedence?: 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": diff --git a/packages/opencode/test/altimate/default-target.test.ts b/packages/opencode/test/altimate/default-target.test.ts index bf22526d4c..aadfd0bea6 100644 --- a/packages/opencode/test/altimate/default-target.test.ts +++ b/packages/opencode/test/altimate/default-target.test.ts @@ -58,6 +58,31 @@ describe("resolveDefaultTarget — registry branch", () => { }) }) +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 }) diff --git a/packages/opencode/test/altimate/workspace/precedence.test.ts b/packages/opencode/test/altimate/workspace/precedence.test.ts index 3cc21641a6..796cacefb7 100644 --- a/packages/opencode/test/altimate/workspace/precedence.test.ts +++ b/packages/opencode/test/altimate/workspace/precedence.test.ts @@ -21,6 +21,7 @@ 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. */ @@ -45,6 +46,7 @@ function bindTo(id = 42, name = "analytics") { 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 @@ -63,6 +65,35 @@ afterEach(() => { 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", () => { From 2ddcbfeff9185be6be4303f1d4a0e4d51c212973 Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Thu, 27 Aug 2026 01:17:39 +0800 Subject: [PATCH 06/30] fix(workspace): say why a dbt-fallback redirect happened and how to insist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A call with no `warehouse` can be redirected because the *fallback* target is served, not the target it would have tried first — and the dbt attempt behind it might well have succeeded. The generic message gave no way to tell those apart. The fallback redirect now names the connection it would have landed on, says plainly that which path it takes is only known once it runs, and gives both exits: name a warehouse, or keep everything local for the session. Marked `metadata.via = "dbt-fallback"` so the two cases stay distinguishable downstream. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RM9xasTNbk2k1eqhpF1Hp5 --- .../src/altimate/workspace/precedence.ts | 31 ++++++++++++++----- .../altimate/workspace/precedence.test.ts | 12 +++++++ 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/packages/opencode/src/altimate/workspace/precedence.ts b/packages/opencode/src/altimate/workspace/precedence.ts index 629b286194..a8006bea33 100644 --- a/packages/opencode/src/altimate/workspace/precedence.ts +++ b/packages/opencode/src/altimate/workspace/precedence.ts @@ -288,7 +288,16 @@ export interface Verdict { const RUN: Verdict = {} -function redirectFor(capability: Capability, entry: ShadowEntry, workspaceName: string, connection: string): Verdict { +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}`, @@ -302,12 +311,20 @@ function redirectFor(capability: Capability, entry: ShadowEntry, workspaceName: workspace: workspaceName, capability, connection, + ...(viaDbtFallback ? { via: "dbt-fallback" } : {}), }, - output: - `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\`.`, + 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\`.`, }, } } @@ -365,7 +382,7 @@ export async function check(sessionID: string, capability: Capability, warehouse const fallbackType = canonicalType(target.fallback.type) const fallbackEntry = fallbackType ? precedence.shadowed.get(fallbackType)?.get(capability) : undefined if (fallbackEntry) { - return redirectFor(capability, fallbackEntry, precedence.workspaceName, target.fallback.name) + return redirectFor(capability, fallbackEntry, precedence.workspaceName, target.fallback.name, true) } } return RUN diff --git a/packages/opencode/test/altimate/workspace/precedence.test.ts b/packages/opencode/test/altimate/workspace/precedence.test.ts index 796cacefb7..c01d023a64 100644 --- a/packages/opencode/test/altimate/workspace/precedence.test.ts +++ b/packages/opencode/test/altimate/workspace/precedence.test.ts @@ -249,6 +249,18 @@ describe("mechanism 4 — the redirect", () => { }) }) +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("mechanism 6 — the escape hatch", () => { test("--integrations=local turns shadowing off for the session", async () => { process.env.ALTIMATE_INTEGRATIONS = "local" From 139a12ec647a7d6e60201024030d9ba72ee0177d Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Thu, 27 Aug 2026 01:26:55 +0800 Subject: [PATCH 07/30] fix(workspace): let the safety checks outrank the redirect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A redirect returns early, so anything the guard was placed above stopped running. Two checks were being jumped over. `sql_execute`'s hard deny on DROP DATABASE / DROP SCHEMA / TRUNCATE says it "cannot be overridden", and the engine's execution tools apply no equivalent list. With the guard above it, a blocked statement against a served connection came back as an instruction to call the engine tool — a way around the block. The deny now runs first. Precedence still comes before the write prompt, because approving a write and then redirecting it asks the user to authorise something that never runs. `sql_explain`'s pre-flight validators turn malformed input into an actionable message. A redirect reads as success, so returning one first sent the model to the engine tool carrying the same bad arguments. The validators now run first. `schema_inspect` has no such pre-flight checks, so its guard stays where it is. Tests assert the ordering from both sides — the deny and the validators survive, and an ordinary call on the same connection is still redirected, so the fix cannot quietly over-correct into never redirecting. Both new sql_execute cases fail against the previous ordering. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RM9xasTNbk2k1eqhpF1Hp5 --- .../src/altimate/tools/sql-execute.ts | 20 ++- .../src/altimate/tools/sql-explain.ts | 14 +- .../altimate/precedence-guard-order.test.ts | 121 ++++++++++++++++++ 3 files changed, 143 insertions(+), 12 deletions(-) create mode 100644 packages/opencode/test/altimate/precedence-guard-order.test.ts diff --git a/packages/opencode/src/altimate/tools/sql-execute.ts b/packages/opencode/src/altimate/tools/sql-execute.ts index 8afadcdca3..413a12d692 100644 --- a/packages/opencode/src/altimate/tools/sql-execute.ts +++ b/packages/opencode/src/altimate/tools/sql-execute.ts @@ -25,19 +25,25 @@ export const SqlExecuteTool = Tool.define("sql_execute", { limit: z.number().optional().default(100).describe("Max rows to return"), }), async execute(args, ctx) { - // altimate_change start — workspace precedence. - // Ahead of the write-permission prompt on purpose: a shadowed write should be - // redirected, not approved and then redirected. - const precedence = await Precedence.check(ctx.sessionID, "sql_execute", args.warehouse) - if (precedence.redirect) return precedence.redirect - // altimate_change end - // altimate_change start - SQL write access control // Permission checks OUTSIDE try/catch so denial errors propagate to the framework const { queryType, blocked } = classifyAndCheck(args.query) if (blocked) { throw new Error("DROP DATABASE, DROP SCHEMA, and TRUNCATE are blocked for safety. This cannot be overridden.") } + // altimate_change end + + // altimate_change start — workspace precedence. + // Strictly AFTER the hard deny above and strictly BEFORE the write prompt below. + // The hard deny says "cannot be overridden", and the engine tools apply no such + // list — redirecting a blocked statement would hand the model a way around it. + // The write prompt is the opposite: approving a write and then redirecting it + // asks the user to authorise something that never runs. + const precedence = await Precedence.check(ctx.sessionID, "sql_execute", args.warehouse) + if (precedence.redirect) return precedence.redirect + // altimate_change end + + // altimate_change start - SQL write access control if (queryType === "write") { await ctx.ask({ permission: "sql_execute_write", diff --git a/packages/opencode/src/altimate/tools/sql-explain.ts b/packages/opencode/src/altimate/tools/sql-explain.ts index 2afcca8db0..9d1490b7dc 100644 --- a/packages/opencode/src/altimate/tools/sql-explain.ts +++ b/packages/opencode/src/altimate/tools/sql-explain.ts @@ -96,11 +96,6 @@ export const SqlExplainTool = Tool.define("sql_explain", { ), }), async execute(args, ctx) { - // altimate_change start — workspace precedence - const precedence = await Precedence.check(ctx.sessionID, "sql_explain", args.warehouse) - if (precedence.redirect) return precedence.redirect - // altimate_change end - // 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) @@ -132,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, 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..d9ea6ee7dd --- /dev/null +++ b/packages/opencode/test/altimate/precedence-guard-order.test.ts @@ -0,0 +1,121 @@ +// 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 { initTool } from "./tool-fixture" +import * as Registry from "../../src/altimate/native/connections/registry" +import { 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" + 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 is redirected rather than prompted for approval", async () => { + // Approving a write and then redirecting it asks the user to authorise something + // that never runs, so precedence still comes before the permission ask. + 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(result.metadata.redirected).toBe(true) + expect(asked).toHaveLength(0) + }) +}) + +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) + }) +}) From 5de772a2997f0fa59955393e2dc80074e4508292 Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Thu, 27 Aug 2026 01:37:59 +0800 Subject: [PATCH 08/30] fix(workspace): keep the write confirmation in front of a redirect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A write against a served connection was redirected before the `sql_execute_write` prompt, and the model's follow-up engine call is checked only under its `datamate_*` key — which the builder's `"*": "allow"` rule matches, while `sql_execute_write` is `"ask"`. So an INSERT, UPDATE or DELETE reached the warehouse without the confirmation the same statement needed a moment earlier. The guard now runs last, after every native safety check: hard deny, then write prompt, then precedence. An earlier draft put precedence first on the grounds that approving a write which then redirects asks the user to authorise something that never runs. That was wrong twice over — the write does run, through the engine, and what the user authorises is the write itself, not which connection carries it. Both checks guard something the other side has no equivalent for, which is why neither can sit behind the redirect. Tests assert the ordering from both ends: a write prompts and is then redirected, a denied write is never redirected, and a read is still redirected with no prompt at all. The two new cases fail against the previous ordering. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RM9xasTNbk2k1eqhpF1Hp5 --- .../src/altimate/tools/sql-execute.ts | 26 +++++++------- .../altimate/precedence-guard-order.test.ts | 34 +++++++++++++++++-- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/packages/opencode/src/altimate/tools/sql-execute.ts b/packages/opencode/src/altimate/tools/sql-execute.ts index 413a12d692..532fcf0f11 100644 --- a/packages/opencode/src/altimate/tools/sql-execute.ts +++ b/packages/opencode/src/altimate/tools/sql-execute.ts @@ -31,19 +31,6 @@ export const SqlExecuteTool = Tool.define("sql_execute", { if (blocked) { throw new Error("DROP DATABASE, DROP SCHEMA, and TRUNCATE are blocked for safety. This cannot be overridden.") } - // altimate_change end - - // altimate_change start — workspace precedence. - // Strictly AFTER the hard deny above and strictly BEFORE the write prompt below. - // The hard deny says "cannot be overridden", and the engine tools apply no such - // list — redirecting a blocked statement would hand the model a way around it. - // The write prompt is the opposite: approving a write and then redirecting it - // asks the user to authorise something that never runs. - const precedence = await Precedence.check(ctx.sessionID, "sql_execute", args.warehouse) - if (precedence.redirect) return precedence.redirect - // altimate_change end - - // altimate_change start - SQL write access control if (queryType === "write") { await ctx.ask({ permission: "sql_execute_write", @@ -54,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 diff --git a/packages/opencode/test/altimate/precedence-guard-order.test.ts b/packages/opencode/test/altimate/precedence-guard-order.test.ts index d9ea6ee7dd..603ca18e2b 100644 --- a/packages/opencode/test/altimate/precedence-guard-order.test.ts +++ b/packages/opencode/test/altimate/precedence-guard-order.test.ts @@ -84,17 +84,45 @@ describe("sql_execute — the hard deny outranks the redirect", () => { expect(result.metadata.redirected).toBe(true) }) - test("a write is redirected rather than prompted for approval", async () => { - // Approving a write and then redirecting it asks the user to authorise something - // that never runs, so precedence still comes before the permission ask. + 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) }) }) From 95b0e15b66272e337685da595d568863d8a0b61d Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Thu, 27 Aug 2026 02:38:48 +0800 Subject: [PATCH 09/30] fix(workspace): carry the fail-open notice onto the failure paths too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When precedence declines to route a call and lets it run locally, the result carries a one-line reason and a `precedence` marker so the decision is neither silent nor invisible to telemetry. Both were attached only to the successful return, so a call that then failed came back with neither. That is the wrong way round. An undetermined target usually means a misconfigured setup, so those calls are more likely than average to fail — the marker went missing in exactly the population it exists to measure, and the measured fail-open rate would read low in a way nothing would flag. Review named the three catch blocks. Auditing every exit after the guard found three more: `sql_explain`'s explicit failure return, and two early error returns in `schema_inspect`. `schema_inspect` now routes all of its failure exits through one local helper rather than three call sites, so overlooking one later is harder. The test reaches the undetermined verdict through the registry rather than dbt — a connection whose type no driver serves resolves as the default target and cannot be canonicalised — and asserts the marker on a real failure, not a mocked one. It fails against the unannotated version. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RM9xasTNbk2k1eqhpF1Hp5 --- .../src/altimate/tools/schema-inspect.ts | 10 ++-- .../src/altimate/tools/sql-execute.ts | 8 +++- .../src/altimate/tools/sql-explain.ts | 9 ++-- .../altimate/precedence-guard-order.test.ts | 48 ++++++++++++++++++- 4 files changed, 65 insertions(+), 10 deletions(-) diff --git a/packages/opencode/src/altimate/tools/schema-inspect.ts b/packages/opencode/src/altimate/tools/schema-inspect.ts index d82037f308..5f67fa554e 100644 --- a/packages/opencode/src/altimate/tools/schema-inspect.ts +++ b/packages/opencode/src/altimate/tools/schema-inspect.ts @@ -21,6 +21,10 @@ export const SchemaInspectTool = Tool.define("schema_inspect", { // 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", { @@ -30,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 @@ -60,7 +64,7 @@ export const SchemaInspectTool = Tool.define("schema_inspect", { }) } 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 532fcf0f11..3ab4061e80 100644 --- a/packages/opencode/src/altimate/tools/sql-execute.ts +++ b/packages/opencode/src/altimate/tools/sql-execute.ts @@ -112,11 +112,15 @@ export const SqlExecuteTool = Tool.define("sql_execute", { }) } 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 9d1490b7dc..73a1a401a3 100644 --- a/packages/opencode/src/altimate/tools/sql-explain.ts +++ b/packages/opencode/src/altimate/tools/sql-explain.ts @@ -145,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, @@ -154,7 +155,7 @@ export const SqlExplainTool = Tool.define("sql_explain", { error, }, output: `Failed to get execution plan: ${error}`, - } + }) } // altimate_change — attaches the fail-open notice when present; no-op otherwise. @@ -169,11 +170,11 @@ export const SqlExplainTool = Tool.define("sql_explain", { }) } 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/test/altimate/precedence-guard-order.test.ts b/packages/opencode/test/altimate/precedence-guard-order.test.ts index 603ca18e2b..d75746a4fa 100644 --- a/packages/opencode/test/altimate/precedence-guard-order.test.ts +++ b/packages/opencode/test/altimate/precedence-guard-order.test.ts @@ -15,9 +15,10 @@ 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 { precedenceInternals, refresh, resetForTests } from "../../src/altimate/workspace/precedence" +import { check, precedenceInternals, refresh, resetForTests } from "../../src/altimate/workspace/precedence" const SESSION = SessionID.make("ses_guard_order") const ORIGINAL_PILOT = process.env.ALTIMATE_WORKSPACE @@ -126,6 +127,51 @@ describe("sql_execute — the hard deny outranks the redirect", () => { }) }) +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) From e57a09d5197646fe166cde4ca40b8b1646a8d58f Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Thu, 27 Aug 2026 02:56:11 +0800 Subject: [PATCH 10/30] fix(workspace): weigh the dbt fallback before giving up on an unknown type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The undetermined verdict was returned before the fallback was examined, so a dbt target whose type cannot be identified failed open even when the registry connection behind it is served. `sql.execute` reaches that fallback whenever the dbt attempt yields nothing — an unrecognised result shape or a throw, not only an absent project — and an undetermined type is *more* likely to be the broken setup that yields nothing. The call then executed locally against exactly the connection precedence exists to route, which is the failure this design is for. This is the third defect in the order of these branches, so rather than reorder them again the decision is now a pure exported function with the order documented and its own suite. Reaching a dbt-sourced target through `check()` needs a real dbt project, which is why the order was never directly testable before and why each fix could reopen a sibling path. Branches, in order: the target's own type is served, then the fallback behind it, then an undetermined type runs locally and says so, then anything else runs. The new suite covers each, including an unserved dbt type with a served fallback, and the case that fails against the previous order. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RM9xasTNbk2k1eqhpF1Hp5 --- .../src/altimate/workspace/precedence.ts | 60 +++++++++++------ .../altimate/workspace/precedence.test.ts | 66 +++++++++++++++++++ 2 files changed, 107 insertions(+), 19 deletions(-) diff --git a/packages/opencode/src/altimate/workspace/precedence.ts b/packages/opencode/src/altimate/workspace/precedence.ts index a8006bea33..57094e1434 100644 --- a/packages/opencode/src/altimate/workspace/precedence.ts +++ b/packages/opencode/src/altimate/workspace/precedence.ts @@ -352,32 +352,43 @@ export async function check(sessionID: string, capability: Capability, warehouse // 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) - 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", - } - } - const entry = precedence.shadowed.get(type)?.get(capability) + const entry = type ? precedence.shadowed.get(type)?.get(capability) : undefined if (entry) { - const connection = target.source === "registry" ? target.name : "the dbt profile's target" + const connection = target.source === "registry" ? (target.name ?? "the default connection") : "the dbt profile's target" return redirectFor(capability, entry, precedence.workspaceName, connection) } - // The dbt target itself is not served — but `sql.execute` falls back to the first - // registry connection whenever the dbt attempt yields nothing, including on an - // unrecognised result shape or a throw. If that fallback is a served connection, - // letting the call proceed would execute locally against exactly what precedence - // exists to route. Redirect rather than gamble on dbt succeeding: a redirect is - // visible and recoverable (name a `warehouse`, or `--integrations=local`), whereas - // the silent local execution is the harm this design is for. + // 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 @@ -385,6 +396,17 @@ export async function check(sessionID: string, capability: Capability, warehouse 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 } diff --git a/packages/opencode/test/altimate/workspace/precedence.test.ts b/packages/opencode/test/altimate/workspace/precedence.test.ts index c01d023a64..567b16ea58 100644 --- a/packages/opencode/test/altimate/workspace/precedence.test.ts +++ b/packages/opencode/test/altimate/workspace/precedence.test.ts @@ -7,6 +7,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test" import { check, + decideForTarget, describeEngineTool, describeNativeTool, forSession, @@ -261,6 +262,71 @@ describe("the dbt-fallback redirect explains itself", () => { }) }) +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" From 8def7676f6dbc4a4688f6b1f4aaa5f7a1cfcb3d9 Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Thu, 27 Aug 2026 03:13:52 +0800 Subject: [PATCH 11/30] fix(workspace): never redirect a caller to a tool it may not call The analyst agent denies everything it does not name, and it names the native warehouse tools but never the engine keys. So on a served connection its permitted reads were redirected to a tool it is forbidden to call, taking away the one thing that agent exists to do. A redirect the caller cannot follow is the same dead end as a redirect to a tool that does not exist, which capability scoping already rules out. Shadowing now requires the destination to be reachable as well as materialised and attributed: the caller's effective rules are captured when precedence is derived, and a destination the rules deny means the call runs locally with a notice saying so. All three redirect sites are gated, not just the named-warehouse one. An absent ruleset means unknown and is treated as reachable, so nothing changes for callers whose permissions were never in question. Confirmed against the running system before and after: an analyst read on a served connection previously returned a redirect it could not act on, and now runs locally with the reason attached. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RM9xasTNbk2k1eqhpF1Hp5 --- .../src/altimate/workspace/precedence.ts | 39 +++++++++++++- packages/opencode/src/session/prompt.ts | 6 ++- packages/opencode/src/session/tools.ts | 4 +- .../altimate/workspace/precedence.test.ts | 52 +++++++++++++++++++ 4 files changed, 97 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/altimate/workspace/precedence.ts b/packages/opencode/src/altimate/workspace/precedence.ts index 57094e1434..9bc1f7310a 100644 --- a/packages/opencode/src/altimate/workspace/precedence.ts +++ b/packages/opencode/src/altimate/workspace/precedence.ts @@ -45,6 +45,7 @@ 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 { engineToolKeys, isEnabled, pinnedWorkspace, type ExistingEntry } from "./engine-sync" import { readLocalBinding } from "./state" @@ -106,6 +107,12 @@ export interface Precedence { 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 => ({ @@ -193,8 +200,13 @@ async function attributedTo(): Promise { * 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): Promise { +export async function refresh( + sessionID: string, + tools: Record, + ruleset?: PermissionNext.Ruleset, +): Promise { const result = await derive(tools) + if (ruleset) result.ruleset = ruleset bySession.set(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. @@ -288,6 +300,23 @@ export interface Verdict { 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" +} + +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, @@ -344,7 +373,9 @@ export async function check(sessionID: string, capability: Capability, warehouse const type = canonicalType(Registry.getConfig(warehouse)?.type) if (!type) return RUN const entry = precedence.shadowed.get(type)?.get(capability) - return entry ? redirectFor(capability, entry, precedence.workspaceName, warehouse) : RUN + 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. @@ -384,6 +415,7 @@ export function decideForTarget( 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) } @@ -393,6 +425,9 @@ export function decideForTarget( 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) } } diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index e7fc3efea0..cae221d027 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1768,7 +1768,11 @@ export namespace SessionPrompt { // 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) + 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( diff --git a/packages/opencode/src/session/tools.ts b/packages/opencode/src/session/tools.ts index 35005de950..bd50911027 100644 --- a/packages/opencode/src/session/tools.ts +++ b/packages/opencode/src/session/tools.ts @@ -82,7 +82,9 @@ 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)) + 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({ diff --git a/packages/opencode/test/altimate/workspace/precedence.test.ts b/packages/opencode/test/altimate/workspace/precedence.test.ts index 567b16ea58..3ecf24c086 100644 --- a/packages/opencode/test/altimate/workspace/precedence.test.ts +++ b/packages/opencode/test/altimate/workspace/precedence.test.ts @@ -262,6 +262,58 @@ describe("the dbt-fallback redirect explains itself", () => { }) }) +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("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 From 8588145838db6803a5ee2be4741ef176e08ec39e Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Thu, 27 Aug 2026 03:43:59 +0800 Subject: [PATCH 12/30] fix(workspace): report only the routing that will actually happen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The listing is what the model reads before choosing a tool, and it was telling some callers the opposite of what they would get. A caller denied the engine key already had its calls run locally, but `warehouse_list` still reported the connection as served by the workspace and counted it as shadowed — so an analyst was told to expect routing that its own permissions rule out. Saying nothing would have been better than that; saying the truth is better still. Every user-facing report — the listing note, the shadowed count, the inventory line and the native tool description — now goes through one helper that returns the capabilities which materialised AND whose destination this caller may actually call. A caller who can reach some but not all of them is reported per capability rather than all-or-nothing. Verified against the running system in both directions: an analyst now sees a plain listing and its read runs locally with the reason attached, while a caller with the permissions still sees the row marked and is still redirected. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RM9xasTNbk2k1eqhpF1Hp5 --- .../src/altimate/workspace/precedence.ts | 39 ++++++++++---- .../altimate/workspace/precedence.test.ts | 51 +++++++++++++++++++ 2 files changed, 81 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/altimate/workspace/precedence.ts b/packages/opencode/src/altimate/workspace/precedence.ts index 9bc1f7310a..98573dab70 100644 --- a/packages/opencode/src/altimate/workspace/precedence.ts +++ b/packages/opencode/src/altimate/workspace/precedence.ts @@ -308,6 +308,21 @@ function reachable(precedence: Precedence, modelKey: string): boolean { 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: @@ -469,6 +484,8 @@ export function describeNativeTool(toolID: string, base: string, precedence?: Pr if (!precedence?.enabled) return base const shadowed = (CAPABILITIES as string[]).includes(toolID) || toolID === "warehouse_list" if (!shadowed) return base + // Say nothing about redirection to a caller whose redirects will not happen. + if (![...precedence.shadowed.keys()].some((t) => servedFor(precedence, t).length > 0)) return base return ( `${base} Serves local connections; types served by workspace "${precedence.workspaceName}" ` + `redirect to that workspace's integration tools.` @@ -503,14 +520,17 @@ export function inventoryLine(precedence: Precedence): string { } } const parts: string[] = [] - for (const [type, byCapability] of precedence.shadowed) { - const served = CAPABILITIES.filter((c) => byCapability.has(c)).map((c) => c.replace(/^(sql|schema)_/, "")) - const local = CAPABILITIES.filter((c) => !byCapability.has(c)).map((c) => c.replace(/^(sql|schema)_/, "")) + 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}: ${served.join("/")} via workspace ${precedence.workspaceName}` + + `${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.` } @@ -519,7 +539,7 @@ function countShadowedConnections(precedence: Precedence): number { try { return Registry.list().warehouses.filter((w) => { const type = canonicalType(w.type) - return !!type && precedence.shadowed.has(type) + return !!type && servedFor(precedence, type).length > 0 }).length } catch { return 0 @@ -531,10 +551,11 @@ export function warehouseListNote(precedence: Precedence | undefined, warehouseT if (!precedence?.enabled) return null const type = canonicalType(warehouseType) if (!type) return null - const byCapability = precedence.shadowed.get(type) - if (!byCapability) return null - const served = CAPABILITIES.filter((c) => byCapability.has(c)).map((c) => c.replace(/^(sql|schema)_/, "")) - const local = CAPABILITIES.filter((c) => !byCapability.has(c)).map((c) => c.replace(/^(sql|schema)_/, "")) + 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/test/altimate/workspace/precedence.test.ts b/packages/opencode/test/altimate/workspace/precedence.test.ts index 3ecf24c086..0206d8d578 100644 --- a/packages/opencode/test/altimate/workspace/precedence.test.ts +++ b/packages/opencode/test/altimate/workspace/precedence.test.ts @@ -314,6 +314,57 @@ describe("a redirect the caller cannot follow is not a redirect", () => { }) }) +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 marks nothing when the caller cannot reach the engine", async () => { + const p = await refresh(SESSION, SNOWFLAKE_TOOLS, analystLike) + expect(warehouseListNote(p, "snowflake")).toBeNull() + }) + + test("...and the routing decision agrees with it", async () => { + await refresh(SESSION, SNOWFLAKE_TOOLS, analystLike) + const verdict = await check(SESSION, "sql_execute", "local_snow") + expect(verdict.redirect).toBeUndefined() + }) + + 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("the inventory line says nothing rather than something false", async () => { + const p = await refresh(SESSION, SNOWFLAKE_TOOLS, analystLike) + expect(inventoryLine(p)).toBe("") + }) + + test("the native tool description makes no redirect claim either", async () => { + const p = await refresh(SESSION, SNOWFLAKE_TOOLS, analystLike) + expect(describeNativeTool("sql_execute", "Execute SQL.", p)).toBe("Execute SQL.") + }) + + 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("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 From a75f90df8db85404ee38a0a3a06239e03960033b Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Thu, 27 Aug 2026 04:00:57 +0800 Subject: [PATCH 13/30] fix(workspace): attribute the engine that is running, not the one on disk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Attribution read the saved MCP entry. 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 claims this workspace while the running engine serves another. Precedence would then route warehouse calls into someone else's engine, which is the mis-routing this design exists to prevent. The attach outcome is the runtime-grounded signal: "attached" means a pinned engine was spawned, "reused" means attach verified one before adopting it. Attribution now requires both that signal and the configured pin, because the outcome alone would not notice a later rewrite naming a different workspace, and the pin alone is what can go stale under a live connection. Also bound the two per-session maps. A long-running server sees an unbounded number of session ids and each entry holds a merged permission ruleset, so they grew with lifetime session count. Same cap and insertion-ordered eviction as the attach module; a dropped entry is simply re-derived next turn. Verified against the running system, since a wrong reading here would silently disable routing everywhere rather than fail loudly: the listing still marks the served connection and the redirect still fires. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RM9xasTNbk2k1eqhpF1Hp5 --- .../src/altimate/workspace/precedence.ts | 60 +++++++++++++++++-- .../altimate/precedence-guard-order.test.ts | 3 + .../altimate/workspace/precedence.test.ts | 50 ++++++++++++++++ 3 files changed, 109 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/altimate/workspace/precedence.ts b/packages/opencode/src/altimate/workspace/precedence.ts index 98573dab70..3c4ab2294d 100644 --- a/packages/opencode/src/altimate/workspace/precedence.ts +++ b/packages/opencode/src/altimate/workspace/precedence.ts @@ -47,7 +47,7 @@ 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 { engineToolKeys, isEnabled, pinnedWorkspace, type ExistingEntry } from "./engine-sync" +import { engineToolKeys, ensure, isEnabled, pinnedWorkspace, type ExistingEntry, type Outcome } from "./engine-sync" import { readLocalBinding } from "./state" import { canonicalType } from "../native/connections/registry" import * as Registry from "../native/connections/registry" @@ -130,9 +130,48 @@ const bySession = new Map() 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) + } +} + +/** + * Did an attach actually produce the engine now serving this session? The saved + * config is not sufficient on its own: an entry can be rewritten — by an IDE, say — + * from unpinned to pinned while MCP goes on serving the process it already connected, + * so the config would claim this workspace while the running engine serves another. + * The attach outcome is the runtime-grounded signal, because `attached` means we + * spawned a pinned engine and `reused` means attach verified one before adopting it. + */ +async function attested(sessionID: string): Promise { + try { + const outcome = precedenceInternals.attachOutcome + ? await precedenceInternals.attachOutcome() + : await ensure(sessionID) + return outcome.kind === "attached" || outcome.kind === "reused" + } catch (err) { + log.warn("could not establish the attach outcome", { err: String(err) }) + return false + } +} + /** 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. */ const announced = new Set() @@ -205,9 +244,9 @@ export async function refresh( tools: Record, ruleset?: PermissionNext.Ruleset, ): Promise { - const result = await derive(tools) + const result = await derive(sessionID, tools) if (ruleset) result.ruleset = ruleset - bySession.set(sessionID, result) + 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. if (!announced.has(sessionID)) { @@ -220,7 +259,7 @@ export async function refresh( return result } -async function derive(tools: Record): Promise { +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 @@ -234,6 +273,14 @@ async function derive(tools: Record): Promise { 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() if (pinned === null || pinned !== String(binding.datamateId)) { log.info("engine not attributable to the bound workspace; precedence off", { @@ -274,6 +321,11 @@ 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 +} + export function resetForTests(): void { bySession.clear() announced.clear() diff --git a/packages/opencode/test/altimate/precedence-guard-order.test.ts b/packages/opencode/test/altimate/precedence-guard-order.test.ts index d75746a4fa..392b1e07d1 100644 --- a/packages/opencode/test/altimate/precedence-guard-order.test.ts +++ b/packages/opencode/test/altimate/precedence-guard-order.test.ts @@ -47,6 +47,9 @@ beforeEach(async () => { 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, diff --git a/packages/opencode/test/altimate/workspace/precedence.test.ts b/packages/opencode/test/altimate/workspace/precedence.test.ts index 0206d8d578..57e7097c91 100644 --- a/packages/opencode/test/altimate/workspace/precedence.test.ts +++ b/packages/opencode/test/altimate/workspace/precedence.test.ts @@ -6,8 +6,10 @@ // 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, describeEngineTool, describeNativeTool, forSession, @@ -42,6 +44,7 @@ const BIGQUERY_TOOLS = { 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(() => { @@ -124,6 +127,53 @@ describe("mechanism 1 — materialised, not declared", () => { }) }) +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("a session with no established attach confers no precedence", async () => { + precedenceInternals.attachOutcome = async () => ({ kind: "engine-missing", declared: 12 }) + const p = await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(p.enabled).toBe(false) + expect(p.disabledReason).toBe("unattributed") + }) + + test("a superseded attach confers no precedence", async () => { + // Superseded means the binding moved while the attach was in flight, so whatever + // is connected was established for a workspace this project has left. + precedenceInternals.attachOutcome = async () => ({ kind: "entry-disabled" }) + const p = await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(p.enabled).toBe(false) + }) + + test("a reused engine counts as established, because attach verified it first", async () => { + precedenceInternals.attachOutcome = async () => ({ kind: "reused", available: 12 }) + const p = await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(p.enabled).toBe(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() + }) +}) + 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" }) From fd1f36c734d75375222dab59c589ed2f72f8c52a Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Thu, 27 Aug 2026 04:07:36 +0800 Subject: [PATCH 14/30] test(workspace): actually exercise a superseded attach MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The test named for a superseded attach was constructing a disabled-entry outcome, so it asserted something true under a name claiming something else and the superseded case was never covered. That variant matters here: it means the binding moved while the attach was in flight, so whatever is connected was established for a workspace this project has already left. Also replaced the sample of refused outcomes with the invariant — every variant except attached and reused must refuse — so a future Outcome variant defaults to refusing rather than silently qualifying for routing. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RM9xasTNbk2k1eqhpF1Hp5 --- .../altimate/workspace/precedence.test.ts | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/opencode/test/altimate/workspace/precedence.test.ts b/packages/opencode/test/altimate/workspace/precedence.test.ts index 57e7097c91..f9467a86d2 100644 --- a/packages/opencode/test/altimate/workspace/precedence.test.ts +++ b/packages/opencode/test/altimate/workspace/precedence.test.ts @@ -142,12 +142,41 @@ describe("attribution is grounded in the attach, not only the saved config", () test("a superseded attach confers no precedence", async () => { // Superseded means the binding moved while the attach was in flight, so whatever - // is connected was established for a workspace this project has left. + // is connected was established for a workspace this project has already left. + precedenceInternals.attachOutcome = async () => ({ kind: "superseded" }) + const p = await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(p.enabled).toBe(false) + expect(p.disabledReason).toBe("unattributed") + }) + + test("a disabled entry confers no precedence either", async () => { precedenceInternals.attachOutcome = async () => ({ kind: "entry-disabled" }) const p = await refresh(SESSION, SNOWFLAKE_TOOLS) expect(p.enabled).toBe(false) }) + 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, reused}, so a new Outcome variant defaults to refusing rather than + // silently qualifying. + const refused: Array<{ kind: string }> = [ + { kind: "disabled" }, + { kind: "unbound" }, + { kind: "engine-missing" }, + { kind: "engine-too-old" }, + { kind: "connect-failed" }, + { kind: "entry-disabled" }, + { kind: "superseded" }, + ] + for (const outcome of refused) { + 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: false }) + } + }) + test("a reused engine counts as established, because attach verified it first", async () => { precedenceInternals.attachOutcome = async () => ({ kind: "reused", available: 12 }) const p = await refresh(SESSION, SNOWFLAKE_TOOLS) From 8740567814b5960e6df1267445662e604d229915 Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Thu, 27 Aug 2026 04:39:47 +0800 Subject: [PATCH 15/30] fix(workspace): attest the running engine without waiting on the attach MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Attribution rested on the saved MCP entry. That entry can be rewritten — by an IDE — from unpinned to pinned while MCP goes on serving the process it already connected, so the config names this workspace while the running engine serves another, and precedence would route warehouse calls into someone else's engine. The attach outcome settles it: "attached" means a pinned engine was spawned, "reused" means attach verified one before adopting it. Both signals are now required, because the outcome alone would not notice a later rewrite naming a different workspace and the pin alone is what goes stale under a live connection. It is read through the attach module's read-only accessor rather than the attach task. An earlier attempt awaited the task, which is deliberately uncapped: the prompt loop bounds its own wait and lets a turn proceed without engine tools past the cap, so awaiting it hung every affected turn for the full connection timeout. Polling the task would also have re-registered the session entry once per turn. "Not known yet" and "never attached" are indistinguishable through that accessor, and both fail open: precedence stays off and the call runs locally with a notice. Wrong in that direction costs a turn's routing, which the next turn repairs; wrong the other way routes credentials into the wrong engine. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RM9xasTNbk2k1eqhpF1Hp5 --- .../src/altimate/workspace/precedence.ts | 40 +++++++++++-------- .../altimate/workspace/precedence.test.ts | 30 ++++++++++++++ 2 files changed, 54 insertions(+), 16 deletions(-) diff --git a/packages/opencode/src/altimate/workspace/precedence.ts b/packages/opencode/src/altimate/workspace/precedence.ts index 3c4ab2294d..11a219f076 100644 --- a/packages/opencode/src/altimate/workspace/precedence.ts +++ b/packages/opencode/src/altimate/workspace/precedence.ts @@ -47,7 +47,7 @@ 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 { engineToolKeys, ensure, isEnabled, pinnedWorkspace, type ExistingEntry, type Outcome } from "./engine-sync" +import { engineToolKeys, isEnabled, pinnedWorkspace, settledOutcome, type ExistingEntry, type Outcome } from "./engine-sync" import { readLocalBinding } from "./state" import { canonicalType } from "../native/connections/registry" import * as Registry from "../native/connections/registry" @@ -130,7 +130,7 @@ const bySession = new Map() export const precedenceInternals: { binding?: () => Promise<{ datamateId: number; datamateName: string } | null> attributedTo?: () => Promise - attachOutcome?: () => Promise + attachOutcome?: () => Promise announce?: (line: string) => Promise } = {} @@ -153,25 +153,33 @@ function remember(sessionID: string, value: Precedence): void { } /** - * Did an attach actually produce the engine now serving this session? The saved - * config is not sufficient on its own: an entry can be rewritten — by an IDE, say — + * 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 claim this workspace while the running engine serves another. - * The attach outcome is the runtime-grounded signal, because `attached` means we - * spawned a pinned engine and `reused` means attach verified one before adopting it. + * so the config would name this workspace while the running engine serves another. + * The attach outcome is the runtime-grounded signal: `attached` means a pinned engine + * was spawned, `reused` means attach verified one before adopting it. + * + * 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 { - try { - const outcome = precedenceInternals.attachOutcome - ? await precedenceInternals.attachOutcome() - : await ensure(sessionID) - return outcome.kind === "attached" || outcome.kind === "reused" - } catch (err) { - log.warn("could not establish the attach outcome", { err: String(err) }) - return false - } + const outcome = precedenceInternals.attachOutcome + ? await precedenceInternals.attachOutcome().catch(() => undefined) + : settledOutcome(sessionID) + if (!outcome) return false + return outcome.kind === "attached" || outcome.kind === "reused" } + /** 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. */ const announced = new Set() diff --git a/packages/opencode/test/altimate/workspace/precedence.test.ts b/packages/opencode/test/altimate/workspace/precedence.test.ts index f9467a86d2..47ff5eb6a0 100644 --- a/packages/opencode/test/altimate/workspace/precedence.test.ts +++ b/packages/opencode/test/altimate/workspace/precedence.test.ts @@ -155,6 +155,36 @@ describe("attribution is grounded in the attach, not only the saved config", () expect(p.enabled).toBe(false) }) + 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("a settled outcome is still read, so the common case is unaffected", async () => { + precedenceInternals.attachOutcome = async () => ({ kind: "attached", available: 12, declared: 12, missing: [] }) + const p = await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(p.enabled).toBe(true) + }) + 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, reused}, so a new Outcome variant defaults to refusing rather than From a06841c5c6a0a433f429fa736553f61c9e38a1e6 Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Thu, 27 Aug 2026 04:50:19 +0800 Subject: [PATCH 16/30] fix(workspace): describe each tool by its own capability, and correct a stale inventory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two ways the model was told something the code would not do. A tool's description claimed redirection whenever the workspace served any capability for that warehouse type. An integration providing execute alone — bigquery, postgresql, databricks — leaves explain and inspect running locally, so those two were being described as redirecting while their bodies correctly stayed local. Worse than inaccurate: it steers the model away from the local tool that does work. Each tool now claims redirection only for its own capability, while the listing keeps its whole-listing note. The inventory was announced once per session. The first turn can legitimately announce that routing is off — an attach outrunning its bounded wait is indistinguishable from no engine — and precedence is deliberately re-derived every turn, so the truth changes under a session that has already been told. The last announced line is now remembered and compared, so a correction is delivered and an unchanged inventory still stays quiet. Both are the same class as an earlier fix: when a decision gains a condition, the code that describes it inherits the condition too. This time at capability granularity rather than type. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RM9xasTNbk2k1eqhpF1Hp5 --- .../src/altimate/workspace/precedence.ts | 33 +++++++---- .../altimate/workspace/precedence.test.ts | 58 +++++++++++++++++++ 2 files changed, 80 insertions(+), 11 deletions(-) diff --git a/packages/opencode/src/altimate/workspace/precedence.ts b/packages/opencode/src/altimate/workspace/precedence.ts index 11a219f076..dcc3015c82 100644 --- a/packages/opencode/src/altimate/workspace/precedence.ts +++ b/packages/opencode/src/altimate/workspace/precedence.ts @@ -182,7 +182,12 @@ async function attested(sessionID: string): Promise { /** 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. */ -const announced = new Set() +/** 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. */ +const announced = new Map() async function announce(line: string): Promise { if (precedenceInternals.announce) return precedenceInternals.announce(line) @@ -257,12 +262,10 @@ export async function refresh( 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. - if (!announced.has(sessionID)) { - const line = inventoryLine(result) - if (line) { - announced.add(sessionID) - void announce(line).catch(() => {}) - } + const line = inventoryLine(result) + if (line && announced.get(sessionID) !== line) { + announced.set(sessionID, line) + void announce(line).catch(() => {}) } return result } @@ -542,10 +545,18 @@ export function annotate; output? */ export function describeNativeTool(toolID: string, base: string, precedence?: Precedence): string { if (!precedence?.enabled) return base - const shadowed = (CAPABILITIES as string[]).includes(toolID) || toolID === "warehouse_list" - if (!shadowed) return base - // Say nothing about redirection to a caller whose redirects will not happen. - if (![...precedence.shadowed.keys()].some((t) => servedFor(precedence, t).length > 0)) 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.` diff --git a/packages/opencode/test/altimate/workspace/precedence.test.ts b/packages/opencode/test/altimate/workspace/precedence.test.ts index 47ff5eb6a0..7d20df5b75 100644 --- a/packages/opencode/test/altimate/workspace/precedence.test.ts +++ b/packages/opencode/test/altimate/workspace/precedence.test.ts @@ -474,6 +474,64 @@ describe("reporting never claims a routing that will not happen", () => { }) }) +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("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("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 From 7461752aa20e53e42f90f19dcced6207123f5d3f Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Thu, 27 Aug 2026 05:10:26 +0800 Subject: [PATCH 17/30] fix(workspace): confirm the pin against disk before it enables routing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Config.get()` is cached per instance, and an IDE rewriting the engine entry writes straight to disk without going through it — so a cached pin can outlive the entry it describes. Staleness is dangerous in one direction only. A stale "pinned to us" helps enable routing; a stale "pinned elsewhere" merely refuses, which is the safe way to be wrong. So the cache is invalidated and re-read only when the cached answer is about to enable, leaving the refusing path cheap instead of re-reading all config on every turn. The residual is stated in the design doc rather than implied: an IDE write still races a read that already happened this turn, and the attach-outcome half of attribution is what stops that from routing anywhere. The pin is the secondary signal, not the load-bearing one. Found by a sibling branch hitting the same class twice — a read of config after another component wrote it — rather than by review of this code. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RM9xasTNbk2k1eqhpF1Hp5 --- .../src/altimate/workspace/precedence.ts | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/altimate/workspace/precedence.ts b/packages/opencode/src/altimate/workspace/precedence.ts index dcc3015c82..6b8f5ccdcb 100644 --- a/packages/opencode/src/altimate/workspace/precedence.ts +++ b/packages/opencode/src/altimate/workspace/precedence.ts @@ -231,9 +231,9 @@ async function currentBinding(): Promise<{ datamateId: number; datamateName: str * 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(): Promise { +async function attributedTo(expected: string): Promise { if (precedenceInternals.attributedTo) return precedenceInternals.attributedTo() - try { + const read = async (): Promise => { const cfg = (await Config.get()) as { mcp?: Record } const entry = cfg.mcp?.[DATAMATE_KEY] if (!entry) return null @@ -242,6 +242,21 @@ async function attributedTo(): Promise { // 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 @@ -292,7 +307,7 @@ async function derive(sessionID: string, tools: Record): Promis log.info("no attach established this session's engine; precedence off", { bound: binding.datamateId }) return EMPTY("unattributed", workspaceName) } - const pinned = await attributedTo() + 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, From 99bdf2d42fb5bbb4b8da43ff6ab9d8e5badd0cd4 Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Thu, 27 Aug 2026 05:11:46 +0800 Subject: [PATCH 18/30] docs(workspace): stop crediting a notification that was not published The module comment said precedence stayed current because the tool cache is invalidated by tools/list_changed. That notification was never actually sent until late in the attach work, so for most of this branch it explained nothing. What kept precedence correct is the per-turn re-derivation from the materialised map. The notification is real now and makes the next re-derivation see a change sooner, which is a live trigger rather than the mechanism. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RM9xasTNbk2k1eqhpF1Hp5 --- packages/opencode/src/altimate/workspace/precedence.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/altimate/workspace/precedence.ts b/packages/opencode/src/altimate/workspace/precedence.ts index 6b8f5ccdcb..e31028b869 100644 --- a/packages/opencode/src/altimate/workspace/precedence.ts +++ b/packages/opencode/src/altimate/workspace/precedence.ts @@ -36,8 +36,9 @@ // 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 -// cache-invalidated by the `tools/list_changed` notification, and `resolveTools` runs -// once per turn. +// 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" From 5de539e64596ffb9d7fc31848b12938de8f76dd2 Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Thu, 27 Aug 2026 05:23:44 +0800 Subject: [PATCH 19/30] fix(workspace): do not let a routing decision outlive its binding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two ways a stale or absent decision could route a call wrongly. Re-linking mid-session is supported, so the turn's snapshot can name a workspace the project has already left. A redirect naming it would send the query to that workspace's engine, with its credentials — the mis-routing this design exists to prevent, arriving through the passage of time rather than a bad read. The binding is now re-checked before any redirect; it is a local cache read, and only the path about to redirect pays for it. A session with no snapshot returned a bare "run". That is indistinguishable from a considered "this connection is not served", so an evicted entry — possible between tool resolution and the call — would let a shadowed connection execute locally with no indication. Unknown now runs locally and says so, like every other undetermined case here. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RM9xasTNbk2k1eqhpF1Hp5 --- .../src/altimate/workspace/precedence.ts | 31 +++++++++++++++-- .../altimate/workspace/precedence.test.ts | 33 +++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/altimate/workspace/precedence.ts b/packages/opencode/src/altimate/workspace/precedence.ts index e31028b869..d41200026f 100644 --- a/packages/opencode/src/altimate/workspace/precedence.ts +++ b/packages/opencode/src/altimate/workspace/precedence.ts @@ -101,6 +101,9 @@ export interface ShadowEntry { 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 @@ -340,7 +343,7 @@ async function derive(sessionID: string, tools: Record): Promis } } if (shadowed.size === 0) return EMPTY("nothing-materialised", workspaceName) - return { workspaceName, enabled: true, shadowed } + return { workspaceName, workspaceId: String(binding.datamateId), enabled: true, shadowed } } /** Read the session's precedence without recomputing it. */ @@ -461,7 +464,31 @@ function redirectFor( */ export async function check(sessionID: string, capability: Capability, warehouse?: string): Promise { const precedence = bySession.get(sessionID) - if (!precedence || !precedence.enabled) return RUN + 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) diff --git a/packages/opencode/test/altimate/workspace/precedence.test.ts b/packages/opencode/test/altimate/workspace/precedence.test.ts index 7d20df5b75..68850400c5 100644 --- a/packages/opencode/test/altimate/workspace/precedence.test.ts +++ b/packages/opencode/test/altimate/workspace/precedence.test.ts @@ -532,6 +532,37 @@ describe("descriptions are per capability, and corrections are delivered", () => }) }) +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 @@ -727,5 +758,7 @@ describe("re-derivation", () => { 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") }) }) From 2a017abac2ae71967f2a12a8f67e334d970b8f39 Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Thu, 27 Aug 2026 05:33:26 +0800 Subject: [PATCH 20/30] fix(workspace): say when routing stops, not only when it changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Comparing the last announced inventory catches a capability appearing or disappearing, but not routing ending altogether: with nothing left to enumerate the line is empty, and an empty line was treated as nothing to say. So a session told its calls were routed through the workspace kept believing that after the engine stopped serving anything, while the calls ran locally. That transition is the one a user most needs. It is now stated explicitly, and only to a session that had previously been told otherwise — a session that never had routing is still told nothing, because for it nothing changed. Third finding in the same class: the code that describes a decision keeps asserting what used to be true. Twice that was a stale claim; this time it was silence, which is the same failure wearing different clothes. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RM9xasTNbk2k1eqhpF1Hp5 --- .../src/altimate/workspace/precedence.ts | 16 ++++++++-- .../altimate/workspace/precedence.test.ts | 30 +++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/altimate/workspace/precedence.ts b/packages/opencode/src/altimate/workspace/precedence.ts index d41200026f..34030d8355 100644 --- a/packages/opencode/src/altimate/workspace/precedence.ts +++ b/packages/opencode/src/altimate/workspace/precedence.ts @@ -193,6 +193,12 @@ async function attested(sessionID: string): Promise { * the line means a correction is delivered and an unchanged one stays quiet. */ const announced = 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." + async function announce(line: string): Promise { if (precedenceInternals.announce) return precedenceInternals.announce(line) try { @@ -281,8 +287,14 @@ export async function refresh( 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. - const line = inventoryLine(result) - if (line && announced.get(sessionID) !== line) { + // 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 previous = announced.get(sessionID) + const line = inventoryLine(result) || (previous ? STOPPED_ROUTING : "") + if (line && previous !== line) { announced.set(sessionID, line) void announce(line).catch(() => {}) } diff --git a/packages/opencode/test/altimate/workspace/precedence.test.ts b/packages/opencode/test/altimate/workspace/precedence.test.ts index 68850400c5..5abb0a4052 100644 --- a/packages/opencode/test/altimate/workspace/precedence.test.ts +++ b/packages/opencode/test/altimate/workspace/precedence.test.ts @@ -522,6 +522,36 @@ describe("descriptions are per capability, and corrections are delivered", () => 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("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) From 57d67b3dd93336d40c9d9f3e439c2042001de927 Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Thu, 27 Aug 2026 05:52:47 +0800 Subject: [PATCH 21/30] fix(workspace): only tell a session routing stopped if it was routing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `refresh()` remembered the last announcement as a bare string, so any non-empty prior line counted as "was routing". An announcement can be non-routing — "shadowing off, the engine could not be attributed" is a statement about a failed attribution, not about served capabilities. A session that got that line first and later refreshed to an attributed engine exposing no warehouse tools was told routing had "stopped", when it had never started. Track the announcement alongside whether it described actual routing, and gate the stopped-routing line on that. Found by automated review. --- .../src/altimate/workspace/precedence.ts | 15 +++++++++++---- .../test/altimate/workspace/precedence.test.ts | 17 +++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/altimate/workspace/precedence.ts b/packages/opencode/src/altimate/workspace/precedence.ts index 34030d8355..0b79b9f9fc 100644 --- a/packages/opencode/src/altimate/workspace/precedence.ts +++ b/packages/opencode/src/altimate/workspace/precedence.ts @@ -191,7 +191,11 @@ async function attested(sessionID: string): Promise { * 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. */ -const announced = new Map() +/** What each session was last 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. */ +const announced = 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: @@ -293,9 +297,12 @@ export async function refresh( // alone can never announce it, so they would go on believing calls are routed while // they run locally. const previous = announced.get(sessionID) - const line = inventoryLine(result) || (previous ? STOPPED_ROUTING : "") - if (line && previous !== line) { - announced.set(sessionID, line) + const current = inventoryLine(result) + const routed = result.enabled && current !== "" + // Only a session that was actually routing can be told routing has stopped. + const line = current || (previous?.routed ? STOPPED_ROUTING : "") + if (line && previous?.line !== line) { + announced.set(sessionID, { line, routed }) void announce(line).catch(() => {}) } return result diff --git a/packages/opencode/test/altimate/workspace/precedence.test.ts b/packages/opencode/test/altimate/workspace/precedence.test.ts index 5abb0a4052..5647242f51 100644 --- a/packages/opencode/test/altimate/workspace/precedence.test.ts +++ b/packages/opencode/test/altimate/workspace/precedence.test.ts @@ -535,6 +535,23 @@ describe("descriptions are per capability, and corrections are delivered", () => 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 session that never had routing is still told nothing", async () => { const lines: string[] = [] precedenceInternals.announce = async (line) => void lines.push(line) From 73034ac942f824de6c547a15b4857d33e345d0b5 Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Thu, 27 Aug 2026 06:02:04 +0800 Subject: [PATCH 22/30] fix(connections): pin the fallback connection before the dbt attempt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `sql.execute` resolved its default connection twice: once when the caller's routing decision was made, and again after `tryExecuteViaDbt` had awaited. The connection registry is a process-wide mutable singleton, so a `warehouse.add`/`remove` landing during that await changed which connection the call fell back to — after the decision had been made against the old one. Read the registry once, before the await. The decided and executed connections are now the same by construction, and a call whose connection disappears mid-flight reports that connection by name instead of quietly running somewhere else. The dbt-first ordering is unchanged. Found by automated review. --- .../altimate/native/connections/register.ts | 30 ++++++++--------- .../test/altimate/default-target.test.ts | 33 +++++++++++++++++++ 2 files changed, 48 insertions(+), 15 deletions(-) diff --git a/packages/opencode/src/altimate/native/connections/register.ts b/packages/opencode/src/altimate/native/connections/register.ts index ab18acda15..feca6f83c7 100644 --- a/packages/opencode/src/altimate/native/connections/register.ts +++ b/packages/opencode/src/altimate/native/connections/register.ts @@ -467,6 +467,15 @@ register("sql.execute", async (params: SqlExecuteParams): Promise { 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/) + }) +}) From 3468a36da10fd5f60a982205bf89ac158cbcefac Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Thu, 27 Aug 2026 06:13:07 +0800 Subject: [PATCH 23/30] fix(workspace): do not remember an announcement that never arrived MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inventory line was recorded as said before it was published, and `announce` swallowed its own failure, so the call site could not tell a delivered line from a dropped one. A toast lost to a briefly unavailable event bridge was therefore remembered as delivered — and every later turn with the same inventory skipped it as unchanged, so the session was never told what its calls were doing. `announce` now reports whether the line reached the session, and a failed delivery restores the previous record so the next turn retries. The record is still written before publishing, so a second refresh in the same window does not send the line twice; restoring the previous value rather than clearing it keeps whether the session had been routing, which a later "routing stopped" depends on. Found by automated review. --- .../src/altimate/workspace/precedence.ts | 42 +++++++++++------ .../altimate/workspace/precedence.test.ts | 45 +++++++++++++++++++ 2 files changed, 74 insertions(+), 13 deletions(-) diff --git a/packages/opencode/src/altimate/workspace/precedence.ts b/packages/opencode/src/altimate/workspace/precedence.ts index 0b79b9f9fc..f61cddcd2b 100644 --- a/packages/opencode/src/altimate/workspace/precedence.ts +++ b/packages/opencode/src/altimate/workspace/precedence.ts @@ -203,21 +203,27 @@ const announced = new Map() const STOPPED_ROUTING = "Workspace integrations: nothing is served by the workspace any more; every connection now runs on the local drivers." -async function announce(line: string): Promise { - if (precedenceInternals.announce) return precedenceInternals.announce(line) +/** 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 { - await AppRuntime.runPromise( - EventV2Bridge.Service.use((events) => - events.publish(TuiEvent.ToastShow, { - title: "Workspace integrations", - message: line, - variant: "info", - duration: 10000, - }), - ), - ) + 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 } } @@ -302,8 +308,18 @@ export async function refresh( // Only a session that was actually routing can be told routing has stopped. const line = current || (previous?.routed ? STOPPED_ROUTING : "") if (line && previous?.line !== line) { + // Record before publishing so a second refresh landing in the same window does not + // send the line twice, then put the record back if delivery failed. The bridge can + // be briefly unavailable, and that failure is recoverable — but only if the line is + // not remembered as delivered, since a later turn with the same inventory would + // otherwise skip it as unchanged and the session would never hear it. announced.set(sessionID, { line, routed }) - void announce(line).catch(() => {}) + void announce(line).then((delivered) => { + if (delivered) return + if (announced.get(sessionID)?.line !== line) return + if (previous) announced.set(sessionID, previous) + else announced.delete(sessionID) + }) } return result } diff --git a/packages/opencode/test/altimate/workspace/precedence.test.ts b/packages/opencode/test/altimate/workspace/precedence.test.ts index 5647242f51..0627ef54a7 100644 --- a/packages/opencode/test/altimate/workspace/precedence.test.ts +++ b/packages/opencode/test/altimate/workspace/precedence.test.ts @@ -552,6 +552,51 @@ describe("descriptions are per capability, and corrections are delivered", () => 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("a session that never had routing is still told nothing", async () => { const lines: string[] = [] precedenceInternals.announce = async (line) => void lines.push(line) From 90e4877933094e531639f801bd04434cc5034d38 Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Thu, 27 Aug 2026 06:22:39 +0800 Subject: [PATCH 24/30] fix(workspace): treat a line as said only once it has arrived MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous fix rolled a failed publication back to the record it had replaced — but that record was itself optimistic. Precedence is re-derived every turn, so two refreshes can publish different lines before either settles; when both failed, the second's rollback reinstated the first's undelivered line as delivered, and a later turn returning to that inventory skipped it as already said. The disclosure was lost for good. Confirmed deliveries and in-flight attempts are now separate. Nothing reaches the delivered map until the line actually arrives, so a failure leaves the session's known state untouched and the next turn retries. The in-flight record exists only to stop the same line being sent twice in one window, and each attempt is its own object, so identity settles which publication is current — one that finishes after a newer one started finds its attempt gone and records nothing. Found by automated review. --- .../src/altimate/workspace/precedence.ts | 43 +++++++++------ .../altimate/workspace/precedence.test.ts | 53 +++++++++++++++++++ 2 files changed, 81 insertions(+), 15 deletions(-) diff --git a/packages/opencode/src/altimate/workspace/precedence.ts b/packages/opencode/src/altimate/workspace/precedence.ts index f61cddcd2b..bbcdb73665 100644 --- a/packages/opencode/src/altimate/workspace/precedence.ts +++ b/packages/opencode/src/altimate/workspace/precedence.ts @@ -153,6 +153,7 @@ function remember(sessionID: string, value: Precedence): void { if (oldest.done) break bySession.delete(oldest.value) announced.delete(oldest.value) + publishing.delete(oldest.value) } } @@ -191,12 +192,23 @@ async function attested(sessionID: string): Promise { * 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 was last 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. */ +/** 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 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 in + * the same window from sending the same line twice; a failed attempt leaves `announced` + * untouched, so the next turn simply tries again. */ +const publishing = 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. */ @@ -307,18 +319,18 @@ export async function refresh( const routed = result.enabled && current !== "" // Only a session that was actually routing can be told routing has stopped. const line = current || (previous?.routed ? STOPPED_ROUTING : "") - if (line && previous?.line !== line) { - // Record before publishing so a second refresh landing in the same window does not - // send the line twice, then put the record back if delivery failed. The bridge can - // be briefly unavailable, and that failure is recoverable — but only if the line is - // not remembered as delivered, since a later turn with the same inventory would - // otherwise skip it as unchanged and the session would never hear it. - announced.set(sessionID, { line, routed }) + if (line && previous?.line !== line && publishing.get(sessionID)?.line !== line) { + // The attempt is its own object, so identity alone settles which one is current: + // a publication that finishes after a newer one started finds its attempt gone and + // records nothing. Nothing reaches `announced` until the line actually arrives, so + // a failure — or a lost race — leaves the session's known state untouched and the + // next turn retries. + const attempt = { line, routed } + publishing.set(sessionID, attempt) void announce(line).then((delivered) => { - if (delivered) return - if (announced.get(sessionID)?.line !== line) return - if (previous) announced.set(sessionID, previous) - else announced.delete(sessionID) + if (publishing.get(sessionID) !== attempt) return + publishing.delete(sessionID) + if (delivered) announced.set(sessionID, attempt) }) } return result @@ -394,6 +406,7 @@ export function trackedSessionCount(): number { export function resetForTests(): void { bySession.clear() announced.clear() + publishing.clear() delete precedenceInternals.announce delete precedenceInternals.binding delete precedenceInternals.attributedTo diff --git a/packages/opencode/test/altimate/workspace/precedence.test.ts b/packages/opencode/test/altimate/workspace/precedence.test.ts index 0627ef54a7..65ee879f4b 100644 --- a/packages/opencode/test/altimate/workspace/precedence.test.ts +++ b/packages/opencode/test/altimate/workspace/precedence.test.ts @@ -597,6 +597,59 @@ describe("descriptions are per capability, and corrections are delivered", () => expect(delivered.some((l) => l.includes("any more"))).toBe(true) }) + test("two announcements failing while they overlap are both still owed", async () => { + // Precedence is re-derived every turn, so a second refresh can publish a different + // line before the first has settled. If the rollback restored the earlier record, + // it would restore a line that was never delivered either — and a later turn + // returning to that inventory would skip it as already said. Nothing may be treated + // as delivered until it arrives, whatever order the publications settle in. + 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) + expect(attempts).toHaveLength(2) + expect(attempts[0]).not.toBe(attempts[1]) + + // Both fail, in the order they were sent. + for (const reject of fail) reject() + await new Promise((resolve) => setTimeout(resolve, 0)) + + // Neither arrived, so the first inventory is still unsaid. + await refresh(SESSION, SNOWFLAKE_TOOLS) + expect(attempts).toHaveLength(3) + expect(attempts[2]).toBe(attempts[0]) + }) + + test("a publication overtaken by a newer one does not record the line it lost to", async () => { + // Out-of-order settlement: the older publication succeeds after a newer one has + // started. It must not write itself in as the session's current knowledge, or the + // newer line is skipped as already said. + const attempts: string[] = [] + const settle: Array<() => void> = [] + precedenceInternals.announce = (line) => { + attempts.push(line) + return new Promise((resolve) => settle.push(resolve)) + } + + await refresh(SESSION, SNOWFLAKE_TOOLS) + await refresh(SESSION, BIGQUERY_TOOLS) + // The FIRST publication lands last. + settle[1]() + await new Promise((resolve) => setTimeout(resolve, 0)) + settle[0]() + await new Promise((resolve) => setTimeout(resolve, 0)) + + // The session's state is the line that actually won, so re-deriving the same + // inventory stays quiet rather than repeating it. + await refresh(SESSION, BIGQUERY_TOOLS) + expect(attempts).toHaveLength(2) + }) + test("a session that never had routing is still told nothing", async () => { const lines: string[] = [] precedenceInternals.announce = async (line) => void lines.push(line) From ebfc6684999d8b23785ce152e424b636c8318699 Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Thu, 27 Aug 2026 06:32:35 +0800 Subject: [PATCH 25/30] fix(connections): bind the fallback to its type, not only its name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pinning the name closed the case where the identity of the default connection changed mid-flight, but not a same-name replacement: the name can be re-added against a different warehouse while the call is suspended in the dbt attempt, and `Registry.get(name)` still consults the mutable registry afterwards. The call would then execute under a routing decision computed for the connection that name used to mean. The decision is a function of the connection's canonical type, so pinning the type pins the decision. A replacement whose type differs is refused with a message naming the connection, rather than run under a verdict that never covered it; a rewrite that keeps the type — including one that only changes an alias, `postgres` to `postgresql` — cannot change where the call is routed and still runs. Found by automated review. --- .../altimate/native/connections/register.ts | 12 +++++++ .../test/altimate/default-target.test.ts | 35 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/packages/opencode/src/altimate/native/connections/register.ts b/packages/opencode/src/altimate/native/connections/register.ts index feca6f83c7..9be834daba 100644 --- a/packages/opencode/src/altimate/native/connections/register.ts +++ b/packages/opencode/src/altimate/native/connections/register.ts @@ -474,6 +474,11 @@ register("sql.execute", async (params: SqlExecuteParams): Promise { 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/) + }) +}) From bf9f35bb025465f366c74fc8c10c53c4632b49ea Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Thu, 27 Aug 2026 06:44:21 +0800 Subject: [PATCH 26/30] fix(workspace): deliver announcements in the order they were decided MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refreshes are serialized by the prompt loop, but publishing is deliberately not awaited — a toast must never be able to stall a turn — so two lines could be in flight at once. Landing out of order left the stale line on screen while the newer one was recorded as the session's state, and the identity guard that decided which publication was current only made the older one silent rather than correct. Publications are now chained per session, so arrival order matches decision order and the last line a session sees is the newest one. Ordering is a property worth having outright rather than a race worth guarding: with the chain in place an overtaken publication cannot occur, so the two tests that guarded that race now assert the ordering itself. Found by automated review. --- .../src/altimate/workspace/precedence.ts | 23 ++++++-- .../altimate/workspace/precedence.test.ts | 55 +++++++++++-------- 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/packages/opencode/src/altimate/workspace/precedence.ts b/packages/opencode/src/altimate/workspace/precedence.ts index bbcdb73665..f0bf29040f 100644 --- a/packages/opencode/src/altimate/workspace/precedence.ts +++ b/packages/opencode/src/altimate/workspace/precedence.ts @@ -154,6 +154,7 @@ function remember(sessionID: string, value: Precedence): void { bySession.delete(oldest.value) announced.delete(oldest.value) publishing.delete(oldest.value) + publishQueue.delete(oldest.value) } } @@ -203,12 +204,19 @@ async function attested(sessionID: string): Promise { * never happened, silencing that line for good. */ const announced = new Map() -/** The announcement currently 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 in - * the same window from sending the same line twice; a failed attempt leaves `announced` +/** 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. */ @@ -327,11 +335,13 @@ export async function refresh( // next turn retries. const attempt = { line, routed } publishing.set(sessionID, attempt) - void announce(line).then((delivered) => { - if (publishing.get(sessionID) !== attempt) return - publishing.delete(sessionID) + const queued = (publishQueue.get(sessionID) ?? Promise.resolve()).then(async () => { + const delivered = await announce(line) + if (publishing.get(sessionID) === attempt) publishing.delete(sessionID) if (delivered) announced.set(sessionID, attempt) }) + publishQueue.set(sessionID, queued) + void queued } return result } @@ -407,6 +417,7 @@ export function resetForTests(): void { bySession.clear() announced.clear() publishing.clear() + publishQueue.clear() delete precedenceInternals.announce delete precedenceInternals.binding delete precedenceInternals.attributedTo diff --git a/packages/opencode/test/altimate/workspace/precedence.test.ts b/packages/opencode/test/altimate/workspace/precedence.test.ts index 65ee879f4b..f87637b22e 100644 --- a/packages/opencode/test/altimate/workspace/precedence.test.ts +++ b/packages/opencode/test/altimate/workspace/precedence.test.ts @@ -28,6 +28,8 @@ 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: {}, @@ -597,12 +599,10 @@ describe("descriptions are per capability, and corrections are delivered", () => expect(delivered.some((l) => l.includes("any more"))).toBe(true) }) - test("two announcements failing while they overlap are both still owed", async () => { - // Precedence is re-derived every turn, so a second refresh can publish a different - // line before the first has settled. If the rollback restored the earlier record, - // it would restore a line that was never delivered either — and a later turn - // returning to that inventory would skip it as already said. Nothing may be treated - // as delivered until it arrives, whatever order the publications settle in. + 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) => { @@ -612,42 +612,49 @@ describe("descriptions are per capability, and corrections are delivered", () => await refresh(SESSION, SNOWFLAKE_TOOLS) await refresh(SESSION, BIGQUERY_TOOLS) - expect(attempts).toHaveLength(2) - expect(attempts[0]).not.toBe(attempts[1]) + // The second waits for the first rather than racing it. + expect(attempts).toHaveLength(1) - // Both fail, in the order they were sent. - for (const reject of fail) reject() - await new Promise((resolve) => setTimeout(resolve, 0)) + 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("a publication overtaken by a newer one does not record the line it lost to", async () => { - // Out-of-order settlement: the older publication succeeds after a newer one has - // started. It must not write itself in as the session's current knowledge, or the - // newer line is skipped as already said. - const attempts: string[] = [] + 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) => { - attempts.push(line) + order.push(line) return new Promise((resolve) => settle.push(resolve)) } await refresh(SESSION, SNOWFLAKE_TOOLS) await refresh(SESSION, BIGQUERY_TOOLS) - // The FIRST publication lands last. - settle[1]() - await new Promise((resolve) => setTimeout(resolve, 0)) + expect(order).toHaveLength(1) + settle[0]() - await new Promise((resolve) => setTimeout(resolve, 0)) + await tick() + expect(order).toHaveLength(2) + settle[1]() + await tick() - // The session's state is the line that actually won, so re-deriving the same - // inventory stays quiet rather than repeating it. + // 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) - expect(attempts).toHaveLength(2) + await tick() + expect(order).toHaveLength(2) }) test("a session that never had routing is still told nothing", async () => { From babb2c5c95476aa8f3624119a3b854a7f5637c08 Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Thu, 27 Aug 2026 06:56:34 +0800 Subject: [PATCH 27/30] fix(workspace): do not resurrect an evicted session's announcement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Publishing is not awaited, so a line can still be in flight when its session falls out of the cache. Writing the delivery back afterwards recreated an entry for a session eviction had already removed — and eviction only ever walks `bySession`, so nothing could reclaim it. The announcement cache would then grow with the lifetime session count, which is the bound the eviction exists to hold. The write-back now happens only while the session is still tracked. The test asserts the behaviour rather than the size alone: a resurrected record would suppress the announcement an evicted session gets when it is derived again, which is the visible consequence and a sharper signal than the count. Found by automated review. --- .../src/altimate/workspace/precedence.ts | 12 ++++++- .../altimate/workspace/precedence.test.ts | 32 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/altimate/workspace/precedence.ts b/packages/opencode/src/altimate/workspace/precedence.ts index f0bf29040f..2e0ed94a5b 100644 --- a/packages/opencode/src/altimate/workspace/precedence.ts +++ b/packages/opencode/src/altimate/workspace/precedence.ts @@ -338,7 +338,11 @@ export async function refresh( const queued = (publishQueue.get(sessionID) ?? Promise.resolve()).then(async () => { const delivered = await announce(line) if (publishing.get(sessionID) === attempt) publishing.delete(sessionID) - if (delivered) announced.set(sessionID, attempt) + // 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 @@ -413,6 +417,12 @@ 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() diff --git a/packages/opencode/test/altimate/workspace/precedence.test.ts b/packages/opencode/test/altimate/workspace/precedence.test.ts index f87637b22e..2feea6b2d7 100644 --- a/packages/opencode/test/altimate/workspace/precedence.test.ts +++ b/packages/opencode/test/altimate/workspace/precedence.test.ts @@ -10,6 +10,7 @@ import { check, decideForTarget, trackedSessionCount, + announcedSessionCount, describeEngineTool, describeNativeTool, forSession, @@ -233,6 +234,37 @@ describe("the per-session caches are bounded", () => { 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", () => { From 6d82419a568d50cbc213fb5001f489813d84c303 Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Thu, 27 Aug 2026 07:04:55 +0800 Subject: [PATCH 28/30] fix(workspace): compare against the newest line, not the last delivered MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inventory can return to what was already announced while a different line is still being published. Comparing only against the delivered line suppressed that correction, and the queue then delivered the stale line last — leaving the session looking at routing guidance that no longer matched where its calls went. The comparison is now against the newest line the session is committed to saying: the one still in flight if there is one, otherwise the one it has actually been told. That replaces the two separate guards — one against the delivered line, one against the pending line — with a single question, and it is the same answer as before whenever nothing is pending. Found by automated review. --- .../src/altimate/workspace/precedence.ts | 15 +++++---- .../altimate/workspace/precedence.test.ts | 31 +++++++++++++++++++ 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/packages/opencode/src/altimate/workspace/precedence.ts b/packages/opencode/src/altimate/workspace/precedence.ts index 2e0ed94a5b..4187dad264 100644 --- a/packages/opencode/src/altimate/workspace/precedence.ts +++ b/packages/opencode/src/altimate/workspace/precedence.ts @@ -327,12 +327,15 @@ export async function refresh( const routed = result.enabled && current !== "" // Only a session that was actually routing can be told routing has stopped. const line = current || (previous?.routed ? STOPPED_ROUTING : "") - if (line && previous?.line !== line && publishing.get(sessionID)?.line !== line) { - // The attempt is its own object, so identity alone settles which one is current: - // a publication that finishes after a newer one started finds its attempt gone and - // records nothing. Nothing reaches `announced` until the line actually arrives, so - // a failure — or a lost race — leaves the session's known state untouched and the - // next turn retries. + // Compare against the newest line this session is committed to saying — the one still + // being published if there is one, otherwise the one it has actually been told. + // Comparing against the delivered line alone would suppress a correction back to it + // while a different line is still in flight, and the queue would then deliver the + // stale one last. + const committed = publishing.get(sessionID)?.line ?? previous?.line + if (line && committed !== 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 () => { diff --git a/packages/opencode/test/altimate/workspace/precedence.test.ts b/packages/opencode/test/altimate/workspace/precedence.test.ts index 2feea6b2d7..6172fc9e05 100644 --- a/packages/opencode/test/altimate/workspace/precedence.test.ts +++ b/packages/opencode/test/altimate/workspace/precedence.test.ts @@ -689,6 +689,37 @@ describe("descriptions are per capability, and corrections are delivered", () => 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("a session that never had routing is still told nothing", async () => { const lines: string[] = [] precedenceInternals.announce = async (line) => void lines.push(line) From c61b1b1c0aa06409a014f7f2fb1a2d5b8c13809f Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Thu, 27 Aug 2026 07:12:05 +0800 Subject: [PATCH 29/30] fix(workspace): ask both announcement questions of one record MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous change made the dedupe compare against the newest line a session is committed to saying, but left the stop decision reading the delivered record alone. With a first routing announcement still in flight, a refresh that served nothing therefore queued no correction — and the routing line arrived after routing had already stopped. Both questions now consult the same record: the announcement still being published if there is one, otherwise the one the session has been told. That removes the second source of truth rather than adding a guard for the case, and it finishes a change that was only half applied. Found by automated review. --- .../src/altimate/workspace/precedence.ts | 18 +++++++------- .../altimate/workspace/precedence.test.ts | 24 +++++++++++++++++++ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/altimate/workspace/precedence.ts b/packages/opencode/src/altimate/workspace/precedence.ts index 4187dad264..ac5e2c4762 100644 --- a/packages/opencode/src/altimate/workspace/precedence.ts +++ b/packages/opencode/src/altimate/workspace/precedence.ts @@ -322,18 +322,18 @@ export async function refresh( // 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 previous = announced.get(sessionID) 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 || (previous?.routed ? STOPPED_ROUTING : "") - // Compare against the newest line this session is committed to saying — the one still - // being published if there is one, otherwise the one it has actually been told. - // Comparing against the delivered line alone would suppress a correction back to it - // while a different line is still in flight, and the queue would then deliver the - // stale one last. - const committed = publishing.get(sessionID)?.line ?? previous?.line - if (line && committed !== line) { + 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 } diff --git a/packages/opencode/test/altimate/workspace/precedence.test.ts b/packages/opencode/test/altimate/workspace/precedence.test.ts index 6172fc9e05..cd31f973a5 100644 --- a/packages/opencode/test/altimate/workspace/precedence.test.ts +++ b/packages/opencode/test/altimate/workspace/precedence.test.ts @@ -720,6 +720,30 @@ describe("descriptions are per capability, and corrections are delivered", () => 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) From 50344716764bda5b25708f07b8c8d1400b829728 Mon Sep 17 00:00:00 2001 From: ralphstodomingo Date: Thu, 27 Aug 2026 07:15:47 +0800 Subject: [PATCH 30/30] test(workspace): assert the invariants rather than samples of them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seven tests retired into three that state the property directly. The attestation block asserted three refused attach outcomes individually alongside a loop over the whole union; the samples were subsumed. The loop now carries what they were adding — the refusal reason, which is what the inventory line and tool descriptions render, so a refusal with the wrong reason is a wrong explanation shown to the user — and covers `undefined`, which it could not reach before. The two qualifying outcomes merge into one loop over the other half of the same allowlist. The four reporting surfaces were asserted one test each against the same unreachable-caller ruleset. The failure they guard is precisely that these drift apart, so they now assert together that every surface agrees with the routing decision. Line count barely moves: an invariant carries more scaffolding than the sample it replaces. The gain is that a new outcome variant or a new reporting surface is covered by construction rather than by remembering to add a case. --- .../altimate/workspace/precedence.test.ts | 100 +++++++++--------- 1 file changed, 49 insertions(+), 51 deletions(-) diff --git a/packages/opencode/test/altimate/workspace/precedence.test.ts b/packages/opencode/test/altimate/workspace/precedence.test.ts index cd31f973a5..6cba906b0f 100644 --- a/packages/opencode/test/altimate/workspace/precedence.test.ts +++ b/packages/opencode/test/altimate/workspace/precedence.test.ts @@ -136,28 +136,6 @@ describe("attribution is grounded in the attach, not only the saved config", () // 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("a session with no established attach confers no precedence", async () => { - precedenceInternals.attachOutcome = async () => ({ kind: "engine-missing", declared: 12 }) - const p = await refresh(SESSION, SNOWFLAKE_TOOLS) - expect(p.enabled).toBe(false) - expect(p.disabledReason).toBe("unattributed") - }) - - test("a superseded attach confers no precedence", async () => { - // Superseded means the binding moved while the attach was in flight, so whatever - // is connected was established for a workspace this project has already left. - precedenceInternals.attachOutcome = async () => ({ kind: "superseded" }) - const p = await refresh(SESSION, SNOWFLAKE_TOOLS) - expect(p.enabled).toBe(false) - expect(p.disabledReason).toBe("unattributed") - }) - - test("a disabled entry confers no precedence either", async () => { - precedenceInternals.attachOutcome = async () => ({ kind: "entry-disabled" }) - const p = await refresh(SESSION, SNOWFLAKE_TOOLS) - expect(p.enabled).toBe(false) - }) - 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 @@ -182,17 +160,18 @@ describe("attribution is grounded in the attach, not only the saved config", () expect(verdict.redirect).toBeUndefined() }) - test("a settled outcome is still read, so the common case is unaffected", async () => { - precedenceInternals.attachOutcome = async () => ({ kind: "attached", available: 12, declared: 12, missing: [] }) - const p = await refresh(SESSION, SNOWFLAKE_TOOLS) - expect(p.enabled).toBe(true) - }) 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, reused}, so a new Outcome variant defaults to refusing rather than - // silently qualifying. - const refused: Array<{ kind: string }> = [ + // silently qualifying. `undefined` is in the list because "in flight" and "never + // attached" are indistinguishable and both must fail open rather than route. + // + // Superseded is the one worth naming: the binding moved while the attach was in + // flight, so whatever is connected was established for a workspace this project + // has already left. + const refused: Array<{ kind: string } | undefined> = [ + undefined, { kind: "disabled" }, { kind: "unbound" }, { kind: "engine-missing" }, @@ -206,14 +185,33 @@ describe("attribution is grounded in the attach, not only the saved config", () 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: false }) + 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 reused engine counts as established, because attach verified it first", async () => { - precedenceInternals.attachOutcome = async () => ({ kind: "reused", available: 12 }) - const p = await refresh(SESSION, SNOWFLAKE_TOOLS) - expect(p.enabled).toBe(true) + test("an established attach qualifies, however it was established", async () => { + // The other half of the same allowlist. `reused` counts because attach verified the + // engine before handing it back; a settled `attached` is the common case and must + // not have been broken by any of the refusal machinery above. + const qualifying = [ + { kind: "attached", available: 12, declared: 12, missing: [] }, + { kind: "reused", available: 12 }, + ] + 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 () => { @@ -468,16 +466,6 @@ describe("reporting never claims a routing that will not happen", () => { { permission: "schema_inspect", pattern: "*", action: "allow" as const }, ] - test("warehouse_list marks nothing when the caller cannot reach the engine", async () => { - const p = await refresh(SESSION, SNOWFLAKE_TOOLS, analystLike) - expect(warehouseListNote(p, "snowflake")).toBeNull() - }) - - test("...and the routing decision agrees with it", async () => { - await refresh(SESSION, SNOWFLAKE_TOOLS, analystLike) - const verdict = await check(SESSION, "sql_execute", "local_snow") - expect(verdict.redirect).toBeUndefined() - }) test("warehouse_list still marks the row for a caller that can reach it", async () => { const p = await refresh(SESSION, SNOWFLAKE_TOOLS, [ @@ -486,14 +474,24 @@ describe("reporting never claims a routing that will not happen", () => { expect(warehouseListNote(p, "snowflake")).toContain("via workspace") }) - test("the inventory line says nothing rather than something false", async () => { + 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) - expect(inventoryLine(p)).toBe("") - }) - - test("the native tool description makes no redirect claim either", async () => { - const p = await refresh(SESSION, SNOWFLAKE_TOOLS, analystLike) - expect(describeNativeTool("sql_execute", "Execute SQL.", p)).toBe("Execute SQL.") + 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 () => {