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