Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
cd3ee5a
feat(database): add Postgres/MySQL client with connect and schema bro…
kenzouno1 Jul 1, 2026
31b08e2
feat(database): add SQL query editor, results grid, and safe execution
kenzouno1 Jul 1, 2026
2ddbe72
fix(database): close read-only bypass, poisoned-commit, and unbounded…
kenzouno1 Jul 1, 2026
6dca7a2
fix(database): use a non-empty SSL sentinel in the connection form
kenzouno1 Jul 1, 2026
cdd246f
Merge remote-tracking branch 'origin/main' into feat/database-client
kenzouno1 Jul 1, 2026
64326de
docs(database): add implementation plan and phase/review reports
kenzouno1 Jul 1, 2026
dc1374e
fix(database): harden connection lifecycle, update validation, and SQ…
kenzouno1 Jul 1, 2026
7b5864a
feat(database): preview table rows from the schema tree
kenzouno1 Jul 1, 2026
f671e8d
docs(database): add stagereview/coderabbit risk review report
kenzouno1 Jul 1, 2026
9d9e682
refactor(database): rework connection row with double-click open and …
kenzouno1 Jul 1, 2026
f71ae46
feat(database): editable data grid with sort, filter, and inline editing
kenzouno1 Jul 1, 2026
d8f9fe4
docs(database): add data grid editor implementation report
kenzouno1 Jul 1, 2026
a6e516b
fix(database): harden data grid saves and async result ordering
kenzouno1 Jul 1, 2026
0642d89
fix(database): verify MySQL TLS hostname and preserve BIGINT precision
kenzouno1 Jul 1, 2026
a4ea870
fix(database): harden data-grid editing and result-cell selection
kenzouno1 Jul 1, 2026
37b42ef
chore(database): remove database-client plan and review reports
kenzouno1 Jul 1, 2026
a45d776
fix(database): bound execute results and evict pg client on rollback …
kenzouno1 Jul 1, 2026
f5ea51c
fix(database): guard cell formatting and tighten column-filter typing
kenzouno1 Jul 1, 2026
1fd2c1b
fix(database): evict/destroy connections when COMMIT or ROLLBACK fails
kenzouno1 Jul 1, 2026
6708c48
fix(database): make the data-tab close button reachable by keyboard
kenzouno1 Jul 1, 2026
fbe9e25
merge: main into feat/database-client
kenzouno1 Jul 2, 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
39 changes: 38 additions & 1 deletion config/localization-coverage-allowlist.json
Original file line number Diff line number Diff line change
@@ -1 +1,38 @@
[]
[
{
"area": "src/renderer",
"filePath": "src/renderer/src/components/database/data-grid-filters.ts",
"kind": "object-property:label",
"text": "LIKE",
"dynamic": false,
"count": 1,
"reason": "SQL operator keyword shown verbatim in the filter dropdown; not translatable copy."
},
{
"area": "src/renderer",
"filePath": "src/renderer/src/components/database/data-grid-filters.ts",
"kind": "object-property:label",
"text": "ILIKE",
"dynamic": false,
"count": 1,
"reason": "SQL operator keyword shown verbatim in the filter dropdown; not translatable copy."
},
{
"area": "src/renderer",
"filePath": "src/renderer/src/components/database/data-grid-filters.ts",
"kind": "object-property:label",
"text": "IS NULL",
"dynamic": false,
"count": 1,
"reason": "SQL operator keyword shown verbatim in the filter dropdown; not translatable copy."
},
{
"area": "src/renderer",
"filePath": "src/renderer/src/components/database/data-grid-filters.ts",
"kind": "object-property:label",
"text": "IS NOT NULL",
"dynamic": false,
"count": 1,
"reason": "SQL operator keyword shown verbatim in the filter dropdown; not translatable copy."
}
]
5 changes: 5 additions & 0 deletions config/packaged-runtime-node-modules.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ const PACKAGED_RUNTIME_PACKAGE_ROOTS = [
'electron-updater',
'i18next',
'jsonc-parser',
'mysql2',
'node-pty',
// Database client drivers — bundled + lazy-imported (await import) from the main
// process; absent from this allowlist => MODULE_NOT_FOUND in packaged builds. The
// graph walk pulls their transitive deps (pg-*, denque, named-placeholders, …).
'pg',
'posthog-node',
// serve-sim (for CLI JS entry + closure + state/middleware + to make packaged require('serve-sim') + its internal relatives work; mirrors other runtime JS like ws/yaml/zod. Natives/dylibs still via extraResources + the node_modules/serve-sim copy in resources from builder. Client if added too.
'serve-sim',
Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,9 @@
"electron-updater": "^6.8.3",
"i18next": "^26.3.1",
"jsonc-parser": "^3.3.1",
"mysql2": "^3.22.5",
"node-pty": "^1.1.0",
"pg": "^8.22.0",
"posthog-node": "^5.33.3",
"qrcode": "^1.5.4",
"react-i18next": "^17.0.8",
Expand Down Expand Up @@ -141,6 +143,7 @@
"@tiptap/react": "^3.22.5",
"@tiptap/starter-kit": "^3.22.5",
"@types/node": "^25.6.0",
"@types/pg": "^8.20.0",
"@types/qrcode": "^1.5.6",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
---
phase: 1
title: "Scaffold and Driver Deps"
status: done
effort: "S"
---

# Phase 1: Scaffold and Driver Deps

## Overview

Stand up the empty `Database` top-level view, add the `pg`/`mysql2` driver deps,
and prove they are **copied into the packaged app** and lazy-import cleanly on all
3 OSes. No real DB behavior yet — this de-risks packaging first, because Orca does
**not** bundle main-process deps into the asar.

## Requirements

- Functional: a `Database` entry in the sidebar opens a placeholder full-screen view.
- Non-functional: drivers resolvable via `await import(...)` from the main process
**in a packaged build**, not only in dev; not present in the renderer eager bundle;
no native rebuild step required.

## Architecture

New top-level view kind `'database'` added to the `activeView` union, routed to a
lazy-loaded `DatabasePage`, opened by a sidebar nav button.

**Packaging reality (red-team F1):** `electron.vite.config.ts` externalizes main
deps (`externalizeDeps`, ~`:174`), so `pg`/`mysql2` stay as bare runtime imports.
The packaged app only ships an **explicit allowlist** of runtime node_modules —
`PACKAGED_RUNTIME_PACKAGE_ROOTS` in `config/packaged-runtime-node-modules.cjs`
(~`:8-25`) — copied and then checked by `verifyPackagedMainRuntimeDeps` in
`electron-builder.config.cjs` `afterPack` (~`:130`). Drivers absent from that list
= `MODULE_NOT_FOUND` at runtime (or a hard `afterPack` throw). They must be added.

**Lazy-import precedent (red-team F4):** the correct model is the main-process
dynamic import `await import('@parcel/watcher')` in
`src/main/ipc/filesystem-watcher.ts:296` (externalized + allowlisted), **not** the
renderer `MermaidBlock.tsx` code-split. The verifier scans for `require("…")`; the
emitted form of an externalized dynamic `import()` may be `import(...)` and slip the
regex — so confirm the emitted output and that the packaged app actually resolves
the module.

## Related Code Files

- Create: `src/shared/database-types.ts` (stub: `DbEngine = 'postgres' | 'mysql'`).
- Create: `src/renderer/src/components/database/DatabasePage.tsx` (placeholder shell).
- Modify: `src/renderer/src/store/slices/ui.ts` — add `'database'` to the
`activeView` union (~`:547`) + `setActiveView` handling (mirror `'skills'`).
- Modify: `src/renderer/src/App.tsx` — lazy import + route `activeView === 'database'`
→ `<DatabasePage />` (mirror `SkillsPage` lazy route ~`:279`/`:2262`).
- Modify: sidebar — add a DB nav button (mirror `SidebarTaskNavButton.tsx`),
lucide `Database` icon, STYLEGUIDE tokens, platform-correct shortcut label.
- Modify: `package.json` — add `pg`, `mysql2`, `@types/pg`.
- **Modify: `config/packaged-runtime-node-modules.cjs`** — add `'pg'` and `'mysql2'`
to `PACKAGED_RUNTIME_PACKAGE_ROOTS` (red-team F1). The graph walk pulls mysql2's
transitive deps (denque, lru-cache, named-placeholders, sqlstring, …).
- Modify: `config/electron-builder.config.cjs` — only if a packaged smoke test shows
asar unpack is additionally needed (expected: allowlist copy is sufficient).

## Implementation Steps

1. `pnpm add pg mysql2 && pnpm add -D @types/pg`.
2. Add `'database'` to `activeView` union + setter in `ui.ts`.
3. Create `DatabasePage.tsx` placeholder (empty state, shadcn primitives, tokens).
4. Lazy-route it in `App.tsx`; add the sidebar nav button (no hardcoded `⌘`).
5. Add `database-types.ts` stub.
6. Add `pg`/`mysql2` to `PACKAGED_RUNTIME_PACKAGE_ROOTS`.
7. Smoke-test lazy import **in a packaged build**: `await import('pg')` /
`import('mysql2/promise')` resolves from the packaged app (not just dev); confirm
`verifyPackagedMainRuntimeDeps` passes; confirm emitted require/import form. Remove
the temp probe after.
8. Confirm drivers are **absent from the renderer eager bundle** by checking the
main-process startup require graph excludes them until first connect (red-team F4)
— do not rely on a renderer cold-start diff, which passes trivially for main-only deps.

## Success Criteria

- [ ] `Database` view opens empty from the sidebar; typecheck + lint pass.
- [ ] **Packaged** build resolves `pg`/`mysql2` at runtime; `verifyPackagedMainRuntimeDeps`
green on macOS/Linux/Windows (red-team F1).
- [ ] Drivers not loaded in the main-process startup require graph until first connect.
- [ ] No `electron/rebuild` needed (pg/mysql2 are pure-JS — confirmed).

## Risk Assessment

- `mysql2` optional native bits (compression/crypto) — confirm it loads without them;
document any disabled optional feature.
- Externalized dynamic `import()` may emit a form the `require()`-only verifier misses,
yielding a silent prod `MODULE_NOT_FOUND` — step 7 must test the **packaged** app,
not dev.

## Red Team Hardening (applied)

- **F1 (Critical):** add `pg`/`mysql2` to `config/packaged-runtime-node-modules.cjs`
allowlist; success criterion = packaged `verifyPackagedMainRuntimeDeps` on 3 OSes.
- **F4 (Medium):** re-anchored lazy-import to main-process `filesystem-watcher.ts:296`;
replaced the meaningless renderer cold-start check with a main-process require-graph
assertion; confirm `import()` vs `require()` emission.
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
---
phase: 2
title: "Connection Model and Secure Persistence"
status: done
effort: "M"
---

# Phase 2: Connection Model and Secure Persistence

<!-- Updated: Validation Session 1 - weak-backend=warn+store; SSL default=smart-by-host; readOnly default=writable(false) -->

## Overview

Define the connection data model (incl. the `readOnly` flag, default **writable**),
persist a global connection list with the password **encrypted with a real OS
backend when available**, and build the connection-management UI. On a weak/absent
crypto backend the app **warns and still stores** (validated decision) so headless/SSH
developers keep working. No live DB driver yet — that's Phase 3.

## Requirements

- Functional: create/read/update/delete connections; list renders; form validates
per engine; password never shown after save; `readOnly` defaults **false** (writable);
SSL defaults **smart-by-host**.
- Non-functional: password encrypted via a strong `safeStorage` backend; when only the
weak `basic_text` backend (or none) is available, show a clear banner and store with
informed consent (warn-and-store) — never a *silent* weak write. List is global.

## Architecture

Mirror the SSH-target list/CRUD pattern, but **do not** reuse the cookie-grade crypto
path for DB passwords (red-team F2/F5/F-decrypt). Connections live in the global JSON
`Store` (`persistence.ts`) as `dbConnections[]`, normalized on load, saved via
`scheduleSave()`.

**Credential security deltas:**
- `encrypt()` (`persistence.ts:213-218`) silently returns plaintext when
`!isEncryptionAvailable()`, and `isEncryptionAvailable()` returns **true** even for
the hardcoded-key `basic_text` backend. For DB passwords, check
`safeStorage.getSelectedStorageBackend()`: strong backend → encrypt; weak
(`basic_text`) or unavailable → **warn-and-store** behind a clear UI banner
(validated). Never silently persist a recoverable secret without the banner.
- Decrypt must be **fail-closed**: the shared `decrypt()` (`:225-239`) returns the
ciphertext on failure (fine for a cookie, wrong for a credential). Use a strict DB
variant that throws → surfaced as "password could not be decrypted on this machine".
- The connection list lives in `orca-data.json`, written by the plain
`writeFile`/`writeFileSync` path (`:3339`/`:3394`), **not** the ACL-hardened
`writeSecureFile` (`src/shared/secure-file.ts`). Route DB secrets through
`writeSecureFile`/a hardened sidecar, or `hardenExistingSecureFile` on
`orca-data.json`, so a warn-and-store password isn't world-readable on disk.
- Encryption happens in **both** persistence write paths (`writeToDiskAsync` ~`:3321`,
`writeToDiskSync` ~`:3376`); iterate `dbConnections[]` and encrypt each nested
password in both, operating on the serialized clone (never mutating `this.state`).

```ts
// src/shared/database-types.ts
export type DbEngine = 'postgres' | 'mysql'
export type DbSslMode = 'disable' | 'verify-full' | 'insecure-no-verify'
export type DbConnection = {
id: string; name: string; engine: DbEngine
host: string; port: number; database: string; user: string
password?: string // encrypted at rest; warn-and-store on weak backend
ssl?: DbSslMode // default SMART-BY-HOST: localhost→disable, remote→verify-full; 'insecure-no-verify' explicit opt-in
readOnly: boolean // default FALSE (writable, validated); missing → false on load; confirm-dialog (P5) guards writes
sshTunnel?: { targetId: string } // RESERVED — not wired in v1
createdAt: number; updatedAt: number
}
```

## Related Code Files

- Modify: `src/shared/database-types.ts` — full `DbConnection` (incl. `readOnly`,
SSL modes) + input/DTO types.
- Modify: `src/main/persistence.ts` — `getDbConnections/.../removeDbConnection`
(mirror `getSshTargets` ~`:5579`); **strong-backend** encrypt on save in BOTH
write paths (~`:3321`/`:3376`); strict decrypt on load; normalize on load with
`readOnly ?? false` (mirror `normalizeSshTarget` ~`:3124`).
- Create: `src/main/database/db-credential-store.ts` — `getSelectedStorageBackend()`
check, strict encrypt/decrypt, warn-and-store signaling. (Named for what it holds —
not a `utils` file.)
- Create: `src/main/ipc/database.ts` — `database:list|add|update|remove` +
`database:encryptionStatus` (drives the form banner).
- Modify: `src/main/ipc/register-core-handlers.ts` — `registerDatabaseHandlers(store)`
with the `registered` guard, **gated by `isTrustedUIRenderer`** (Phase 3 F15).
- Modify: `src/preload/index.ts` — `window.api.database` namespace.
- Create: `src/renderer/src/store/slices/database.ts` (mirror `slices/ssh.ts`).
- Create: `src/renderer/src/components/database/ConnectionList.tsx`,
`ConnectionForm.tsx` (engine→default port; SSL select defaulting smart-by-host;
`readOnly` toggle default off; encryption-unavailable banner); mount in `DatabasePage.tsx`.

## Implementation Steps

1. Flesh out `database-types.ts` (incl. `readOnly` default false, SSL modes).
2. Build `db-credential-store.ts` with `getSelectedStorageBackend()` + strict crypto;
weak backend = **warn-and-store** (validated) signaled to the form banner.
3. Add `dbConnections` to `PersistedState` + CRUD + encrypt(both paths)/strict-decrypt/
normalize(`readOnly ?? false`) in `persistence.ts`.
4. Route secret-at-rest through `writeSecureFile`/sidecar or harden `orca-data.json`.
5. Implement `ipc/database.ts` CRUD + `encryptionStatus`; register (trusted-sender gated).
6. Add `window.api.database` to preload; build slice; build `ConnectionList` +
`ConnectionForm` (smart-by-host SSL default; write-only password; `readOnly` toggle
default off; banner when encryption weak/unavailable).
7. Tests: round-trip asserts ciphertext != plaintext on strong backend **and** exercises
the `getSelectedStorageBackend()==='basic_text'`/unavailable branch (warn-and-store,
not mocked `true`); normalize coerces missing `readOnly` → false and missing `ssl` →
smart-by-host; CRUD calls `scheduleSave`; both write paths encrypt nested passwords.

## Success Criteria

- [ ] Add/edit/delete connections; list survives restart; `readOnly` defaults writable;
SSL defaults smart-by-host.
- [ ] Strong-backend machine: password encrypted (assert in test).
- [ ] Weak/absent backend: form shows the banner and stores with consent (warn-and-store);
no SILENT weak write (test the false branch).
- [ ] Legacy/missing `readOnly` loads writable; missing `ssl` resolves smart-by-host.
- [ ] Typecheck + lint + unit tests green.

## Risk Assessment

- Warn-and-store (validated) writes a weaker secret on keychain-less Linux — the banner
must be unmistakable so the user understands the at-rest weakness; pair with the
hardened-file step so it isn't *also* world-readable.
- Schema drift: normalize on load so older `orca-data.json` files don't crash.

## Red Team Hardening (applied)

- **F2 (Critical):** gate on `getSelectedStorageBackend()`; warn-and-store banner
instead of silent plaintext (no template existed — build the banner).
- **F-decrypt (Medium):** strict fail-closed decrypt for DB creds.
- **F5 (High):** harden the at-rest file (secure-file/sidecar), not ciphertext-only.
- **F3/F7 (Medium):** `readOnly` defined here, default **writable** (validated); missing
normalizes to writable — the P5 confirm dialog is the write safety net.
- **Two-write-path note:** encrypt nested passwords in both `writeToDiskAsync`/`Sync`.
Loading