(MOT-4361) feat(database,console): writable sql panel, driver decode fixes, page-wide consistency, shared config dialog - #713
Conversation
… overhaul
Worker:
- mysql result columns report SQL type names (varchar, int) instead of
the protocol enum's debug names (MYSQL_TYPE_VAR_STRING)
- postgres row decoder handles the internal one-byte "char" type
(OID 18): listTables read pg_class.relkind and failed on every
postgres server with 'error deserializing column 2'
SQL panel:
- writes allowed: reads route through database::query, writes through
database::execute — the same surfaces agents use, so engine policy
and database::row-changed see both alike
- DDL (drop/truncate/alter) gets an alert-ink note naming the change
before it runs and a past-tense echo after ('dropped table x')
instead of MySQL's truthful-but-misleading '0 affected'
- errText unwraps the SDK's wire-error envelope; every failure used to
render '[object Object]'
Page-wide consistency (from the impeccable critique pair):
- user-work panels keep-mounted across mode switches; header refresh
threads a token into fetchers instead of remount keys, so it can no
longer wipe filter drafts
- shared toolbar grammar, derived AA warn/alert inks in both themes,
page-wide focus-visible, live regions (sql status, changes role=log,
data row/page announcements), focus-within beside hover reveals
- collapsible table rail with type-to-filter; universal schema prefix
stripped from display; health labels+demotes replication daemons and
rolls durations up to minutes/hours; table list tagged by source db
so a failed switch can't render the previous database's schema
- inspector header in the shared bar grammar with an explicit close
(a cell cursor used to pin the panel open with no way out)
- in-place configuration via the console's shared dialog when the
running console exports it, workers-tab navigation as the fallback
The workers-page configuration editor joins the curated component registry, the package manifest, and the regenerated vendor shim — the first page-level composite in the kit. A worker page can now offer 'configure' in place (via a host.components runtime lookup) instead of deep-linking to the workers tab and stranding the operator on the workers list when the editor closes. Consoles predating the export degrade to the navigation path; reading the registry at runtime rather than importing the name is what keeps an old console from failing the page's module load. Also: CodeEditor's completions separator becomes a visible '\n' — it was an invisible U+0001 that renders as an empty string in editors, greps, and diffs, and cost a design review a false 'autocomplete is broken' P1 before a live probe disproved it.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
skill-check — worker0 verified, 54 skipped (no docs/).
Four for four. Nicely done. |
📝 WalkthroughWalkthroughThe change exposes ChangesConsole component surface
Database driver metadata
Database console
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant SqlPanel
participant runAdhocSql
participant DatabaseRPC
participant DatabasePage
User->>SqlPanel: Submit SQL
SqlPanel->>runAdhocSql: Route SQL by statement type
runAdhocSql->>DatabaseRPC: Call database::query or database::execute
DatabaseRPC-->>runAdhocSql: Return rows or write metadata
runAdhocSql-->>SqlPanel: Return AdhocResult
SqlPanel->>DatabasePage: Notify successful write
DatabasePage->>SqlPanel: Refresh retained panel state
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (3)
database/ui/styles.css (1)
857-858: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
.db-erd-stat.quietrepeats the base color.Both rules set
color: var(--color-ink-faint), so thequietmodifier changes nothing. Give it a dimmer value or remove the rule.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@database/ui/styles.css` around lines 857 - 858, Update the `.db-erd-stat.quiet` rule so it has a visibly dimmer color than the base `.db-erd-stat` style, or remove the redundant modifier rule if no distinct quiet appearance is needed.database/ui/src/page/index.tsx (1)
484-512: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winMount the retained diagram and changes panels on first visit.
The wrappers at Lines 484-512 render on the first page load, so
ErdPanelruns itsschemaDiagramread andChangesPanelopens its row-change subscription even when the user stays on the data tab. The diagram layout is the most expensive worker call on this page.Track visited modes and render each retained panel only after its first visit. State retention still works, because the panel then stays mounted.
♻️ Proposed change: gate the first mount on a visited set
+ // A retained panel keeps its state after the first visit; it does not need + // to pay for its first read before that visit happens. + const [visited, setVisited] = useState<Set<PanelMode>>(() => new Set(['data'])) + useEffect(() => { + setVisited((cur) => (cur.has(mode) ? cur : new Set(cur).add(mode))) + }, [mode])- {modes.includes('diagram') ? ( + {modes.includes('diagram') && visited.has('diagram') ? ( <div className="db-keep" hidden={mode !== 'diagram'}>- {modes.includes('changes') ? ( + {modes.includes('changes') && visited.has('changes') ? ( <div className="db-keep" hidden={mode !== 'changes'}>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@database/ui/src/page/index.tsx` around lines 484 - 512, Update the page component’s mode-retention logic to track which modes have been visited, and gate the retained ErdPanel and ChangesPanel wrappers on that visited-mode state. Mark the active mode visited when it is first entered, while keeping each panel mounted after its initial visit so its state and subscriptions are retained during later mode switches.database/ui/src/page/db-data.ts (1)
203-211: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMove the routing docblock onto
isReadOnlySql.The block at Lines 203-208 documents the read check, but it now sits directly above
stripSqlComments. Attach it toisReadOnlySqland givestripSqlCommentsits own one-line description.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@database/ui/src/page/db-data.ts` around lines 203 - 211, Move the existing routing explanation docblock from above stripSqlComments to immediately above isReadOnlySql. Add a concise one-line description directly above stripSqlComments identifying it as the SQL-comment removal helper, without changing either function’s behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@console/web/src/components/ui/CodeEditor.tsx`:
- Around line 213-221: Update the completionsKey serialization used by
CodeEditor so arbitrary completion strings, including those containing newline
characters, round-trip without splitting into multiple labels. Replace the
delimiter-based join/split flow with JSON or another collision-free encoding,
while preserving the existing filtering and completion update behavior in the
effect.
In `@console/web/src/lib/console-api.ts`:
- Around line 93-96: Update WorkerConfigurationDialog before exposing it through
Host.components so configurationId is constrained to the current owner/tab
context or explicitly authorized. Validate the identifier before every
configuration load and save, rejecting unauthorized targets rather than passing
the untrusted value through. Keep the shared editor available only for
authorized configurations; freezing Host.components alone is insufficient.
In `@database/ui/src/lib/errors.ts`:
- Around line 42-48: Update unwrap to parse JSON only when current.message
begins with the handler error: envelope prefix, preserving ordinary messages and
error metadata even when they contain JSON; add a regression test covering a
non-wrapper message containing JSON.
In `@database/ui/src/page/db-data.ts`:
- Around line 209-219: Update stripSqlComments and the isReadOnlySql flow to
mask single- and double-quoted string literals with placeholders during the same
preprocessing pass as comment removal, preserving the surrounding SQL structure
for keyword checks. Ensure WRITE_ANYWHERE does not match keywords inside
literals while existing comment stripping and read-only validation remain
intact.
In `@database/ui/src/page/result-grid.tsx`:
- Around line 203-205: Update the row rendering in the table grid so its
keyboard handlers and tabIndex are applied only when keyboard is absent; when
keyboard is enabled, leave rows without Tab stops while preserving the grid cell
roving behavior and existing row-click handling.
---
Nitpick comments:
In `@database/ui/src/page/db-data.ts`:
- Around line 203-211: Move the existing routing explanation docblock from above
stripSqlComments to immediately above isReadOnlySql. Add a concise one-line
description directly above stripSqlComments identifying it as the SQL-comment
removal helper, without changing either function’s behavior.
In `@database/ui/src/page/index.tsx`:
- Around line 484-512: Update the page component’s mode-retention logic to track
which modes have been visited, and gate the retained ErdPanel and ChangesPanel
wrappers on that visited-mode state. Mark the active mode visited when it is
first entered, while keeping each panel mounted after its initial visit so its
state and subscriptions are retained during later mode switches.
In `@database/ui/styles.css`:
- Around line 857-858: Update the `.db-erd-stat.quiet` rule so it has a visibly
dimmer color than the base `.db-erd-stat` style, or remove the redundant
modifier rule if no distinct quiet appearance is needed.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: f390288c-17d4-46ee-8c47-eb0d12d758ae
📒 Files selected for processing (29)
console/web/public/vendor/console-ui.jsconsole/web/src/components/ui/CodeEditor.tsxconsole/web/src/lib/console-api.tsconsole/web/src/lib/console-ui-conformance.test.tsdatabase/src/driver/mysql.rsdatabase/src/driver/postgres.rsdatabase/src/ui.rsdatabase/ui/src/configuration/index.tsxdatabase/ui/src/lib/errors.test.tsdatabase/ui/src/lib/errors.tsdatabase/ui/src/lib/rpc.tsdatabase/ui/src/page/ChangesPanel.tsxdatabase/ui/src/page/ErdPanel.tsxdatabase/ui/src/page/HealthPanel.tsxdatabase/ui/src/page/PlanTree.tsxdatabase/ui/src/page/SchemaTree.tsxdatabase/ui/src/page/SqlPanel.tsxdatabase/ui/src/page/TableDataPanel.tsxdatabase/ui/src/page/db-data.tsdatabase/ui/src/page/icons.tsxdatabase/ui/src/page/index.tsxdatabase/ui/src/page/pagination.tsxdatabase/ui/src/page/result-grid.tsxdatabase/ui/src/page/run-sql.test.tsdatabase/ui/src/page/useDatabaseRead.tsdatabase/ui/src/page/useRowChanges.tsdatabase/ui/styles.csspackages/console-ui/component-names.mjspackages/console-ui/index.d.ts
| // '\n' as separator: identifiers can't contain it, and unlike the | ||
| // invisible control character it replaced, it can't masquerade as an | ||
| // empty string in an editor or a grep. | ||
| const completionsKey = (completions ?? []).join('\n') | ||
| React.useEffect(() => { | ||
| const editor = editorRef.current | ||
| if (!ready || !editor) return | ||
| const words = completionsKey | ||
| ? completionsKey.split('').filter(Boolean) | ||
| ? completionsKey.split('\n').filter(Boolean) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Repo files matching target:"
fd -a 'CodeEditor\.tsx|SqlPanel\.tsx' . || true
echo
echo "CodeEditor relevant lines:"
if [ -f console/web/src/components/ui/CodeEditor.tsx ]; then
sed -n '180,245p' console/web/src/components/ui/CodeEditor.tsx | cat -n
fi
echo
echo "SqlPanel references to CodeEditor completions:"
fd -a 'SqlPanel\.tsx' . | while read -r f; do
echo "--- ${f#"$PWD/"}"
rg -n "completion|Completions|CodeEditor|table|schema" "$f" || true
done
echo
echo "Direct usages of CodeEditor completions prop in repo:"
rg -n "<CodeEditor|CodeEditor\(" . -g '*.tsx' -g '*.ts' || trueRepository: iii-hq/workers
Length of output: 5587
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "SqlPanel completions construction and CodeEditor usage:"
sed -n '90,120p' database/ui/src/page/SqlPanel.tsx | cat -n
sed -n '200,214p' database/ui/src/page/SqlPanel.tsx | cat -n
echo
echo "All strings passed into CodeEditor completions prop (static source analysis):"
python3 - <<'PY'
from pathlib import Path
import re
for path in Path('.').rglob('*'):
if not path.is_file():
continue
if path.suffix not in {'.tsx','.ts'}:
continue
text = path.read_text(errors='ignore').splitlines()
for i,line in enumerate(text,1):
m = re.search(r'\bcompletions\s*=\s*(\S+)', line)
if m:
print(f"{path}:{i}: {line.strip()}")
PY
echo
echo "Type declaration for CodeEditor completions prop:"
rg -n "completions\\?:" packages/console-ui/ -g '*.ts' -g '*.tsx' || true
sed -n '370,390p' packages/console-ui/index.d.ts 2>/dev/null || trueRepository: iii-hq/workers
Length of output: 3860
Use lossless completion serialization.
completions is a readonly string[], and SqlPanel passes database table names into it. If a table name contains \n, CodeEditor joins and splits with \n, so that entry produces two completion labels. Use JSON or another collision-free encoding.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@console/web/src/components/ui/CodeEditor.tsx` around lines 213 - 221, Update
the completionsKey serialization used by CodeEditor so arbitrary completion
strings, including those containing newline characters, round-trip without
splitting into multiple labels. Replace the delimiter-based join/split flow with
JSON or another collision-free encoding, while preserving the existing filtering
and completion update behavior in the effect.
| // The one page-level composite in the kit: worker pages offer "configure" | ||
| // in place instead of navigating to the workers tab and stranding the | ||
| // operator there when the editor closes. | ||
| WorkerConfigurationDialog, |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Repo files matching console-api/database page/workers/config:"
git ls-files | rg '(^|/)(console-api|index\.tsx|.*config|.*worker|.*configuration).*' || true
echo
echo "console/web/src/lib/console-api.ts outline:"
if [ -f console/web/src/lib/console-api.ts ]; then
ast-grep outline console/web/src/lib/console-api.ts || true
echo
sed -n '1,180p' console/web/src/lib/console-api.ts | cat -n
fi
echo
echo "database/ui/src/page/index.tsx relevant lines:"
if [ -f database/ui/src/page/index.tsx ]; then
sed -n '60,280p' database/ui/src/page/index.tsx | cat -n
fi
echo
echo "Search WorkerConfigurationDialog definition/usages:"
rg -n "WorkerConfigurationDialog|ConfigurationDialog|configurationId|ConfigurationSave|saveConfiguration|configuration|configure" console web database -S || trueRepository: iii-hq/workers
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "WorkerConfigurationDialog component:"
fd -a 'WorkerConfigurationDialog' console/web/src/pages console/web/src/lib | sed 's#^\./##' || true
for f in $(fd 'WorkerConfigurationDialog' console/web/src/pages console/web/src/lib); do
echo "--- $f"
wc -l "$f"
ast-grep outline "$f" || true
sed -n '1,280p' "$f" | cat -n
done
echo
echo "ConsoleApi and Injectable Iii host components/types:"
rg -n "export interface ConsoleApi|interface ConsoleApi|type Host|Components|components|buildConsoleApi|ExtensionIii|register\(configurationId|WorkerConfiguration" console/web/src -S
files=$(rg -l "ConsoleApi|ExtensionIii|buildConsoleApi|components" console/web/src)
for f in $files; do
echo "--- $f"
sed -n '1,220p' "$f" | cat -n
doneRepository: iii-hq/workers
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Locate focused files with exact names only:"
for d in console/web/src/pages console/web/src/lib; do
echo "-- $d"
find "$d" -maxdepth 3 \( -path '*WorkerConfigurationDialog*' -o -path '*injectable*' -o -path '*console-api*' \) -print | sort
done
echo
echo "Find WorkerConfigurationDialog using rg exact filename:"
rg -n "export function WorkerConfigurationDialog|function WorkerConfigurationDialog|WorkerConfigurationDialog" console/web/src --glob '!**/*.css' --glob '!**/*.png' --glob '!**/*.svg' | head -80
echo
echo "Find injectable UI page component:"
rg -n "export interface Inject|interface Inject|function Injectable|console-ui|api\.components|buildConsoleApi|workerId|currentWorker|functionId" console/web/src --glob '!**/*.css' --glob '!**/*.png' --glob '!**/*.svg' | head -200
echo
echo "ConsoleApi type definitions:"
fd -a '.*\.ts$' console/web/src/types console/web/src | while read -r f; do
if rg -q "ConsoleApi|ExtensionIii|components" "$f"; then
echo "--- $f"
cat -n "$f" | sed -n '1,220p'
fi
done
echo
echo "Relevant package metadata:"
for f in console/web/package.json package.json; do
if [ -f "$f" ]; then
echo "--- $f"
cat "$f" | sed -n '1,220p'
fi
doneRepository: iii-hq/workers
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Targeted searches for the exact components/types referenced in this PR.
printf 'Exact symbol searches:\n'
rg -n --fixed-strings 'WorkerConfigurationDialog' console/web/src --glob '!**/*.css' --glob '!**/*.png' --glob '!**/*.svg' || true
printf '\nconsole-api exports/building:\n'
rg -n --fixed-strings 'console-api' console/web/src --glob '!**/*.css' --glob '!**/*.png' --glob '!**/*.svg' || true
printf '\nHost/components interfaces:\n'
rg -n 'interface ConsoleApi|type ConsoleApi|ConsoleApi|components:' console/web/src/types console/web/src/lib console/web/src --glob '!**/*.css' --glob '!**/*.png' --glob '!**/*.svg' || true
printf '\nConsoleApi usage by filename only:\n'
rg -n --fixed-strings 'ConsoleApi' console/web/src --glob '!**/*.css' --glob '!**/*.png' --glob '!**/*.svg' || trueRepository: iii-hq/workers
Length of output: 5015
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "console/web/src/types/injectable-ui.ts:"
sed -n '1,240p' console/web/src/types/injectable-ui.ts | cat -n
echo
echo "console/web/src/lib/ui-loader.tsx:"
sed -n '1,190p' console/web/src/lib/ui-loader.tsx | cat -n
echo
echo "console/web/src/pages/Workers/components/WorkerConfigurationDialog.tsx:"
sed -n '1,240p' console/web/src/pages/Workers/components/WorkerConfigurationDialog.tsx | cat -n
echo
echo "console/web/src/pages/Workers/index.tsx relevant:"
sed -n '1,130p' console/web/src/pages/Workers/index.tsx | cat -n
echo
echo "database/ui/src/page/index.tsx render/usage:"
sed -n '220,310p' database/ui/src/page/index.tsx | cat -n
echo
echo "console/web/src/main.tsx boot global:"
sed -n '1,70p' console/web/src/main.tsx | cat -n
echo
echo "console/web/src/pages/Workers/components/WorkerConfigurationDialog.tsx usages of configurationId/client:"
rg -n "configurationId|configuration::(list|get|set)|register|api|host|onClose|configuration" console/web/src/pages/Workers/components/WorkerConfigurationDialog.tsxRepository: iii-hq/workers
Length of output: 28786
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "WorkerEditor and schema hook files containing configurationId/save/list:"
rg -n "WorkerEditor|useConfigurationSchema|configuration::list|configuration::set|configurationId" console/web/src/pages/Configuration console/web/src/hooks console/web/src/lib --glob '!**/*.css' --glob '!**/*.png' --glob '!**/*.svg' | head -200
for f in \
console/web/src/pages/Configuration/tabs/WorkersTab/hooks/useConfigurationSchema.ts \
console/web/src/pages/Configuration/tabs/WorkersTab/WorkerEditor.tsx \
console/web/src/hooks/use-hash-route.ts
do
if [ -f "$f" ]; then
echo
echo "--- $f"
ast-grep outline "$f" || true
sed -n '1,260p' "$f" | cat -n
fi
doneRepository: iii-hq/workers
Length of output: 29979
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "console/web/src/pages/Configuration/tabs/WorkersTab/api.ts:"
for f in \
console/web/src/pages/Configuration/tabs/WorkersTab/api.ts \
console/web/src/pages/Configuration/tabs/WorkersTab/hooks.ts
do
echo "--- $f"
ast-grep outline "$f" || true
sed -n '1,180p' "$f" | cat -n
done
echo
echo "console/web/src/lib/console-config.ts:"
sed -n '1,110p' console/web/src/lib/console-config.ts | cat -n
echo
echo "configuration schema view and value caching:"
rg -n "type ConfigurationSchemaView|interface ConfigurationSchemaView|ConfigurationValue|Cache|invalidate|useConfigurationValue|setConfiguration" console/web/src/pages/Configuration/tabs/WorkersTab console/web/src/lib --glob '!**/*.css' --glob '!**/*.png' --glob '!**/*.svg' | head -200Repository: iii-hq/workers
Length of output: 16050
🌐 Web query:
iii-hq workers configuration::set configuration::list authorization id caller worker
💡 Result:
In the III (iii-hq) ecosystem, the configuration system is managed by the "configuration" worker [1]. The commands configuration::set and configuration::list are part of the internal trigger-based API used to manage worker settings [1][2]. 1. configuration::list: This command enumerates all registered configuration entries [1]. It returns an array of objects containing the ID, name, description, and schema of each configuration, but importantly does not return the stored values themselves [1][2]. To retrieve a specific value, you must use configuration::get with the appropriate ID [1][2]. 2. configuration::set: This command replaces the stored value for a registered configuration ID [1]. It validates the provided value against the registered JSON schema and fires a configuration:updated event, which allows other workers to react to the change [1]. Regarding the "authorization id" and "caller worker" context: - Authorization: RBAC (Role-Based Access Control) and authorization in III are primarily handled by the iii-worker-manager worker [3][4]. When configured with an RBAC block, you specify an auth_function_id (a function invoked per connection to authorize the worker) [3][4]. - Caller Worker: The term "caller worker" refers to a worker that initiates a request (e.g., calling another function or worker) [5]. There is no specific command called "authorization id caller worker"; instead, authorization is governed by the listener configuration and the auth_function_id function that validates the caller's context [4]. If you are attempting to configure these settings, you generally use the iii CLI to trigger these configuration functions or edit the backing files (typically YAML files under ./data/configuration/) which the configuration worker watches for updates [1][6][7]. Top results: [1][3][4][6]
Citations:
- 1: https://workers.iii.dev/workers/configuration
- 2: https://github.com/iii-hq/workers/blob/main/console/web/src/pages/Configuration/tabs/WorkersTab/api.ts
- 3: https://workers.iii.dev/workers/iii-worker-manager
- 4: https://iii.dev/docs/creating-workers/worker-manager
- 5: Docs: rewrite quickstart as a step-by-step tutorial iii#1452
- 6: https://github.com/iii-hq/workers/tree/main/database
- 7: https://github.com/iii-hq/workers/blob/main/storage/README.md
🌐 Web query:
site:github.com/iii-hq/workers configuration::get configuration::set caller_worker_id configuration worker
💡 Result:
The configuration::get and configuration::set functions are part of the built-in configuration worker in the iii workers system, which provides a centralized, schema-validated, and observable bus for managing worker configurations [1][2]. configuration::get This function reads a registered configuration entry by its identifier [3]. It is typically used by workers at boot time to fetch authoritative values or during runtime to refresh settings [4][5]. Example (Rust): trigger_with_retry( iii, "configuration::get", json!({ "id": CONFIG_ID }), timeout_ms,).await configuration::set This function updates or replaces the configuration value for a specific registered identifier [3]. It validates the provided value against the JSON schema previously registered for that ID [3]. Note that configuration::set replaces the entire value, meaning callers should perform a read-modify-write cycle if they only intend to update a portion of the configuration [6]. Example (TypeScript): await client.trigger('configuration::set', { id: CONSOLE_CONFIG_ID, value, }) Regarding caller_worker_id: The iii-sdk environment automatically injects metadata fields, such as _caller_worker_id, into every iii.trigger payload [7]. Developers should ensure their handler structures are designed to tolerate these engine-injected fields (for example, by not using strict Serde attributes like #[serde(deny_unknown_fields)] on function input types) to prevent deserialization errors [1][7].
Citations:
- 1: https://github.com/iii-hq/workers/blob/main/docs/sops/binary-worker.md
- 2: https://github.com/iii-hq/workers/blob/main/context-manager/README.md
- 3: d27671d
- 4: https://github.com/iii-hq/workers/blob/main/telegram-bot/architecture/configuration.md
- 5: https://github.com/iii-hq/workers/blob/main/telegram-bot/src/configuration.rs
- 6: https://github.com/iii-hq/workers/blob/main/console/web/src/lib/console-config.ts
- 7: c53f9cb
IDOR (CWE-639): Authorization Bypass Through User-Controlled Key (IDOR)
Reachability: Internal · Exploitability: Moderate
Guard WorkerConfigurationDialog inputs before exposing the shared config editor.
Host.components lets injected pages render this dialog. It accepts configurationId directly and then calls configuration load/save with that untrusted ID. Determine the target from owner/tab context, or authorize the ID before each read or write. Object.freeze(Host.components) does not restrict available identifiers.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@console/web/src/lib/console-api.ts` around lines 93 - 96, Update
WorkerConfigurationDialog before exposing it through Host.components so
configurationId is constrained to the current owner/tab context or explicitly
authorized. Validate the identifier before every configuration load and save,
rejecting unauthorized targets rather than passing the untrusted value through.
Keep the shared editor available only for authorized configurations; freezing
Host.components alone is insufficient.
| const text = current.message | ||
| if (!text) return current | ||
| const brace = text.indexOf('{') | ||
| if (brace === -1) return current | ||
| let parsed: unknown | ||
| try { | ||
| parsed = JSON.parse(text.slice(brace)) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Only unwrap handler error: envelopes.
unwrap parses any message that contains {. An ordinary driver error such as request failed: {"message":"context"} loses its original code and message.
Check for the handler error: prefix before parsing. Add a regression test for a non-wrapper message that contains JSON.
Proposed fix
- const brace = text.indexOf('{')
- if (brace === -1) return current
+ const prefix = 'handler error:'
+ if (!text.startsWith(prefix)) return current
+ const json = text.slice(prefix.length).trimStart()
+ if (!json.startsWith('{')) return current
let parsed: unknown
try {
- parsed = JSON.parse(text.slice(brace))
+ parsed = JSON.parse(json)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const text = current.message | |
| if (!text) return current | |
| const brace = text.indexOf('{') | |
| if (brace === -1) return current | |
| let parsed: unknown | |
| try { | |
| parsed = JSON.parse(text.slice(brace)) | |
| const text = current.message | |
| if (!text) return current | |
| const prefix = 'handler error:' | |
| if (!text.startsWith(prefix)) return current | |
| const json = text.slice(prefix.length).trimStart() | |
| if (!json.startsWith('{')) return current | |
| let parsed: unknown | |
| try { | |
| parsed = JSON.parse(json) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@database/ui/src/lib/errors.ts` around lines 42 - 48, Update unwrap to parse
JSON only when current.message begins with the handler error: envelope prefix,
preserving ordinary messages and error metadata even when they contain JSON; add
a regression test covering a non-wrapper message containing JSON.
| function stripSqlComments(sql: string): string { | ||
| return sql.replace(/--[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '') | ||
| } | ||
|
|
||
| export function isReadOnlySql(sql: string): boolean { | ||
| const stripped = sql | ||
| .replace(/--[^\n]*/g, '') | ||
| .replace(/\/\*[\s\S]*?\*\//g, '') | ||
| .trim() | ||
| const stripped = stripSqlComments(sql).trim() | ||
| if (!stripped) return false | ||
| if (stripped.replace(/;+\s*$/, '').includes(';')) return false | ||
| if (!READ_ONLY_LEAD.test(stripped)) return false | ||
| if (/^pragma\b/i.test(stripped) && stripped.includes('=')) return false | ||
| return !WRITE_ANYWHERE.test(stripped) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Mask string literals before the keyword scan.
WRITE_ANYWHERE matches inside quoted literals. A read statement such as select * from t where status = 'update' is therefore routed to database::execute. The panel then shows the write notice and renders the execute-shaped outcome (0 affected) instead of the row grid, so a plain SELECT looks like it returned nothing.
Replace quoted spans with a placeholder in the same pass that strips comments.
🐛 Proposed fix: strip comments and mask literals
-function stripSqlComments(sql: string): string {
- return sql.replace(/--[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '')
-}
+function stripSqlComments(sql: string): string {
+ return sql.replace(/--[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '')
+}
+
+/** Keyword scanning must not read the contents of a literal. */
+function maskLiterals(sql: string): string {
+ return sql.replace(/'(?:''|[^'])*'/g, "''").replace(/"(?:""|[^"])*"/g, '""')
+}
export function isReadOnlySql(sql: string): boolean {
- const stripped = stripSqlComments(sql).trim()
+ const stripped = maskLiterals(stripSqlComments(sql)).trim()📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function stripSqlComments(sql: string): string { | |
| return sql.replace(/--[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '') | |
| } | |
| export function isReadOnlySql(sql: string): boolean { | |
| const stripped = sql | |
| .replace(/--[^\n]*/g, '') | |
| .replace(/\/\*[\s\S]*?\*\//g, '') | |
| .trim() | |
| const stripped = stripSqlComments(sql).trim() | |
| if (!stripped) return false | |
| if (stripped.replace(/;+\s*$/, '').includes(';')) return false | |
| if (!READ_ONLY_LEAD.test(stripped)) return false | |
| if (/^pragma\b/i.test(stripped) && stripped.includes('=')) return false | |
| return !WRITE_ANYWHERE.test(stripped) | |
| function stripSqlComments(sql: string): string { | |
| return sql.replace(/--[^\n]*/g, '').replace(/\/\*[\s\S]*?\*\//g, '') | |
| } | |
| /** Keyword scanning must not read the contents of a literal. */ | |
| function maskLiterals(sql: string): string { | |
| return sql.replace(/'(?:''|[^'])*'/g, "''").replace(/"(?:""|[^"])*"/g, '""') | |
| } | |
| export function isReadOnlySql(sql: string): boolean { | |
| const stripped = maskLiterals(stripSqlComments(sql)).trim() | |
| if (!stripped) return false | |
| if (stripped.replace(/;+\s*$/, '').includes(';')) return false | |
| if (!READ_ONLY_LEAD.test(stripped)) return false | |
| if (/^pragma\b/i.test(stripped) && stripped.includes('=')) return false | |
| return !WRITE_ANYWHERE.test(stripped) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@database/ui/src/page/db-data.ts` around lines 209 - 219, Update
stripSqlComments and the isReadOnlySql flow to mask single- and double-quoted
string literals with placeholders during the same preprocessing pass as comment
removal, preserving the surrounding SQL structure for keyword checks. Ensure
WRITE_ANYWHERE does not match keywords inside literals while existing comment
stripping and read-only validation remain intact.
| {/* With the roving cursor the table is a grid for real — the role | ||
| makes the per-cell `gridcell`s valid instead of orphaned. */} | ||
| <table className="db-grid" role={keyboard ? 'grid' : undefined}> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Remove row Tab stops when keyboard is set.
TableDataPanel passes both keyboard and onRowClick. Each row then receives tabIndex={0} while a grid cell is also the roving Tab stop. Keyboard users must tab through every row before leaving the grid.
Apply the row keyboard handlers and tabIndex only when keyboard is absent.
Proposed fix
- tabIndex={onRowClick ? 0 : undefined}
+ tabIndex={onRowClick && !keyboard ? 0 : undefined}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@database/ui/src/page/result-grid.tsx` around lines 203 - 205, Update the row
rendering in the table grid so its keyboard handlers and tabIndex are applied
only when keyboard is absent; when keyboard is enabled, leave rows without Tab
stops while preserving the grid cell roving behavior and existing row-click
handling.
Fixes MOT-4361
The database console page overhaul from the 2026-08-04/05 live review sessions: two driver-level bugs, the sql panel gaining sanctioned writes, a page-wide consistency migration, and the console sharing its worker-configuration dialog with injected pages.
Worker (Rust)
MYSQL_TYPE_VAR_STRING) instead of SQL names (varchar) — leaked into grid headers and every agent readingcolumns[].type, and silently broke the UI's type-category logic. Newsql_type_name()maps the wire type (charset 63 splits text/binary twins, UNSIGNED honoured). Verified against the live mysql."char"decode: the row decoder had no arm for the internal one-byte"char"type (OID 18) and the unknown-type fallback'sStringFromSql rejects it — solistTables(which readspg_class.relkind) failed on every postgres server witherror deserializing column 2. New arm decodes i8→text. Verified against live postgres 18.4 and via ad-hocSELECT relkind.SQL panel
isReadOnlySqlroutes instead of gates — reads →database::query, writes →database::execute, the same surfaces agents use, so engine policy anddatabase::row-changedtreat both alike. Trailing;stripped for the prepared paths.0 affected.errText: unwraps the SDK's wire-error envelope to the driver's own code+message — every failure used to render[object Object].Page-wide consistency (impeccable critiques: sql 27/40, page 25/40)
refreshTokeninto fetchers instead of remount keys (and now actually reaches health, where it was a silent no-op)..db-toolbarbar grammar, derived AA warn/alert inks in both themes (raw tokens sit ~3:1 at 11px), page-wide:focus-visiblehairline, ghost→faint on functional text,:focus-withinbeside hover-only reveals.role="log", data rows/page), freshness badge became a real disclosure button (its caveat was hover-onlytitletext),aria-expandedon tree/plan twists,role="grid"legitimizing the roving-cursor gridcells, keyboard hints in the data footer, Escape exits the Monaco tab-trap, ERD nodes take F to focus.public.×14 was eating the name width), passive-mode notes on sql/health, list tagged by source db — a failed database switch can no longer render the previous database's tables under the new header.13233.0s; durations roll up (3h 40m); as-of stamp; stale-dim while refetching.Console: shared configuration dialog
WorkerConfigurationDialogjoins the curated component registry, the package manifest, and the regenerated vendor shim (conformance-test pinned). The database page opens the console's own config editor in place via ahost.componentsruntime lookup — dialog chrome, dirty guard, custom-form resolution and save all stay host-owned. Consoles predating the export degrade to the workers-tab navigation; the runtime lookup (never a static import) is what keeps an old console from failing the page's module load. Also: CodeEditor's completions separator is a visible'\n'now, replacing an invisible U+0001 that read asjoin('')in every editor and grep.Verification
cargo fmt/clippy --all-features/cargo testgreen (347+28+3+1); driver fixes proven with throwaway integration tests against the live mysql and postgres 18.4tscclean, 32/32 vitest (routing, ddlInfo, errText, commonSchema), biome clean per touched filetsc -bclean, conformance test 3/3 (registry ↔ manifest ↔ types), shim regenerated (34 components)Notes for reviewers
.impeccable/critique/snapshots and the providerCargo.lockdrift are intentionally not part of this PR.Summary by CodeRabbit