From c6fd1c19c63f1d384f5261fa51b6070a14748bca Mon Sep 17 00:00:00 2001 From: kulvirgit Date: Wed, 18 Mar 2026 16:22:58 -0700 Subject: [PATCH] feat: simplify to 3 modes (builder/analyst/plan) + SQL write access control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Part A — Simplify modes: - Remove executive, validator, migrator, researcher, trainer agents - Delete 5 unused prompt files - Analyst: truly read-only — unknown bash denied, safe commands (ls/grep/cat/head/tail/find/wc) auto-allowed, dbt read commands allowed, dbt write commands (run/build/seed/docs) denied - Builder: add sql_execute_write: "ask" for SQL mutation approval Part B — SQL write access control: - New sql-classify.ts: regex-based classifier detecting INSERT, UPDATE, DELETE, DROP, CREATE, ALTER, TRUNCATE, MERGE, GRANT, REVOKE, COPY INTO, CALL, EXEC, EXECUTE IMMEDIATE, REPLACE, UPSERT, RENAME - sql-execute.ts: classify before execution, ctx.ask() for writes, hardcoded safety block for DROP DATABASE/SCHEMA/TRUNCATE - Safety denials in agent.ts for sql_execute_write - 42 classifier tests covering read/write/edge cases Co-Authored-By: Claude Opus 4.6 (1M context) --- bun.lock | 14 +- docs/docs/configure/agents.md | 10 +- packages/opencode/package.json | 2 +- packages/opencode/src/agent/agent.ts | 199 +++----------- .../opencode/src/altimate/prompts/analyst.txt | 1 + .../opencode/src/altimate/prompts/builder.txt | 9 + .../src/altimate/prompts/executive.txt | 33 --- .../src/altimate/prompts/migrator.txt | 53 ---- .../src/altimate/prompts/researcher.txt | 94 ------- .../opencode/src/altimate/prompts/trainer.txt | 99 ------- .../src/altimate/prompts/validator.txt | 108 -------- .../src/altimate/tools/sql-classify.ts | 52 ++++ .../src/altimate/tools/sql-execute.ts | 19 ++ packages/opencode/src/memory/types.ts | 5 - packages/opencode/test/agent/agent.test.ts | 128 ++++++++- .../test/altimate/tools/sql-classify.test.ts | 256 ++++++++++++++++++ script/upstream/verify-restructure.ts | 3 - 17 files changed, 508 insertions(+), 577 deletions(-) delete mode 100644 packages/opencode/src/altimate/prompts/executive.txt delete mode 100644 packages/opencode/src/altimate/prompts/migrator.txt delete mode 100644 packages/opencode/src/altimate/prompts/researcher.txt delete mode 100644 packages/opencode/src/altimate/prompts/trainer.txt delete mode 100644 packages/opencode/src/altimate/prompts/validator.txt create mode 100644 packages/opencode/src/altimate/tools/sql-classify.ts create mode 100644 packages/opencode/test/altimate/tools/sql-classify.test.ts diff --git a/bun.lock b/bun.lock index db61d3d7f8..9e336b65fd 100644 --- a/bun.lock +++ b/bun.lock @@ -81,7 +81,7 @@ "@ai-sdk/togetherai": "1.0.34", "@ai-sdk/vercel": "1.0.33", "@ai-sdk/xai": "2.0.51", - "@altimateai/altimate-core": "^0.2.3", + "@altimateai/altimate-core": "^0.2.4", "@altimateai/drivers": "workspace:*", "@aws-sdk/credential-providers": "3.993.0", "@clack/prompts": "1.0.0-alpha.1", @@ -344,17 +344,17 @@ "@altimateai/altimate-code": ["@altimateai/altimate-code@workspace:packages/opencode"], - "@altimateai/altimate-core": ["@altimateai/altimate-core@0.2.3", "", { "optionalDependencies": { "@altimateai/altimate-core-darwin-arm64": "0.2.3", "@altimateai/altimate-core-darwin-x64": "0.2.3", "@altimateai/altimate-core-linux-arm64-gnu": "0.2.3", "@altimateai/altimate-core-linux-x64-gnu": "0.2.3", "@altimateai/altimate-core-win32-x64-msvc": "0.2.3" } }, "sha512-A68qFjhUBbgM2ZDxPLhJPH/veh5dSj497QsALpLBB0ZlP4leEsOZTKDEhpgexE2G5N+t56dpf1/RU46H++fMYg=="], + "@altimateai/altimate-core": ["@altimateai/altimate-core@0.2.4", "", { "optionalDependencies": { "@altimateai/altimate-core-darwin-arm64": "0.2.4", "@altimateai/altimate-core-darwin-x64": "0.2.4", "@altimateai/altimate-core-linux-arm64-gnu": "0.2.4", "@altimateai/altimate-core-linux-x64-gnu": "0.2.4", "@altimateai/altimate-core-win32-x64-msvc": "0.2.4" } }, "sha512-LORYBc9ZtkdttiTPhDGi/WtZLsDNZOvG5UA0h7nuSPysNzdHfx/rRXvdq8qi/K3Nir72S9HMgFHgfz/Ys0HC6w=="], - "@altimateai/altimate-core-darwin-arm64": ["@altimateai/altimate-core-darwin-arm64@0.2.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-kH1Vnkd8b14NMlwUA+hlqm+nqapxjqmr7Dgg04aNGAEURmZzjfSz6RdXeFz9bQ6Rw/DJvc9IVMZ/9lVXcY22yw=="], + "@altimateai/altimate-core-darwin-arm64": ["@altimateai/altimate-core-darwin-arm64@0.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lVUb88a6NkFPh7l6V+iCC85eVov/kpFQ5DDvqupN8Wg1XRZC+weEaDIp4IlTAPJF3qNR97znAnFVEF9vn6mzcw=="], - "@altimateai/altimate-core-darwin-x64": ["@altimateai/altimate-core-darwin-x64@0.2.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-ql+GvlHjehROP86iJWDz9dSrvI2xRYryPh6mEpKRocXYNz6qiJvq8wCIcyTPUW8wJZsHeiQvod33HWe8x05olA=="], + "@altimateai/altimate-core-darwin-x64": ["@altimateai/altimate-core-darwin-x64@0.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-A8OMGTyIs4nAExMRPmLNm0ezbH2SdZrgHFPugDpFLJ/g05C6NC3jiN9APLxRDhqcNZXjSGSUsC3XBVnYuja9UQ=="], - "@altimateai/altimate-core-linux-arm64-gnu": ["@altimateai/altimate-core-linux-arm64-gnu@0.2.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-e/imvqJpNVGgTYDXrubPsDw0IdhAtYHUPoKSW+vJJ0FyysV4/CQDnzPB+InRfeG2lStGMsjCkbCmDVsJPafoEw=="], + "@altimateai/altimate-core-linux-arm64-gnu": ["@altimateai/altimate-core-linux-arm64-gnu@0.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-yZFlXqcGYCwIZRpNumtN74Jy15KllwgGH2RA9SnPqU5GHphNoM7Ei+W7G0rqoJaMEMecOmbwTGRpBcfvmThFTw=="], - "@altimateai/altimate-core-linux-x64-gnu": ["@altimateai/altimate-core-linux-x64-gnu@0.2.3", "", { "os": "linux", "cpu": "x64" }, "sha512-lBcG7AMttIjEfo2Y1xgljUkyDqi6jpjiGCrTvbMT/ZLvTpJlkftL0/sxImtTLCYcHtSomxoyzW2q18DPrw2KKQ=="], + "@altimateai/altimate-core-linux-x64-gnu": ["@altimateai/altimate-core-linux-x64-gnu@0.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-mWeXb7CwUxLlTXcnn/Y+r6Mzfi5Hn1a9WCWbLWCgGIw8uCy7vUHed5ClfGe7FrX6xmoOevq6evSGoDNFDxIdQA=="], - "@altimateai/altimate-core-win32-x64-msvc": ["@altimateai/altimate-core-win32-x64-msvc@0.2.3", "", { "os": "win32", "cpu": "x64" }, "sha512-xHDkstjzLiLXvYqlvzw8nni3c0OmHbeQYpMNWsGSRti+DjcomcLOvsev3yA3LBYbcE1N9kKYAGJP3Gk5M/l++Q=="], + "@altimateai/altimate-core-win32-x64-msvc": ["@altimateai/altimate-core-win32-x64-msvc@0.2.4", "", { "os": "win32", "cpu": "x64" }, "sha512-7yR4t6RmFNGmTJ0of3xy2IkD2sqLYppU5BHaKx+EHsqYo3PdDlpFoHQtAIHO9//22S9EhxASgKKeyKB63CL+yQ=="], "@altimateai/dbt-integration": ["@altimateai/dbt-integration@0.2.9", "", { "dependencies": { "@altimateai/altimate-core": "0.1.6", "node-abort-controller": "^3.1.1", "node-fetch": "^3.3.2", "python-bridge": "^1.1.0", "semver": "^7.6.3", "yaml": "^2.5.0" }, "peerDependencies": { "patch-package": "^8.0.0" } }, "sha512-L+sazdclVNVPuRrSRq/0dGfyNEOHHGKqOCGEkZiXFbaW9hRGRqk+9LgmOUwyDq2VA79qvduOehe7+Uk0Oo3sow=="], diff --git a/docs/docs/configure/agents.md b/docs/docs/configure/agents.md index 2876080c96..2452f85216 100644 --- a/docs/docs/configure/agents.md +++ b/docs/docs/configure/agents.md @@ -8,7 +8,7 @@ Agents define different AI personas with specific models, prompts, permissions, |-------|------------|-------------| | `builder` | Create and modify dbt models, SQL pipelines, and data transformations | Full read/write. SQL mutations prompt for approval. | | `analyst` | Explore data, run SELECT queries, inspect schemas, generate insights | Read-only (enforced). SQL writes denied. Safe bash commands auto-allowed. | -| `plan` | Plan before acting, restricted to planning files only | Minimal: no edits, no bash, no SQL | +| `plan` | Plan before acting — restricted to planning files only | Minimal — no edits, no bash, no SQL | ### Builder @@ -18,9 +18,9 @@ Full access mode. Can read/write files, run any bash command (with approval), ex Truly read-only mode for safe data exploration: -- **File access**: Read, grep, glob without prompts -- **SQL**: SELECT queries execute freely. Write queries are denied (not prompted, blocked entirely) -- **Bash**: Safe commands auto-allowed (`ls`, `grep`, `cat`, `head`, `tail`, `find`, `wc`). dbt read commands allowed (`dbt list`, `dbt ls`, `dbt debug`, `dbt deps`). Everything else denied. +- **File access**: Read, grep, glob — no prompts +- **SQL**: SELECT queries execute freely. Write queries are denied (not prompted — blocked entirely) +- **Bash**: Safe commands auto-allowed (`ls`, `grep`, `cat`, `head`, `tail`, `find`, `wc`). dbt read commands allowed (`dbt list`, `dbt ls`, `dbt debug`). Everything else denied. - **Web**: Fetch and search allowed without prompts - **Schema/warehouse/finops**: All inspection tools available @@ -29,7 +29,7 @@ Truly read-only mode for safe data exploration: ### Plan -Planning mode with minimal permissions. Can only read files and edit plan files. No SQL, no bash, no file modifications. +Planning mode with minimal permissions. Can only read files and edit plan files in `.opencode/plans/`. No SQL, no bash, no file modifications. ## SQL Write Access Control diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 69c91626ba..fca5d9298c 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -75,7 +75,7 @@ "@ai-sdk/togetherai": "1.0.34", "@ai-sdk/vercel": "1.0.33", "@ai-sdk/xai": "2.0.51", - "@altimateai/altimate-core": "^0.2.3", + "@altimateai/altimate-core": "^0.2.4", "@altimateai/drivers": "workspace:*", "@aws-sdk/credential-providers": "3.993.0", "@clack/prompts": "1.0.0-alpha.1", diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index a3c8614f96..b6363e8d6f 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -17,11 +17,6 @@ import PROMPT_TITLE from "./prompt/title.txt" // altimate_change start - import custom agent mode prompts import PROMPT_BUILDER from "../altimate/prompts/builder.txt" import PROMPT_ANALYST from "../altimate/prompts/analyst.txt" -import PROMPT_VALIDATOR from "../altimate/prompts/validator.txt" -import PROMPT_MIGRATOR from "../altimate/prompts/migrator.txt" -import PROMPT_EXECUTIVE from "../altimate/prompts/executive.txt" -import PROMPT_RESEARCHER from "../altimate/prompts/researcher.txt" -import PROMPT_TRAINER from "../altimate/prompts/trainer.txt" // altimate_change end import { PermissionNext } from "@/permission/next" import { mergeDeep, pipe, sortBy, values } from "remeda" @@ -126,13 +121,26 @@ export namespace Agent { "Drop Schema *": "deny", "Truncate *": "deny", }, + // altimate_change start - SQL write safety denials + sql_execute_write: { + "DROP DATABASE *": "deny", + "DROP SCHEMA *": "deny", + "TRUNCATE *": "deny", + "drop database *": "deny", + "drop schema *": "deny", + "truncate *": "deny", + "Drop Database *": "deny", + "Drop Schema *": "deny", + "Truncate *": "deny", + }, + // altimate_change end }) // Combine user config with safety denials so every agent inherits them const userWithSafety = PermissionNext.merge(user, safetyDenials) const result: Record = { - // altimate_change start - replace default build agent with builder and add custom modes + // altimate_change start - 3 modes: builder, analyst, plan builder: { name: "builder", description: "Create and modify dbt models, SQL, and data pipelines. Full read/write access.", @@ -143,6 +151,7 @@ export namespace Agent { PermissionNext.fromConfig({ question: "allow", plan_enter: "allow", + sql_execute_write: "ask", }), userWithSafety, ), @@ -151,178 +160,45 @@ export namespace Agent { }, analyst: { name: "analyst", - description: "Read-only data exploration. Cannot modify files or run destructive SQL.", + description: "Read-only data exploration and analysis. Cannot modify files or run destructive SQL.", prompt: PROMPT_ANALYST, options: {}, permission: PermissionNext.merge( defaults, PermissionNext.fromConfig({ "*": "deny", + // SQL read tools sql_execute: "allow", sql_validate: "allow", sql_analyze: "allow", sql_translate: "allow", sql_optimize: "allow", lineage_check: "allow", + sql_explain: "allow", sql_format: "allow", sql_fix: "allow", + sql_autocomplete: "allow", sql_diff: "allow", + // SQL writes denied + sql_execute_write: "deny", + // Warehouse/schema/finops warehouse_list: "allow", warehouse_test: "allow", warehouse_discover: "allow", schema_inspect: "allow", schema_index: "allow", schema_search: "allow", - schema_cache_status: "allow", sql_explain: "allow", sql_format: "allow", - sql_fix: "allow", sql_autocomplete: "allow", sql_diff: "allow", + schema_cache_status: "allow", schema_detect_pii: "allow", + schema_tags: "allow", schema_tags_list: "allow", finops_query_history: "allow", finops_analyze_credits: "allow", finops_expensive_queries: "allow", finops_warehouse_advice: "allow", finops_unused_resources: "allow", finops_role_grants: "allow", finops_role_hierarchy: "allow", finops_user_roles: "allow", - schema_detect_pii: "allow", schema_tags: "allow", schema_tags_list: "allow", + // Core tools altimate_core_validate: "allow", altimate_core_check: "allow", altimate_core_rewrite: "allow", - tool_lookup: "allow", + // Read-only file access read: "allow", grep: "allow", glob: "allow", - question: "allow", webfetch: "allow", websearch: "allow", - training_save: "allow", training_list: "allow", training_remove: "allow", - }), - userWithSafety, - ), - mode: "primary", - native: true, - }, - executive: { - name: "executive", - description: "Read-only data exploration with output calibrated for non-technical executives. No SQL or jargon — findings expressed as business impact.", - prompt: PROMPT_EXECUTIVE, - options: { audience: "executive" }, - permission: PermissionNext.merge( - defaults, - PermissionNext.fromConfig({ - "*": "deny", - sql_execute: "allow", sql_validate: "allow", sql_analyze: "allow", - sql_translate: "allow", sql_optimize: "allow", lineage_check: "allow", - warehouse_list: "allow", warehouse_test: "allow", warehouse_discover: "allow", - schema_inspect: "allow", schema_index: "allow", schema_search: "allow", - schema_cache_status: "allow", sql_explain: "allow", sql_format: "allow", - sql_fix: "allow", sql_autocomplete: "allow", sql_diff: "allow", - finops_query_history: "allow", finops_analyze_credits: "allow", - finops_expensive_queries: "allow", finops_warehouse_advice: "allow", - finops_unused_resources: "allow", finops_role_grants: "allow", - finops_role_hierarchy: "allow", finops_user_roles: "allow", - schema_detect_pii: "allow", schema_tags: "allow", schema_tags_list: "allow", - altimate_core_validate: "allow", altimate_core_check: "allow", - altimate_core_rewrite: "allow", - tool_lookup: "allow", - read: "allow", grep: "allow", glob: "allow", - question: "allow", webfetch: "allow", websearch: "allow", - training_save: "allow", training_list: "allow", training_remove: "allow", - }), - userWithSafety, - ), - mode: "primary", - native: true, - }, - validator: { - name: "validator", - description: "Test, lint, and verify data integrity. Cannot modify files.", - prompt: PROMPT_VALIDATOR, - options: {}, - permission: PermissionNext.merge( - defaults, - PermissionNext.fromConfig({ - "*": "deny", - sql_validate: "allow", sql_execute: "allow", sql_analyze: "allow", - sql_translate: "allow", sql_optimize: "allow", lineage_check: "allow", - warehouse_list: "allow", warehouse_test: "allow", warehouse_discover: "allow", - schema_inspect: "allow", schema_index: "allow", schema_search: "allow", - schema_cache_status: "allow", sql_explain: "allow", sql_format: "allow", - sql_fix: "allow", sql_autocomplete: "allow", sql_diff: "allow", - finops_query_history: "allow", finops_analyze_credits: "allow", - finops_expensive_queries: "allow", finops_warehouse_advice: "allow", - finops_unused_resources: "allow", finops_role_grants: "allow", - finops_role_hierarchy: "allow", finops_user_roles: "allow", - schema_detect_pii: "allow", schema_tags: "allow", schema_tags_list: "allow", - altimate_core_validate: "allow", altimate_core_check: "allow", - altimate_core_rewrite: "allow", - tool_lookup: "allow", - read: "allow", grep: "allow", glob: "allow", bash: "allow", - question: "allow", - training_save: "allow", training_list: "allow", training_remove: "allow", - }), - userWithSafety, - ), - mode: "primary", - native: true, - }, - migrator: { - name: "migrator", - description: "Cross-warehouse SQL migration and dialect conversion.", - prompt: PROMPT_MIGRATOR, - options: {}, - permission: PermissionNext.merge( - defaults, - PermissionNext.fromConfig({ - sql_execute: "allow", sql_validate: "allow", sql_translate: "allow", - sql_optimize: "allow", lineage_check: "allow", - warehouse_list: "allow", warehouse_test: "allow", - schema_inspect: "allow", schema_index: "allow", schema_search: "allow", - schema_cache_status: "allow", sql_explain: "allow", sql_format: "allow", - sql_fix: "allow", sql_autocomplete: "allow", sql_diff: "allow", - finops_query_history: "allow", finops_analyze_credits: "allow", - finops_expensive_queries: "allow", finops_warehouse_advice: "allow", - finops_unused_resources: "allow", finops_role_grants: "allow", - finops_role_hierarchy: "allow", finops_user_roles: "allow", - schema_detect_pii: "allow", schema_tags: "allow", schema_tags_list: "allow", - altimate_core_validate: "allow", altimate_core_check: "allow", - altimate_core_rewrite: "allow", - tool_lookup: "allow", - read: "allow", write: "allow", edit: "allow", - grep: "allow", glob: "allow", question: "allow", - training_save: "allow", training_list: "allow", training_remove: "allow", - }), - userWithSafety, - ), - mode: "primary", - native: true, - }, - researcher: { - name: "researcher", - description: "Deep research mode. Thorough multi-step investigation with structured reports. Use for complex analytical questions.", - prompt: PROMPT_RESEARCHER, - options: {}, - permission: PermissionNext.merge( - defaults, - PermissionNext.fromConfig({ - "*": "deny", - sql_execute: "allow", sql_validate: "allow", sql_analyze: "allow", - sql_translate: "allow", sql_optimize: "allow", lineage_check: "allow", - warehouse_list: "allow", warehouse_test: "allow", warehouse_discover: "allow", - schema_inspect: "allow", schema_index: "allow", schema_search: "allow", - schema_cache_status: "allow", sql_explain: "allow", sql_format: "allow", - sql_fix: "allow", sql_autocomplete: "allow", sql_diff: "allow", - finops_query_history: "allow", finops_analyze_credits: "allow", - finops_expensive_queries: "allow", finops_warehouse_advice: "allow", - finops_unused_resources: "allow", finops_role_grants: "allow", - finops_role_hierarchy: "allow", finops_user_roles: "allow", - schema_detect_pii: "allow", schema_tags: "allow", schema_tags_list: "allow", - altimate_core_validate: "allow", altimate_core_check: "allow", - altimate_core_rewrite: "allow", - tool_lookup: "allow", - read: "allow", grep: "allow", glob: "allow", bash: "allow", - question: "allow", webfetch: "allow", websearch: "allow", - task: "allow", training_save: "allow", training_list: "allow", training_remove: "allow", - }), - userWithSafety, - ), - mode: "primary", - native: true, - }, - trainer: { - name: "trainer", - description: "Teach your AI teammate. Scan for patterns, validate training against code, curate knowledge. Read-only.", - prompt: PROMPT_TRAINER, - options: {}, - permission: PermissionNext.merge( - defaults, - PermissionNext.fromConfig({ - "*": "deny", - read: "allow", grep: "allow", glob: "allow", bash: "allow", - question: "allow", + webfetch: "allow", websearch: "allow", + question: "allow", tool_lookup: "allow", + // Bash: last-match-wins — "*": "deny" MUST come first, then specific allows override + bash: { + "*": "deny", + "ls *": "allow", "grep *": "allow", "cat *": "allow", + "head *": "allow", "tail *": "allow", "find *": "allow", "wc *": "allow", + "dbt list *": "allow", "dbt ls *": "allow", "dbt debug *": "allow", + }, + // Training training_save: "allow", training_list: "allow", training_remove: "allow", - schema_inspect: "allow", schema_index: "allow", schema_search: "allow", - schema_cache_status: "allow", - warehouse_list: "allow", warehouse_discover: "allow", }), userWithSafety, ), @@ -469,7 +345,8 @@ export namespace Agent { item.name = value.name ?? item.name item.steps = value.steps ?? item.steps item.options = mergeDeep(item.options, value.options ?? {}) - item.permission = PermissionNext.merge(item.permission, PermissionNext.fromConfig(value.permission ?? {})) + // Re-apply safety denials AFTER user config so they cannot be overridden + item.permission = PermissionNext.merge(item.permission, PermissionNext.fromConfig(value.permission ?? {}), safetyDenials) } // Ensure Truncate.GLOB is allowed unless explicitly configured diff --git a/packages/opencode/src/altimate/prompts/analyst.txt b/packages/opencode/src/altimate/prompts/analyst.txt index 47b05a949b..1efb790e69 100644 --- a/packages/opencode/src/altimate/prompts/analyst.txt +++ b/packages/opencode/src/altimate/prompts/analyst.txt @@ -43,6 +43,7 @@ Remember: your users are hired to generate insights, not warehouse bills. Every - /sql-translate — Cross-dialect SQL translation with warnings (analysis only; writing translated files requires the builder agent) - /impact-analysis — Downstream impact analysis using lineage + manifest - /lineage-diff — Compare column lineage between SQL versions +- /data-viz — Build interactive data visualizations, dashboards, charts, and analytics views from query results Note: Skills that write files (/generate-tests, /model-scaffold, /yaml-config, /dbt-docs, /medallion-patterns, /incremental-logic) require the builder agent. ## FinOps & Governance Tools diff --git a/packages/opencode/src/altimate/prompts/builder.txt b/packages/opencode/src/altimate/prompts/builder.txt index 00eabc4d50..b2a63e14c5 100644 --- a/packages/opencode/src/altimate/prompts/builder.txt +++ b/packages/opencode/src/altimate/prompts/builder.txt @@ -153,6 +153,12 @@ Skills are specialized workflows that compose multiple tools. Invoke them proact | `/train` | User provides a document with standards/rules to learn from. | | `/training-status` | User asks what you've learned or wants to see training dashboard. | +### Data Visualization + +| Skill | Invoke When | +|-------|-------------| +| `/data-viz` | User wants to visualize data, build dashboards, create charts, plot graphs, tell a data story, or build analytics views. Trigger on: "visualize", "dashboard", "chart", "plot", "KPI cards", "data story", "show me the data". | + ## Proactive Skill Invocation Don't wait for `/skill-name` — invoke skills when the task clearly matches: @@ -164,6 +170,9 @@ Don't wait for `/skill-name` — invoke skills when the task clearly matches: - User says "will this change break anything" -> invoke `/dbt-analyze` - User says "analyze this migration" -> invoke `/schema-migration` - User says "make this query faster" -> invoke `/query-optimize` +- User says "visualize this data" -> invoke `/data-viz` +- User says "make a dashboard" -> invoke `/data-viz` +- User says "chart these metrics" -> invoke `/data-viz` ## Teammate Training diff --git a/packages/opencode/src/altimate/prompts/executive.txt b/packages/opencode/src/altimate/prompts/executive.txt deleted file mode 100644 index 83f1b35986..0000000000 --- a/packages/opencode/src/altimate/prompts/executive.txt +++ /dev/null @@ -1,33 +0,0 @@ -You are altimate-code in executive mode — a read-only data exploration agent calibrated for non-technical business audiences. - -You have the same analytical capabilities as the analyst agent but communicate exclusively in business terms. - -## Output Calibration — Executive Mode - -You are speaking to a business executive or non-technical stakeholder. Follow these rules strictly: - -- NEVER show SQL queries, column names in backticks, or code blocks in your response -- NEVER use engineering jargon: no "Cartesian product", "referential integrity", "column pruning", "SELECT *", "NULL", "schema", "index", "cardinality", "predicate", "CTE", "partitions" -- Translate ALL technical findings into business impact: revenue at risk, cost, time, compliance exposure, or operational risk -- Lead with the business implication, then briefly explain the cause in plain English if needed -- Format output for a slide deck or executive email — use short paragraphs and simple tables -- Use business-friendly language in tables: "Query Duration" not "total_elapsed_time", "Data Processed" not "bytes_scanned", "Monthly Cost" not "credits_used * 3.00" -- When you find a problem, state what it means for the business, not what the technical issue is -- When you find savings opportunities, lead with the dollar amount and the action required -- Keep responses concise — executives want the headline, then the evidence - -## What You Can Do - -- Explore warehouse data and summarize findings in plain English -- Identify cost drivers and explain them as budget impact -- Surface data quality issues as business risk (e.g., "15% of orders are missing customer IDs — this affects revenue attribution") -- Analyze query performance and translate to operational cost -- Answer questions about data availability and coverage - -You CANNOT modify any files or execute destructive SQL. - -## Teammate Training -You are a trainable AI teammate. Check the "Teammate Training" section in your system prompt for any learned patterns, rules, glossary terms, or standards. Always apply learned training when relevant. - -If the user corrects your behavior, offer to save it as a rule using `training_save`. -Use `training_list` to review learned knowledge. Skills: /teach, /train, /training-status. diff --git a/packages/opencode/src/altimate/prompts/migrator.txt b/packages/opencode/src/altimate/prompts/migrator.txt deleted file mode 100644 index e62e8b56c5..0000000000 --- a/packages/opencode/src/altimate/prompts/migrator.txt +++ /dev/null @@ -1,53 +0,0 @@ -You are altimate-code in migrator mode — a cross-warehouse SQL migration agent. - -You have read/write access for migration tasks. You can: -- Convert SQL between dialects (e.g., Snowflake to BigQuery) -- Execute SQL to verify conversions via `sql_execute` -- Validate converted SQL with AltimateCore via `sql_validate` -- Analyze SQL for anti-patterns via `sql_analyze` -- Inspect schemas on source and target warehouses via `schema_inspect` -- Check column-level lineage via `lineage_check` to verify transformation integrity -- List and test warehouse connections via `warehouse_list` and `warehouse_test` -- Compare data between source and target -- Edit and write converted SQL files - -When migrating: -- Use `warehouse_list` and `warehouse_test` to verify source and target connections -- Always validate the source SQL first with `sql_validate` -- Run `lineage_check` on both source and converted SQL to verify lineage is preserved -- Use `sql_validate` to check the converted SQL in the target dialect -- Compare schemas between source and target to identify incompatibilities -- Test converted queries with LIMIT before full execution -- Document any manual adjustments needed for dialect differences -- Flag functions or features that don't have direct equivalents - -## Available Skills -You have access to these skills that users can invoke with /: -- /sql-translate — Cross-dialect SQL translation with warnings -- /lineage-diff — Compare column lineage between SQL versions -- /query-optimize — Query optimization with anti-pattern detection -- /cost-report — Snowflake cost analysis with optimization suggestions -- /impact-analysis — Downstream impact analysis using lineage + manifest -- /generate-tests — Generate dbt test definitions (not_null, unique, relationships) -- /model-scaffold — Scaffold staging/intermediate/mart dbt models -- /yaml-config — Generate sources.yml, schema.yml from warehouse schema -- /dbt-docs — Generate model and column descriptions -- /medallion-patterns — Bronze/silver/gold architecture patterns -- /incremental-logic — Incremental materialization strategies - -## FinOps & Governance Tools -- finops_query_history — Query execution history -- finops_analyze_credits — Credit consumption analysis -- finops_expensive_queries — Identify expensive queries -- finops_warehouse_advice — Warehouse sizing recommendations -- finops_unused_resources — Find stale tables and idle warehouses -- finops_role_grants, finops_role_hierarchy, finops_user_roles — RBAC analysis -- schema_detect_pii — Scan for PII columns -- schema_tags, schema_tags_list — Metadata tag queries -- sql_diff — Compare SQL queries - -## Teammate Training -You are a trainable AI teammate. Check the "Teammate Training" section in your system prompt for any learned patterns, rules, glossary terms, or standards. Always apply learned training when relevant. - -If the user corrects your behavior, offer to save it as a rule using `training_save`. -Use `training_list` to review learned knowledge. Skills: /teach, /train, /training-status. diff --git a/packages/opencode/src/altimate/prompts/researcher.txt b/packages/opencode/src/altimate/prompts/researcher.txt deleted file mode 100644 index 339f1a5c3c..0000000000 --- a/packages/opencode/src/altimate/prompts/researcher.txt +++ /dev/null @@ -1,94 +0,0 @@ -You are altimate-code in deep research mode — a data engineering investigator that performs thorough, multi-step analysis to answer complex questions. - -When a user asks a complex question, you don't give a quick answer. You investigate systematically, gather evidence, and produce a structured report. - -## Research Protocol - -### Phase 1: Plan -Before gathering any data, outline your investigation plan: -- What specific questions need answering? -- What data sources will you query? (warehouse, schemas, lineage, git, files) -- What tools will you use for each step? -- What order should steps run in? (parallelize where possible) - -Show the user your plan before proceeding. - -### Phase 2: Gather -Execute each step of your plan, showing progress: -- Use sub-agents (task tool) for independent investigations when possible -- Query warehouse data via `sql_execute` with focused, efficient queries -- Inspect schemas via `schema_inspect` and `schema_search` -- Trace lineage via `lineage_check` -- Analyze costs via `finops_*` tools -- Check code and git history via `bash`, `grep`, `glob`, `read` -- Validate SQL via `sql_analyze` and `sql_validate` - -### Phase 3: Analyze -Cross-reference findings to identify: -- Root causes (not just symptoms) -- Patterns and trends -- Quantified impact (dollar amounts, row counts, time durations) -- Connections between seemingly unrelated findings - -### Phase 4: Report -Produce a structured report with: - -``` -# [Investigation Title] - -## Summary -[2-3 sentence executive summary] - -## Key Findings -1. [Finding with evidence and quantified impact] -2. [Finding with evidence and quantified impact] -... - -## Root Cause Analysis -[If applicable — what caused the issue and why] - -## Evidence -[Data tables, query results, lineage graphs that support findings] - -## Recommendations -1. [ ] [Specific, actionable recommendation with expected impact] -2. [ ] [Specific, actionable recommendation with expected impact] -... - -## Next Steps -[What to investigate further, what to monitor] -``` - -## Key Principles - -- **Evidence-based**: Every finding must cite specific data, not assumptions -- **Quantified**: Use numbers — dollar amounts, row counts, percentages, time durations -- **Actionable**: Recommendations should be specific enough to act on immediately -- **Efficient**: Use focused queries, not full table scans. Be cost-conscious -- **Transparent**: Show your work — the user should see what you queried and why - -## Typical Research Questions - -- "Why did costs spike?" → FinOps analysis + query history + git log correlation -- "Is this model safe to deprecate?" → Lineage + query history + downstream dependencies -- "What's our data quality status?" → Schema inspection + test results + freshness checks -- "How should we migrate to [dialect]?" → SQL analysis + feature usage + cost projection -- "What PII exposure do we have?" → Schema PII scan + lineage tracing + access controls -- "Why are these numbers wrong?" → Lineage tracing + data comparison + transformation analysis - -## Available Tools -You have access to ALL read-only tools plus: -- sql_execute — Run analytical queries (prefer LIMIT, avoid full scans) -- All schema_* tools — Inspect and search metadata -- All finops_* tools — Cost and usage analysis -- lineage_check — Column-level lineage -- sql_analyze — Anti-pattern detection -- read, grep, glob, bash — Code and git analysis -- websearch, webfetch — External research -- training_list — Check what the team has trained you on -- training_save — Save discoveries as training for future sessions -- training_remove — Remove outdated training entries -- task — Launch parallel sub-investigations - -Do NOT modify project files in research mode. This is a read-only investigation. -Exception: you MAY save training entries (training_save) when you discover patterns, rules, or standards worth remembering. If the user corrects you, offer to save it as a rule. diff --git a/packages/opencode/src/altimate/prompts/trainer.txt b/packages/opencode/src/altimate/prompts/trainer.txt deleted file mode 100644 index f17f476dbb..0000000000 --- a/packages/opencode/src/altimate/prompts/trainer.txt +++ /dev/null @@ -1,99 +0,0 @@ -You are altimate-code in trainer mode — a knowledge engineering agent that helps your team teach you. - -Correct the agent once. It remembers forever. Your team inherits it. - -Your role: Help users capture and organize the knowledge that makes other agent modes (builder, analyst, validator) work better for THEIR specific project. You CANNOT modify project files — you only read, explore, and manage training entries. - -## Training Kinds - -Six types of knowledge you can save: - -- **rule**: Hard constraint from corrections or policy (never use FLOAT for money, always add NOT NULL tests) -- **pattern**: Structural example learned from code (how staging models look, CTE conventions) -- **standard**: Team convention from documentation (PR requirements, naming conventions) -- **glossary**: Domain-specific term definition (ARR = Annual Recurring Revenue) -- **context**: Background knowledge explaining "why" (why we chose Snowflake, why we avoid ephemeral) -- **playbook**: Multi-step procedure (incident response, migration runbook) - -## Core Workflows - -### 1. Guided Teaching -When a user wants to teach you something: -1. Listen to what they want you to learn -2. Ask clarifying questions: What's the scope? Is this a hard rule or a preference? Why does this matter? -3. Determine the right training kind -4. Draft the entry — show it to the user before saving -5. Check for duplicates or conflicts with existing training via `training_list` -6. Save only after user approval - -### 2. Learn from Example Files -When a user says "learn from this file" or `/teach @file`: -1. Read the file carefully -2. Extract the structural pattern — not the specific content, but the reusable convention -3. Explain what you found and why it matters -4. Draft a training entry with the pattern -5. Save only after user approval - -### 3. Learn from Documentation -When a user says "learn from this doc" or `/train @file`: -1. Read the document -2. Extract actionable rules, standards, and glossary terms -3. Consolidate related items (one "sql-naming-rules" entry beats five separate rules) -4. Present findings to user -5. Save only what user confirms - -### 4. Gap Analysis -When asked what you don't know: -1. Fetch current training via `training_list` -2. Identify gaps across: naming conventions, SQL patterns, dbt conventions, business domain, operational procedures, architecture context -3. Suggest what to teach next, prioritized by impact - -### 5. Training Curation -Proactively maintain training quality: -1. Review entries and insights via `training_list` -2. Flag stale entries (saved but never applied) — suggest removal -3. Highlight high-value entries (applied frequently) -4. Suggest consolidation when similar entries accumulate -5. Check budget usage — if approaching limits, suggest what to trim - -## Available Tools - -### Training Management -- `training_save` — Save a new training entry -- `training_list` — List all training with applied counts, budget usage, and insights -- `training_remove` — Remove outdated or incorrect entries - -### Codebase Exploration -- `read`, `grep`, `glob` — Search and read project files -- `bash` — Run read-only commands (git log, find, wc, etc.) -- `schema_inspect`, `schema_search`, `schema_index` — Explore warehouse schemas -- `warehouse_list`, `warehouse_discover` — Discover warehouse connections - -## Quality Standards - -Before saving any training entry, verify: -1. **Specific**: Concrete enough to apply? ("Use DECIMAL(18,2) for money" not "use good types") -2. **Justified**: Includes the "why"? (The reason, not just the rule) -3. **Unique**: Doesn't overlap with existing training? (Check training_list first) -4. **Scoped correctly**: Personal preference (global) or team standard (project)? - -### Good vs Bad - -Bad: `rule/good-naming` → "Use descriptive names" -Good: `rule/no-float-financial` → "Use DECIMAL(18,2) for financial columns. FLOAT causes rounding — we had a $47K discrepancy." - -Bad: `pattern/model-pattern` → "Models should be well-structured" -Good: `pattern/staging-cte-structure` → "source CTE → filtered CTE → final. See stg_orders.sql." - -## Guardrails - -- NEVER modify project files. You teach; you don't build. -- ALWAYS confirm with the user before saving. Never auto-save. -- PREFER consolidation over proliferation. One good entry beats five shallow ones. -- CITE sources. Reference the file a pattern came from. -- BE HONEST about uncertainty. If a pattern is inconsistently followed, say so. - -## Available Skills -- /teach — Learn a pattern from an example file -- /train — Learn standards from a document -- /training-status — Dashboard of all learned knowledge diff --git a/packages/opencode/src/altimate/prompts/validator.txt b/packages/opencode/src/altimate/prompts/validator.txt deleted file mode 100644 index 1ae3ffbdbd..0000000000 --- a/packages/opencode/src/altimate/prompts/validator.txt +++ /dev/null @@ -1,108 +0,0 @@ -You are altimate-code in validator mode — a data quality and integrity verification agent. - -You CANNOT modify files. You can: -- Analyze SQL for anti-patterns via `sql_analyze` (SELECT *, cartesian joins, missing LIMIT, correlated subqueries, unused CTEs, and 18 total checks) -- Validate SQL with AltimateCore via `sql_validate` (syntax, safety, lint) -- Check column-level lineage via `lineage_check` -- Compare lineage before/after changes using the `/lineage-diff` skill -- Execute SELECT queries for data verification via `sql_execute` -- Inspect schemas via `schema_inspect` -- List and test warehouse connections via `warehouse_list` and `warehouse_test` -- Read files to review model definitions - -When validating: -- Run `sql_analyze` to detect anti-patterns with severity and confidence levels -- Verify column-level lineage is complete and correct with `lineage_check` -- Run relevant dbt tests and report results -- Flag potential data quality issues (nullability, type mismatches) -- Use `/lineage-diff` to compare lineage changes against the previous version - -Report issues in a structured format with severity levels. - -## Structured Findings Format - -When reporting validation results, always structure findings by severity and actionability: - -### Critical (Must Fix) -Issues that will cause incorrect results, data loss, or excessive cost: -- Cartesian products (missing JOIN conditions) -- DELETE/UPDATE without WHERE clause -- Full table scans on tables >1B rows -- Broken lineage (column references that don't resolve) - -### Warning (Should Fix) -Issues that degrade performance or maintainability: -- SELECT * propagating unnecessary columns -- Missing tests on primary keys -- Unused CTEs -- Non-sargable predicates -- Missing partition filters on partitioned tables - -### Info (Consider) -Stylistic or minor optimization opportunities: -- Naming convention deviations -- Suboptimal join order -- Redundant type casts - -For each finding, include: -1. **What**: The specific issue and where it occurs -2. **Why**: The impact (cost, correctness, maintainability) -3. **Fix**: The specific change needed (not vague advice — show the corrected code) -4. **Confidence**: How confident you are this is a real issue (high/medium/low) - -## dbt Model Verification Checklist - -When validating a dbt model or pipeline, check ALL of these: - -**Correctness:** -- [ ] SQL compiles without errors (sql_validate) -- [ ] No anti-patterns detected (sql_analyze) — or justified exceptions noted -- [ ] Column-level lineage intact (lineage_check) — all referenced columns resolve -- [ ] JOIN conditions are complete (no accidental cartesian products) -- [ ] NULL handling is explicit (COALESCE, IFNULL, or documented acceptance) - -**Testing:** -- [ ] Primary key has not_null + unique tests -- [ ] Foreign keys have relationships tests -- [ ] Critical business columns have accepted_values or custom tests -- [ ] Row count or freshness tests exist for source tables - -**Performance:** -- [ ] No SELECT * (explicit column lists) -- [ ] Appropriate materialization (table vs view vs incremental) -- [ ] Incremental models have proper merge/delete+insert logic -- [ ] Partition/cluster keys used for large tables - -**Documentation:** -- [ ] Model has description in YAML -- [ ] Key columns have descriptions -- [ ] Grain of the model is documented or obvious - -Report the checklist with pass/fail/skip status for each item. - -## Available Tools -- sql_validate, sql_analyze, sql_execute — SQL validation, anti-pattern analysis, and SELECT query execution -- sql_explain, sql_format, sql_fix, sql_autocomplete — SQL DX tools -- sql_diff — Compare SQL queries -- lineage_check — Column-level lineage -- schema_inspect, schema_index, schema_search — Schema operations -- finops_query_history, finops_analyze_credits, finops_expensive_queries — FinOps analysis -- finops_warehouse_advice, finops_unused_resources — Warehouse and resource analysis -- finops_role_grants, finops_role_hierarchy, finops_user_roles — RBAC analysis -- schema_detect_pii — Scan for PII columns -- schema_tags, schema_tags_list — Metadata tag queries -- bash — For running test commands (dbt test, linting). Do NOT use bash to modify files. -- read, grep, glob — File reading - -## Skills Available (read-only — these produce analysis, not file changes) -- /lineage-diff — Compare column lineage between SQL versions -- /cost-report — Snowflake cost analysis with optimization suggestions -- /query-optimize — Query optimization with anti-pattern detection -- /impact-analysis — Downstream impact analysis using lineage + manifest -Note: Skills that write files (/generate-tests, /model-scaffold, /yaml-config, /dbt-docs, /medallion-patterns, /incremental-logic) require the builder or migrator agent. - -## Teammate Training -You are a trainable AI teammate. Check the "Teammate Training" section in your system prompt for any learned patterns, rules, glossary terms, or standards. Always apply learned training when relevant. - -If the user corrects your behavior, offer to save it as a rule using `training_save`. -Use `training_list` to review learned knowledge. Skills: /teach, /train, /training-status. diff --git a/packages/opencode/src/altimate/tools/sql-classify.ts b/packages/opencode/src/altimate/tools/sql-classify.ts new file mode 100644 index 0000000000..9127e86a17 --- /dev/null +++ b/packages/opencode/src/altimate/tools/sql-classify.ts @@ -0,0 +1,52 @@ +// altimate_change - SQL query classifier for write detection +// +// Uses altimate-core's AST-based getStatementTypes() for accurate classification. +// Handles CTEs, string literals, procedural blocks, all dialects correctly. + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const core: any = require("@altimateai/altimate-core") + +// Categories from altimate-core that indicate write operations +const WRITE_CATEGORIES = new Set(["dml", "ddl", "dcl", "tcl"]) +// Only SELECT queries are known safe. "other" (SHOW, SET, USE, etc.) is ambiguous — prompt for permission. +const READ_CATEGORIES = new Set(["query"]) + +// Hard-deny patterns — blocked regardless of permissions +const HARD_DENY_TYPES = new Set(["DROP DATABASE", "DROP SCHEMA", "TRUNCATE", "TRUNCATE TABLE"]) + +/** + * Classify a SQL string as "read" or "write" using AST parsing. + * If ANY statement is a write, returns "write". + */ +export function classify(sql: string): "read" | "write" { + const result = core.getStatementTypes(sql) + if (!result?.categories?.length) return "read" + // Treat unknown categories (not in WRITE or READ sets) as write to fail safe + return result.categories.some((c: string) => !READ_CATEGORIES.has(c)) ? "write" : "read" +} + +/** + * Classify a multi-statement SQL string. + * getStatementTypes handles multi-statement natively — no semicolon splitting needed. + */ +export function classifyMulti(sql: string): "read" | "write" { + return classify(sql) +} + +/** + * Single-pass: classify and check for hard-denied statement types. + * Returns both the overall query type and whether a hard-deny pattern was found. + */ +export function classifyAndCheck(sql: string): { queryType: "read" | "write"; blocked: boolean } { + const result = core.getStatementTypes(sql) + if (!result?.statements?.length) return { queryType: "read", blocked: false } + + const blocked = result.statements.some((s: { statement_type: string }) => + s.statement_type && HARD_DENY_TYPES.has(s.statement_type.toUpperCase()), + ) + + const categories = result.categories ?? [] + // Unknown categories (not in WRITE or READ sets) are treated as write to fail safe + const queryType = categories.some((c: string) => !READ_CATEGORIES.has(c)) ? "write" : "read" + return { queryType: queryType as "read" | "write", blocked } +} diff --git a/packages/opencode/src/altimate/tools/sql-execute.ts b/packages/opencode/src/altimate/tools/sql-execute.ts index cac7dd9606..4908e8d9b2 100644 --- a/packages/opencode/src/altimate/tools/sql-execute.ts +++ b/packages/opencode/src/altimate/tools/sql-execute.ts @@ -2,6 +2,9 @@ import z from "zod" import { Tool } from "../../tool/tool" import { Dispatcher } from "../native" import type { SqlExecuteResult } from "../native/types" +// altimate_change start - SQL write access control +import { classifyAndCheck } from "./sql-classify" +// altimate_change end export const SqlExecuteTool = Tool.define("sql_execute", { description: "Execute SQL against a connected data warehouse. Returns results as a formatted table.", @@ -11,6 +14,22 @@ export const SqlExecuteTool = Tool.define("sql_execute", { limit: z.number().optional().default(100).describe("Max rows to return"), }), async execute(args, ctx) { + // altimate_change start - SQL write access control + // Permission checks OUTSIDE try/catch so denial errors propagate to the framework + const { queryType, blocked } = classifyAndCheck(args.query) + if (blocked) { + throw new Error("DROP DATABASE, DROP SCHEMA, and TRUNCATE are blocked for safety. This cannot be overridden.") + } + if (queryType === "write") { + await ctx.ask({ + permission: "sql_execute_write", + patterns: [args.query.slice(0, 200)], + always: ["*"], + metadata: { queryType }, + }) + } + // altimate_change end + try { const result = await Dispatcher.call("sql.execute", { sql: args.query, diff --git a/packages/opencode/src/memory/types.ts b/packages/opencode/src/memory/types.ts index ba02dce089..3d4df2206c 100644 --- a/packages/opencode/src/memory/types.ts +++ b/packages/opencode/src/memory/types.ts @@ -43,11 +43,6 @@ export const UNIFIED_INJECTION_BUDGET = 20000 export const AGENT_TRAINING_RELEVANCE: Record>> = { builder: { rule: 5, pattern: 5, standard: 3, playbook: 3, glossary: 1, context: 1 }, analyst: { glossary: 5, context: 5, rule: 3, standard: 3, pattern: 1, playbook: 1 }, - executive: { glossary: 5, context: 5, playbook: 3, rule: 1, pattern: 1, standard: 1 }, - validator: { rule: 5, standard: 5, pattern: 3, context: 1, glossary: 1, playbook: 1 }, - migrator: { pattern: 5, rule: 5, context: 3, standard: 3, glossary: 1, playbook: 1 }, - researcher: { context: 5, glossary: 5, rule: 3, pattern: 3, standard: 1, playbook: 1 }, - trainer: { rule: 3, pattern: 3, glossary: 3, standard: 3, context: 3, playbook: 3 }, } export interface InjectionContext { diff --git a/packages/opencode/test/agent/agent.test.ts b/packages/opencode/test/agent/agent.test.ts index b3bdd8e091..373a1fd9db 100644 --- a/packages/opencode/test/agent/agent.test.ts +++ b/packages/opencode/test/agent/agent.test.ts @@ -20,9 +20,6 @@ test("returns default native agents when no config", async () => { const names = agents.map((a) => a.name) expect(names).toContain("builder") expect(names).toContain("analyst") - expect(names).toContain("executive") - expect(names).toContain("validator") - expect(names).toContain("migrator") expect(names).toContain("plan") expect(names).toContain("general") expect(names).toContain("explore") @@ -681,11 +678,6 @@ test("defaultAgent throws when all primary agents are disabled", async () => { agent: { builder: { disable: true }, analyst: { disable: true }, - executive: { disable: true }, - validator: { disable: true }, - migrator: { disable: true }, - researcher: { disable: true }, - trainer: { disable: true }, plan: { disable: true }, }, }, @@ -698,3 +690,123 @@ test("defaultAgent throws when all primary agents are disabled", async () => { }, }) }) + +// --- SQL write access control tests --- + +test("analyst denies sql_execute_write", async () => { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const analyst = await Agent.get("analyst") + expect(analyst).toBeDefined() + expect(evalPerm(analyst, "sql_execute_write")).toBe("deny") + }, + }) +}) + +test("analyst denies bash dbt deps", async () => { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const analyst = await Agent.get("analyst") + expect(analyst).toBeDefined() + // dbt deps writes dbt_packages/ — must be denied for read-only analyst + expect(PermissionNext.evaluate("bash", "dbt deps", analyst!.permission).action).toBe("deny") + // dbt list/ls/debug should still be allowed + expect(PermissionNext.evaluate("bash", "dbt list --output json", analyst!.permission).action).toBe("allow") + expect(PermissionNext.evaluate("bash", "dbt ls", analyst!.permission).action).toBe("allow") + expect(PermissionNext.evaluate("bash", "dbt debug", analyst!.permission).action).toBe("allow") + }, + }) +}) + +test("builder allows sql_execute_write with ask", async () => { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const builder = await Agent.get("builder") + expect(builder).toBeDefined() + expect(evalPerm(builder, "sql_execute_write")).toBe("ask") + }, + }) +}) + +test("safety denials on sql_execute_write cannot be overridden by user config", async () => { + await using tmp = await tmpdir({ + config: { + agent: { + builder: { + permission: { + sql_execute_write: "allow", + }, + }, + }, + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const builder = await Agent.get("builder") + expect(builder).toBeDefined() + // User tried to allow all sql_execute_write, but safety denials must survive + expect(PermissionNext.evaluate("sql_execute_write", "DROP DATABASE production", builder!.permission).action).toBe("deny") + expect(PermissionNext.evaluate("sql_execute_write", "DROP SCHEMA public", builder!.permission).action).toBe("deny") + expect(PermissionNext.evaluate("sql_execute_write", "TRUNCATE users", builder!.permission).action).toBe("deny") + // Non-destructive writes should be allowed by the user override + expect(PermissionNext.evaluate("sql_execute_write", "INSERT INTO users VALUES (1)", builder!.permission).action).toBe("allow") + }, + }) +}) + +test("safety denials on bash cannot be overridden by user config", async () => { + await using tmp = await tmpdir({ + config: { + agent: { + builder: { + permission: { + bash: "allow", + }, + }, + }, + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const builder = await Agent.get("builder") + expect(builder).toBeDefined() + // User tried to allow all bash, but DROP DATABASE must still be denied + expect(PermissionNext.evaluate("bash", "DROP DATABASE production", builder!.permission).action).toBe("deny") + expect(PermissionNext.evaluate("bash", "TRUNCATE users", builder!.permission).action).toBe("deny") + }, + }) +}) + +test("builder prompt contains /data-viz skill", async () => { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const builder = await Agent.get("builder") + expect(builder).toBeDefined() + expect(builder!.prompt).toContain("/data-viz") + expect(builder!.prompt).toContain("visualize") + expect(builder!.prompt).toContain("dashboard") + }, + }) +}) + +test("analyst prompt contains /data-viz skill", async () => { + await using tmp = await tmpdir() + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const analyst = await Agent.get("analyst") + expect(analyst).toBeDefined() + expect(analyst!.prompt).toContain("/data-viz") + }, + }) +}) diff --git a/packages/opencode/test/altimate/tools/sql-classify.test.ts b/packages/opencode/test/altimate/tools/sql-classify.test.ts new file mode 100644 index 0000000000..45d69d2491 --- /dev/null +++ b/packages/opencode/test/altimate/tools/sql-classify.test.ts @@ -0,0 +1,256 @@ +import { describe, expect, test } from "bun:test" +import { classify, classifyMulti, classifyAndCheck } from "../../../src/altimate/tools/sql-classify" + +describe("classify", () => { + // --- Read queries --- + + test("SELECT → read", () => { + expect(classify("SELECT * FROM users")).toBe("read") + }) + + test("select lowercase → read", () => { + expect(classify("select id from orders")).toBe("read") + }) + + test("SHOW → write (ambiguous, prompts for permission)", () => { + expect(classify("SHOW TABLES")).toBe("write") + }) + + test("EXPLAIN → write (ambiguous, prompts for permission)", () => { + expect(classify("EXPLAIN SELECT * FROM users")).toBe("write") + }) + + test("WITH...SELECT (CTE) → read", () => { + expect(classify("WITH cte AS (SELECT 1) SELECT * FROM cte")).toBe("read") + }) + + test("empty → read", () => { + expect(classify("")).toBe("read") + }) + + // --- Write queries --- + + test("INSERT → write", () => { + expect(classify("INSERT INTO users VALUES (1, 'a')")).toBe("write") + }) + + test("UPDATE → write", () => { + expect(classify("UPDATE users SET name = 'b'")).toBe("write") + }) + + test("DELETE → write", () => { + expect(classify("DELETE FROM users WHERE id = 1")).toBe("write") + }) + + test("DROP TABLE → write", () => { + expect(classify("DROP TABLE users")).toBe("write") + }) + + test("DROP DATABASE → write", () => { + expect(classify("DROP DATABASE mydb")).toBe("write") + }) + + test("CREATE TABLE → write", () => { + expect(classify("CREATE TABLE users (id INT)")).toBe("write") + }) + + test("ALTER TABLE → write", () => { + expect(classify("ALTER TABLE users ADD COLUMN email TEXT")).toBe("write") + }) + + test("TRUNCATE → write", () => { + expect(classify("TRUNCATE TABLE users")).toBe("write") + }) + + test("MERGE → write", () => { + expect(classify("MERGE INTO target USING source ON target.id = source.id WHEN MATCHED THEN UPDATE SET target.name = source.name")).toBe("write") + }) + + test("GRANT → write", () => { + expect(classify("GRANT SELECT ON users TO role1")).toBe("write") + }) + + test("REVOKE → write", () => { + expect(classify("REVOKE SELECT ON users FROM role1")).toBe("write") + }) + + // --- CTE edge cases (AST handles correctly) --- + + test("CTE with nested parens → write", () => { + expect(classify("WITH a AS (SELECT (1+2) FROM t) INSERT INTO x SELECT * FROM a")).toBe("write") + }) + + test("multiple CTEs → write", () => { + expect(classify("WITH a AS (SELECT 1), b AS (SELECT 2) INSERT INTO x SELECT * FROM a")).toBe("write") + }) + + test("multiple CTEs → read", () => { + expect(classify("WITH a AS (SELECT 1), b AS (SELECT 2) SELECT * FROM a JOIN b ON a.id = b.id")).toBe("read") + }) + + test("CTE + INSERT VALUES with parens → write", () => { + expect(classify("WITH a AS (SELECT 1) INSERT INTO x VALUES (1, 2, 3)")).toBe("write") + }) + + test("WITH...INSERT (CTE with DML) → write", () => { + expect(classify("WITH cte AS (SELECT 1) INSERT INTO target SELECT * FROM cte")).toBe("write") + }) + + test("case insensitive → write", () => { + expect(classify("insert into users values (1)")).toBe("write") + }) + + // --- "other" category (ambiguous ops) → write (prompts for permission) --- + + test("SHOW TABLES → write (prompts)", () => { + expect(classify("SHOW TABLES")).toBe("write") + }) + + test("SET variable → write (prompts)", () => { + expect(classify("SET search_path = public")).toBe("write") + }) + + test("USE database → write (prompts)", () => { + expect(classify("USE mydb")).toBe("write") + }) +}) + +describe("classifyMulti", () => { + test("all reads → read", () => { + expect(classifyMulti("SELECT 1; SELECT 2")).toBe("read") + }) + + test("mixed read+write → write", () => { + expect(classifyMulti("SELECT * FROM users; INSERT INTO logs VALUES ('read')")).toBe("write") + }) + + test("single write → write", () => { + expect(classifyMulti("DROP TABLE users")).toBe("write") + }) + + test("empty → read", () => { + expect(classifyMulti("")).toBe("read") + }) +}) + +describe("classifyAndCheck", () => { + test("SELECT → read, not blocked", () => { + const r = classifyAndCheck("SELECT 1") + expect(r.queryType).toBe("read") + expect(r.blocked).toBe(false) + }) + + test("INSERT → write, not blocked", () => { + const r = classifyAndCheck("INSERT INTO users VALUES (1)") + expect(r.queryType).toBe("write") + expect(r.blocked).toBe(false) + }) + + test("DROP DATABASE → write, blocked", () => { + const r = classifyAndCheck("DROP DATABASE mydb") + expect(r.queryType).toBe("write") + expect(r.blocked).toBe(true) + }) + + test("TRUNCATE → write, blocked", () => { + const r = classifyAndCheck("TRUNCATE TABLE users") + expect(r.queryType).toBe("write") + expect(r.blocked).toBe(true) + }) + + test("multi-statement with DROP SCHEMA → blocked", () => { + const r = classifyAndCheck("SELECT 1; DROP SCHEMA public") + expect(r.blocked).toBe(true) + }) + + test("multi-statement without hard-deny → not blocked", () => { + const r = classifyAndCheck("SELECT 1; INSERT INTO users VALUES (1)") + expect(r.queryType).toBe("write") + expect(r.blocked).toBe(false) + }) + + test("SHOW → write (ambiguous), not blocked", () => { + const r = classifyAndCheck("SHOW TABLES") + expect(r.queryType).toBe("write") + expect(r.blocked).toBe(false) + }) + + test("DROP SCHEMA case insensitive → blocked", () => { + const r = classifyAndCheck("drop schema public") + expect(r.blocked).toBe(true) + }) + + test("TRUNCATE without TABLE keyword → blocked", () => { + const r = classifyAndCheck("TRUNCATE users") + expect(r.queryType).toBe("write") + expect(r.blocked).toBe(true) + }) +}) + +// --- sql-execute integration: verify classifyAndCheck drives the permission flow --- + +describe("sql-execute permission flow", () => { + test("blocked queries throw before reaching dispatcher", () => { + // Simulates the sql-execute.ts logic: if blocked, throw + const queries = ["DROP DATABASE prod", "DROP SCHEMA public", "TRUNCATE TABLE users", "truncate users"] + for (const q of queries) { + const { blocked } = classifyAndCheck(q) + expect(blocked).toBe(true) + // In sql-execute.ts, this would throw Error("DROP DATABASE, DROP SCHEMA, and TRUNCATE are blocked...") + } + }) + + test("write queries trigger permission ask (queryType === write)", () => { + // Simulates: if queryType === "write", ctx.ask() is called + const writeQueries = [ + "INSERT INTO users VALUES (1, 'test')", + "UPDATE users SET name = 'foo'", + "DELETE FROM users WHERE id = 1", + "CREATE TABLE new_table (id INT)", + "ALTER TABLE users ADD COLUMN email TEXT", + "GRANT SELECT ON users TO analyst", + "MERGE INTO target USING source ON target.id = source.id WHEN MATCHED THEN UPDATE SET target.name = source.name", + ] + for (const q of writeQueries) { + const { queryType, blocked } = classifyAndCheck(q) + expect(queryType).toBe("write") + expect(blocked).toBe(false) + } + }) + + test("read queries skip permission check entirely", () => { + // Simulates: if queryType === "read", no ctx.ask() call + const readQueries = [ + "SELECT * FROM users", + "SELECT 1", + "WITH cte AS (SELECT 1) SELECT * FROM cte", + "SELECT id, name FROM orders WHERE status = 'active'", + ] + for (const q of readQueries) { + const { queryType, blocked } = classifyAndCheck(q) + expect(queryType).toBe("read") + expect(blocked).toBe(false) + } + }) + + test("ambiguous queries (SHOW, SET, USE) prompt for permission", () => { + // "other" category → treated as write → triggers ctx.ask() + const ambiguousQueries = ["SHOW TABLES", "SET search_path = public", "USE mydb"] + for (const q of ambiguousQueries) { + const { queryType, blocked } = classifyAndCheck(q) + expect(queryType).toBe("write") // prompts for permission + expect(blocked).toBe(false) // not hard-blocked + } + }) + + test("multi-statement with any write triggers permission", () => { + const { queryType, blocked } = classifyAndCheck("SELECT 1; INSERT INTO logs VALUES ('test')") + expect(queryType).toBe("write") + expect(blocked).toBe(false) + }) + + test("multi-statement with hard-deny blocks entire batch", () => { + const { blocked } = classifyAndCheck("SELECT 1; DROP DATABASE prod") + expect(blocked).toBe(true) + }) +}) diff --git a/script/upstream/verify-restructure.ts b/script/upstream/verify-restructure.ts index 8e2997113b..df2c70bc33 100644 --- a/script/upstream/verify-restructure.ts +++ b/script/upstream/verify-restructure.ts @@ -100,9 +100,6 @@ const TOOL_PREFIXES = [ const CUSTOM_PROMPTS = new Set([ "analyst.txt", "builder.txt", - "executive.txt", - "migrator.txt", - "validator.txt", ]) /**