Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
c8f8282
feat: sandbox plugin JavaScript with CSP
1-DARK Oct 11, 2026
c6abee3
Merge branch 'main' into feat/sandbox-plugin-js-csp-secrets
1-DARK Oct 12, 2026
5d3f481
fix: address review feedback (slot callbacks via RPC, CSP, read-only …
1-DARK Oct 12, 2026
8aa4cc4
Merge branch 'main' into feat/sandbox-plugin-js-csp-secrets
1-DARK Oct 12, 2026
55d5993
Merge branch 'main' into feat/sandbox-plugin-js-csp-secrets
1-DARK Oct 13, 2026
e0f5c0e
Merge branch 'main' into feat/sandbox-plugin-js-csp-secrets
debba Oct 13, 2026
2b077ea
fix: enforce read-only queries for sandboxed plugins
1-DARK Oct 13, 2026
0fa2640
Merge main and enforce sandbox plugin read-only queries
1-DARK Oct 13, 2026
78bf2b0
Merge upstream main into sandbox plugin branch
1-DARK Oct 13, 2026
04de575
Fix query classification after upstream merge
1-DARK Oct 13, 2026
739eaaf
Merge branch 'main' into feat/sandbox-plugin-js-csp-secrets
1-DARK Oct 13, 2026
6d30b8d
Merge branch 'main' into feat/sandbox-plugin-js-csp-secrets
1-DARK Oct 14, 2026
5bb3434
Merge remote-tracking branch 'upstream/main' into feat/sandbox-plugin…
1-DARK Oct 14, 2026
8ca6c6e
fix: adapt plugin slot tests and remove query row cap
1-DARK Oct 14, 2026
23f5737
Merge remote-tracking branch 'origin/feat/sandbox-plugin-js-csp-secre…
1-DARK Oct 14, 2026
b7eaa18
Merge branch 'main' into feat/sandbox-plugin-js-csp-secrets
debba Oct 14, 2026
64a3c5c
Merge branch 'main' into feat/sandbox-plugin-js-csp-secrets
1-DARK Oct 14, 2026
6bafdcc
Merge upstream/main into feat/sandbox-plugin-js-csp-secrets
1-DARK Oct 17, 2026
44cb71e
Merge remote-tracking branch 'origin/feat/sandbox-plugin-js-csp-secre…
1-DARK Oct 17, 2026
3e7329a
fix: preserve compatibility for published plugin slots
1-DARK Oct 17, 2026
801dd29
Merge upstream/main into feat/sandbox-plugin-js-csp-secrets
1-DARK Oct 19, 2026
ea41be7
fix: address sandbox plugin compatibility feedback
1-DARK Oct 19, 2026
4f7a967
Merge branch 'main' into feat/sandbox-plugin-js-csp-secrets
1-DARK Oct 20, 2026
81b540c
test: raise timeout for Kubernetes connection test
1-DARK Oct 21, 2026
51beddf
Merge upstream/main into feat/sandbox-plugin-js-csp-secrets
1-DARK Oct 21, 2026
6cc89d8
fix: pass sandbox plugin CI checks
1-DARK Oct 21, 2026
387b7d8
Merge upstream/main into feat/sandbox-plugin-js-csp-secrets
1-DARK Oct 25, 2026
f10efe8
fix(ci): format, refresh web inventory, move sandbox protocol into de…
1-DARK Oct 25, 2026
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
7 changes: 7 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ on:
- 'src-tauri/**'
- 'tests/**'
- 'packages/explain/**' # imported from source by the host
- 'packages/plugin-guest/**' # bundled into the Rust-embedded guest runtime
- 'package.json'
- 'pnpm-lock.yaml'
- 'eslint.config.js'
Expand All @@ -24,6 +25,7 @@ on:
- 'src-tauri/**'
- 'tests/**'
- 'packages/explain/**' # imported from source by the host
- 'packages/plugin-guest/**' # bundled into the Rust-embedded guest runtime
- 'package.json'
- 'pnpm-lock.yaml'
- 'eslint.config.js'
Expand Down Expand Up @@ -54,6 +56,11 @@ jobs:
- name: Install dependencies
run: pnpm install

- name: Check embedded plugin guest runtime is up to date
run: |
pnpm run build:plugin-guest
git diff --exit-code -- src-tauri/resources/guest/runtime.js

- name: Typecheck
run: pnpm run typecheck

Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Shared logic lives in `src-tauri/src/application/` (and plain modules), so the a
<!-- gitnexus:start -->
# GitNexus — Code Intelligence

This project is indexed by GitNexus as **tabularis** (10537 symbols, 25156 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
This project is indexed by GitNexus as **tabularis** (24869 symbols, 59001 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.

> Index stale? Run `node .gitnexus/run.cjs analyze` from the project root — it auto-selects an available runner. No `.gitnexus/run.cjs` yet? `npx gitnexus analyze` (npm 11 crash → `npm i -g gitnexus`; #1939).

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"dev": "pnpm --filter @tabularis/web-ui dev",
"dev:web": "node scripts/dev-web.mjs",
"build": "pnpm --filter @tabularis/web-ui build",
"build:plugin-guest": "node packages/plugin-guest/build.mjs",
"lint": "pnpm --filter @tabularis/web-ui lint",
"lint:theme": "node scripts/check-theme-tokens.mjs",
"test:contrast": "vitest --config packages/web-ui/vitest.config.ts run packages/web-ui/tests/utils/themeContrast.test.ts",
Expand Down Expand Up @@ -113,6 +114,7 @@
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.26",
"esbuild": "^0.27.4",
"gitnexus": "^1.6.9",
"globals": "^16.5.0",
"jsdom": "^28.1.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/create-plugin/scripts/smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ function scaffoldOne(kind: 'network' | 'file', withUi: boolean): void {
const manifest = JSON.parse(readFileSync(join(target, '.tabularium'), 'utf8')) as {
ui_extensions?: Array<{api_version?: string}>;
};
if (manifest.ui_extensions?.[0]?.api_version !== '0.2.0') {
if (!/^\d+\.\d+\.\d+/.test(manifest.ui_extensions?.[0]?.api_version ?? '')) {
throw new Error('UI extension manifest is missing plugin API compatibility metadata');
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/create-plugin/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {titleCase, validateDbType, validateName, validateQuote} from './validate
const PACKAGE_VERSION = (createRequire(import.meta.url)('../package.json') as {version: string})
.version;
// Caret range written into scaffolded plugins; bump alongside packages/plugin-api.
const PLUGIN_API_VERSION = '0.2.0';
const PLUGIN_API_VERSION = '0.4.0';
const MIN_TABULARIS_VERSION = '0.9.20';

function main(argv: string[]): number {
Expand Down
3 changes: 2 additions & 1 deletion packages/create-plugin/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022"],
"types": ["node"],
"strict": true,
"noImplicitAny": true,
"noUnusedLocals": true,
Expand All @@ -20,7 +21,7 @@
},
"verbatimModuleSyntax": true,
"outDir": "dist",
"rootDir": "src"
"rootDir": "../.."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

WARNING: rootDir points to repo root instead of package source directory

rootDir: "../.." (repo root) while include: ["src/**/*"] resolves to packages/create-plugin/src/**/*. This mismatch causes TypeScript to emit declaration paths like packages/create-plugin/src/... under dist/, which is likely unintended. Should be rootDir: "." or rootDir: "src".


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

WARNING: rootDir points to repository root (../..) instead of package source directory

This causes TypeScript to include all repo files in compilation, slowing down builds and potentially causing conflicts. Should be "rootDir": "src" like before.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

},
"include": ["src/**/*"],
"exclude": ["dist", "node_modules", "scripts", "tests", "templates"]
Expand Down
9 changes: 6 additions & 3 deletions packages/explain/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,13 @@ SQLite `EXPLAIN QUERY PLAN` rows:
import { parseExplain, parseExplainFor, parseRawExplain } from "@tabularis/explain";

// Sniff the format of a pasted blob or an uploaded file:
const plan = parseExplain(raw);
const plan = await parseExplain(raw);

// Or name the engine when the caller knows it:
const hinted = parseExplainFor(raw, "mysql");
const hinted = await parseExplainFor(raw, "mysql");

// Or parse the raw payload a host driver handed over:
const fromDriver = parseRawExplain({ engine, format, payload, original_query });
const fromDriver = await parseRawExplain({ engine, format, payload, original_query });
```

A host obtains the bytes however it likes — a driver, a pasted textarea, an
Expand All @@ -86,6 +86,9 @@ registerExplainParser({
```

Import the parser package before calling `parseRawExplain` or source detection.
These helpers are asynchronous because plugin parsers run in isolated guest
frames. Await `detectFormat`, `detectFormatFor`, `parseExplain`,
`parseExplainFor`, `parseRawExplain`, and `resolveExplainOutput`.
Registering the same format again replaces it, which supports plugin upgrades;
call `unregisterExplainParser(format)` when unloading it.

Expand Down
37 changes: 21 additions & 16 deletions packages/explain/src/parsers/source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ export function explainEngineFromDriverName(name: string): ExplainEngine | null
*
* Recognises the two Postgres shapes before trying registered custom sniffers.
*/
export function detectFormat(raw: string): ExplainSourceFormat {
return detectFormatFor(raw, null);
export async function detectFormat(raw: string): Promise<ExplainSourceFormat> {
return await detectFormatFor(raw, null);
}

/**
Expand All @@ -104,16 +104,19 @@ export function detectFormat(raw: string): ExplainSourceFormat {
* Built-in hints retain their historical decisions. Custom parsers are tried
* in registry order and only through their side-effect-free sniffers.
*/
export function detectFormatFor(raw: string, engine: ExplainEngine | null): ExplainSourceFormat {
export async function detectFormatFor(
raw: string,
engine: ExplainEngine | null,
): Promise<ExplainSourceFormat> {
switch (engine) {
case 'postgres':
return detectPostgresFormat(raw);
return Promise.resolve(detectPostgresFormat(raw));
case 'mysql':
if (looksLikeJson(raw)) return 'mysql-json';
if (looksLikeJson(raw)) return Promise.resolve('mysql-json');
if (raw.trim() === '') {
throw new Error('Unsupported EXPLAIN file format: input is empty');
}
return 'mysql-text';
return Promise.resolve('mysql-text');
case 'sqlite':
throw new Error(
'SQLite EXPLAIN QUERY PLAN has no text form here: pass its ' +
Expand All @@ -122,16 +125,14 @@ export function detectFormatFor(raw: string, engine: ExplainEngine | null): Expl
case null: {
const builtinFormat = detectPostgresFormatOrNull(raw);
if (builtinFormat !== null) return builtinFormat;

const customFormat = sniffRegisteredFormat(raw, null);
const customFormat = await sniffRegisteredFormat(raw, null);
if (customFormat !== null) return customFormat;

throw new Error(
'Unsupported EXPLAIN file format: expected Postgres JSON or text output',
);
}
default: {
const format = sniffRegisteredFormat(raw, engine);
const format = await sniffRegisteredFormat(raw, engine);
if (format !== null) return format;
throw new Error(`Unsupported EXPLAIN file format for engine '${engine}'`);
}
Expand All @@ -150,10 +151,10 @@ function detectPostgresFormatOrNull(raw: string): BuiltinExplainSourceFormat | n
return null;
}

function sniffRegisteredFormat(
async function sniffRegisteredFormat(
raw: string,
engine: ExplainEngine | null,
): ExplainSourceFormat | null {
): Promise<ExplainSourceFormat | null> {
const normalizedEngine = engine?.trim().toLowerCase() ?? null;

for (const parser of listExplainParsers()) {
Expand All @@ -166,7 +167,7 @@ function sniffRegisteredFormat(
if (parser.sniff === undefined) continue;

try {
if (parser.sniff(raw)) return parser.format;
if (await parser.sniff(raw)) return parser.format;
} catch {
// A sniffer is advisory; one broken parser must not block later parsers.
}
Expand All @@ -190,13 +191,17 @@ function looksLikePostgresText(raw: string): boolean {
*
* Equivalent to `parseExplainFor(raw, null)`.
*/
export function parseExplain(raw: string): ExplainPlan {
export function parseExplain(raw: string): Promise<ExplainPlan> {
return parseExplainFor(raw, null);
}

/** Parse a payload, using the caller's engine hint when there is one. */
export function parseExplainFor(raw: string, engine: ExplainEngine | null): ExplainPlan {
return parserFor(detectFormatFor(raw, engine)).parse(raw);
export async function parseExplainFor(
raw: string,
engine: ExplainEngine | null,
): Promise<ExplainPlan> {
const format = await detectFormatFor(raw, engine);
return await parserFor(format).parse(raw);
}

/**
Expand Down
8 changes: 4 additions & 4 deletions packages/explain/src/raw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export type ExplainQueryOutput =
{kind: 'raw'; raw: RawExplainOutput} | {kind: 'plan'; plan: ExplainPlan};

/** Parse a driver's raw EXPLAIN payload into a plan. */
export function parseRawExplain(raw: RawExplainOutput): ExplainPlan {
export async function parseRawExplain(raw: RawExplainOutput): Promise<ExplainPlan> {
const parser = getExplainParser(raw.format);
if (parser === null) {
throw new Error(
Expand All @@ -52,7 +52,7 @@ export function parseRawExplain(raw: RawExplainOutput): ExplainPlan {
);
}

const plan = parser.parse(raw.payload);
const plan = await parser.parse(raw.payload);
return {
...plan,
driver: raw.engine,
Expand All @@ -61,6 +61,6 @@ export function parseRawExplain(raw: RawExplainOutput): ExplainPlan {
}

/** Normalise either shape of `ExplainQueryOutput` into a plan. */
export function resolveExplainOutput(output: ExplainQueryOutput): ExplainPlan {
return output.kind === 'plan' ? output.plan : parseRawExplain(output.raw);
export function resolveExplainOutput(output: ExplainQueryOutput): Promise<ExplainPlan> {
return output.kind === 'plan' ? Promise.resolve(output.plan) : parseRawExplain(output.raw);
}
4 changes: 2 additions & 2 deletions packages/explain/src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ export interface RegisteredExplainParser {
/** Human label for format pickers. */
readonly label?: string;
/** Parse the raw payload or throw an Error. */
parse(payload: string): ExplainPlan;
parse(payload: string): ExplainPlan | Promise<ExplainPlan>;
/** Cheap, side-effect-free source detection. */
sniff?(payload: string): boolean;
sniff?(payload: string): boolean | Promise<boolean>;
}

const builtinParsersByFormat = new Map(
Expand Down
42 changes: 22 additions & 20 deletions packages/explain/tests/parsers/postgres.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,22 +45,22 @@ const POSTGRES_NESTED = `[

describe('postgres parsers', () => {
describe('detectFormat', () => {
it('accepts a JSON array', () => {
expect(detectFormat(' \n [\n {} ]')).toBe('postgres-json');
it('accepts a JSON array', async () => {
expect(await detectFormat(' \n [\n {} ]')).toBe('postgres-json');
});

it('accepts a JSON object', () => {
expect(detectFormat('{ "Plan": {} }')).toBe('postgres-json');
it('accepts a JSON object', async () => {
expect(await detectFormat('{ "Plan": {} }')).toBe('postgres-json');
});

it('rejects plain text without a cost header', () => {
expect(() => detectFormat('Seq Scan on users (cost=0.00..12.34 rows=100)')).toThrow(
/Unsupported/,
);
it('rejects plain text without a cost header', async () => {
await expect(
detectFormat('Seq Scan on users (cost=0.00..12.34 rows=100)'),
).rejects.toThrow(/Unsupported/);
});

it('recognises text output with a cost header', () => {
expect(detectFormat(POSTGRES_TEXT_FLAT)).toBe('postgres-text');
it('recognises text output with a cost header', async () => {
expect(await detectFormat(POSTGRES_TEXT_FLAT)).toBe('postgres-text');
});
});

Expand Down Expand Up @@ -109,20 +109,22 @@ describe('postgres parsers', () => {
});

describe('parseExplain error paths', () => {
it('rejects an empty array', () => {
expect(() => parseExplain('[]')).toThrow(/empty/);
it('rejects an empty array', async () => {
await expect(parseExplain('[]')).rejects.toThrow(/empty/);
});

it('rejects a missing Plan key', () => {
expect(() => parseExplain('[{"NotAPlan": 1}]')).toThrow(/Plan/);
it('rejects a missing Plan key', async () => {
await expect(parseExplain('[{"NotAPlan": 1}]')).rejects.toThrow(/Plan/);
});

it('rejects invalid JSON', () => {
expect(() => parseExplain('[not json]')).toThrow(/Failed to parse/);
it('rejects invalid JSON', async () => {
await expect(parseExplain('[not json]')).rejects.toThrow(/Failed to parse/);
});

it('rejects an unsupported format', () => {
expect(() => parseExplain('-> Nested Loop (cost=0.00..1.23)')).toThrow(/Unsupported/);
it('rejects an unsupported format', async () => {
await expect(parseExplain('-> Nested Loop (cost=0.00..1.23)')).rejects.toThrow(
/Unsupported/,
);
});
});

Expand Down Expand Up @@ -187,8 +189,8 @@ describe('postgres parsers', () => {
});

describe('parseExplain routing', () => {
it('routes text output to the text parser', () => {
const plan = parseExplain(POSTGRES_TEXT_FLAT);
it('routes text output to the text parser', async () => {
const plan = await parseExplain(POSTGRES_TEXT_FLAT);
expect(plan.root.node_type).toBe('Seq Scan');
});
});
Expand Down
Loading
Loading