Skip to content

feat(wren): add strict query mode for SQL policy enforcement#1500

Open
goldmedal wants to merge 4 commits intoCanner:mainfrom
goldmedal:claude/elated-almeida
Open

feat(wren): add strict query mode for SQL policy enforcement#1500
goldmedal wants to merge 4 commits intoCanner:mainfrom
goldmedal:claude/elated-almeida

Conversation

@goldmedal
Copy link
Copy Markdown
Contributor

@goldmedal goldmedal commented Apr 1, 2026

Summary

  • Add configurable strict query mode to the wren standalone CLI that validates all SQL queries against the MDL manifest before execution
  • Table validation: in strict mode, every table reference must be an MDL-defined model — queries like SELECT * FROM pg_shadow are rejected with MODEL_NOT_FOUND
  • Function deny list: configurable denied_functions list blocks dangerous functions (e.g., pg_read_file, dblink) with BLOCKED_FUNCTION error
  • Configuration via ~/.wren/config.json only (no CLI args), with WREN_HOME env var to override the config directory

Config format

{
  "strict_mode": true,
  "denied_functions": ["pg_read_file", "dblink", "lo_import"]
}

Files changed

File Change
wren/src/wren/config.py NewWrenConfig dataclass + load_config()
wren/src/wren/policy.py Newvalidate_sql_policy() with table + function checks
wren/src/wren/model/error.py Add MODEL_NOT_FOUND, BLOCKED_FUNCTION codes + SQL_POLICY_CHECK phase
wren/src/wren/engine.py Accept config, call validator in _plan()
wren/src/wren/cli.py Load config from WREN_HOME, pass to engine
wren/README.md Document config.json in quick start

Test plan

  • 10 config loading tests (missing file, malformed JSON, partial config, etc.)
  • 15 policy validation tests (table check, CTE exclusion, function deny, case-insensitive, nested subqueries)
  • 4 engine integration tests (strict blocks unknown table, allows MDL table, blocks denied function, non-strict backward compat)
  • All 50 unit tests passing, lint clean

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Optional user config (WREN_HOME or ~/.wren/config.json) to enforce SQL security policies
    • Strict mode to reject queries referencing undefined tables
    • Deny list to block specified SQL functions (case-insensitive)
  • Documentation

    • README updated with config schema, examples, and reordered "Run queries" step (WREN_HOME override noted)
  • Tests

    • New unit tests covering config loading, SQL policy checks, and engine behavior

Add configurable strict mode to scope all SQL access to MDL-defined
models and block dangerous functions before queries reach the database.

- New ~/.wren/config.json with strict_mode and denied_functions settings
- Table validation: reject queries referencing tables not in MDL
- Function deny list: block specified functions (case-insensitive)
- Config file-only (no CLI args), WREN_HOME env var support
- New error codes: MODEL_NOT_FOUND, BLOCKED_FUNCTION

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@github-actions github-actions bot added the documentation Improvements or additions to documentation label Apr 1, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 1, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 41cee85f-45d5-4e6a-8ffd-d6b5ef74885a

📥 Commits

Reviewing files that changed from the base of the PR and between 0d479da and f7d03e0.

📒 Files selected for processing (1)
  • wren/src/wren/cli.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • wren/src/wren/cli.py

📝 Walkthrough

Walkthrough

Adds a new immutable WrenConfig loaded from WREN_HOME/~/.wren/config.json, CLI wiring to load and pass it to the engine, engine integration to enforce SQL policies during planning, a policy module validating sqlglot ASTs for unknown models and denied functions, tests, and README updates.

Changes

Cohort / File(s) Summary
Configuration module & tests
wren/src/wren/config.py, wren/tests/unit/test_config.py
New WrenConfig dataclass and load_config(Path) reading config.json, validating types, normalizing denied_functions to lowercase frozenset, returning defaults or raising WrenError on invalid input; unit tests cover defaults, loading, normalization, and error cases.
Policy validation
wren/src/wren/policy.py, wren/tests/unit/test_policy.py
New validate_sql_policy(ast, model_names, config) enforcing strict_mode (reject unknown tables/TVFs with MODEL_NOT_FOUND) and denied_functions (block functions as BLOCKED_FUNCTION), with CTE scope awareness and case-insensitive matching; comprehensive tests added.
Engine integration & tests
wren/src/wren/engine.py, wren/tests/unit/test_engine.py
WrenEngine.__init__ gains config parameter (defaults to WrenConfig()); _plan runs policy validation before manifest/model extraction and changes error-wrapping behavior when policy checks are active; tests updated to assert policy-driven outcomes.
CLI wiring
wren/src/wren/cli.py
CLI now derives _WREN_HOME from WREN_HOME env var (fallback ~/.wren), loads config via load_config, passes config into WrenEngine, and prints config load errors to stderr then exits with status 1.
Policy/error enums
wren/src/wren/model/error.py
Added ErrorCode.MODEL_NOT_FOUND, ErrorCode.BLOCKED_FUNCTION, and ErrorPhase.SQL_POLICY_CHECK.
Documentation
wren/README.md
Docs: describe optional ~/.wren/config.json schema (strict_mode, denied_functions), renumbered run-query steps, and clarify WREN_HOME override.
Tests / minor cleanups
wren/tests/unit/test_cte_rewriter.py
Minor test cleanups (removed unused imports, collapsed SQL literals) with no behavior changes.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant CLI as "CLI"
  participant Config as "Config Loader\n(load_config)"
  participant Engine as "WrenEngine"
  participant Policy as "Policy Validator\n(validate_sql_policy)"
  CLI->>Config: resolve WREN_HOME\ncall load_config(path)
  Config-->>CLI: return WrenConfig or raise WrenError
  CLI->>Engine: construct WrenEngine(..., config)
  Engine->>Policy: validate_sql_policy(ast, model_names, config)
  Policy-->>Engine: success or raise WrenError
  Engine->>Engine: proceed with manifest rewrite / planning
  Engine-->>CLI: return plan or raise WrenError
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

python

Suggested reviewers

  • douenergy

Poem

🐇 I sniff the config, small and neat,
I hop through SQL to guard each seat.
Tables and funcs I check with care,
keeping queries tidy, safe, and fair.
Hooray — the wren will watch and share!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(wren): add strict query mode for SQL policy enforcement' directly and accurately describes the main feature being introduced - a strict query mode with SQL policy enforcement including table and function validation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
wren/src/wren/cli.py (1)

130-150: ⚠️ Potential issue | 🟠 Major

Convert config/engine construction failures into CLI exits.

load_config() and WrenEngine(...) now run before the surrounding command try blocks, so a malformed or unreadable config.json will bubble out as a Typer traceback instead of the existing Error: + exit code 1 behavior.

Proposed fix
 def _build_engine(
     datasource: str | None,
     mdl: str | None,
     connection_info: str | None,
     connection_file: str | None,
     *,
     conn_required: bool = True,
 ):
     from wren.config import load_config  # noqa: PLC0415
     from wren.engine import WrenEngine  # noqa: PLC0415
     from wren.model.data_source import DataSource  # noqa: PLC0415
+    from wren.model.error import WrenError  # noqa: PLC0415
@@
-    config = load_config(_WREN_HOME)
-    return WrenEngine(
-        manifest_str=manifest_str,
-        data_source=ds,
-        connection_info=conn_dict,
-        config=config,
-    )
+    try:
+        config = load_config(_WREN_HOME)
+        return WrenEngine(
+            manifest_str=manifest_str,
+            data_source=ds,
+            connection_info=conn_dict,
+            config=config,
+        )
+    except WrenError as e:
+        typer.echo(f"Error: {e}", err=True)
+        raise typer.Exit(1) from e
 def dry_plan(
@@
 ):
     """Plan SQL through MDL and print the expanded SQL (no DB required)."""
     from wren.config import load_config  # noqa: PLC0415
     from wren.engine import WrenEngine  # noqa: PLC0415
     from wren.model.data_source import DataSource  # noqa: PLC0415
+    from wren.model.error import WrenError  # noqa: PLC0415
@@
-    config = load_config(_WREN_HOME)
+    try:
+        config = load_config(_WREN_HOME)
+    except WrenError as e:
+        typer.echo(f"Error: {e}", err=True)
+        raise typer.Exit(1) from e
     with WrenEngine(
         manifest_str=manifest_str, data_source=ds, connection_info={}, config=config
     ) as engine:

Also applies to: 290-312

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@wren/src/wren/cli.py` around lines 130 - 150, The config loading and
WrenEngine construction can raise exceptions that currently escape the CLI and
produce tracebacks; wrap the calls to load_config(_WREN_HOME) and the
WrenEngine(...) constructor in the same kind of try/except handling used for
DataSource so that any error is caught, printed via typer.echo with a clear
"Error:" message, and then exit with raise typer.Exit(1); specifically update
the block that creates config = load_config(_WREN_HOME) and the return
WrenEngine(...) to handle and convert errors (e.g., FileNotFoundError,
JSONDecodeError, ValueError or generic Exception) into a CLI-friendly
error+typer.Exit(1), and apply the same wrapping to the analogous construction
around lines 290-312.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wren/src/wren/config.py`:
- Around line 54-62: Replace the current permissive coercions in config parsing
with explicit type checks: in the config loader (look for strict_mode,
denied_raw, denied_functions in wren/src/wren/config.py) ensure strict_mode is a
bool (raise WrenError with ErrorCode.GENERIC_USER_ERROR if not), ensure
denied_raw is a list (already checked) and then validate every element in
denied_raw is a string before lowercasing and turning into denied_functions
(raise WrenError if any element is non-string); then add regression unit tests
in wren/tests/unit/test_config.py that assert a WrenError is raised for
{"strict_mode": "false"} and for a config where denied_functions contains mixed
types (e.g., ["safe", 1, {"obj":true}]).

In `@wren/src/wren/engine.py`:
- Around line 178-179: The current fix flattens CTE names via
CTERewriter._collect_user_cte_names and passes a global set to
validate_sql_policy, which allows out-of-scope CTEs to hide real tables; change
the flow so validate_sql_policy receives scope-aware CTE visibility (e.g., have
CTERewriter._collect_user_cte_names return a scoped structure such as a mapping
of AST nodes/with-blocks to their CTE name sets or a stack-based scope resolver,
and update validate_sql_policy to consult that scoped map when checking table
references) and add a regression test using the provided nested-shadowing SQL
snippet to assert that an outer FROM secret_table is not considered safe by an
inner WITH secret_table. Ensure you update function signatures
(CTERewriter._collect_user_cte_names and validate_sql_policy) and all call sites
in engine.py (where user_cte_names is produced) to use the new scoped
representation.

In `@wren/src/wren/policy.py`:
- Around line 47-54: The current _check_tables loop only inspects exp.Table
nodes and skips nodes with empty name, letting table-valued functions (TVFs)
like unnest/read_csv/generate_series bypass strict-mode validation; update
_check_tables (the loop over ast.find_all(exp.Table)) to also detect TVFs by: 1)
scanning FROM-clause expressions for exp.Func or known TVF node types (e.g.,
exp.Unnest, exp.ReadCSV, exp.GenerateSeries) since exp.TableFunction doesn't
exist in sqlglot 30.1.0, 2) applying the same name/whitelist/model checks for
those TVF nodes (treat their function name or node type as the referenced table)
and 3) in strict mode raise WrenError when a TVF is used and not in user CTEs or
model_names; ensure you reference the same helper variables
(user_cte_names_lower, model_names_lower) and keep existing continue/raise
behavior.

---

Outside diff comments:
In `@wren/src/wren/cli.py`:
- Around line 130-150: The config loading and WrenEngine construction can raise
exceptions that currently escape the CLI and produce tracebacks; wrap the calls
to load_config(_WREN_HOME) and the WrenEngine(...) constructor in the same kind
of try/except handling used for DataSource so that any error is caught, printed
via typer.echo with a clear "Error:" message, and then exit with raise
typer.Exit(1); specifically update the block that creates config =
load_config(_WREN_HOME) and the return WrenEngine(...) to handle and convert
errors (e.g., FileNotFoundError, JSONDecodeError, ValueError or generic
Exception) into a CLI-friendly error+typer.Exit(1), and apply the same wrapping
to the analogous construction around lines 290-312.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 35a6ce89-4d53-42e2-aa34-8dcc63d1a25f

📥 Commits

Reviewing files that changed from the base of the PR and between a524a75 and cfb1432.

📒 Files selected for processing (9)
  • wren/README.md
  • wren/src/wren/cli.py
  • wren/src/wren/config.py
  • wren/src/wren/engine.py
  • wren/src/wren/model/error.py
  • wren/src/wren/policy.py
  • wren/tests/unit/test_config.py
  • wren/tests/unit/test_engine.py
  • wren/tests/unit/test_policy.py

…king

- Validate config.json types strictly: reject non-bool strict_mode and
  non-string denied_functions entries instead of silently coercing
- Wrap load_config/WrenEngine construction in try/except in CLI for
  friendly error output instead of raw tracebacks
- Make CTE name visibility scope-aware: walk up the AST to find
  ancestor WITH clauses instead of using a flat global set, preventing
  nested CTEs from shadowing outer table references
- Block table-valued functions (read_csv, generate_series, unnest) in
  strict mode — they previously bypassed validation via empty name or
  non-Table AST nodes
- Remove user_cte_names parameter from validate_sql_policy (now internal)
- Add 9 regression tests covering all review findings

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wren/src/wren/cli.py`:
- Line 14: The WREN_HOME environment value isn't being expanded so values like
"~/ .wren" remain literal; update the _WREN_HOME initialization to expand the
environment variable with os.path.expanduser before converting to Path (i.e.,
get os.environ.get("WREN_HOME", Path.home() / ".wren"), pass the retrieved
string through os.path.expanduser or expanduser on Path, then wrap with Path) so
the _WREN_HOME variable correctly resolves leading "~"; reference the symbol
_WREN_HOME and use os.path.expanduser (or Path(...).expanduser()) to implement
the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ecb2cac1-dc08-4211-ba3a-4b1660b231d2

📥 Commits

Reviewing files that changed from the base of the PR and between cfb1432 and 636d360.

📒 Files selected for processing (8)
  • wren/src/wren/cli.py
  • wren/src/wren/config.py
  • wren/src/wren/engine.py
  • wren/src/wren/policy.py
  • wren/tests/unit/test_config.py
  • wren/tests/unit/test_cte_rewriter.py
  • wren/tests/unit/test_engine.py
  • wren/tests/unit/test_policy.py
✅ Files skipped from review due to trivial changes (2)
  • wren/tests/unit/test_cte_rewriter.py
  • wren/tests/unit/test_policy.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • wren/src/wren/config.py
  • wren/tests/unit/test_engine.py
  • wren/src/wren/policy.py

- Add .expanduser() to _WREN_HOME so WREN_HOME=~/.wren resolves correctly
- Change error fallback guard from strict_mode-only to
  strict_mode or denied_functions, ensuring policy enforcement is never
  bypassed when only denied_functions is configured

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@wren/src/wren/cli.py`:
- Around line 315-319: The config loading block that calls
load_config(_WREN_HOME) only catches WrenError; update the exception handling to
also catch OSError (filesystem errors) so filesystem issues are reported the
same way as WrenError: catch OSError alongside WrenError around load_config,
call typer.echo with the error message and raise typer.Exit(1) from the caught
exception. Reference the load_config call, the WrenError exception class, and
typer.Exit to locate where to add the OSError handling.
- Around line 145-155: The current try/except only catches WrenError, but
load_config can raise other exceptions (e.g., PermissionError from
config_path.exists()), so broaden the exception handling around the load_config
call and WrenEngine creation: catch Exception (in addition to WrenError) when
calling load_config and constructing WrenEngine in cli.py, convert it to a clean
CLI error by calling typer.echo with the exception message and then raise
typer.Exit(1) from the caught exception; reference load_config, WrenError,
WrenEngine and typer.Exit to locate the block to modify.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4f0b34f0-2cbd-4693-8dbd-208d417d8888

📥 Commits

Reviewing files that changed from the base of the PR and between 636d360 and 0d479da.

📒 Files selected for processing (2)
  • wren/src/wren/cli.py
  • wren/src/wren/engine.py

Filesystem errors (e.g. PermissionError) from load_config now produce
a clean "Error:" message instead of a raw traceback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@goldmedal goldmedal requested a review from douenergy April 2, 2026 07:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant