Files
SuperBizAgent-java/openspec/changes/archive/2026-07-21-single-react-mysql-readonly-tool/design.md
T

85 lines
4.9 KiB
Markdown

# Design: single-react-mysql-readonly-tool
## Architecture
```text
MysqlToolRequest + ToolCallRequestEnvelope
|
v
MysqlToolAdapter
- parse typed request
- MysqlSqlValidator -> MysqlQueryPlan
- ToolBoundary.execute
|
+--> MysqlReadOnlyExecutor (JDBC or isolated fake)
| - read-only connection
| - PreparedStatement params
| - timeout/max rows/cancel
|
+--> MysqlResultProjector
- bounded cells/rows/bytes
- sensitive column redaction
- MysqlToolResult JSON
```
The adapter validates the typed request before entering ToolBoundary. Once validation succeeds, the executor and projector run inside the existing canonical lifecycle. Validation failures have no canonical record because no database call is authorized; execution/projection failures after `PROJECTING` become canonical `ERROR` records.
## Configuration model
`MysqlDataSourceDefinition` is an immutable logical definition:
- logical ID
- JDBC URL, username and password supplied by configuration/Secret
- default schema
- exact schema/table/column allowlist
- query timeout seconds, max rows, max cell characters and max result bytes
The Agent sees only the logical ID. The stage does not create a dynamic datasource registry or reuse the application persistence datasource. A caller supplies a `Map<String, DataSource>` to the JDBC executor, allowing isolated test data sources and later production wiring.
## SQL validator
`MysqlSqlValidator` uses `CCJSqlParserUtil.parseStatements` and rejects unless there is exactly one `Select` with a `PlainSelect` body and no CTE/compound body. It rejects `SubSelect`, `SetOperationList`, `ValuesStatement`, `ForUpdate`, metadata statements, wildcard projection except `COUNT(*)`, unsupported joins, unsupported functions, and unknown table/from items.
The validator builds an alias map for every base table and checks every table/column reference in select items, joins, where, group by, having and order by. Unqualified columns must resolve to exactly one allowlisted table; qualified columns must resolve through a declared alias/table and exact allowlist. It counts `JdbcParameter` nodes and requires an exact match with `params`.
The function allowlist starts with `COUNT`, `SUM`, `AVG`, `MIN`, and `MAX`. `COUNT(*)` is represented as a function-level exception; a projection `*` or `table.*` is rejected. Any parser exception or policy ambiguity produces a `MysqlSecurityException` before execution.
## JDBC executor
`JdbcMysqlReadOnlyExecutor` resolves the validated logical ID to a caller-supplied DataSource, opens a connection, calls `setReadOnly(true)`, prepares the validated SQL, binds parameters in order, applies `setQueryTimeout` and `setMaxRows`, and reads column labels plus structured values. It checks `RunContext` cancellation/deadline while iterating and cancels/closes the statement on abort.
The executor returns a Harness-only raw record. It never returns a JDBC connection, SQL exception details, credentials or stack traces to the Agent. JDBC errors are mapped by ToolBoundary to a stable safe error.
## Result projection
`MysqlResultProjector` parses only the executor raw record and emits `MysqlToolResult`:
- immutable ordered columns
- bounded ordered rows
- cell values converted to JSON-safe scalar/text values
- redaction for sensitive column names (`password`, `token`, `secret`, `api_key`, etc.)
- `returned_count` equal to projected rows
- `truncated=true` for row/cell/byte removal
- `NO_EVIDENCE` for a successful empty result
Projection never exposes raw JDBC metadata, connection coordinates, internal error messages or the unbounded raw result.
## Script safety cleanup
`scripts/query_mysql.py` will require `SUPERBIZ_MYSQL_HOST`, `SUPERBIZ_MYSQL_PORT`, `SUPERBIZ_MYSQL_USERNAME`, `SUPERBIZ_MYSQL_PASSWORD` and `SUPERBIZ_MYSQL_DATABASE` (with no external defaults), accept only one SELECT statement, and reject all non-SELECT/metadata/write SQL before opening a connection. It will call `rollback`/close defensively and remove the interactive write path.
## Interface and compatibility impact
- L2 internal Harness classes plus the JSqlParser build dependency.
- No public HTTP/SSE/Chat contract changes.
- No changes to legacy tools, recorder, JPA entities, or current application datasource.
- The main capability spec will be synchronized after archive.
## Risks and mitigations
- JSqlParser AST API drift: lock version 4.6 and run parser security fixtures.
- Alias/column ambiguity: fail closed rather than guessing.
- JDBC cancellation is driver-dependent: check Run state before/while iteration and call `Statement.cancel()` on abort.
- Sensitive result values: redact by column name before JSON serialization and keep raw only in canonical Harness storage.
- A malicious query can still be expensive within SELECT: timeout, max rows, read-only connection and configured byte limits remain mandatory.