Skip to content

Releases: alisadeghiaghili/local-sql-agent

6.2.0 — schema-qualified table names and a checked schema qualifier

Choose a tag to compare

@github-actions github-actions released this 29 Sep 07:00
c60586c

Tables with the same name in different schemas can be described and
queried, and a query's schema is now checked against the allowlist.

Fixed

  • schema.yaml table keys may name their schema (PR #133). A key may be qualified, such as sales.Customer or OtherDb.dbo.Customer; brackets are allowed. This is what lets sales.Customer and ref.Customer both be described. Before, a qualified key never matched a query, so every query on it was refused as an unknown table.
  • An ambiguous table name is refused with the fix named (PR #133). FROM Customer, when two schemas have a Customer, is refused with the new guard reason ambiguous_table. The message names the references to use, so the model's retry corrects it, and the web UI explains it in Persian.
  • The prompt shows how to reference a shared name (PR #133). Such tables get a Reference as: [sales].[Customer] line. Prompts for schemas with unique names are unchanged.

Security

  • A query's schema is checked, not ignored (PR #133). Before, the guard matched tables by bare name only, so with Customer allowlisted in sales, a query on [hr].[Customer] passed and read a table that is not allowlisted. An explicitly qualified reference must now match the table's known schema. A table with no known schema (a bare key without db_schema) still accepts any schema, because there is nothing to check it against.

Upgrading

  • No action needed for a schema.yaml with unique table names. A generated query that names the wrong schema is now refused, and the model's retry corrects it.
  • If the same table name exists in several schemas, key each one with its schema, such as sales.Customer: and ref.Customer:.
  • Set db_schema on every bare-keyed table. Without it, that table's schema cannot be checked. See docs/design/TABLE-NAMES.md.

6.1.0 — more than one warehouse data source

Choose a tag to compare

@github-actions github-actions released this 29 Sep 07:00
c87aa1a

A deployment can query more than one warehouse database, including
databases on different servers. Existing setups keep working unchanged.

Added

  • Multiple warehouse data sources (PR #131). project_config/datasources.yaml is optional and lists named sources. Each source names only the environment variable holding its connection string, for example url_env: DB_URL_MAIN. The connection string itself stays in .env, because project_config/ is versioned and reviewed.
    • Without the file there is one source, default, using DB_CONNECTION_URL, exactly as before.
    • A template is in project_config.example/datasources.example.yaml; the design and roadmap are in docs/design/DATASOURCES.md.
  • Each table names its source (PR #131). A schema.yaml table may set datasource:; a table without it belongs to the default source. Several databases on one server are one source: a table in another database on that server sets a multi-part db_schema: "OtherDb.dbo", rendered [OtherDb].[dbo].[Table].
  • Queries are routed by their tables (PR #131). The source is derived from the tables a query reads, never chosen by the model. One query runs on one source. A query whose tables span two sources is refused with the new guard reason cross_datasource, and the web UI explains why in Persian.
  • Every source is checked (PR #131).
    • Startup refuses an application database that matches any source, and a table assigned to a source that is not configured. Admin-panel schema.yaml drafts are checked the same way.
    • /health pings every source; database_detail names each one when there are several.
    • scripts/verify_deployment.py and the admin panel run the connectivity, read-only login, row cap and query timeout checks once per source.
    • Schema drift compares each source's tables against that source's own server.
  • The audit record names the data source (PR #131). The new datasource field is the source the query's SQL targeted. Older records simply lack it.

Upgrading

  • No action needed for a single-database setup.
  • To add a second database on the same server, keep one source and give the table a multi-part db_schema, such as OtherDb.dbo.
  • To add a database on another server, copy datasources.example.yaml to project_config/datasources.yaml, set one DB_URL_* variable per source in .env, add datasource: to that server's tables in schema.yaml, and restart. Create the read-only login on every source's server (docs/db-hardening.md), then run python scripts/verify_deployment.py.
  • Changing datasources.yaml needs a restart.

6.0.2 — one API address for both pages, a settings-driven launcher, and visible startup logs

Choose a tag to compare

@alisadeghiaghili alisadeghiaghili released this 28 Sep 12:39
06368e1

Running the API and the web UI on different ports or hosts no longer needs
guesswork.

Added

  • python -m api starts the server from settings (PR #127). It uses API_HOST (default 127.0.0.1) and API_PORT (default 8000) and sends no Server header. An invalid API_PORT fails with a message naming it. The documented uvicorn api.server:app ... command still works unchanged.
  • The server logs its allowed CORS origins at startup (PR #127). It also shows a hint in both pages when the backend cannot be reached. The hint names the two likely causes (the backend is not running, or this page's origin is missing from CORS_ALLOWED_ORIGINS) and prints the origin to add.

Fixed

  • The admin panel uses the same API address as the main UI (PR #127). Both pages read it from web/js/config.js, with the same precedence: ?base=, then a saved value, then the file. If DEFAULT_BASE_URL is empty, both derive the address from the page's own host and DEFAULT_API_PORT. Before this, the admin panel ignored the file and started from http://localhost:8000.
  • API addresses are normalised and validated (PR #127). An address typed without http:// is no longer treated as a relative path. Previously that sent requests such as /admin/<host>:<port>/admin/... to the static server. Now:
    • a missing scheme is added;
    • whitespace, paths and credentials are dropped;
    • IPv6 addresses work;
    • anything else is rejected with a message, and the previous address stays in use.
  • The application's own log lines are shown (PR #127). INFO-level lines, such as the startup banner and the CORS line, were silently dropped under both documented start commands. When nothing else has configured logging, they now go to stderr at LOG_LEVEL (default INFO).

Upgrading

  • No action needed for an existing setup.
  • To use python -m api on a server reached from other machines, set API_HOST=0.0.0.0; its default listens on the local machine only.
  • If the UI is served from anywhere other than localhost:8080, set CORS_ALLOWED_ORIGINS to that origin, for example http://172.16.101.42:8077.

6.0.1 — pooled connections pinged only after idling, and a diagnostic kit for the DBA

Choose a tag to compare

@alisadeghiaghili alisadeghiaghili released this 27 Sep 13:14
2a9272b

The remaining SELECT 1 traffic to the warehouse is gone. A diagnostic kit
helps a DBA find what is really driving disk activity.

Changed

  • Pooled connections are pinged only after they have been idle (PR #124). With pool_pre_ping, every checkout of a warehouse connection sent a SELECT 1 first, so the warehouse saw one ping per query.
    • A connection is now pinged only if it has been idle for at least DB_POOL_PING_IDLE_SECONDS (default 60). 0 pings on every checkout, and DB_POOL_PRE_PING=false never pings.
    • A failed ping refreshes the whole pool, and the checkout is retried on a fresh connection.
    • Measured on 20 back-to-back questions: 20 pings in 5.2.0 and 6.0.0, none now. An idle application sends nothing.
    • Trade-off: a connection the server drops before it reaches the idle threshold makes one query fail before the pool recovers.
  • /health always sends its own SELECT 1 (PR #124), so it still verifies the database when the checkout was not pinged.
  • Comments and test docstrings describe behaviour, not history (PR #125).

Added

  • A read-only diagnostic kit for the DBA (PR #124). docs/dba/warehouse-load-diagnostics.sql and docs/dba/README.md show:

    • whether AUTO_CLOSE is on;
    • this application's sessions (program_name = 'local-sql-agent');
    • which database file (data, log or tempdb) is busy;
    • the queries with the most physical reads;
    • login audit and trigger checks.

    SELECT 1 reads no data pages, so disk activity that coincides with it usually comes from something else.

Upgrading

  • No action needed. The new DB_POOL_PING_IDLE_SECONDS defaults to 60; set it to 0 to keep pinging on every checkout.
  • Upgrading from 5.x: follow the Upgrading steps of 6.0.0 first (the system prompt must be at <PROJECT_CONFIG_DIR>/system_prompt.md).

6.0.0 — filters that apply, less warehouse load, and a public tree without one deployment's names

Choose a tag to compare

@alisadeghiaghili alisadeghiaghili released this 27 Sep 07:15
155e07e

A release about trust in what the screen says, and about the load the
application puts on the systems around it.

A hall named in a question could be silently ignored, and a filter chip could
show a value the SQL never applied. Both now either work or say plainly that
they did not. Two concurrency defects are fixed: the request cap did not
cover the analyst's main route, and the SQLite application database shared a
single connection between every request thread. The admin panel stops
re-running expensive warehouse checks every thirty seconds.

The major version is for one operational change. The system prompt now lives
in the deployment's own project-config directory, and the server will not
start until it is there. See Upgrading.

Fixed

  • A hall named in the question now filters the answer (PR #119). The dimension vocabulary only matched a question that contained a stored value in full, so naming just a hall's distinguishing word matched nothing. A new token tier matches on a value's distinctive words when the question names the dimension; when two values tie, the analyst is asked which one, never guessed for. Also in this PR:
    • The vocabulary is now warmed at startup by default, so the first question after a restart is no longer answered as if the value had not been named.
    • A named dimension whose vocabulary could not be checked produces a Persian warning instead of silence.
    • After generation, every text filter the turn presents as applied is checked against the SQL. A missing one triggers one regeneration and, if it is still missing, a Persian warning naming it.
    • Editing an inherited filter on an "among those…" turn now rebuilds the query rather than changing only the chip.
    • Choosing an answer in a "which one?" clarification is now sent to the server in live mode instead of changing only the local chip.
  • Every pipeline route is under the concurrency cap, for the whole response (PR #117). The cap covered only POST /query, and a streaming response released its slot as soon as its headers were sent. It is now pure ASGI middleware and covers /query, /query/stream, v2 turns (streaming or not) and the assumptions PATCH. The slot is held until the body is fully sent, and released on error or disconnect. All blocking pipeline work shares one worker-thread bound (QUERY_THREAD_LIMIT). The 503 SERVER_OVERLOAD response carries the request id again.
  • The SQLite application database no longer shares one connection across threads (PR #120). Concurrent requests could end each other's transactions. File-backed SQLite now gives each request its own connection, with WAL journal mode and a busy timeout, and its -wal/-shm files get the same restricted permissions as the database.
  • Approving an access request is all-or-nothing (PR #115). Approval runs in one transaction. A failure part-way leaves the request open with no key changed, so it can simply be retried. An approval and a denial of the same request can no longer both succeed.
  • No more backend English in the failure banners (PR #118). The generic database message is no longer shown under the Persian lead sentence. A database-specific message appears as a labelled technical detail («پیام پایگاه داده:»), and the guard's English rule text stays in the SQL panel rather than in the banner.
  • Table names in the migration tool's reseed step are quoted (PR #114), and the web UI tests' Node timeout is long enough for slow CI runners.

Changed

  • Much less load on the warehouse (PR #121). A DBA reported a steady stream of SELECT 1 from the application. Most of it came from the admin panel, which re-ran the deployment checks and a full schema-drift scan of the warehouse catalogue every 30 seconds. The deployment checks include a rolled-back CREATE TABLE and a WAITFOR probe.
    • Those cards now load when the page opens and on their own refresh button, with a server-side cache (ADMIN_EXPENSIVE_CACHE_TTL_SECONDS, default 300).
    • The DDL and WAITFOR checks run from the panel only when explicitly requested.
    • /health caches its database ping (HEALTH_CACHE_TTL_SECONDS, default 15).
    • pool_pre_ping is configurable (DB_POOL_PRE_PING).
    • SQL Server connections identify themselves as local-sql-agent (DB_APPLICATION_NAME).
    • The deployment runbook lists everything the application sends to the warehouse, and how often.
  • The public repository no longer names one deployment's schema (PR #122). Its schema qualifiers and table names are replaced by the example schema's own names everywhere.
    • The example configuration is now self-consistent: every relationship and every golden-set case refers to tables and columns the example schema defines, and the example Order has foreign keys to Ring, Broker and Symbol.
    • database/relationship_map.py honours PROJECT_CONFIG_DIR.
    • A test scans the contents and paths of every tracked file for the removed names; it holds them only as salted hashes.
  • The system prompt moved to the project-config directory (PR #122). It is now read from <PROJECT_CONFIG_DIR>/system_prompt.md, with a generic example in project_config.example/. prompts/few_shots.md and prompts/business_glossary.md, which nothing read at runtime, are removed.

Upgrading

  • Required: before upgrading, copy your current prompts/system_prompt.md to <PROJECT_CONFIG_DIR>/system_prompt.md. Without it:
    • the API server refuses to start with RuntimeError: System prompt not found: <path>;
    • app.py exits with status 1;
    • the eval CLI raises FileNotFoundError;
    • the webapp raises the same RuntimeError.
  • A relative PROJECT_CONFIG_DIR is now resolved against the repository root, not the working directory. A service started from another directory should check it, or use an absolute path.
  • Startup now reads the prefetched dimensions' values from the warehouse. Set DIMENSION_VOCABULARY_WARM_ON_STARTUP=false if startup must not touch the warehouse; set DIMENSION_VOCABULARY_TOKEN_FALLBACK_ENABLED=false to keep whole-value matching only.
  • New optional settings: ADMIN_EXPENSIVE_CACHE_TTL_SECONDS, HEALTH_CACHE_TTL_SECONDS, DB_POOL_PRE_PING, DB_APPLICATION_NAME (see .env.example).
  • Tests that check a deployment's own values now read them from optional files under <PROJECT_CONFIG_DIR>/_test_fixtures/ (see project_config.example/_test_fixtures/README.md) and skip without them.

5.2.0 — request access, and database errors that no longer leak

Choose a tag to compare

@alisadeghiaghili alisadeghiaghili released this 24 Sep 16:48
1958f8a

A security fix and the first of the refusal actions the design has always
specified but the product never offered.

The security fix is the same class as finding #11 of the 4.12.1 audit, this
time for the database rather than the model endpoint: when a query failed, the
raw database error reached the analyst, carrying the server's host, port and
instance, the database login name, the database name, and the full SQL with its
bound parameter values. No passwords and no data rows were exposed, but it was
a map of the infrastructure handed to anyone who could make a query fail.

The rest is about what an analyst can do when the answer is "no". A refusal
over a restricted column now offers a way to ask for that column; a refusal of
any kind now shows the statement that was refused; and every failure is
explained in Persian, in the analyst's terms, instead of in the backend's
English. The test matrix also grew to Windows and macOS, which surfaced a real
bug in how the router measured latency on Windows.

Security

  • Database error text no longer leaks infrastructure detail (PR #109). Before this change, when a query failed, database/executor.py wrapped the raw SQLAlchemy error and both the v2 conversation path and the v1 /query path showed it to the analyst. Depending on the database that exposed the server host, port and instance, the driver name and version, the database login name, the database name, and — for any execution error — the full SQL and the bound parameter values. It is the same class of problem as finding #11 in the 4.12.1 audit (the model endpoint), this time for the database. Now one shared classifier (database/errors.py) handles every path: connection and login failures become DATABASE_UNAVAILABLE and timeouts become QUERY_TIMEOUT, both with generic messages; a malformed query shows only the database's own sentence (for example Invalid column name 'X'. (207)). It fails closed: database text is shown only when the error is positively recognised as a statement error, and a final check replaces any text that still contains an IP address, a port, user@host, or driver/host/login wording. The full raw error is still written to the server log. Recognised across SQL Server, PostgreSQL, MySQL and SQLite.

Added

  • "Request access" on a denied-column refusal (PR #110). When the guard refuses a question because a column is restricted for the analyst's account, the refusal card now offers «درخواست دسترسی». A security admin reviews the request in the admin panel and approves or denies it (a denial needs a reason, which the analyst can see; the analyst sees their requests' status in the account menu). Approval removes that column from denied_columns on every live key the person holds; revoked keys are never touched. The column is taken from the server-side audit record, never from what the browser sends, and approval goes through the existing permission-changing code, so no new code path can widen access. A second request for the same column while one is open merges into it. New table access_requests.
  • "See the SQL" on a guard refusal (PR #105). The statement the guard refused is now kept, as guard.rejected_sql, and shown behind a collapsed «دیدن SQL», clearly labelled as not run. It deliberately does not reuse Turn.sql, which means "the SQL that ran". The refused statement is now also kept in the audit record, so operators can see what was blocked.
  • The audit-log report shows what the guard refused (PR #106). scripts/analyze_audit_log.py gains a section with refusals counted by reason and by subject (for example SERVERPROPERTY ×2). Verbatim refused statements appear only with --include-examples, so the default report stays safe to copy off the server.

Changed

  • Every failure the analyst sees now has a Persian sentence (PR #107). One sentence per error code and one per guard-refusal reason; the backend's English message no longer appears in those banners (it stays in the server log). Failures raised at the HTTP level, such as maintenance mode, now keep their real code instead of being reported as a network error. The out-of-scope message no longer names a specific deployment's domain.
  • CI runs on Windows and macOS as well as Linux (PRs #101, #104), across Python 3.11, 3.12 and 3.13. Getting there fixed a real bug: router latency budgets did not work on Windows before Python 3.13, where time.monotonic() resolves to only about 15.6 ms, so any backend call faster than that measured as zero and a budget breach was never detected; latency is now measured with time.perf_counter().

Fixed

  • Identifiers containing ] no longer break retrieval queries (PR #103). The two query builders in retrieval/ wrapped names in square brackets by hand without escaping; they now use sqlglot to quote identifiers.
  • The CI doctest step no longer opens real database connections (PR #102). Doctests in eval/runner.py started background threads that tried to reach the configured warehouse host.

Upgrading

  • The new access_requests table is created automatically when the application starts, like the other application-database tables, so the default setup needs no manual step.
  • Deployments that manage the application database schema with Alembic instead should run alembic upgrade head from the repository root; it applies migration 0004_access_requests.

5.1.0 — the guard's last denylist becomes an allowlist

Choose a tag to compare

@alisadeghiaghili alisadeghiaghili released this 20 Sep 19:53
3b87974

A security release for the SQL guard. Tables and columns had long since moved
to an allowlist, but functions and session variables were still governed by a
denylist — three names and two identifier prefixes. That asymmetry left a
reconnaissance channel open, and an adversarial review of the guard found it.

The gap had two spellings. A query that names no table never engages the
table/column allowlist at all: it escapes the allowlist rather than tripping
it. And a query that does name a real, allowlisted table could still carry a
metadata function in its SELECT list — SERVERPROPERTY, OBJECT_NAME,
COL_NAME and their relatives produce an output column that is neither a
column reference nor a table reference, so nothing inspected it. The second
spelling is the reason this release is broader than the table-reference rule
first proposed: requiring a table closes only the first.

Closing it properly meant finishing the job the module started — moving the
last construct from a denylist to an allowlist, which also covers metadata
functions SQL Server ships in future rather than only the ones found.

Security

  • The guard now restricts every accepted query to warehouse-reading shape:
    allowlisted tables and columns, read through ordinary data functions, and
    nothing else. Three rules run at the end of validate_sql, after every
    existing rule, so a query that already violated an earlier rule still reports
    that rule's own more specific reason — the only verdicts that change are
    ones that were previously accepted.
    • Server, session and connection state is refused wherever it appears in the
      tree — projection, WHERE, ORDER BY, a CTE body, a scalar subquery — not
      only in the SELECT list. GETDATE()/CURRENT_TIMESTAMP is deliberately
      exempt: it reads the wall clock, not the server's identity.
    • Unrecognised function calls are refused unless named in a new allowlist,
      seeded with the measured set of legitimate T-SQL data functions that
      sqlglot has no typed class for. Everything an ordinary analytic query
      uses — aggregates, casts, date parts, window functions, STRING_AGG,
      IIF — parses to a typed node and never reaches this check.
    • A query must reference at least one non-CTE table.

Added

  • A no_table_reference value on the guard verdict's reason, for the
    table-less case. It is additive: clients already fall back to the generic
    refusal action for any reason they do not specially handle. The v2 contract
    document now lists it, and a test asserts every reason the guard can emit
    appears there — the registration was two places and silently drifted to a
    stale document, so it is now three places with a failing build behind it.

Fixed

  • The rejection reported for a state-reading construct named the sqlglot
    class (PARAMETER, CURRENTUSER) rather than the construct. It now names
    what the analyst wrote (@@VERSION, CURRENT_USER, OBJECT_ID), with any
    call arguments cut so a model-generated literal never reaches the structured
    subject field — which the contract documents as analyst-facing and never
    internal detail.
  • anyio is bumped past CVE-2026-63374 and CVE-2026-64847, both published
    after the pin was last refreshed and caught by the pip-audit CI gate.

5.0.0 — analyst UI redesign

Choose a tag to compare

@alisadeghiaghili alisadeghiaghili released this 16 Sep 13:37

The analyst UI redesign. Major, because it changes muscle memory: the topbar,
the anatomy of a turn, the composer's position, and the admin navigation all
move. Nothing about the query path or the SQL guard changes; this release is
about what the screen tells you and how much of it you have to take on trust.

The organising idea is that a conversational analytics UI is a claims
interface. Every answer asserts something about the warehouse, and the screen
either shows what that claim rests on or asks the analyst to assume it. So
what a turn had buried — the resolved question, the assumptions applied, the
stage the pipeline reached, why a refusal happened — is now visible before
the result rather than folded into a drawer.

Changed — analyst-visible

  • The run-mode switch is gone from the topbar. Run mode decided whether
    the numbers came from the warehouse or from built-in fixtures, and nothing
    else on screen distinguished the two, so one mis-click sat between an
    analyst and a screen of synthetic figures that look exactly like real ones.
    Simulated mode stays fully available at ?live=0, where choosing it takes
    intent, and remains the automatic fallback when a backend cannot serve the
    v2 conversational path.
  • Turn anatomy is outcome-first. The resolved question, assumption chips
    and clarifications render before the result node, never only inside the
    details drawer. Each failure mode — guard rejection, model unavailable,
    truncated output, forbidden SQL, execution error — renders its own anatomy:
    what happened, why, and a real next action. A guard refusal is resolved
    before a result card is considered, so a refusal can no longer appear as a
    zero-row result.
  • The composer sits below the transcript, pinned to the viewport bottom.
  • Admin gains a health summary rail, a sticky section jump nav, and a
    global refresh with a last-updated stamp; per-section refresh is demoted.

Added

  • Design tokens in one file (web/styles/tokens.css). Surfaces import
    them and never redefine a hex — light and dark redefine the same custom
    properties, which is what stops the two themes drifting apart.
  • An inline SVG icon set (web/js/icons.js), built with DOM APIs so CSP
    script-src 'self' stays clean.
  • A user menu carrying theme, language and the API key — identity moved
    out of the topbar's open surface.
  • Generated SQL is prettified and highlighted like a code editor, with
    T-SQL rules for bracket identifiers and N'…' strings that stock Prism gets
    wrong. Highlighting is presentation-only: the copied text stays byte-exact.
  • A catalog-driven chart engine. Chart form is selected from a closed
    vocabulary of analytical jobs rather than by an agent, and the boundary is
    specified and pinned by tests — a wrong chart form manufactures a false
    narrative, and that error class must stay deterministic and testable.

Fixed

  • Demo mode died at boot: a const the boot path read was declared below its
    use, so module evaluation threw and the whole UI never painted.
  • All SQL highlighting was silently dead — the "already patched" marker was an
    enumerable key on the grammar, so Prism iterated it as a token and threw.
  • A settled result could sit behind the sticky composer.
  • Line and split-bar charts were offered for non-sequential data because a
    calendar word was matched as a substring rather than a whole token.
  • The line chart drew its time axis right-to-left in an RTL document.
  • The site's flagship hero query filtered on an alias that was never joined,
    so the landing page advertised SQL that cannot run.
  • The marketing site fetched four font families from Google's CDN on every
    visit, leaking visitor IPs from the page that sells local-only analytics.

Accessibility

  • Light-palette contrast now clears WCAG 1.4.3: the brand teal measured
    3.74:1 and secondary text 4.40:1 against a 4.5:1 floor. Hue and saturation
    are unchanged; only lightness moved.
  • Service health no longer rides on colour alone (WCAG 1.4.1) — state is
    carried in text and shape, so it survives both colour-vision deficiency and
    a monochrome screen.
  • Interactive control edges reach 3:1 via a dedicated token; RTL layout and
    24×24 touch targets corrected across the chrome.
  • Chrome ships no emoji: font-dependent colour glyphs render differently per
    OS, ignore currentColor, and carry no reliable accessible name.

Internal

  • Turn errors carry a request_id and a structured guard reason.
  • The design policy, its invariants, and the chart-engine boundary are
    written down under docs/design/ and pinned by tests rather than convention.
  • Includes everything in 4.12.1.

v4.12.1 — security remediation

Choose a tag to compare

@alisadeghiaghili alisadeghiaghili released this 16 Sep 07:13

Closes nineteen findings from an independent security audit (code review across MLSecOps, data, API/web and infrastructure, plus a live penetration test). Each fix carries a regression test under tests/security_audit/.

High — admin-panel stored XSS → privilege escalation (closed in four layers); webapp/ column-ACL bypass (real principal now threaded); world-readable secrets/data files (owner-only via core/fileperms.py).

Medium — internal model endpoint no longer leaks in error text on either the v1 or v2 path (the v2 turn leak was found by a post-remediation live re-test); HTTP security headers + --no-server-header; cached /health; CSV/Excel formula-injection defused; warehouse values fenced in the prompt; Flask login hardened (self-lifting throttle, hardened cookies, session-bound CSRF); dependencies pinned with requirements.lock and pip-audit in CI.

Full suite green (2760 passed). See CHANGELOG.md for the complete list.

v4.12.0 — opt-in result interpretation

Choose a tag to compare

@alisadeghiaghili alisadeghiaghili released this 08 Sep 13:48
8d243d4

Reported as "interpretation stopped working". It never worked on this path.

session/engine.py set interpretation=None as a literal, from the commit that introduced the file, and never ran an interpret stage at all — so the web UI's fifth pipeline step, labelled "تفسیر", could only ever sit at "در انتظار". That was invisible while none of the five steps moved, and conspicuous the moment 4.11.0 made the other four tick. Interpretation existed only on the /query path.

Added

The conversational path can summarise its own result, and the analyst decides whether it does.

Producing a summary sends up to twenty rows of real query results to the model, so it is opt-in per request rather than on for a whole deployment: AskTurnRequest.interpret defaults to false, matching /query's own default since phase 2, and the web UI exposes it as a checkbox under the question box that each analyst sets for themselves and that persists in their own browser. The label states the cost, not just the benefit.

Per-analyst rather than per-deployment because the person who asked the question is the one who knows whether these particular rows should go to a model.

Asking for it also runs an interpret stage, so the fifth pipeline step reports what actually happened rather than staying decorative.

Changed

The interpretation logic moved to llm/interpret.py. It lived in api/runner.py, reachable only from /query; the session layer sits below the API layer and cannot import from it. Moved rather than copied, because it carries a data-governance gate — the refusal to send rows to a remote backend without LLM_ALLOW_REMOTE — and two copies of a gate is one copy that gets fixed and one that does not. api.runner._interpret is now a thin wrapper, and its existing gate tests cover the shared implementation unchanged.

Full changelog: https://github.com/alisadeghiaghili/local-sql-agent/blob/main/CHANGELOG.md