Skip to content

fix(db,agent,security,docs): the MySQL health panel fabricated an absence, and four more - #512

Open
cevheri wants to merge 2 commits into
mainfrom
chore/backlog-round15
Open

fix(db,agent,security,docs): the MySQL health panel fabricated an absence, and four more#512
cevheri wants to merge 2 commits into
mainfrom
chore/backlog-round15

Conversation

@cevheri

@cevheri cevheri commented Aug 27, 2026

Copy link
Copy Markdown
Member

Backlog round 15. Five entries closed, two restored that a merge had deleted, six new ones opened for what the work found.

Every change below was implemented by an agent, reviewed by a fresh-context adversarial reviewer that reproduced its findings, repaired, and then verified by a second fresh-context pass that re-proved each finding closed by mutation rather than by reading the report. What the reviewers found is in the sections; nothing was adopted on a report's word.

D32 - the MySQL health panel stated a capability as absent on servers that had it

getHealth() asked for LEFT(sql_text, 100) from performance_schema.events_statements_summary_by_digest. That table has no sql_text column - the digest text is DIGEST_TEXT; SQL_TEXT belongs to events_statements_current. A bare catch turned the resulting ERROR 1054 (42S22) into a row:

{ query: "Performance schema not available", calls: 0, avgTime: "N/A" }

Measured through the real provider on four servers brought up for it:

server @@performance_schema old health line getSlowQueries() on the same connection
MySQL 26.7.0 1 "Performance schema not available" 5 real rows
MySQL 26.7.0 --performance-schema=OFF 0 same sentence 0 rows
Percona Server 8.4.11-11 1 same sentence 5 real rows
MariaDB 12.3.2 0 same sentence 0 rows

So the panel beside it disproved the statement, and off-ness never reached that catch at all: an OFF server answers the digest table with zero rows rather than raising. The health read now uses the statement the Queries panel uses, one mapper feeds both surfaces, and an unreadable source leaves the list empty rather than describing itself - a sentence wearing a row's clothes is a fabricated measurement, and slowQueryCount counts rows.

getSlowQueries() no longer swallows either, so a denied grant (ERROR 1142) or a tenant with no performance_schema database (ERROR 1049, OceanBase) now reaches the Queries panel as PanelUnavailable naming the server's own error, instead of an empty list explained by an instruction that cannot fix it.

What the reviewer found, and what the repair did. The first implementation named getMonitoringSnapshot() as the carrier for the refusal reason, in both the code comment and docs/providers/mysql.md. There is no such method - grep returned only those two lines. The repair made the carrier real (getSlowQueries() rejects -> getMonitoringData() records errors.slowQueries -> QueriesTab -> PanelUnavailable) and pinned it with a 1142 fixture. Separately, the Performance-Schema-OFF test passed with the fix reverted: its mock answered the broken sql_text statement with [[], []], modelling a server that does not exist. All three digest fixtures now refuse sql_text the way a real server does, and the verifier reproduced the vacuity both ways - reverted code plus the unfaithful fixture passes, reverted code plus the faithful one fails.

AU3 - the two cross-origin headers, decided

Cross-Origin-Opener-Policy: same-origin is delivered. same-origin and not same-origin-allow-popups: nothing here opens a popup it then scripts - OIDC is a top-level redirect in both directions, the one window.open passes "noopener,noreferrer". It is read only for a top-level traversable, so it has no place in the baked set; it is a no-op in a non-secure context, so the plain-HTTP channels are unaffected; and it is deliberately not paired with COEP: require-corp, which would demand a CORP header from every cross-origin subresource and break the documented off-origin NEXT_PUBLIC_MONACO_VS_PATH setup for a capability nothing here uses.

Cross-Origin-Resource-Policy is refused, and the reason is structural rather than a doubt about the value. It could never have broken the editor - Fetch reaches the CORP check only when response tainting is opaque, which a same-origin load never is. What refuses it: CORP acts only on a subresource; src/proxy.ts's matcher skips subresources; the one path that does reach them is next.config.ts's headers(), baked at build time - and CORP's correct value is the single one in the set that depends on deployment topology rather than on the application. There is nowhere a configurable CORP can land where it would act. What it gives up is a same-site cross-origin timing oracle, now written into docs/SECURITY.md under Known limits and tracked as AU4.

next.config.ts no longer restates a list of header names. It probes securityHeaders() with every option src/lib/security/headers.ts declares and drops any header whose value or presence moves - so the CSP's -Report-Only rename reaches the filter as an absence - plus a named document-only set. A new header is refused until it is classified: adding COEP: require-corp to securityHeaders() fails two tests rather than being stamped onto /logo.svg.

B76 - declined: "unavailable" collapsed six causes, and the split's premise was wrong

declined: "repointed" is now its own code, and the five that stay collapsed each carry the reason they have no distinct remedy, beside the union rather than only in a report.

The first draft justified the split on "this cause's remedy differs: it persists until the operator changes the connection back", and shipped a sentence saying so. That is false, and the reviewer traced it before I did: the route writes the new identity onto the run it opens, so the next question continues that run and the conversation carries normally. Exactly one question is declined. The same refuted argument appeared in the union's doc comment, the thread-context docblock and the route comment; all four are corrected. The test had pinned only the true first half of the sentence - both halves are pinned now, and a route test pins the behaviour ("the decline lasts one question").

B12 - a failed statement recorded no duration

ExecutionBudgetTracker charges maxTotalRunMs on the failure path as well as the success one, but tool-refused carried no duration, so the rail's database-time meter sat below what was actually enforced whenever a statement failed. The refusal now carries the span - a T2 contract change, made in the union's own voice: a duration is a measurement the server took, unlike the engine's message, which stays quoted text.

The reviewer's mutation: replacing chargedBeforeMs with 0 - turning the delta into the run's cumulative total - left 256 pass / 0 fail. The only equality assertion ran on a run whose first and only execution was the failing one, so the delta was trivially the total. A two-execution harness run now separates them, on both refusal branches; a ledger entry written before this change folds as a statement with no duration rather than a measured zero.

B38 - a run was offered on an engine that could not run it

POST /api/agent/runs now answers 400 with the posture's own sentence when the mode is agent and the requested workflow sends statements on an engine with no read-only execution path - before a run id exists, before the capability gate probes the model, before any drive. The operations workflow still opens on the same connection, because it sends no statement at all. Under Automatic the refusal lands after the classify call, since the workflow has to be known first.

The reviewer's finding: the list this refusal reads was advisory copy until now (it decided a sentence on the login hero) and its drift guard named 2 of 15 engines. Implementing queryReadOnly on a provider without updating the list would have turned a wrong sentence into a 400 refusing a run the factory would have executed - with every test green. The guard now derives the DatabaseType union and the factory's dispatch from source and reads queryReadOnly off each real provider class, so drift fails in both directions. Proved: the same mutation (a queryReadOnly stub on TrinoProvider) fails the new guard and passes the old one.

Restored: two entries a merge deleted

#510 branched before #509 merged, and its copy of docs/BACKLOG.md overwrote both entries #509 had added - the SSH host-key pin and the live-SSH fixture gap. Measured knowledge, gone from main, with docs/providers/README.md left citing "D32" for a pin while D32 had become the MySQL entry. Both are restored from 35294140 as D34/D35 (D32/D33 are #510's now), the citation is repointed, and the section index has its count back.

Browser verification

Built app, real Chrome via Playwright, bun start on 3111.

AU3. /login carries seven headers including cross-origin-opener-policy: same-origin and no cross-origin-resource-policy. /logo.svg, /monaco/vs/loader.js, editor.main.js and editor.main.css carry nosniff and DENY and nothing else - the document-only classification holds at the wire, not only in the config object. In the page: one Monaco editor, five workers loaded (json, css, html, ts, editorWorkerHost), a real query returned 5 rows in 2 ms, the ERD rendered 85 nodes through the ELK worker, crossOriginIsolated === false, 0 iframes.

B38. On Sample (LibreDB), agent mode, workflow named explicitly: POST /api/agent/runs => 400, the rail rendered the refusal, "No activity yet" - no run opened, no classify call, no drive. Then operations on the same connection: opened, drove a live model, 8 steps, succeeded. Both arms, one connection.

D32. MySQL (local) monitoring: "Performance schema not available" is absent from the page. The Queries panel shows real digest rows - and the top row is the slow-query read itself, which is B77 below, seen live.

What was NOT verified, and why

  • B76's decline could not be produced from the UI. The comparison is server-side, so the connection must be one the server holds; in the default STORAGE_PROVIDER=local mode only the seeds are, and editing a seed writes a browser-local copy, after which the rail refuses the run on the eligibility path before any thread check. The decline is driven end to end at the API level and its sentence is pinned by a component test; what has never been observed is a person reaching it. Recorded as B79 rather than left as an assumption.
  • The B38 refusal reads the posture's whole paragraph into the error line while the amber card two elements above shows the identical text. Reusing the posture is right; the register is wrong. Seen in the browser, recorded as B80.
  • D34/D35 (SSH) have no UI to drive, and CI still runs the mocked ssh2 handshake - that is D35 itself.
  • Backlog ids cited from source comments would have dangled: deleting the five closed entries left fifty-odd citations in src/ and tests/ naming entries that no longer exist, which is the class T4 tracks. The second commit repoints every one of them, read rather than substituted - where the id was the name of a defect class the class is named instead, where a comment described a defect the same round had fixed the sentence was wrong and not just its id, and where what survives is a residual the citation moved to the entry that still exists (T6, B78). grep -rnE '\b(D32|B12|B38|B76|AU3)\b' src tests next.config.ts e2e scripts is empty.

Gates

format clean - lint 0 errors - typecheck clean - knip clean - test 13058 pass / 0 fail - build exit 0 - test:coverage + coverage:check 43652/43652 lines, 100.00% - build:lib + attw green on every subpath - security:check OK.

Opened by this round

AU4 the timing oracle CORP would have closed - B77 slowQueryCount is a cap, not a count, and the rows behind it are our own introspection traffic (59 digests, LIMIT 5, top row the slow-query read) - B78 the delta is unambiguous only because one frozen concurrency constant says so - B79 the re-pointed decline is unreachable from the UI - B80 the refusal renders one paragraph twice - T6 a shared mock that answers a statement its engine refuses makes every test built on it a test of the mock.

…ence, and four more

D32 - MySQL's health slow-query read asked for `LEFT(sql_text, 100)` from
`performance_schema.events_statements_summary_by_digest`, a column that table does not have,
and a bare catch turned the resulting `ERROR 1054` into the row
`{ query: "Performance schema not available", calls: 0, avgTime: "N/A" }`. Measured on four
live servers (MySQL 26.7.0 on and off, Percona 8.4.11-11, MariaDB 12.3.2): the sentence
appeared on every one of them, including the two where `@@performance_schema` is 1 and the
Queries panel beside it returned real rows. The health read now uses the statement the panel
uses, one mapper feeds both, and an unreadable source empties the line instead of describing
itself - a fabricated absence is exactly what the absence rule (#477) exists to prevent.
`getSlowQueries()` no longer swallows either, so a denied grant (1142) or an absent tenant
schema (1049) reaches the Queries panel as the server's own sentence.

AU3 - `Cross-Origin-Opener-Policy: same-origin` is delivered from `securityHeaders()`, and
`Cross-Origin-Resource-Policy` is refused with the reason recorded beside the header rule:
CORP acts only on a subresource, the only delivery path that reaches a subresource is
`next.config.ts`'s build-time `headers()`, and CORP's correct value is the one in the set
that depends on deployment topology. What that refusal gives up is now tracked as AU4.
`next.config.ts` derives its baked set by probing `securityHeaders()` rather than restating a
list of names, so a header that varies with an option cannot be baked by accident.

B76 - `declined: "repointed"` is split out of the collapsed `"unavailable"` code, and the five
that stay collapsed each carry the reason they have no distinct remedy. The first draft of the
rail's sentence claimed the decline persists until the connection is pointed back; it does not -
the route writes the new identity onto the run it opens, so exactly one question is declined.
Both halves of the corrected sentence are pinned, and a route test pins the behaviour.

B12 - a statement that failed at the database records the span the tracker charged it, so the
rail's database-time meter no longer counts completed reads only. The charge is a delta, pinned
by a two-execution harness run; a ledger entry written before this change folds as a statement
with no duration rather than a measured zero.

B38 - a run whose workflow sends statements is refused on an engine with no read-only execution
path at the point it is opened: 400 with the posture's own sentence, before a run id, a capability
probe or a model turn is spent. The `operations` workflow still opens on the same connection,
because it sends no statement at all. The drift guard behind the refusal now measures every
provider in the `DatabaseType` union rather than restating the list.

Also restores two backlog entries #510 overwrote when it branched before #509 merged, renumbered
D34/D35, and repoints the citation that named the wrong entry.

Verified in Chrome against the built app: COOP present on the document and absent on
`/logo.svg` and every `/monaco/vs/*` response, editor, five Monaco workers, a query and the ERD
export all unaffected; the B38 refusal answering 400 with no run opened, and an `operations` run
succeeding on the same connection; the fabricated MySQL sentence gone from the monitoring panel.
B79 and B80 record what that drive found and could not reach.
… closed

Deleting an entry when the work lands is this repo's convention, and T4 records what it
costs: nothing checks a citation from a source comment, so every comment naming a deleted
id goes silently wrong. Closing D32, B12, B38, B76 and AU3 would have left fifty-odd of
them across `src/` and `tests/`.

Each one was read rather than substituted. Where the sentence explains why the code is the
way it is, it now cites #512, the change that made it so. Where the id was the NAME of a
defect class rather than a pointer - "a marker keyed on the throw would be emitted for
something other than off-ness, D32 one level up" - the class is named instead, because a PR
number does not carry that meaning. Where a comment described the defect in the present
tense although the same round had fixed it, the sentence was wrong and not just its id:
`digestTableMockExecute`'s docblock claimed the shared fixture still invents digest rows.
And where what survives is a residual rather than the closed entry, the citation moved to
the entry that still exists - T6 for the shared fixture nothing pins, B78 for the delta that
rests on one frozen concurrency constant.

`grep -rnE '\b(D32|B12|B38|B76|AU3)\b' src tests next.config.ts e2e scripts` is now empty.

Comment-only: `bun run test` 13062 pass / 0 fail, typecheck and format clean.
@sonarqubecloud

Copy link
Copy Markdown

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.

1 participant