Skip to content

feat(plugins): sandbox plugin UI extensions and EXPLAIN parsers in CSP-restricted iframes - #981

Open
1-DARK wants to merge 28 commits into
TabularisDB:mainfrom
1-DARK:feat/sandbox-plugin-js-csp-secrets
Open

1-DARK wants to merge 28 commits into
TabularisDB:mainfrom
1-DARK:feat/sandbox-plugin-js-csp-secrets

Conversation

@1-DARK

@1-DARK 1-DARK commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Plugin UI extensions and custom EXPLAIN parsers previously ran in Tabularis’s JavaScript context. This PR moves them into sandboxed iframes and limits communication with the host to an asynchronous, allow-listed API.

Closes #973

Changes

  • Added a sandbox guest runtime and host bridge that validate messages and their senders.
  • Exposed a limited plugin API for read-only queries, plugin-owned settings, notifications, translations, theme information, plugin-authored modals, and opening HTTP(S) URLs.
  • Routed plugin queries through Rust SQL classification and database-enforced read-only execution for SQLite, PostgreSQL, and MySQL. Unknown or unsupported queries and drivers are rejected.
  • Moved custom EXPLAIN parsers into the sandbox. Parser calls are asynchronous, and parser errors do not prevent other plugins from loading.
  • Added a host-mediated Google OAuth token exchange for the Google Sheets plugin.
  • Hardened connection exports: secrets are excluded by default, and exporting them requires native confirmation.
  • Updated the plugin API types, tests, and documentation.

Security

The guest frame receives no database credentials, filesystem access, direct Tauri commands, or general-purpose backend proxy. Its CSP blocks network access. The host validates all bridge requests and only handles explicitly allowed operations.

Breaking change and migration

Plugins that use host globals, the host React tree, or network requests from UI code must migrate to the sandbox API. See the plugin tutorial, plugin API guide, and sandbox design notes.

Testing

  • pnpm typecheck
  • pnpm lint
  • Plugin isolation acceptance tests
  • Rust query-classification and SQLite read-only tests
  • Manual release smoke checks:
    • Verify the editor, ER diagram, visual explain, AI panel, and plugin registry.
    • Verify a UI plugin’s API actions and confirm write queries are rejected.
    • Verify a custom EXPLAIN parser displays its plan and a throwing parser does not affect other plugins.

@1-DARK
1-DARK force-pushed the feat/sandbox-plugin-js-csp-secrets branch from 9ba84b4 to c8f8282 Compare October 11, 2026 18:13
},
};

void connectFrame(frame, handlers, controller.signal).then(async (rpc) => {

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: Race condition between connection cleanup and promise resolution

The connectFrame promise resolution (line 118-133) checks disposed after awaiting, but the cleanup function (lines 135-140) runs synchronously during unmount. If the component unmounts while connectFrame is pending, the cleanup sets disposed = true and aborts the signal, but the then callback may still execute after cleanup completes. The rpcRef.current?.close() in cleanup runs before rpcRef.current is set, leaving the resolved RPC unclosed. Consider storing the connectFrame promise and awaiting it in cleanup, or using a ref to track the pending connection.

Comment thread src/plugins/sandbox/connectFrame.ts Outdated
});
// Sandboxed frames have an opaque origin; the source window is checked above
// and the transferred port is unique to this frame instance.
frame.contentWindow?.postMessage({ type: "tabularis:init" }, "*", [port2]);

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.

SUGGESTION: Use specific target origin instead of * for postMessage

The frame.contentWindow?.postMessage({ type: "tabularis:init" }, "*", [port2]) uses * as target origin when transferring the message port. While the receiving end validates event.source === window.parent, using a specific origin (or at least the frame's known origin) would provide defense-in-depth. Since the frame uses a custom tabularis-plugin: protocol, consider using that origin or storing the frame's origin at connection time.

Suggested change
frame.contentWindow?.postMessage({ type: "tabularis:init" }, "*", [port2]);
frame.contentWindow?.postMessage({ type: "tabularis:init" }, frame.src, [port2]);

Comment thread src/plugins/sandbox/rpc.ts Outdated
port.start();
}

call(method: string, params: unknown[] = []): Promise<unknown> {

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: RPC calls have no timeout — potential DoS if guest frame hangs

The call method creates a Promise that only resolves when the guest responds. If the guest frame crashes, hangs, or maliciously stops responding, the host will wait indefinitely. This could block UI interactions or leak memory from pending calls. Add a configurable timeout (e.g., 30s) to call that rejects with a timeout error and cleans up the pending entry.

Comment thread src/utils/pluginExplainLoader.ts Outdated
document.body.append(frame);

const controller = new AbortController();
const timeout = window.setTimeout(() => controller.abort(), 15_000);

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: 15-second hard timeout for parser module load may be too short

The setTimeout(() => controller.abort(), 15_000) gives only 15 seconds for the plugin frame to load, establish RPC, and return parser metadata. Large plugin bundles or slow machines could exceed this, causing false failures. Consider making this configurable or increasing to 30-60 seconds. The 50ms cancellation polling interval (line 69-71) is also aggressive and may impact performance; 100-200ms would suffice.

Comment thread src/utils/pluginExplainLoader.ts Outdated
...(entry.label ?? descriptor.label
? { label: entry.label ?? descriptor.label }
: {}),
parse: async (payload: string): Promise<ExplainPlan> =>

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.

CRITICAL: session.rpc.call in async parse/sniff functions — session may be closed

The parse and sniff functions (lines 178-185) capture session by reference and call session.rpc.call. If disposePluginExplainParsers is called concurrently (e.g., plugin disabled), session.close() is invoked which closes the RPC port and rejects pending calls. Subsequent parse/sniff calls will throw "Plugin frame closed" errors. The registry doesn't guard against this; callers of parseRawExplain or detectFormat will receive unhandled rejections. Add a check for session.rpc validity or handle "Plugin frame closed" errors gracefully in the proxy functions.

use tauri::http::{header, Request, Response, StatusCode};
use tauri::{AppHandle, Runtime};

const GUEST_CSP: &str = "sandbox allow-scripts; default-src 'none'; script-src 'self'; \

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.

SUGGESTION: Consider adding trusted-types directive to guest CSP

The guest CSP (sandbox allow-scripts; default-src 'none'; script-src 'self'; ...) allows 'self' for scripts but doesn't restrict trusted-types. Adding trusted-types 'none' (or a specific policy name) would prevent DOM XSS via innerHTML, document.write, etc., providing defense-in-depth for the sandboxed frame. This is especially valuable since plugin bundles run in this context.

Comment thread packages/plugin-guest/src/runtime.ts Outdated
function loadExplainModule(): Promise<Record<string, unknown>[]> {
if (explainModulePromise) return explainModulePromise;
explainModulePromise = new Promise((resolve, reject) => {
const pluginScript = document.createElement("script");

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: loadExplainModule trusts guestWindow.__tabularis_explain_parser__ without validation

The loadExplainModule function (lines 179-201) reads guestWindow.__tabularis_explain_parser__ set by the plugin bundle and filters for valid parser objects. However, a malicious or buggy plugin could set this to a prototype-polluted object or a getter that throws. The filtering (lines 188-195) checks typeof value === "object" and function properties, but doesn't guard against getters with side effects. Consider using Object.hasOwn and explicit property access to avoid prototype chain issues.

setHeight(Math.min(MAX_HEIGHT, Math.max(0, requested)));
},
"slot.context": () => structuredClone(contextRef.current),
"api.query": async (query: unknown) => {

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.

SUGGESTION: Query length limit of 100,000 chars should be a named constant

The hardcoded 100_000 character limit for SQL queries (line 60) should be a named constant (e.g., MAX_PLUGIN_QUERY_LENGTH) for maintainability and to allow consistent limits across the codebase. This also makes it easier to adjust if needed.

"api.setting.set": async (key: unknown, value: unknown) => {
if (typeof key !== "string" || !key) throw new Error("Setting key is required");
const serialized = JSON.stringify(value);
if (typeof serialized !== "string" || serialized.length > 65_536) {

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.

SUGGESTION: Settings size limit of 64 KiB should be a named constant

The 65_536 byte limit (line 86) for serialized plugin settings should be a named constant (e.g., MAX_PLUGIN_SETTING_SIZE) for clarity and consistency.

@kilo-code-bot

kilo-code-bot Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: 20 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 14
SUGGESTION 6
Issue Details (click to expand)

WARNING

File Line Issue
packages/web-ui/src/plugins/sandbox/SandboxedPluginFrame.tsx 81 Guest-controlled ui.modal(true) promotes the iframe to a full-viewport, max-z-index overlay (zIndex: 2_147_483_647) with no user-gesture check or host-side dismissal; a plugin can block or spoof the host UI
packages/web-ui/src/plugins/sandbox/SandboxedPluginFrame.tsx 334 Portaled position: fixed iframe is only re-positioned on height/resize/scroll; it is not clipped by scroll containers and can drift from its slot
packages/web-ui/src/plugins/sandbox/SandboxedPluginFrame.tsx 275 Race between connection cleanup and promise resolution (disposed check vs. rpcRef assignment)
src-tauri/src/application/queries.rs 384 Plugin read-only queries are unbounded (None row limit); no host-side cap on rows sent to the sandboxed plugin
packages/create-plugin/tsconfig.json 24 rootDir points to repository root (../..) instead of the package source directory
packages/plugin-api/src/types.ts 78 client_secret is optional in GoogleOAuthTokenRequest but required for standard OAuth flows
packages/plugin-api/src/slots.ts 90 Breaking change: new required fields (connectionName, onDatabaseChange, database) on the connection-modal.connection_content slot context
packages/plugin-guest/src/runtime.ts 131 exchangeGoogleOAuthToken accepts request: unknown without validation
packages/plugin-guest/src/runtime.ts 148 callHostResult uses an unsafe as Promise<T> cast without runtime validation
packages/plugin-guest/src/runtime.ts 402 Callback passed to hydratePluginSlotContext silently swallows all errors
src-tauri/tauri.conf.json 30 CSP img-src allows the broad https: scheme
src-tauri/tauri.conf.json 32 CSP connect-src allows the whole raw.githubusercontent.com domain
src-tauri/tauri.conf.json 38 dangerousDisableAssetCspModification disables nonce/hash injection for style-src
packages/web-ui/src/utils/pluginExplainLoader.ts 68 15-second hard timeout for parser module load may be too short; also, parse/sniff proxies (lines 176-188) have no handling for a closed session ("Plugin frame closed") if a call races with disposePluginExplainParsers

SUGGESTION

File Line Issue
packages/plugin-guest/src/runtime.ts 152 Plugin-specific (google-sheets) hard-coded compatibility hack in the generic guest runtime and host RPC allow-list; needs a removal plan or a generic mechanism
packages/plugin-guest/src/runtime.ts 340 loadExplainModule trusts guestWindow.__tabularis_explain_parser__ without strict validation (getters / prototype chain)
src-tauri/src/transport/desktop/sandbox_protocol.rs 10 Consider adding a trusted-types directive to the guest CSP
packages/web-ui/src/plugins/sandbox/SandboxedPluginFrame.tsx 138 Query length limit 100_000 is duplicated (also in application/queries.rs:367); extract a shared named constant
packages/web-ui/src/plugins/sandbox/SandboxedPluginFrame.tsx 169 Settings size limit 65_536 should be a named constant
packages/web-ui/src/plugins/sandbox/connectFrame.ts 43 postMessage(..., '*', [port2]) uses a wildcard target origin; prefer the frame's known origin
Files Reviewed (incremental, since `6cc89d83`)
  • New head f10efe8c contains the PR-authored CI fix (formatting, relaxing the create-plugin smoke api_version check to a semver regex, moving sandbox_protocol.rs into transport/desktop, refreshing the web inventory) plus a merge of upstream main that relocated the frontend under packages/web-ui and split Tauri commands into application/ and transport/. No new behavioral issues were found in the PR-authored changes.
  • All previously reported findings were re-verified at their new paths. Line numbers above reflect the current tree. The earlier CRITICAL on pluginExplainLoader.ts is downgraded and merged into the timeout WARNING: sessions are now refcounted and disposePluginExplainParsers unregisters parsers before closing sessions, so only a narrow in-flight race remains.
  • Earlier inline comments are outdated against the new file layout, so findings are carried in this summary only.

Fix these issues in Kilo Cloud

Previous Review Summaries (6 snapshots, latest commit 6cc89d8)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 6cc89d8)

Status: 21 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 14
SUGGESTION 6
Issue Details (click to expand)

CRITICAL

File Line Issue
src/utils/pluginExplainLoader.ts 177 session.rpc.call in async parse/sniff functions — session may be closed

WARNING

File Line Issue
src/plugins/sandbox/SandboxedPluginFrame.tsx 80 Guest-controlled ui.modal(true) promotes the iframe to a full-viewport, max-z-index overlay with no user-gesture check or host-side dismissal — a plugin can block/spoof the host UI
src/plugins/sandbox/SandboxedPluginFrame.tsx 297 Portaled position: fixed iframe is only re-positioned on height/resize/scroll; it is not clipped by scroll containers and can drift from its slot
src-tauri/src/commands.rs 5264 Plugin read-only queries are unbounded (None row limit); no host-side cap on rows sent to the sandboxed plugin
packages/create-plugin/tsconfig.json 24 rootDir points to repository root (../..) instead of package source directory
packages/plugin-api/src/types.ts 78 client_secret marked optional in GoogleOAuthTokenRequest but required for standard OAuth flows
packages/plugin-api/src/slots.ts 88 Breaking change: new required fields added to connection-modal.connection_content slot context
packages/plugin-guest/src/runtime.ts 126 exchangeGoogleOAuthToken accepts request: unknown without validation
packages/plugin-guest/src/runtime.ts 142 callHostResult uses unsafe as Promise<T> cast without runtime validation
packages/plugin-guest/src/runtime.ts 317 Callback passed to hydratePluginSlotContext silently swallows all errors
src-tauri/tauri.conf.json 30 CSP img-src allows broad https: scheme
src-tauri/tauri.conf.json 32 CSP connect-src allows full raw.githubusercontent.com domain
src-tauri/tauri.conf.json 38 dangerousDisableAssetCspModification disables nonce/hash injection for style-src
src/plugins/sandbox/SandboxedPluginFrame.tsx 232 Race condition between connection cleanup and promise resolution
src/utils/pluginExplainLoader.ts 68 15-second hard timeout for parser module load may be too short

SUGGESTION

File Line Issue
packages/plugin-guest/src/runtime.ts 145 Plugin-specific (google-sheets) hard-coded compatibility hack in the generic guest runtime and host RPC allow-list; needs a removal plan or generic mechanism
packages/plugin-guest/src/runtime.ts 259 loadExplainModule trusts guestWindow.__tabularis_explain_parser__ without strict validation
src-tauri/src/plugins/sandbox_protocol.rs 10 Consider adding trusted-types directive to CSP
src/plugins/sandbox/SandboxedPluginFrame.tsx 119 Query length limit of 100,000 chars should be a named constant
src/plugins/sandbox/SandboxedPluginFrame.tsx 147 Settings size limit of 64 KiB should be a named constant
src/plugins/sandbox/connectFrame.ts 43 Use specific target origin instead of * for postMessage
Files Reviewed (incremental, since `81b540c8`)
  • The new PR-authored commit 6cc89d83 ("fix: pass sandbox plugin CI checks") is a formatting/lint change across ~43 files: Prettier-style quote/indent normalization and relative-to-@/ import alias rewrites in sandbox, explain-loader, guest runtime, plugin-api, explain package, create-plugin CLI, Connections.tsx, and tests. With whitespace and line endings ignored, no behavioral changes were found, so no new issues were raised.
  • The @/types/pluginSlots import in src/plugins/sandbox/slotContext.ts is import type, so it is erased and the esbuild guest bundle (which has no @/ alias) is not affected.
  • All previously reported findings were re-verified as still present in current code (only line numbers shifted slightly due to reformatting).

Fix these issues in Kilo Cloud

Previous review (commit 81b540c)

Status: 21 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 14
SUGGESTION 6
Issue Details (click to expand)

CRITICAL

File Line Issue
src/utils/pluginExplainLoader.ts 178 session.rpc.call in async parse/sniff functions — session may be closed

WARNING

File Line Issue
src/plugins/sandbox/SandboxedPluginFrame.tsx 82 Guest-controlled ui.modal(true) promotes the iframe to a full-viewport, max-z-index overlay with no user-gesture check or host-side dismissal — a plugin can block/spoof the host UI
src/plugins/sandbox/SandboxedPluginFrame.tsx 297 Portaled position: fixed iframe is only re-positioned on height/resize/scroll; it is not clipped by scroll containers and can drift from its slot
src-tauri/src/commands.rs 5264 Plugin read-only queries are unbounded (None row limit); no host-side cap on rows sent to the sandboxed plugin
packages/create-plugin/tsconfig.json 21 rootDir points to repository root (../..) instead of package source directory
packages/plugin-api/src/types.ts 78 client_secret marked optional in GoogleOAuthTokenRequest but required for standard OAuth flows
packages/plugin-api/src/slots.ts 88 Breaking change: new required fields added to connection-modal.connection_content slot context
packages/plugin-guest/src/runtime.ts 126 exchangeGoogleOAuthToken accepts request: unknown without validation
packages/plugin-guest/src/runtime.ts 142 callHostResult uses unsafe as Promise<T> cast without runtime validation
packages/plugin-guest/src/runtime.ts 317 Callback passed to hydratePluginSlotContext silently swallows all errors
src-tauri/tauri.conf.json 30 CSP img-src allows broad https: scheme
src-tauri/tauri.conf.json 32 CSP connect-src allows full raw.githubusercontent.com domain
src-tauri/tauri.conf.json 38 dangerousDisableAssetCspModification disables nonce/hash injection for style-src
src/plugins/sandbox/SandboxedPluginFrame.tsx 232 Race condition between connection cleanup and promise resolution
src/utils/pluginExplainLoader.ts 68 15-second hard timeout for parser module load may be too short

SUGGESTION

File Line Issue
packages/plugin-guest/src/runtime.ts 145 Plugin-specific (google-sheets) hard-coded compatibility hack in the generic guest runtime and host RPC allow-list; needs a removal plan or generic mechanism
packages/plugin-guest/src/runtime.ts 259 loadExplainModule trusts guestWindow.__tabularis_explain_parser__ without strict validation
src-tauri/src/plugins/sandbox_protocol.rs 10 Consider adding trusted-types directive to CSP
src/plugins/sandbox/SandboxedPluginFrame.tsx 119 Query length limit of 100,000 chars should be a named constant
src/plugins/sandbox/SandboxedPluginFrame.tsx 147 Settings size limit of 64 KiB should be a named constant
src/plugins/sandbox/connectFrame.ts 42 Use specific target origin instead of * for postMessage
Files Reviewed (incremental, since `ea41be7d`)
  • The only PR-authored change in ea41be7d..81b540c8 is a test timeout bump in tests/components/modals/NewConnectionModal.test.tsx (no issues). The remainder of the range is a merge of main (v0.29.0 release content), which is not part of this PR's own changes.
  • All previously reported findings are in files unchanged by this range and were re-verified as still present (src-tauri/tauri.conf.json lines shifted slightly; only its version field changed).

Fix these issues in Kilo Cloud

Previous review (commit ea41be7)

Status: 21 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 14
SUGGESTION 6
Issue Details (click to expand)

CRITICAL

File Line Issue
src/utils/pluginExplainLoader.ts 178 session.rpc.call in async parse/sniff functions — session may be closed

WARNING

File Line Issue
src/plugins/sandbox/SandboxedPluginFrame.tsx 82 Guest-controlled ui.modal(true) promotes the iframe to a full-viewport, max-z-index overlay with no user-gesture check or host-side dismissal — a plugin can block/spoof the host UI
src/plugins/sandbox/SandboxedPluginFrame.tsx 297 Portaled position: fixed iframe is only re-positioned on height/resize/scroll; it is not clipped by scroll containers and can drift from its slot
src-tauri/src/commands.rs 5264 Plugin read-only queries are unbounded (None row limit); no host-side cap on rows sent to the sandboxed plugin
packages/create-plugin/tsconfig.json 21 rootDir points to repository root (../..) instead of package source directory
packages/plugin-api/src/types.ts 78 client_secret marked optional in GoogleOAuthTokenRequest but required for standard OAuth flows
packages/plugin-api/src/slots.ts 88 Breaking change: new required fields added to connection-modal.connection_content slot context
packages/plugin-guest/src/runtime.ts 126 exchangeGoogleOAuthToken accepts request: unknown without validation
packages/plugin-guest/src/runtime.ts 142 callHostResult uses unsafe as Promise<T> cast without runtime validation
packages/plugin-guest/src/runtime.ts 317 Callback passed to hydratePluginSlotContext silently swallows all errors
src-tauri/tauri.conf.json 32 CSP img-src allows broad https: scheme
src-tauri/tauri.conf.json 34 CSP connect-src allows full raw.githubusercontent.com domain
src-tauri/tauri.conf.json 40 dangerousDisableAssetCspModification disables nonce/hash injection for style-src
src/plugins/sandbox/SandboxedPluginFrame.tsx 232 Race condition between connection cleanup and promise resolution
src/utils/pluginExplainLoader.ts 68 15-second hard timeout for parser module load may be too short

SUGGESTION

File Line Issue
packages/plugin-guest/src/runtime.ts 145 Plugin-specific (google-sheets) hard-coded compatibility hack in the generic guest runtime and host RPC allow-list; needs a removal plan or generic mechanism
packages/plugin-guest/src/runtime.ts 259 loadExplainModule trusts guestWindow.__tabularis_explain_parser__ without strict validation
src-tauri/src/plugins/sandbox_protocol.rs 10 Consider adding trusted-types directive to CSP
src/plugins/sandbox/SandboxedPluginFrame.tsx 119 Query length limit of 100,000 chars should be a named constant
src/plugins/sandbox/SandboxedPluginFrame.tsx 147 Settings size limit of 64 KiB should be a named constant
src/plugins/sandbox/connectFrame.ts 42 Use specific target origin instead of * for postMessage
Files Reviewed (incremental, PR-relevant changes in `ea41be7d`)
  • src/plugins/sandbox/SandboxedPluginFrame.tsx - 2 new issues (modal takeover, portaled fixed iframe)
  • packages/plugin-guest/src/runtime.ts - 1 new issue (plugin-specific compat hack); getHost shim and applyGuestTheme reviewed, no further issues
  • src-tauri/src/commands.rs - capability-aware read-only classification reviewed (improvement); unbounded row limit still present
  • src/plugins/sandbox/slotContext.ts, src/types/pluginSlots.ts - setSecretField plumbing, validated on host, no issues
  • .github/workflows/ci.yml, packages/plugin-api/*, packages/create-plugin/src/cli.ts - version bump to 0.4.0 and guest-runtime freshness check, no issues
  • tests/*, src/i18n/locales/*, plugins/*.md - no issues

Fix these issues in Kilo Cloud

Previous review (commit 3e7329a)

Status: 18 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 12
SUGGESTION 5
Issue Details (click to expand)

CRITICAL

File Line Issue
src/utils/pluginExplainLoader.ts 178 session.rpc.call in async parse/sniff functions — session may be closed

WARNING

File Line Issue
src-tauri/src/commands.rs 4913 Plugin read-only queries are now unbounded (Some(100) → None); no host-side cap on rows sent to the sandboxed plugin
packages/create-plugin/tsconfig.json 21 rootDir points to repository root (../..) instead of package source directory
packages/plugin-api/src/types.ts 78 client_secret marked optional in GoogleOAuthTokenRequest but required for standard OAuth flows
packages/plugin-api/src/slots.ts 75 Breaking change: Added new required fields to connection-modal.connection_content slot context
packages/plugin-guest/src/runtime.ts 123 exchangeGoogleOAuthToken accepts request: unknown without validation
packages/plugin-guest/src/runtime.ts 139 callHostResult uses unsafe as Promise<T> cast without runtime validation
packages/plugin-guest/src/runtime.ts 256 Callback passed to hydratePluginSlotContext silently swallows all errors
src-tauri/tauri.conf.json 32 CSP img-src allows broad https: scheme
src-tauri/tauri.conf.json 34 CSP connect-src allows full raw.githubusercontent.com domain
src-tauri/tauri.conf.json 40 dangerousDisableAssetCspModification disables nonce/hash injection for style-src
src/plugins/sandbox/SandboxedPluginFrame.tsx 190 Race condition between connection cleanup and promise resolution
src/utils/pluginExplainLoader.ts 68 15-second hard timeout for parser module load may be too short

SUGGESTION

File Line Issue
src-tauri/src/plugins/sandbox_protocol.rs 10 Consider adding trusted-types directive to CSP
src/plugins/sandbox/SandboxedPluginFrame.tsx 82 Query length limit of 100,000 chars should be a named constant
src/plugins/sandbox/SandboxedPluginFrame.tsx 110 Settings size limit of 64 KiB should be a named constant
src/plugins/sandbox/connectFrame.ts 42 Use specific target origin instead of * for postMessage
src/plugins/sandbox/rpc.ts - RPC calls have no timeout — potential hang if guest frame stalls
Files Reviewed (incremental, PR-relevant changes)
  • src-tauri/src/commands.rs - 1 new issue
  • packages/plugin-guest/src/runtime.ts - 3 issues (new defaultValue translation fallback reviewed, no new issues)
  • tests/fixtures/pluginSlotProbe.tsx, tests/fixtures/pluginSlotProbeTransport.ts, tests/plugins/pluginIsolationAcceptance.test.ts - no issues
  • plugins/UI_SANDBOX_DESIGN.md, plugins/PLUGIN_GUIDE.md, packages/plugin-api/README.md - no issues

Fix these issues in Kilo Cloud

Previous review (commit 04de575)

Status: 17 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 11
SUGGESTION 5
Issue Details (click to expand)

CRITICAL

File Line Issue
src/utils/pluginExplainLoader.ts 178 session.rpc.call in async parse/sniff functions — session may be closed

WARNING

File Line Issue
packages/create-plugin/tsconfig.json 21 rootDir points to repository root (../..) instead of package source directory
packages/plugin-api/src/types.ts 78 client_secret marked optional in GoogleOAuthTokenRequest but required for standard OAuth flows
packages/plugin-api/src/slots.ts 75 Breaking change: Added new required fields to connection-modal.connection_content slot context
packages/plugin-guest/src/runtime.ts 120 exchangeGoogleOAuthToken accepts request: unknown without validation
packages/plugin-guest/src/runtime.ts 136 callHostResult uses unsafe as Promise<T> cast without runtime validation
packages/plugin-guest/src/runtime.ts 253 Callback passed to hydratePluginSlotContext silently swallows all errors
src-tauri/tauri.conf.json 32 CSP img-src allows broad https: scheme
src-tauri/tauri.conf.json 34 CSP connect-src allows full raw.githubusercontent.com domain
src-tauri/tauri.conf.json 40 dangerousDisableAssetCspModification disables nonce/hash injection for style-src
src/plugins/sandbox/SandboxedPluginFrame.tsx 190 Race condition between connection cleanup and promise resolution
src/utils/pluginExplainLoader.ts 68 15-second hard timeout for parser module load may be too short

SUGGESTION

File Line Issue
src-tauri/src/plugins/sandbox_protocol.rs 10 Consider adding trusted-types directive to CSP
src/plugins/sandbox/SandboxedPluginFrame.tsx 82 Query length limit of 100,000 chars should be a named constant
src/plugins/sandbox/SandboxedPluginFrame.tsx 110 Settings size limit of 64 KiB should be a named constant
src/plugins/sandbox/connectFrame.ts 42 Use specific target origin instead of * for postMessage
src/utils/pluginExplainLoader.ts 178 session.rpc.call in async parse/sniff functions — session may be closed
Files Reviewed (17 files)
  • packages/create-plugin/tsconfig.json - 1 issue
  • packages/plugin-api/src/types.ts - 1 issue
  • packages/plugin-api/src/slots.ts - 1 issue
  • packages/plugin-guest/src/runtime.ts - 3 issues
  • src-tauri/src/plugins/sandbox_protocol.rs - 1 issue
  • src-tauri/tauri.conf.json - 3 issues
  • src/plugins/sandbox/SandboxedPluginFrame.tsx - 3 issues
  • src/plugins/sandbox/connectFrame.ts - 1 issue
  • src/utils/pluginExplainLoader.ts - 3 issues

Fix these issues in Kilo Cloud

Previous review (commit c8f8282)

Status: 23 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 2
WARNING 16
SUGGESTION 5
Issue Details (click to expand)

CRITICAL

File Line Issue
src/utils/pluginExplainLoader.ts 178 session.rpc.call in async parse/sniff functions — session may be closed
packages/plugin-guest/src/runtime.ts 189 loadExplainModule trusts guestWindow.__tabularis_explain_parser__ without validation

WARNING

File Line Issue
packages/plugin-guest/src/runtime.ts 119 exchangeGoogleOAuthToken accepts request: unknown without validation
packages/plugin-guest/src/runtime.ts 135 callHostResult uses unsafe as Promise<T> cast without runtime validation
packages/plugin-guest/src/runtime.ts 244 Callback passed to hydratePluginSlotContext silently swallows all errors
packages/plugin-api/src/types.ts 78 GoogleOAuthTokenRequest marks client_secret as optional but it's required for standard OAuth flows
packages/plugin-api/src/slots.ts 75 Added new required fields to connection-modal.connection_content slot context (breaking change)
src-tauri/src/commands.rs 6794 Missing indentation in if block
src-tauri/tauri.conf.json 30 CSP img-src allows broad https: scheme
src-tauri/tauri.conf.json 32 CSP connect-src allows full raw.githubusercontent.com domain
src-tauri/tauri.conf.json 38 dangerousDisableAssetCspModification disables nonce/hash injection for style-src
packages/create-plugin/tsconfig.json 21 rootDir points to repo root instead of package source directory
src/plugins/sandbox/SandboxedPluginFrame.tsx 184 Race condition between connection cleanup and promise resolution
src/plugins/sandbox/connectFrame.ts 42 postMessage target origin *
src/utils/pluginExplainLoader.ts 68 15-second hard timeout for parser module load may be too short
src/plugins/sandbox/SandboxedPluginFrame.tsx 81 Query length limit of 100,000 chars should be a named constant
src/plugins/sandbox/SandboxedPluginFrame.tsx 107 Settings size limit of 64 KiB should be a named constant
src/components/ui/FilterRow.tsx 146 Key includes filter.value causing remount on every keystroke

SUGGESTION

File Line Issue
src-tauri/src/plugins/sandbox_protocol.rs 10 CSP missing trusted-types directive
packages/plugin-api/src/types.ts 112 Breaking change: setSetting/setSettings now return Promise<void> instead of void
src/components/ui/FilterValuePicker.tsx 199 Dialog missing aria-modal="true" attribute
src/components/ui/FilterValuePicker.tsx 273 Potential key collision on v.value
src/components/ui/FilterRow.tsx 146 Key includes filter.value causing remount on every keystroke
Files Reviewed (47 files)
  • src/plugins/sandbox/SandboxedPluginFrame.tsx - 4 issues
  • src/plugins/sandbox/connectFrame.ts - 1 issue
  • src/plugins/sandbox/rpc.ts - 0 issues (previous timeout issue fixed)
  • src/plugins/sandbox/slotContext.ts - 0 issues
  • src/utils/pluginExplainLoader.ts - 3 issues
  • src/utils/approvalExplain.ts - 0 issues
  • src/utils/sqlAnalysis.ts - 0 issues
  • src/contexts/PluginSlotProvider.tsx - 0 issues
  • src/pages/Connections.tsx - 0 issues
  • src/pages/VisualExplainPage.tsx - 0 issues
  • src/components/modals/AiApprovalModal.tsx - 0 issues
  • packages/plugin-guest/src/runtime.ts - 4 issues
  • packages/plugin-api/src/types.ts - 3 issues
  • packages/plugin-api/src/slots.ts - 1 issue
  • packages/plugin-api/src/hooks.ts - 0 issues
  • packages/plugin-api/src/index.ts - 0 issues
  • packages/plugin-api/src/version.ts - 0 issues
  • packages/plugin-api/README.md - 0 issues
  • packages/plugin-api/package.json - 0 issues
  • src-tauri/src/plugins/sandbox_protocol.rs - 1 issue
  • `src-tauri/src/plugins/co

[Snapshot truncated.]

Additional previous summary content was truncated to keep this comment within platform limits.


Reviewed by claude-sonnet-5.5 · Input: 282 · Output: 7.3K · Cached: 1.3M

Review guidance: REVIEW.md from base branch main

@debba debba left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Hey @1-DARK, thanks a lot for this one. Moving UI extensions and EXPLAIN parsers out of the host context into sandboxed iframes, with a MessagePort and an explicit method allow-list, is something I really wanted to see. The guest and host source checks and the confined file reader in sandbox_protocol.rs look solid. I checked out the branch: typecheck, lint, the full vitest suite (5508 tests) and the Rust tests all pass.

Before merging, though, a few things need fixing:

  1. Slot contexts with callbacks never reach the guest. connection-modal.connection_content passes onDatabaseChange and connection-modal.extra_fields passes setExtraField (NewConnectionModal.tsx). Functions can't be cloned through postMessage, so rpc.call("slot.context", ...) in SandboxedPluginFrame.tsx:125 and :145 throws DataCloneError. The error is swallowed and the guest keeps an empty context. In practice the Elasticsearch and Google Sheets connection fields render nothing (both check context.driver first and then call onDatabaseChange), and the SQL Server extra fields break the same way. Those callbacks need to become host RPC methods, with only plain data sent in the context.
  2. App CSP in tauri.conf.json:29-30.
    • index.html has an inline <style>. Going by the Tauri source (set_csp / replace_csp_nonce), on release builds Tauri adds a nonce to style-src, and once a nonce is there browsers ignore 'unsafe-inline', so the <style> tags Monaco injects at runtime would be blocked. Dev builds won't show this, and I haven't confirmed it on a release build yet. Either set dangerousDisableAssetCspModification: ["style-src"] or move that style out of index.html.
    • img-src has no https:, so registry and driver icons (raw.githubusercontent.com, codeberg) and the install-modal screenshots would be blocked.
  3. The read-only check isn't a security boundary. isReadOnlyQuery (SandboxedPluginFrame.tsx:63) returns true for WITH d AS (DELETE FROM users RETURNING *) SELECT * FROM d, SELECT * INTO t2 FROM t, SELECT ... INTO OUTFILE, PRAGMA writable_schema = 1 and SELECT pg_terminate_backend(...). Either enforce read-only at the connection level (a read-only transaction or session) or tone down the claim in the description and docs.
  4. Scope. The export_connections_payload change (secrets off by default plus the native confirm, and the new export_connections_payload_unconfirmed for backups) isn't in the description. I'd move it to its own PR, or at least document it here.
  5. Google Sheets. google-auth.js does its OAuth token exchange with fetch, and the guest CSP (connect-src 'none') blocks that with no host alternative, so it can't migrate as things stand. We need a path for it before this lands.

Smaller things: cargo fmt reports diffs in your hunks of sandbox_protocol.rs, plugins/commands.rs and commands.rs; RPC calls have no timeout (a hung parser leaves EXPLAIN spinning forever); and a plugin file named runtime.js or host.css gets shadowed by the guest assets (sandbox_protocol.rs:99-102).

Once these are sorted I'll run my manual checks on a release build and we can get it in.

@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Preview build

The preview build of commit 4f7a967 failed (skipped). See the workflow run for logs.

@1-DARK

1-DARK commented Oct 12, 2026

Copy link
Copy Markdown
Contributor Author

Hey @debba, regarding the Google Sheets point: the plugin lives in a separate repository outside this PR’s workspace, so I couldn’t migrate it here. I added a host-mediated exchangeGoogleOAuthToken API so the guest can exchange OAuth tokens without making network requests, and documented the migration steps in plugins/PLUGIN_TUTORIAL.md.

Would you like me to open a companion PR in the plugin repository, or will that migration be handled separately?

@1-DARK
1-DARK requested a review from debba October 12, 2026 09:10
@debba

debba commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator

@1-DARK yes feel free to create a companion PR in Google Sheets plugin if you want

Comment thread packages/plugin-guest/src/runtime.ts Outdated
};
},
openUrl: (url: string) => callHost("api.openUrl", url),
exchangeGoogleOAuthToken: (request: unknown) => callHostResult("api.oauth.googleToken", request),

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: exchangeGoogleOAuthToken accepts request: unknown without validation

The handler takes request: unknown and passes it directly to the host via callHostResult. While the host validates, the guest runtime should validate the request structure (e.g., grant_type, required fields) before forwarding to fail fast with clear errors and avoid sending malformed requests.


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

Comment thread packages/plugin-guest/src/runtime.ts Outdated

function callHostResult<T>(method: string, ...args: unknown[]): Promise<T> {
if (!rpcPort) return Promise.reject(new Error("Plugin host connection is unavailable"));
return rpcPort.call(method, args) as Promise<T>;

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: callHostResult uses unsafe as Promise<T> cast without runtime validation

The function returns rpcPort.call(method, args) as Promise<T> with a generic type assertion. If the host returns a different shape, callers get a false sense of type safety. Should either narrow the return type based on method or validate the response structure.


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

Comment thread packages/plugin-guest/src/runtime.ts Outdated
const next = Array.isArray(args) ? args[0] : args;
if (next && typeof next === "object" && !Array.isArray(next)) {
currentContext = hydratePluginSlotContext(next as Record<string, unknown>, (callback, ...values) => {
void rpc.call(callback, values).catch(() => undefined);

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: Callback passed to hydratePluginSlotContext silently swallows all errors

The callback (callback, ...values) => { void rpc.call(callback, values).catch(() => undefined); } catches and ignores all RPC failures. This hides communication errors between plugin and host, making debugging difficult and potentially leaving the plugin in an inconsistent state.


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

Comment thread packages/plugin-api/src/types.ts Outdated
grant_type: "authorization_code" | "refresh_token";
code?: string;
client_id: string;
client_secret?: string;

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: GoogleOAuthTokenRequest marks client_secret as optional but it's required for standard OAuth flows

For grant_type: "authorization_code" and grant_type: "refresh_token", the Google token endpoint requires client_secret unless PKCE (code_verifier) is used. The type should either make it required or use a discriminated union to enforce the correct field combinations.


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

Comment thread packages/plugin-api/src/slots.ts Outdated
@@ -73,6 +73,9 @@ export type SlotContextMap = {
};
"connection-modal.connection_content": {
driver: string;

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.

SUGGESTION: Added new required fields to connection-modal.connection_content slot context

The slot context now requires database, connectionName, and onDatabaseChange. This is a breaking change for existing plugins using this slot — they will receive new fields but TypeScript consumers destructuring the context may need updates. Acceptable for 0.3.0 minor bump, but worth noting in migration docs.


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

Comment thread src-tauri/src/commands.rs Outdated
if let Some(ids) = &connection_ids {
let selected: std::collections::HashSet<&str> =
ids.iter().map(String::as_str).collect();
let selected: std::collections::HashSet<&str> = ids.iter().map(String::as_str).collect();

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: Missing indentation in if block

Line 6794 (let selected: ...) is missing 4 spaces of indentation inside the if let Some(ids) = &connection_ids { block starting at line 6793. The surrounding lines (6795-6799) are properly indented, making this a clear formatting bug that would be caught by rustfmt.


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

Comment thread packages/plugin-guest/src/runtime.ts Outdated
};
},
openUrl: (url: string) => callHost("api.openUrl", url),
exchangeGoogleOAuthToken: (request: unknown) => callHostResult("api.oauth.googleToken", request),

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: exchangeGoogleOAuthToken accepts request: unknown without validation

The handler takes request: unknown and passes it directly to the host via callHostResult. While the host validates, the guest runtime should validate the request structure (e.g., grant_type, required fields) before forwarding to fail fast with clear errors and avoid sending malformed requests.


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

Comment thread packages/plugin-guest/src/runtime.ts Outdated

function callHostResult<T>(method: string, ...args: unknown[]): Promise<T> {
if (!rpcPort) return Promise.reject(new Error("Plugin host connection is unavailable"));
return rpcPort.call(method, args) as Promise<T>;

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: callHostResult uses unsafe as Promise<T> cast without runtime validation

The function returns rpcPort.call(method, args) as Promise<T> with a generic type assertion. If the host returns a different shape, callers get a false sense of type safety. Should either narrow the return type based on method or validate the response structure.


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

Comment thread packages/plugin-guest/src/runtime.ts Outdated
const next = Array.isArray(args) ? args[0] : args;
if (next && typeof next === "object" && !Array.isArray(next)) {
currentContext = hydratePluginSlotContext(next as Record<string, unknown>, (callback, ...values) => {
void rpc.call(callback, values).catch(() => undefined);

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: Callback passed to hydratePluginSlotContext silently swallows all errors

The callback (callback, ...values) => { void rpc.call(callback, values).catch(() => undefined); } catches and ignores all RPC failures. This hides communication errors between plugin and host, making debugging difficult and potentially leaving the plugin in an inconsistent state.


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

Comment thread packages/plugin-api/src/types.ts Outdated
grant_type: "authorization_code" | "refresh_token";
code?: string;
client_id: string;
client_secret?: string;

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: GoogleOAuthTokenRequest marks client_secret as optional but it's required for standard OAuth flows

For grant_type: "authorization_code" and grant_type: "refresh_token", the Google token endpoint requires client_secret unless PKCE (code_verifier) is used. The type should either make it required or use a discriminated union to enforce the correct field combinations.


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

Comment thread packages/plugin-api/src/slots.ts Outdated
@@ -73,6 +73,9 @@ export type SlotContextMap = {
};
"connection-modal.connection_content": {
driver: string;

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.

SUGGESTION: Added new required fields to connection-modal.connection_content slot context

The slot context now requires database, connectionName, and onDatabaseChange. This is a breaking change for existing plugins using this slot — they will receive new fields but TypeScript consumers destructuring the context may need updates. Acceptable for 0.3.0 minor bump, but worth noting in migration docs.


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

Comment thread src-tauri/src/commands.rs Outdated
if let Some(ids) = &connection_ids {
let selected: std::collections::HashSet<&str> =
ids.iter().map(String::as_str).collect();
let selected: std::collections::HashSet<&str> = ids.iter().map(String::as_str).collect();

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: Missing indentation in if block

Line 6794 (let selected: ...) is missing 4 spaces of indentation inside the if let Some(ids) = &connection_ids { block starting at line 6793. The surrounding lines (6795-6799) are properly indented, making this a clear formatting bug that would be caught by rustfmt.


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

Comment thread src-tauri/tauri.conf.json
"script-src": "'self'",
"worker-src": "'self' blob:",
"style-src": "'self' 'unsafe-inline'",
"img-src": "'self' data: blob: asset: http://asset.localhost https:",

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: CSP img-src allows broad https: scheme

The img-src directive includes https: which permits loading images from any HTTPS origin. This weakens the CSP by allowing third-party image loads. Should be restricted to specific trusted domains or at minimum https://asset.localhost and https://raw.githubusercontent.com if those are the only sources.


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

Comment thread src-tauri/tauri.conf.json
"style-src": "'self' 'unsafe-inline'",
"img-src": "'self' data: blob: asset: http://asset.localhost https:",
"font-src": "'self' data:",
"connect-src": "ipc: http://ipc.localhost https://raw.githubusercontent.com https://oauth2.googleapis.com",

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: CSP connect-src allows full raw.githubusercontent.com domain

The connect-src directive allows connections to https://raw.githubusercontent.com (any path). While used for the plugin registry (compat.rs: registry.json), this permits arbitrary fetches from raw.githubusercontent.com. Should be restricted to the specific registry path if possible, or at least documented as a known broad permission.


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

Comment thread src-tauri/tauri.conf.json
"base-uri": "'self'",
"form-action": "'none'"
},
"dangerousDisableAssetCspModification": ["style-src"],

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: dangerousDisableAssetCspModification disables nonce/hash injection for style-src

The dangerousDisableAssetCspModification: ["style-src"] opts out of Tauri's automatic CSP nonce/hash insertion for the asset protocol. This means inline styles loaded via asset: won't get nonces, relying solely on 'unsafe-inline' in style-src. This reduces CSP effectiveness for asset-protocol resources.


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

"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.

Comment thread packages/plugin-api/src/types.ts Outdated
getSetting: <T = unknown>(key: string, defaultValue?: T) => T;
setSetting: (key: string, value: unknown) => void;
setSettings: (updates: Record<string, unknown>) => void;
setSetting: (key: string, value: unknown) => Promise<void>;

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.

SUGGESTION: Breaking change: setSetting/setSettings now return Promise<void> instead of void

The UsePluginSettingReturn interface changed setSetting and setSettings from synchronous void to async Promise<void>. This is a breaking API change for plugin authors. While documented in PLUGIN_GUIDE.md and PLUGIN_TUTORIAL.md, the version bump to 0.3.0 should be accompanied by a clear migration guide entry. The change is intentional but warrants explicit mention in release notes.


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

@1-DARK

1-DARK commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

Hey @debba, I’ve pushed the changes for all the points from your review.Whenever you have time, could you take another look? Thanks!

@debba debba left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @1-DARK for the quick turnaround, and for the exchangeGoogleOAuthToken addition. I checked out the branch again and went through 5d3f4816 point by point (the later commits are merges of main).

A lot of it is sorted now:

  • Slot callbacks: stripping functions out of the context and rebuilding them in the guest as stubs that call slot.onDatabaseChange / slot.setExtraField / slot.setCredentialFieldsHidden on the host is a clean approach. The argument checks on the host side plus sandboxSlotContext.test.ts make it easy to trust.
  • App CSP: img-src now allows https: and dangerousDisableAssetCspModification: ["style-src"] is in place.
  • RPC timeout: 30s with a test, good.
  • Asset shadowing: renaming to __runtime__.js / __host.css__ fixes it.
  • OAuth exchange: I looked at this one closely since it's a network call made on behalf of a plugin. The URL is hardcoded to Google's token endpoint, fields and grant_type are allow-listed, sizes are capped, and only known response fields go back to the frame. That looks right to me.

Typecheck, lint, the vitest suite and the sandbox_protocol Rust tests pass on my side, and CI is green.

What still needs work before I can merge:

  1. Read-only enforcement. The commit message mentions it, but only the wording changed: the docs now say isReadOnlyQuery is not a security boundary, while api.query still checks the text and then calls the regular execute_query (SandboxedPluginFrame.tsx:84-91). I ran the filter on this branch and then ran what it lets through against real databases:

    • SQLite: PRAGMA user_version = 42 passes the filter and writes to the database file.
    • PostgreSQL 16: WITH d AS (DELETE FROM users RETURNING *) SELECT * FROM d passes and empties the table; SELECT * INTO t2 FROM users passes and creates t2.
    • pg_terminate_backend(...), lo_export(...) and INTO OUTFILE pass as well.

    Since the whole point of the sandbox is that plugin code is untrusted, the host has to enforce this. We already do it for MCP, so you can reuse that path: ai_activity::classify_query_kind to reject writes, then execute_read_only_query from driver_trait.rs (mcp/mod.rs ~1330 shows both together). Please use both, not just the read-only transaction. In my tests BEGIN READ ONLY stopped the CTE DELETE and SELECT INTO, but pg_terminate_backend still ran, and SQLite's query_only still allowed PRAGMA writable_schema = 1. The Rust classifier catches those. A small command that does both, called from api.query, would close the gap, and you can keep isReadOnlyQuery as a quick first check.

  2. PR description. It still says "Write queries and statements the host can't classify as read-only are rejected" and still has the TODO: link migration notes placeholder. The export_connections_payload change is only mentioned in UI_SANDBOX_DESIGN.md. Please update the description so it covers the export hardening and links the migration notes in PLUGIN_TUTORIAL.md.

Minor, non-blocking:

  • Google Sheets companion PR: the host side is there now, but the plugin itself stops working once this ships until it migrates to exchangeGoogleOAuthToken. If you get the chance to open the companion PR in the Sheets plugin repo (a draft is fine), I can test both together and confirm the token exchange works from the webview.
  • src-tauri/src/commands.rs:6794: let selected is outdented inside the if let block and cargo fmt flags it.
  • api.oauth.googleToken is callable by any sandboxed plugin, not just Sheets. It's harmless as written, but gating it on the plugin id (or a manifest permission) would keep the surface smaller.
  • The guest runtime uses the same 30s RpcPort timeout, so an api.query that runs longer than that fails in the plugin while the query keeps running on the host. Probably worth either a longer timeout for queries or a note in the docs.
  • api.toast opens a native modal dialog on every call, so a misbehaving plugin can keep the user stuck clicking OK. Debouncing it or using a non-modal toast would be safer.
  • The guest modal in runtime.ts:157 hardcodes dark colors (#111827, #f9fafb), so it stays dark on light themes. The theme colors are already in the slot context, so it could use those.

Thanks again, this is very close. Once the query path goes through the database's read-only mode, I'll run my manual checks on a release build.

@1-DARK

1-DARK commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

Hi @debba, I addressed the read-only enforcement feedback in commit 2b077ea and fixed a query-classification regression after the upstream merge in 04de575. I updated the PR description with the connection-export hardening and migration notes as well. The checks pass locally; PostgreSQL integration tests need a running PostgreSQL service.
Could you review the updated branch when you have a chance?

@1-DARK
1-DARK requested a review from debba October 13, 2026 15:35
"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 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.

Comment thread packages/plugin-api/src/types.ts Outdated
grant_type: "authorization_code" | "refresh_token";
code?: string;
client_id: string;
client_secret?: string;

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: client_secret marked optional in GoogleOAuthTokenRequest but required for standard OAuth flows

The authorization_code grant type requires client_secret per OAuth 2.0 spec (RFC 6749). Making it optional allows invalid requests that will fail at Google's token endpoint. Consider making it conditionally required based on grant_type, or at minimum document that it's required for authorization_code.


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

Comment thread packages/plugin-api/src/slots.ts Outdated
@@ -73,6 +73,9 @@ export type SlotContextMap = {
};
"connection-modal.connection_content": {
driver: string;

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: Breaking change: Added new required fields to connection-modal.connection_content slot context

Added database: string, connectionName: string, and onDatabaseChange: (value: string) => void as required fields. Existing plugins using this slot will break at runtime when these fields are missing. This is a breaking API change that should be documented and versioned.


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

callback(hidden);
},
"api.query": async (query: unknown) => {
if (typeof query !== "string" || query.trim().length === 0 || query.length > 100_000) {

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.

SUGGESTION: Query length limit of 100,000 chars should be a named constant

Hardcoded 100_000 appears in both the host handler (line 82) and the Tauri command execute_plugin_read_only_query (commands.rs:4789). Extract to a shared constant (e.g., MAX_PLUGIN_QUERY_LENGTH) for consistency and maintainability.


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

"api.setting.set": async (key: unknown, value: unknown) => {
if (typeof key !== "string" || !key) throw new Error("Setting key is required");
const serialized = JSON.stringify(value);
if (typeof serialized !== "string" || serialized.length > 65_536) {

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.

SUGGESTION: Settings size limit of 64 KiB should be a named constant

Hardcoded 65_536 (64 KiB) for serialized settings size. Extract to a named constant (e.g., MAX_PLUGIN_SETTING_SIZE) for clarity and easier adjustment.


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

},
};

void connectFrame(frame, handlers, controller.signal).then(async (rpc) => {

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: Race condition between connection cleanup and promise resolution

connectFrame promise resolves, then checks disposed flag. If cleanup runs between resolution and the check, rpc.close() is called but rpcRef.current is still set. The disposed check should be atomic with the RPC assignment, or use a separate initialization state.


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

Comment thread src/plugins/sandbox/connectFrame.ts Outdated
});
// Sandboxed frames have an opaque origin; the source window is checked above
// and the transferred port is unique to this frame instance.
frame.contentWindow?.postMessage({ type: "tabularis:init" }, "*", [port2]);

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.

SUGGESTION: Use specific target origin instead of * for postMessage

frame.contentWindow?.postMessage({ type: "tabularis:init" }, "*", [port2]) uses wildcard target origin. Since the frame URL is known (tabularis-plugin://...), use the exact origin or 'self' equivalent to prevent message leakage if the frame is navigated.


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

Comment thread src/utils/pluginExplainLoader.ts Outdated
document.body.append(frame);

const controller = new AbortController();
const timeout = window.setTimeout(() => controller.abort(), 15_000);

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: 15-second hard timeout for parser module load may be too short

window.setTimeout(() => controller.abort(), 15_000) gives only 15s for the hidden iframe to load, establish RPC, and return parser metadata. On slow machines or large parser bundles, this can cause false failures. Consider making it configurable or increasing to 30s.


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

Comment thread src/utils/pluginExplainLoader.ts Outdated
...(entry.label ?? descriptor.label
? { label: entry.label ?? descriptor.label }
: {}),
parse: async (payload: string): Promise<ExplainPlan> =>

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.

CRITICAL: session.rpc.call in async parse/sniff functions — session may be closed

The registered parse and sniff functions (lines 178-185) capture session by reference. If another plugin registers the same format, replacedSession.close() is called (line 192), which closes the RPC port. Subsequent parse/sniff calls on the old session will fail with "Plugin frame closed". Use a session reference that checks validity at call time, or clone the RPC port.


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

@debba debba left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @1-DARK, this round closes the main gap. The new execute_plugin_read_only_query command is what I was hoping for: the Rust classifier runs first, then the driver's read-only path, and anything other than SQLite/PostgreSQL/MySQL fails closed. Nice touch adding the classifier test with all the payloads from my last review. I also like that the smaller points went into the same push: OAuth gated to google-sheets, the 5 minute query timeout, toasts instead of native dialogs, and the guest modal following the theme. The updated description reads well now.

Typecheck, lint, vitest (5862 tests) and cargo test --lib all pass on the branch, and CI is green.

Two things before I can merge:

  1. Current main breaks 7 tests. #1078 landed after your last merge from main and added tests/fixtures/pluginSlotProbe.tsx, which loads plugins the old way (read_plugin_file returning a __tabularis_plugin__ bundle rendered in the host tree). Git merges cleanly, but when I merged main locally, the plugin-context tests in RowEditorPanel.test.tsx (4) and TableToolbar.test.tsx (3) failed. The slots now render SandboxedPluginFrame, which throws i18n.getResourceBundle is not a function in that setup. Not your fault, just timing, but could you merge main again and adapt the fixture and those tests to the iframe model? Otherwise main goes red on merge.
  2. api.query now caps at 100 rows. execute_plugin_read_only_query passes Some(100) (commands.rs:4810). Before this PR, usePluginQuery went through execute_query with no limit. The guest only gets columns/rows, so a plugin silently sees the first 100 rows without knowing it. I'd pass None (or a much higher cap) and mention it in the docs, or return truncated to the plugin.

Minor, non-blocking: the diff has quite a few rustfmt-only hunks in the drivers and ai_activity.rs. They're fine (that formatting debt was already on main), just a bit noisy to review. And whenever you have time, the Sheets companion PR is still welcome, so I can test the token exchange end to end.

Once these two are in, I'll do my manual checks on a release build and we can get it merged.

@1-DARK
1-DARK requested a review from debba October 14, 2026 07:25
@1-DARK

1-DARK commented Oct 14, 2026

Copy link
Copy Markdown
Contributor Author

Hi @debba , both requested fixes are in commit 8ca6c6e: the plugin slot tests now use the iframe model, and api.query no longer silently caps results at 100 rows.Could you review the updated branch when you have a chance?

@debba debba left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @1-DARK, the slot tests and the row cap are sorted in 8ca6c6e7.

I can't merge this yet though, because it breaks existing plugins that aren't part of this repo. Google Sheets is the clearest case: once this ships, its OAuth flow stops working until the plugin migrates to exchangeGoogleOAuthToken, and other plugins that load UI extensions the old way are affected the same way. Users who update Tabularis would end up with broken plugins and no clear path forward.

Before this can land I need the Sheets companion PR (a draft is fine) so I can test both together, plus a check of the other published plugins that use UI slots, with either a migration for each or a compatibility path so they keep working in the meantime.

Comment thread src-tauri/src/commands.rs Outdated
match params.driver.as_str() {
"sqlite" | "postgres" | "mysql" | "mariadb" => {
driver
.execute_read_only_query(&params, &query, None, 1, schema.as_deref())

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: Plugin read-only queries are now unbounded

Changing the row limit from Some(100) to None removes the only host-side cap on rows returned to an untrusted sandboxed plugin. A plugin (or a buggy one) can run SELECT * FROM big_table, forcing the host to materialize the full result set in memory, serialize it over Tauri IPC, and then push it through the MessagePort RPC into the iframe. This can freeze or OOM the app. Consider a generous but finite cap (e.g. a named constant like PLUGIN_QUERY_MAX_ROWS) and/or a size limit on the serialized result, and document the truncation so plugins can paginate.


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

@1-DARK
1-DARK requested a review from debba October 17, 2026 17:15

@debba debba left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @1-DARK for sticking with this one, and for going through the published plugins one by one in UI_SANDBOX_DESIGN.md. That audit was what I was after, and I appreciate you opening the Sheets companion PR right away. The defaultValue fallback for Cloudflare D1 is a nice catch too.

I went through the plugins myself to double check. I loaded the released bundles of every plugin in the registry that has UI extensions or EXPLAIN parsers into a page that mirrors SandboxedPluginFrame, with the same guest CSP, sandbox flags and RPC. The good news: DynamoDB, Elasticsearch and SQL Server (extra field plus credential toggle) work as they are, and the Oracle, SQL Server and BigQuery EXPLAIN parsers load fine. On your branch, typecheck, lint, vitest (6375 tests) and the Rust sandbox and classifier tests all pass.

A few things still keep me from merging:

  1. The Sheets companion fails. exchangeCode calls getHost(), but @tabularis/plugin-api is mapped to __TABULARIS_API__ and the guest API has no getHost, so the wizard stops with getHost is not a function and the host is never called. Either expose getHost in the guest API or import exchangeGoogleOAuthToken directly, since the guest already provides it. Also, min_runtime_version should be the release that ships this PR. 0.28.0 is already out and doesn't have the API.
  2. Plugin modals get clipped. usePluginModal renders a position: fixed overlay inside the iframe, but the iframe height comes from the body's ResizeObserver, which a fixed overlay doesn't change. The Sheets "Connect Google Account" wizard ends up squeezed into a ~150px frame where you only see the title. The companion still uses the modal, so this needs a host-side fix, for example a guest call that lets the frame grow into a full overlay while a modal is open.
  3. BigQuery is missing from the audit and breaks once you merge main. #629 added setSecretField / secretFields to connection-modal.extra_fields, and BigQuery 0.2.0 relies on them while hiding the Password field. serializePluginSlotContext only forwards setExtraField, so the credential input never shows up and only ADC works. Please add setSecretField to the forwarded callbacks with the same kind of validation as the others.
  4. The branch conflicts with main. ai_activity.rs now goes through classify_query_kind_for_dialect, so the extra side-effect functions and tests need to be carried over onto that. PLUGIN_GUIDE.md conflicts as well. Also please drop the manual plugin-api bump to 0.3.0: release-please already published 0.3.0 without exchangeGoogleOAuthToken, so that version number is taken.
  5. Users on Sheets 0.3.0. After updating Tabularis they can't connect a Google account until the plugin updates. Please make sure the companion is ready to release first, and ideally show a hint in the host when an old Sheets bundle fails to load its OAuth flow.

Smaller things, fine as follow-ups if you prefer:

  • src-tauri/resources/guest/runtime.js is out of date with runtime.ts (the defaultValue change isn't in it). Release builds regenerate it, but cargo builds and tests embed the committed file. I'd rather not commit it, or at least check it in CI.
  • The frame doesn't inherit the host text colour, so the Cloudflare D1 help box renders black text on dark themes. Setting color and the theme variables in the guest from the theme you already pass would fix it.
  • The CSP doesn't stop the frame from navigating itself to an external URL. Not a big deal since plugin binaries have network access anyway, but I'd soften the "blocks network access" wording or block that navigation too.

Once these are sorted I'll try the whole thing again on a release build together with the Sheets companion.

@debba

debba commented Oct 19, 2026

Copy link
Copy Markdown
Collaborator

Adding the screenshots from the harness I mentioned in the review (released plugin bundles loaded into a page that mirrors SandboxedPluginFrame, same guest CSP and sandbox flags, rendered in Chromium so it's close to the app but not WebKitGTK).

Google Sheets 0.3.0, "Connect Google Account" wizard. The position: fixed modal stays inside the iframe, which doesn't grow, so only the title and the first step heading are visible:

Sheets OAuth modal clipped inside the sandbox frame

Cloudflare D1 help box. It works, but the frame doesn't inherit the host text colour, so the labels and help text come out dark on a dark theme:

Cloudflare D1 help box with dark text on a dark theme

const handlers = {
"ui.modal": (open: unknown) => {
if (typeof open !== "boolean") throw new Error("Modal state must be a boolean");
setModalOpen(open);

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: Untrusted guest can take over the whole host window via ui.modal

ui.modal is an unauthenticated, guest-controlled RPC. When open === true, the iframe style (lines 269-278) becomes position: fixed; inset: 0; width: 100vw; height: 100dvh; z-index: 2147483647, covering the entire Tabularis window and intercepting all pointer/keyboard input. A plugin (including a malicious or buggy one) can call ui.modal(true) at any time without ever rendering a dialog, which blocks the host UI (including the means to disable/uninstall the plugin) and enables convincing UI spoofing of host dialogs. The host should only honour ui.modal(true) after a user gesture, and should auto-reset it (e.g. on unmount of the slot, or after a timeout) and give the user a host-rendered way to dismiss.


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

aria-hidden="true"
style={{ width: "100%", height, pointerEvents: "none" }}
/>
{createPortal(iframe, document.body)}

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: Portaled position: fixed iframe is not clipped or reliably tracked

The iframe is rendered into document.body with position: fixed at a rect measured from an anchor div, and the measurement effect (line 55-72) only re-runs on [height], window resize, and capture-phase scroll. The anchor can move without any of these events (sibling layout changes, sidebar/panel resize, tab switches, a host modal opening/closing, CSS transitions), leaving the iframe visually detached from its slot. Also, because it is outside the slot's DOM subtree it ignores overflow clipping of its scroll containers (e.g. the connection modal body), so the plugin UI can paint over the modal header/footer or outside the modal, and it sits at zIndex: 1, which may fall below or above host overlays unpredictably. A ResizeObserver on the anchor does not detect position-only changes; consider observing ancestors / using IntersectionObserver or a per-frame requestAnimationFrame check while visible, and clip to the nearest scrollable ancestor.


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

Comment thread packages/plugin-guest/src/runtime.ts Outdated
return rpcPort.call(method, args) as Promise<T>;
}

window.addEventListener("securitypolicyviolation", (event) => {

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.

SUGGESTION: Plugin-specific hard-coded hack in the generic guest runtime

The securitypolicyviolation listener special-cases pluginId === "google-sheets" and https://oauth2.googleapis.com, and the host mirrors it with api.compatibility.legacyGoogleOAuth. Keeping third-party plugin identifiers in the generic sandbox runtime (and in the host RPC allow-list) couples the platform to one plugin and is easy to forget to remove. Consider a documented, time-boxed removal (TODO with target version) or a generic mechanism (e.g. a manifest-declared legacy shim) instead. Note also blockedURI is only reported for same-origin-visible URLs; if it is "inline"/"" the toast will silently never fire.


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

@1-DARK
1-DARK requested a review from debba October 19, 2026 17:01

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feat]: Sandbox plugin JavaScript, add a CSP and stop exporting secrets by default

2 participants