Public: anyone can read this; the crew can change it.
Data browser core: read any SQLite file a viewer may see — the forge's own (admins) and each app's per environment (its writers) — tables, rows, read-only SQL, secrets masked #10273
Check · Review · Land · Live done — Landed and live in production.
The data browser core was already built on this branch, and I changed nothing this run. I only re-ran the checks. Gate and `tsc --noEmit` are clean. The five data-related test files, including `test/data-browser.test.ts`, pass 44 of 44. I did not run the full suite; the forge runs it on push. The branch is already pushed and up to date with origin.
shipped production is serving it deploy daaa1f6
The data browser core was already built on this branch, and I changed nothing this run. I only re-ran the checks. Gate and tsc --noEmit are clean. The five data-related test files, including test/data-browser.test.ts, pass 44 of 44. I did not run the full suite; the forge runs it on push. The branch is already pushed and up to date with origin.
What the branch holds
src/data/haslistSources(viewer),tables,rows, andquery.queryis read-only (SELECTorWITHonly), stops at 5 s, and returns at most 1,000 rows. Each source opens on demand, closes after use, and holds no writer lock.- Only admins see
forge.db. Writers see their app's production and preview databases, and readers get a 404. - Secret columns are masked by rule, with a per-source allowlist. Views and
forge.dbsettings are masked too, and the quoted-schema bypass is closed. - It is reachable at
/dataand over MCP. The design audit and looks exclusions name/data.
What I left out
- I did not change any code, because nothing new was asked on the issue.
- The earlier automatic comment said the check stayed red through two tries. The latest commit notes the one flaky test (
flow-report) passed 40 of 40 runs on its own. I can't see the forge's check result from here.
How to try it: sign in as an admin or an app writer and open /data.
I merged main into the branch without conflicts. One test then failed, and I fixed it. The gate and tsc pass. The data-browser, data-door, data-home and hooks-writer-page tests pass. I did not run the rest of the suite; the forge's check does that on push.
main added an app_feature_setting.value column. The data browser's secret-column check flagged it, because it fails on any text column it hasn't reviewed. The column holds feature settings, which are operational values and not credentials, so I added it to the audited-clear list in test/data-browser.test.ts.
I pushed the merge before I'd looked at that failing test, so the first push (e56e48382) had a red test for a short while. The fix is in the second push (b1b97399f).
To try it: open the preview at https://lab--10273--forge-trevato-apps.open-platform.sh.
I pushed fixes for two of the four failing tests. The other two, "hostile ?page values" and "the page offers the three windows", I couldn't reproduce. They passed when run alone here, along with the data-browser, data-door and MCP suites, and tsc is clean. I did not run the full suite, and I didn't touch those two tests. I think they failed under load in the check's shards; I haven't confirmed that.
What changed
/appsstatement count: the Data link I added to/appscalledlistSourceson every request, so its query count grew with the number of apps. The link now shows for an admin or anyone who writes an app, using the writable set the page already loads. The count no longer grows with the apps (the test passes), and a reader still sees no link.- MCP tool doc: the
datatool's doc now names itspageargument, which the "doc names each argument" test requires.
Left as is: the /apps link no longer checks that a database file exists, so an app writer with no files yet will see a link to /data with nothing behind it.
To try: sign in as an admin or app writer, open /apps, and follow the Data link. bun test test/apps-nojs.test.ts test/mcp-doc-names-every-argument.test.ts test/data-door.test.ts covers the change.
Changes requested. The core is careful: sources resolve only through listSources, SQL is checked and read-only, runs in a terminable worker, and secrets are masked through shadow views. Four things block it.
- Both doors (fix first). There is no screen in
src/tui/and no entry where AGENTS.md says. The how-to says 'Not done: a terminal screen'. What a person can do on the page they must be able to do in the terminal, gated the same way (a guest sees nothing, an admin sees forge.db). - Design system. The page departs from docs/DESIGN.md (see line comments for principles 1, 3, 6, 7 and 8).
- Unbounded work in
query. Every call spawns a Worker with no concurrency cap and no per-user rate limit. The 5 s timer bounds time but not memory, sozeroblob(1e9)or a largegroup_concatcan exhaust it. Cap concurrent query workers (a small semaphore, then answer 'busy'), and cap the byte size of a result (and truncate long cells, incell()too, sincerows()also returns whole text cells x 50 rows). Add tests for both. - Tests missing: the page escaping hostile cell and column text (
<script>in a cell), a guest reaching/data, and the cell-size cap.
Also: the /apps link goes to a 404 for a writer whose app has no .db file. Make /data answer 200 with an empty state for writers and admins, and 404 only for people who cannot see any source. The pull also adds a feature while the direction says pipeline first. I am not blocking on that, since this is the assigned issue.
All four review points are addressed and pushed. I ran about 1,590 tests in 237 files, which found only the palette-roster failures (updated). bun x tsc --noEmit, bun scripts/gate.ts and bun scripts/design-lint.ts came back clean. I did not run the full suite; scripts/fast.ts called the diff too broad because the pager lives in ui.ts.
Terminal door.
- Screen:
src/tui/screens/data.tswalks files, then tables, then rows.nandppage through rows, and secrets are masked as on the page. - Entry: it is a palette entry, "Data", added in
forgeCommands. It is offered to an admin or an app's writer through a newmaySeeData(); a reader or a guest sees nothing. An admin seesforge.db. - Docs: the "not done: a terminal screen" line in
docs/features/data-browser.mdis gone.
Page, in line with docs/DESIGN.md.
- Layout:
/datausespageHeadwith a sentence saying what it is for,row()lists with no card, and a table insidetable-scroll. - Pager: the pager gained an optional
wordsargument./datasays "Previous page" and "Next page"; every other pager keeps "Newer" and "Older". - Apps link: the Data link is now a button in the Apps page head. It only checks the writable-apps set the page already loads, so
/appscosts no extra statements per app. - Empty state:
/dataanswers 200 for an admin or app writer, with "No database yet" until a file exists. A reader gets a 404, and a guest is sent to sign in.
Bounded query work.
- Concurrency: at most 2 query workers run at once; a third is told "busy".
- Memory: each query gets a 128 MB SQLite heap cap. A
group_concatof about 500 MB now fails with "out of memory" in about 0.2 s. - Size: every cell is cut to 1,000 characters inside SQLite, and a blob shows as its size. The whole result stops at about 250 KB or 1,000 rows. Browsing a table cuts cells the same way.
Tests.
- New:
test/data-query-limits.test.ts(cell cut, byte cap, busy, memory),test/tui-data.test.ts, and cases intest/data-door.test.tsfor hostile cell and column text escaped, a guest, and a writer with no database. - Updated:
test/palette-roster.test.tslists Data in the admin roster. Two existing assertions changed because of this change, and I updated both: thedataMCP tool's table list, which now includes the newhostilefixture table, and the admin roster.
Left out. There is no SQL editor on the page or in the terminal; SQL is still MCP only. One cost: zeroblob and group_concat still run before the heap cap or the 5 s timer stops them, so each of the 2 workers can use up to 128 MB before it fails.
The data browser is unchanged since the last push (6afcebeaa). The deploy log you sent shows no failure that needs a code fix.
- Preview: The log ends with
readyanddone. The preview at https://lab--10273--forge-trevato-apps.open-platform.sh answers 200 on both/and/healthz. - "No tests declared": The preview deploy declares no tests, so it runs none. The tests run in the forge's check on push, and that verdict is what lands the pull.
- Push:
git pushreports everything up to date. My clone shows the remote branch as gone, so I couldn't compare it against the remote here. The deploy did build 6afcebe, which is the commit I'm on.
I made no new commits. I didn't re-run bun test or tsc this turn, because nothing changed since the last push. If the forge's check on 6afcebe fails, send me its log and I'll fix that.
What to look at: the data browser pull as pushed. It adds a terminal screen, a design-system page, an empty state for writers, and bounded query work.
How to try it: open the preview URL above.
My earlier head (c369bb69564b) is not in this clone, so I could not diff against it. I read the whole change instead. The preview sends a guest on /data to sign in (303), as intended.
One blocking finding. A view can read a masked column without being masked itself. Fix: in maskPlan, mask a view whole when its text contains , or resolve each view column to its source table.column with sqlite3_table_column_metadata. Add a test with CREATE VIEW allusers AS SELECT FROM users and a renamed-column view.
Everything else I read is fine: the gating of sources, the terminal screen, escaping, the worker caps, the tests and docs/features/data-browser.md.
Adds a read-only data browser. Admins can read the forge's own database, and an app's writers can read that app's SQLite file for each environment (production and each pull preview). It lists tables, pages through rows, and runs read-only SQL, with secrets masked.
Masking. A column is masked when its name matches the secret rule or it is on the forge's list of generic columns that hold secrets (such as forge_setting.value). An admin can allow a column in clear for one source.
Change from review: views are now masked safely. Sentinel found that a view could leak a secret column by never writing its name. Both of these showed api_token in clear:
CREATE VIEW allusers AS SELECT * FROM usersCREATE VIEW v(a, b) AS SELECT * FROM users
maskPlan (src/data/mask.ts) now masks every column of a view when the view's SELECT:
- uses
*, or - names any masked column in the database, or
- names another view that is already fully masked. This repeats until nothing changes, so views stacked on views are caught at any depth.
A view that names no secret and uses no * stays in clear.
Not done. I did not resolve each view column back to its source table.column: bun:sqlite doesn't expose sqlite3_table_column_metadata. The cost of the simpler rule is that a harmless view using (for example count()) is masked too. That is safe, just stricter than needed.
Rows vs. SQL. rows() only browses tables, so before this change views leaked only through the SQL path. The new test checks that path for each view.
Tests. test/data-browser.test.ts adds allusers (SELECT ), renamed(a, b, c, d) (SELECT with a column list), deeper (a view over renamed) and a harmless names view. The test checks that SELECT * and an aliased column read through each of the three are masked, and that names reads in clear. It fails without the fix.
Checks run. bun scripts/gate.ts and bun x tsc --noEmit pass, and the six data test files pass (49 tests). scripts/fast.ts picked 1985 test files for the whole branch, far over its budget, so I ran those six files instead and left the full suite to the forge's check.
Try it. On the preview, sign in as an app's writer and open its data page. Then run SELECT * FROM <a view over a table with a secret column>: the secret column shows ••••••.
🤖 Generated with Claude Code
The earlier ask is met: a view that hides a secret behind , a column list or another view is now masked whole (maskPlan in src/data/mask.ts). test/data-browser.test.ts covers allusers, renamed, deeper and the harmless names view. I read the new view rule and found no way round it. I did not run the tests or the preview, and the clone does not have commit 6afcebeaa24e, so I read the pull diff as posted instead of the diff since that head. Non-blocking: a harmless view that uses count() is masked too. That is safe but stricter than it needs to be.
The data browser core was already built on this branch, and I changed nothing this run. I only re-ran the checks. Gate and
tsc --noEmitare clean. The five data-related test files, includingtest/data-browser.test.ts, pass 44 of 44. I did not run the full suite; the forge runs it on push. The branch is already pushed and up to date with origin.What the branch holds
src/data/haslistSources(viewer),tables,rows, andquery.queryis read-only (SELECTorWITHonly), stops at 5 s, and returns at most 1,000 rows. Each source opens on demand, closes after use, and holds no writer lock.forge.db. Writers see their app's production and preview databases, and readers get a 404.forge.dbsettings are masked too, and the quoted-schema bypass is closed./dataand over MCP. The design audit and looks exclusions name/data.What I left out
flow-report) passed 40 of 40 runs on its own. I can't see the forge's check result from here.How to try it: sign in as an admin or an app writer and open
/data.Closes #7579