Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions packages/browser-extension/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,26 @@ All notable changes to the browser extension are documented here. Versions match
`manifest.json`. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.4.0] - 2026-08-20

Carries the engine changes the extension bundles, plus its own model picker.

### Added
- The schema now states what a column's type cannot: the unit of an integer timestamp, and the shape of
a JSON column, including whether it holds objects or a list. Comparing epoch milliseconds against
epoch seconds matches every row and raises no error, and a guessed JSON key matches none, so both
produced confident wrong answers. Supported on SQLite, PostgreSQL, MySQL, DuckDB and Oracle.
- A filter that compares a column against a value the column does not hold is now reported, rather than
answering zero as though nothing matched the question.

### Fixed
- Model listing reports the provider's own error instead of coming back empty, so a rejected key, a rate
limit and an outage are no longer indistinguishable.
- Models that cannot answer a question are no longer offered in the picker.
- Output from reasoning models no longer appears in answers, explanations or the live token stream.
- A hosted provider configured with a local base URL is refused instead of being sent the API key.
- Uploaded CSV and Parquet files now get the same column descriptions as a database connection.

## [0.3.2] - 2026-08-18

Carries an engine fix the extension bundles. Nothing in the extension's own code changed.
Expand Down
103 changes: 71 additions & 32 deletions packages/browser-extension/STORE-CERTIFICATION-NOTES.md
Original file line number Diff line number Diff line change
@@ -1,50 +1,89 @@
# Notes for Certification

Paste the section below into **Submission Options > Notes for Certification** when resubmitting to the
Microsoft Edge Add-ons store. It answers policy 1.3.1 (Product is Testable), which the 08/13/2026
review flagged. Product ID: 248cd48a-7dbe-4cfd-8ec0-df1e07231acd
Paste the block below into **Submission Options > Notes for Certification** every time the extension is
submitted or resubmitted. This field is **private to the review team** and is not shown on the public
listing, so a temporary API key can safely go in it.

The 08/13/2026 and 08/18/2026 reviews both flagged policy 1.3.1 (Product is Testable) with identical
wording. The cause was not the wording of these notes: the field reaching Microsoft did not contain
them. The release workflow can send them automatically, but its Edge upload step is gated behind
`EDGE_PUBLISH_ENABLED` and has never run, so every submission so far has been manual. If the field is
filled in by hand, it must be filled in with this.

## Before submitting

1. Create a **free Groq API key** at <https://console.groq.com> (no card required) and paste it into
the block below where it says `PASTE_KEY_HERE`. Groq's free tier is rate limited and costs nothing.
2. Note the date you created it. **Revoke it once the review completes** - it exists only for the
reviewer.
3. Do not reuse a key that has billing attached, and never commit a real key to this file.
4. Confirm the model named below still exists: `curl https://api.groq.com/openai/v1/models -H "Authorization: Bearer $KEY"`.
Providers retire models without notice - Groq removed every Llama chat model, and the name previously
printed here answered 404, which would read to a reviewer as a product that does not work.

---

Product ID: 248cd48a-7dbe-4cfd-8ec0-df1e07231acd

**Why no test account credentials are provided**
**Test credentials**

AskSQL has no accounts and no sign-in, so there is no account to issue. What it does need is an AI
provider, which the user supplies. So that the review does not depend on you creating one, here is a
temporary key we created for this submission and will revoke afterwards:

Provider: Groq
API key: PASTE_KEY_HERE

Nothing else is needed. There is no database to connect to for this test, no server of ours, and
nothing to install.

**Test it in about two minutes**

1. Save these six lines as `sales.csv` anywhere on the machine:

id,customer,region,amount
1,Ada,EU,1200.50
2,Grace,NA,980.00
3,Kat,NA,1500.25
4,Ada,EU,300.00
5,Linus,APAC,75.99

2. Click the AskSQL toolbar icon to open the side panel, then open **Settings**.
3. Under **AI provider**, choose **Groq**, paste the key above, click **Fetch models**, pick
`openai/gpt-oss-20b`, and click **Test provider**. It reports success.
4. Under **Connections**, click **Add connection**, choose **Data files**, and select `sales.csv`.
The file is read inside the browser into DuckDB-WASM; nothing is uploaded.
5. In the side panel, ask: **"What is the total amount per region?"**
You should see the SQL it wrote, and a result of three rows: EU 1500.50, NA 2480.25, APAC 75.99.

AskSQL has no accounts, no sign-in, and no server of our own. Nothing is hosted by us, so there is no
credential we could issue. The extension stores its settings locally and talks only to two things the
user chooses: their own data files, and their own AI model provider.
Asking "delete all rows" is a good second test: the extension refuses it, because the generated SQL is
checked and only read-only statements are allowed to run.

Because of that, testing needs no credentials from us. It needs a model provider and a data file, and
both can be supplied at no cost in a few minutes.
**If you prefer to use no key at all**

**Fastest way to test, with no API key and no account (about 5 minutes)**
Steps 1, 2 and 4 work with no provider configured and no network access: the file loads, the tables
and columns are listed, and the UI is fully exercised. Only step 5, which needs a model, requires the
key. A local model also works: install Ollama from ollama.com, run `ollama pull qwen2.5-coder:7b`, and
choose provider **Ollama** with base URL `http://localhost:11434/v1` and no key.

1. Install Ollama from https://ollama.com (free, no account required) and run:
`ollama pull qwen2.5-coder:7b`
2. Start Ollama with `OLLAMA_ORIGINS=* ollama serve` so it serves on http://127.0.0.1:11434.
The variable matters: fetching the model list works without it, but asking a question fails with
403, because Ollama rejects the extension's origin on POST requests.
3. Open the extension's Options page, choose provider **Ollama**, click **Fetch models**, pick the
model, and click **Test provider**. It should report success.
4. Add a connection: click **Add connection**, choose **Data files**, and select any CSV or Excel
file. Any small spreadsheet works; no database server is needed.
5. Open the side panel and ask a question about the file, for example "how many rows are there?" or
"show me the first 10 records".
**Permissions**

**Alternative, if you prefer a hosted provider**
Host permissions are optional and requested per site, only when the tester configures an AI endpoint or
an AskSQL server at that address. They are never requested up front.

Any OpenAI, Anthropic or Groq API key works. Enter it in the Options page under the matching provider
and follow steps 3 to 5 above. We cannot include one of our keys in this submission, because the key
would be visible to anyone who reads the listing and would be billed to us.
`declarativeNetRequestWithHostAccess` is used for exactly one purpose: removing the `Origin` header
from requests to the AI endpoint the user configured. Local AI servers such as Ollama and LM Studio
reject a `chrome-extension://` origin by default, which would otherwise make the extension unusable
with a local model. The rule removes a header. It never adds, forges, blocks or redirects anything, and
never applies to any other address. PRIVACY.md in the package documents this.

**What the extension sends where**
**Data handling**

Questions and database schema go only to the provider the user configures, over a connection they
control. Data files are read in the browser and never uploaded to us. The extension has no analytics
and no backend. Generated SQL is read-only and is checked before it runs, so a query cannot modify
the user's data.
No analytics, no telemetry, and no server operated by us. Only the schema - table and column names -
and the question the user typed are sent to the AI endpoint the user chose. Row data and query results
are never sent. Data files are read in the browser and never uploaded.

**If anything blocks the review**

Please include the Product ID in any reply and we will respond quickly with whatever else is helpful,
including a recorded walkthrough if that is easier than running it locally.
Please include the Product ID in any reply. We will respond quickly with whatever helps, including a
recorded walkthrough if that is easier than running it.
14 changes: 11 additions & 3 deletions packages/browser-extension/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"manifest_version": 3,
"name": "AskSQL",
"short_name": "AskSQL",
"version": "0.3.2",
"version": "0.4.0",
"description": "Ask your database questions in plain language. Read-only by design, zero telemetry. Query files in-browser or your AskSQL server.",
"minimum_chrome_version": "116",
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk17oLrOl3oNPAgteUmUQDhUJJo+lxrQDt7baBBxmyQ3w3nE1g7IOZVGtq6q+gItzvs8aJAe+dHKanZynRWJfeK8zqCiBNDzyMqnRpAMOOtvKlJJOTqe9N9Lj/RJgxRgfylPnYJoPNXMAPd2y4ceeJX6yj0GpTPiS9AIfTO6p6WXB9YGWl5gdwjhGuYEAB+S7vD+M8yaCCn8C1b2+ne0yboTf/3nUyh6XysrAPafNCMlUbU1qBjAQGNJgXdsbRtWfaIbKorKhN4tWgsc3SsXZBdIxXsz/oL3EMzspjWjTEIrf/n5p1q3kNJ+SQ40SunWXdjDY4q/LnPnGmQFCUona7wIDAQAB",
Expand All @@ -29,8 +29,16 @@
"default_path": "sidepanel/index.html"
},
"options_page": "options/index.html",
"permissions": ["sidePanel", "storage", "contextMenus", "declarativeNetRequestWithHostAccess"],
"optional_host_permissions": ["http://*/*", "https://*/*"],
"permissions": [
"sidePanel",
"storage",
"contextMenus",
"declarativeNetRequestWithHostAccess"
],
"optional_host_permissions": [
"http://*/*",
"https://*/*"
],
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
}
Expand Down
2 changes: 1 addition & 1 deletion packages/browser-extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "asksql-browser-extension",
"private": true,
"type": "module",
"version": "0.3.2",
"version": "0.4.0",
"description": "AskSQL for Chromium browsers (Edge + Chrome): zero-backend DuckDB-WASM file chat and @asksql/server sidecar mode in a Manifest V3 side panel.",
"scripts": {
"fetch-duckdb-extensions": "node scripts/fetch-duckdb-extensions.mjs",
Expand Down
10 changes: 9 additions & 1 deletion packages/browser-extension/src/listModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,15 @@ import { ensureProviderOriginAccess } from './providerAccess.js';
const LISTABLE_HOSTED: ReadonlySet<ProviderName> = new Set(['openai', 'groq', 'nvidia']);
const MODEL_LOOKUP_TIMEOUT_MS = 10_000;

const isNotChatModel = (name: string): boolean => /embed|embedding|rerank|retriever|[-/]parse$|\bocr\b/i.test(name);
/**
* Listed beside chat models but rejected by /chat/completions. Groq is why speech, TTS and classifier
* models are here: of its 13 entries only 7 can chat, and the list is alphabetical, so a broken one sits
* where the user picks. `\bguard\b` and not `guard`, because gpt-oss-safeguard DOES chat.
*/
const isNotChatModel = (name: string): boolean =>
/embed|embedding|rerank|retriever|[-/]parse$|\bocr\b|whisper|\btts\b|speech|transcribe|orpheus|\bguard\b|moderation/i.test(
name,
);

/** The endpoint we can list models from, if any (anthropic/google/azure have no such listing). */
export function listableBaseUrl(provider: ProviderName, configuredBaseURL: string | undefined): string | undefined {
Expand Down
2 changes: 1 addition & 1 deletion packages/browser-extension/src/options/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ function ProviderSection({
id="model"
type="text"
value={provider.model}
placeholder="e.g. llama-3.3-70b-versatile"
placeholder="click Fetch models, or type a model id"
onChange={(e) => edit({ ...provider, model: e.target.value })}
/>
{canListModels && (
Expand Down
36 changes: 36 additions & 0 deletions packages/browser-extension/test/listModels.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,42 @@ describe('fetchProviderModels', () => {
expect(await fetchProviderModels('nvidia', undefined, 'nvapi-1')).toEqual(['meta/llama-3.3-70b-instruct']);
});

// Groq's real catalogue as of 2026-08-19, verified model by model against /chat/completions: six of
// the thirteen reject a chat request ("does not support chat completions", "text classification models
// do not support streaming", "requires terms acceptance"). The list is alphabetical, so two broken ones
// sat at positions 2 and 3 - where a user picks - and the query failed after the model list looked fine.
it('offers only the Groq models that can actually answer a query', async () => {
globalThis.fetch = vi.fn(async () =>
jsonResponse(200, {
data: [
{ id: 'allam-2-7b' },
{ id: 'canopylabs/orpheus-arabic-saudi' },
{ id: 'canopylabs/orpheus-v1-english' },
{ id: 'groq/compound' },
{ id: 'groq/compound-mini' },
{ id: 'meta-llama/llama-prompt-guard-2-22m' },
{ id: 'meta-llama/llama-prompt-guard-2-86m' },
{ id: 'openai/gpt-oss-120b' },
{ id: 'openai/gpt-oss-20b' },
{ id: 'openai/gpt-oss-safeguard-20b' },
{ id: 'qwen/qwen3.6-27b' },
{ id: 'whisper-large-v3' },
{ id: 'whisper-large-v3-turbo' },
],
}),
) as typeof fetch;
// gpt-oss-safeguard is kept on purpose: it contains "guard" but chats, so the test is \bguard\b.
expect(await fetchProviderModels('groq', undefined, 'gsk-test')).toEqual([
'allam-2-7b',
'groq/compound',
'groq/compound-mini',
'openai/gpt-oss-120b',
'openai/gpt-oss-20b',
'openai/gpt-oss-safeguard-20b',
'qwen/qwen3.6-27b',
]);
});

it('lists ollama models, filtering out embedding models', async () => {
globalThis.fetch = vi.fn(async () =>
jsonResponse(200, { models: [{ name: 'llama3.2' }, { name: 'nomic-embed-text' }] }),
Expand Down
16 changes: 16 additions & 0 deletions packages/core/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# @asksql/core

## 0.9.0

### Minor Changes

- Describe what a column's type leaves out, and fix a set of AI provider issues.

The schema now states two things a column type cannot: the unit of an integer timestamp, and the shape
of a JSON column. Comparing epoch milliseconds against epoch seconds matches every row and raises no
error, and a guessed JSON key matches none, so both produced confident wrong answers. Every engine emits
the hint in its own syntax, from one shared implementation.

Also fixed: model listing now reports the provider's own error instead of returning an empty list;
models that cannot answer a question are no longer offered; a hosted provider configured with a local
base URL is refused rather than sent the API key; and reasoning-model output no longer appears in
answers, explanations, or the token stream.

## 0.8.1

### Patch Changes
Expand Down
14 changes: 9 additions & 5 deletions packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { PostgresConnector } from '@asksql/postgres';

const model = await resolveModel({
provider: 'groq', // openai | anthropic | google | azure | groq | nvidia | ollama | openai-compatible
model: 'llama-3.3-70b-versatile',
model: 'openai/gpt-oss-20b', // an example: use whatever your provider lists at /models
apiKey: process.env.GROQ_API_KEY,
});

Expand Down Expand Up @@ -113,10 +113,14 @@ Beyond ask -> approve -> run, all optional:
- **Schema pruning + token budget** - large catalogs are pruned to the most relevant tables
under a token budget (`config.pruner`) before prompting.
- **Privacy by default** - only the schema is ever sent. `allowDataInPrompt` (default off) is the
opt-in for sampled cell values; with it off they are stripped at the single exit from the catalog,
so a connector that samples cannot leak them into any prompt - the first prompt, a repair,
`explain`, or `explainSchema`. The MongoDB engine takes the same option, gating the values its
document sampling infers. Declared enum labels come from the schema and are kept either way.
opt-in for cell values, and it now gates three channels, not one: sampled column values, stripped
from the catalog so a connector that samples cannot leak them into any prompt (the first prompt, a
repair, `explain`, or `explainSchema`); the key NAMES inside a JSON column, where the default states
how many recur but not which, because a map with a stable key set is structurally identical to a
record; and the distinct values named in a coded-column repair, where the default attaches a caveat
for the reader instead. The MongoDB engine takes the same option for the values its document
sampling infers. Declared enum labels come from the schema and are kept either way. Query results
are never sent on any path.

Prompts, model sampling, guard policy, and grounding (glossary, few-shots) are configurable
without forking; see
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@asksql/core",
"version": "0.8.1",
"version": "0.9.0",
"description": "AskSQL engine: schema catalog, AST SQL guard, prompt pipeline, LLM orchestration. Zero database drivers.",
"type": "module",
"main": "./dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const FK_CLOSURE_HOPS = 2;
const VALUE_SAMPLE_CAP = 80;

/** A sampled/enum value rendered into the schema: `|` is replaced, whitespace flattened, length capped. */
function sanitizeValue(v: string): string {
export function sanitizeValue(v: string): string {
const flat = v.replace(/\s+/gu, ' ').trim().replace(/\|/gu, '/');
return flat.length > VALUE_SAMPLE_CAP ? flat.slice(0, VALUE_SAMPLE_CAP) : flat;
}
Expand Down
Loading
Loading