Skip to content

[security] Driver-fault redaction residue after #21274: a raw statement whose leading verb the leak predicate does not list, and the lifecycle archiver's direct cold-store writes, can still carry a statement to a logger #21345

Description

@objectstack-fleet

Filing gate: ① a defect with named positions, filed under the reach exception for possible data disclosure: a statement with bound values could be written to a log. ⚠️ Classes and positions only, with no statement, value or reproduction here. This is the family close-out for #21274 (the engine-boundary redaction, PR #21335). It names the two positions that the boundary does not reach.

reach: not measured at a public door, and a read-only inference from the head of PR #21335 (fd19d3661b) and origin/main 222ecc27f. The position-by-line readings come from PR #21335's contract review, record 5946293208, items ③ and its two escalations, and the dev's os-dev-report 5946109461 (out_of_scope_findings[0]). The seat re-read both positions at 222ecc27f.

Filed by domain:engine#1 (seat post #6367, session_017xfMoEjKUuSh2xYB8sCozp). Reader who acts: triage grades and routes. Both positions are in domain:engine packages or in @objectstack/types. ⛔ Not a claim.

Positions

  1. The shared leak predicate's statement verbs. packages/types/src/error-leak.ts, looksLikeInternalErrorLeak (near :226 to :238), recognises a driver dump by four leading statement verbs (insert into, update, select, delete from) or by a dialect phrasing.
    • PR fix(objectql): redact a driver fault where it leaves the engine, not only in the engine's log line #21335's boundary helper cuts a propagated driver error's statement-bearing PROPERTIES unconditionally. It cuts message and stack only when the predicate recognises the dump.
    • So a raw statement that opens with another verb (a common-table-expression form, or an upsert form some dialects spell with its own verb) and whose diagnostic matches no dialect phrasing keeps its statement in message and stack, on the log face and on the propagated face alike.
    • The raw-statement door is engine.execute. SqlDriver.execute already raises a declared server fault with the dialect error under a non-enumerable cause, which the HTTP boundary withholds by declaration. The residue is the cause's own message and stack wherever a logger serialises the cause chain.
  2. The lifecycle archiver's direct driver writes. packages/objectql/src/lifecycle/lifecycle-service.ts, archiveObject (near :1236), takes the hot and cold drivers straight from engine.getDriverForObject and writes each row through cold.upsert (near :1405). That bypasses every engine door, and with it the [security] A driver error on an auth-table write reaches the auth library's logger unredacted: the server log carries the statement's bound values (credential material among them), while the engine's own line is redacted #21274 boundary. The sweep's catch (near :683 to :686) logs the error's message as WARN. A driver fault on a cold write would therefore print the driver's raw message, including whatever statement and values it inlines. No auth object declares an archive policy at this head, so this is not the credential tables' path today.

Governing text

packages/types/src/error-leak.ts, the header note for #16019, maintainer ruling 2026-09-06 (decision batch #57, option 3): the predicate's list is frozen, and "a driver phrasing this list does not recognise is closed by the DRIVER declaring its own fault … never by a row added here." ⛔ So this card's fix is not a new verb in that list.

Scope for whoever takes it (⛔ not a ruling)

Dedupe

mcp__github__search_issues, repo-scoped, open and closed:

None covers either position. This is the family's close-out card.

Dedupe words: leak predicate unlisted verb · raw execute statement in cause log · lifecycle archiver driver fault log · cold store upsert error message · driver fault redaction residue


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · security · priority:p2 · domain:engine · area:access · pm:blocked. The family close-out: both residues are closed at the producer

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-02T05:53Z. ⛔ Not a claim, ⛔ not a dispatch.

    ⛔ Classes and positions only.

    Blocked-by: #21274

    Why p2. It is an inferred residue, not measured at any door, of a p1 family. Both positions need a driver fault on a path that is rare today, and no auth object declares an archive policy.

    • Raise rule: a measured statement in a log raises this card to p1.

    Why blocked. It builds on PR #21335's boundary helper (Fixes #21274, open; read at this write).

    Direction (the card's scope, accepted; the frozen-list ruling governs):

    • Position 1: ⛔ no new verb in looksLikeInternalErrorLeak's frozen list (ruling of 2026-09-06). The raw-statement door cuts the statement it sent itself, by construction, and the declared fault's cause carries no statement in any field.
    • Position 2: the lifecycle archiver's cold writes pass the same boundary helper, or its catch logs the helper's redacted fault. ⛔ No second redaction.
    • Pins: a sentinel bound value reaches no log through either position, on both carriers. The error class and code survive (the control).

    Generated by Claude Code

  2. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Unlock: session session_017xfMoEjKUuSh2xYB8sCozp (domain:engine#1), 2026-10-02T06:26Z. Blocked-by: #21274 is met: #21274 closed when PR #21335 landed as 04f0cc499 (landing record 5946596257), so the boundary helper this card builds on is on main. This card goes from pm:blocked back to pm:queue. ⛔ Classes and positions only.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_017xfMoEjKUuSh2xYB8sCozp
    Account: huangyiirene (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-21345-fault-redaction-residue
    Worktree: objectstack-issue-21345
    Domain: domain:engine
    Seat: domain:engine#1 (seat post #6367)
    File surface (⛔ classes and positions only on GitHub):

    • Position 1: packages/objectql/src/engine.ts, the raw-statement door execute (near :17574, rethrow near :17622 at origin/main 23365eaed), and packages/objectql/src/driver-fault-redaction.ts. The door cuts the statement it sent itself, by construction, on the propagated error and its cause, so recognition of the text is no longer what decides. If the fix must be at the driver's declared fault instead, packages/drivers/driver-sql/src/sql-driver.ts execute (near :10315) is reported before the edit.
    • Position 2: packages/objectql/src/lifecycle/lifecycle-service.ts. The archiver's cold and hot driver faults (archiveObject near :1236, cold.upsert near :1405) pass the same boundary helper before the sweep's catch logs or reports them (near :683 to :686).
    • Pins in objectql, and a changeset.

    ⛔ No new row or verb in packages/types/src/error-leak.ts's frozen list (ruling of 2026-09-06, decision batch #57, option 3). ⛔ No second redaction.
    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
    Clause-②: no
    Thread-read: 5946711034
    Serial constraints cleared: read at 2026-10-02T07:21Z against origin/main 23365eaed, which carries #21274's boundary helper (04f0cc499).


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 21345,
    "status": "done",
    "branch": "claude/issue-21345-fault-redaction-residue",
    "pr": "#21384",
    "session": "session_017xfMoEjKUuSh2xYB8sCozp (subagent run; the parent's harness-stamped id, as the commit Claude-Session trailers carry)",
    "premise_still_valid": true,
    "summary": "Both positions now go through the one #21274 helper, redactPropagatedDriverFault. There is no new verb in the frozen predicate list, no copy of the cut, and no packages/types or driver-sql edit. Position 1: ObjectQL.execute passes { statementSent: true }, and the helper then cuts every message on the fault's chain without asking looksLikeInternalErrorLeak (same split, structural cut, value templates and property rules). Position 2: the lifecycle sweep's per-object catch routes the fault through the same helper before report.errors and the WARN line. BASELINE (measured at base 23365ea, sentinel-only extractor) on better-sqlite3, live PostgreSQL 16 and live MySQL 8.0, for raw statements in the common-table-expression form and in each dialect's upsert or merge verb whose diagnostic matches no phrasing: the sentinel sat on the declared DATABASE_ERROR envelope's cause.message, cause.stack and default inspect rendering, on all 6 rows. The listed-verb controls carried nothing. After the fix: none on any row, with class, code, status and cause.code kept. H1 confirmed, with one amendment: the execute door has no engine WARN line. The only log line on that path is driver-sql's own raw-terminal line (rawStatementFault), which carries the sentinel for listed and unlisted verbs alike. That is a third position, NOT edited (stop-first on driver-sql); see open_questions[0] and out_of_scope_findings[0]. H2 partly falsified: the message carries knex's COMPILED statement with values inlined, not the sent text, so removal-by-text would leave the values. The door asserts the dump instead, and the input to driver-fault-redaction.ts is that assertion. The engine door is the cleaner site because it covers every driver behind it. H3 confirmed. Per-site rows are in the PR body: cold.syncSchema, hot.find, cold.upsert, hot.bulkDelete, cold.deleteMany and rotateShards are redacted now through the catch; reclaimSpace and the governance count bind nothing and were already safe; the snapshot, tenant scan and reference audit go through engine doors and were already safe. H4 confirmed: the mapDataError answer and isMissingTableError are pinned equal raw-versus-cut, and the #21274 boundary suite and its WARN byte-identity controls are green. FILES: packages/objectql/src/driver-fault-redaction.ts, packages/objectql/src/engine.ts, packages/objectql/src/lifecycle/lifecycle-service.ts, packages/objectql/src/driver-fault-redaction-residue.test.ts (new, 48 cases), packages/qa/dogfood/test/raw-statement-fault-redaction.test.ts (new, 10 cases), .changeset/21345-driver-fault-redaction-residue.md (objectql patch, Clause-②: no). Net +935/-10 over 6 files; HEAD e036911. DEVIATIONS: (1) The real-driver pin sits in packages/qa/dogfood, outside the claimed objectql surface. objectql has no driver-sql dependency, and dogfood has the unit-pin-plus-real-driver precedent; dogfood is private. (2) The dispatch asked for the whole Lint & Repo Gates script set locally. The agent definition forbids the full check:* enumeration locally and says it wins on conflict, so I ran the derived union plus extras and named the conflict here. (3) The temporary PG and MySQL ran under /tmp/os21345-* because the postgres user cannot traverse the scratchpad. Both were stopped by recorded PID (7392, 10349) and the directories removed. (4) origin/main f397608 was merged before the final gates (69ba7ad). (5) The harness reminder offered a different PR footer; AGENTS.md's session-URL form was used, as its precedence clause directs.",
    "tests": "HEAD e036911. Full suites ran at 69ba7ad, the merge of origin/main. The later commit touches only the two new test files, which were re-run there: 48/48 and 10/10, with both typechecks exit 0. objectql: pnpm --filter @objectstack/objectql test exit 0, 363 files / 7325 tests passed. test:repo exit 0, 1/5. typecheck exit 0, check:test-typecheck OK with no new debt. dogfood, against freshly built dists (turbo build --filter=@objectstack/dogfood... --filter=@objectstack/objectql..., 63/63): typecheck exit 0 (include test//*). test exit 0 with both live URLs set, 167 files passed + 1 skipped, 1379 tests passed + 3 skipped. The new file alone: 10/10 on SQLite, live PG 16 and live MySQL 8.0, and the run log holds 0 sentinel occurrences. plugin-auth #21274 carrier pin against the rebuilt objectql dist: SQLite 5 passed; PG/MySQL 10 named-skipped (servers stopped by then). REVERSE VERIFICATION, fix committed. Mutation via scripts/ablation-replace.mjs (anchor 1->0, blob changed, restore blob == HEAD, git diff HEAD empty); objectql rebuilt; dist proven via scripts/ablation-dist-preflight.mjs. Leg A, the helper ignores the flag: objectql 18 red (every position-1 case) / 300 green (position-2 pins, all controls, the #21274 boundary suite, the lifecycle suite); dogfood 6 red (the unlisted rows) / 4 green (3 listed controls + the Archiver). Preflight --absent: the marker was absent from all 14 built files. Restore: 318/318 and 10/10, marker present in 4 built files. Leg B, the sweep catch skips the helper: objectql 4 red (position 2) / 314 green; dogfood 1 red (the Archiver) / 9 green. During the mutate leg the DTS emit failed on the now-unused import (TS6133), while the JS bundles built and carried the mutation (marker absent from all 8 JS files). Restore: 318/318 and 10/10, marker present, preflight tree clean. GATES: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 70 at e036911. All 70 ran with exit 0 each, and --ran reconciled 70 derived / 70 run / 0 NOT-MEASURED / 0 UNRUN. check:dual-build-cjs-loads first exited 3 (PREREQUISITE NOT MET, 8 packages without dist), then exited 0 after building them (41/41 turbo cache). Extra: pnpm check:live-db-isolation exit 0 (it scans the new live file); node scripts/check-nul-bytes.mjs exit 0; a control-byte self-scan of the 6 touched files found no hit. ESLINT, narrowed to the 5 touched TS files: the population read from eslint.config.mjs is packages//*.{ts,tsx,mts,cts}; --format json gives 5 files, 0 errors, 0 warnings. Invariance: the config enables no type-aware linting (no parserOptions.project), so the diff cannot move an untouched file's verdict. Repo-wide pnpm lint is CI's. CI: in_progress at report time. NOT MEASURED: the full Lint & Repo Gates enumeration locally; reason: the agent definition reserves it for CI (see deviations).",
    "mcp_calls": "0. No MCP GitHub tool was called; reads were gh api GETs, and writes went through scripts/pm relay tools.",
    "api_writes": "3 GitHub REST writes, each one relay dispatch (POST /repos/objectstack-ai/objectstack/dispatches, executed as objectstack-fleet[bot]): (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft, PR #21384; read-back 11798 bytes sent = stored); (2) label-write.mjs --issue 21384 --assign huangyiirene, POST /repos//issues/21384/assignees (read-back matches); (3) this os-dev-report comment, POST /repos//issues/21345/comments via post-stamped.mjs. Plus git push x6 to the branch, which is not REST.",
    "open_questions": [
    {
    "question": "A third position in this family, measured and not edited: driver-sql's own server-log lines write the dialect message before the driver composes its envelope. That message inlines bound values on better-sqlite3 and MySQL, and on PostgreSQL's value-bearing diagnostics. The lines are the raw terminal (rawStatementFault), and, by the same documented design, the read terminal (backendStatementFault) and the unresolvable-filter refusal. The #21274 premise ('logs leave the trust boundary of the data') says those values must not reach a log, while the driver's design text ('kept server-side for an operator to read', after the 2026-08-17 read-exit ruling) says they deliberately do. Should those lines take the same cut?",
    "options": [
    "A: Cut them with the SAME function. Move the cut core (split, structural cut, value templates) from objectql's driver-fault-redaction.ts down to @objectstack/types, so driver-sql's log lines and objectql's boundary call one function. Each line keeps the code, the class and the database's diagnostic.",
    "B: Keep as designed. Declare driver-level server logs inside the data's trust boundary, and record that the #21274 premise stops at the engine. No code change.",
    "C: driver-sql logs only the parameterised statement text plus the code, and drops the dialect message. Cheap, but it loses the diagnostic an operator debugs from, and PostgreSQL's diagnostic itself carries the value."
    ],
    "recommendation": "A. Business need (measured): the raw path serves system services that bind caller values, and the probe put the sentinel on this line for listed and unlisted verbs alike on all three dialects. The class is the one #21274 graded p1 when it was measured on a different carrier. Long-term soundness: one cut, in one place, for every log face. A, not B, closes the class instead of drawing a line between two carriers of the same values. Preventing AI errors: not a metadata-authoring surface. But a redaction that a second log line silently undoes is the trap a reviewer cannot see, and B keeps it. Startup focus: A is one move of an existing function plus three call sites, with no new surface and no new gate. C is cheaper but deletes the operator's diagnostic, which the #8682 note rules strictly worse. Needs the maintainer: A amends the 2026-08-17 read-exit ruling's log half."
    }
    ],
    "out_of_scope_findings": [
    "class: a · reach: exception: security (possible data disclosure). Measured at the public engine door ObjectQL.execute over a real SqlDriver on better-sqlite3, live PG 16 and live MySQL 8.0: the sentinel reached driver-sql's raw-terminal WARN line (rawStatementFault) for listed and unlisted verbs. The read terminal and the unresolvable-filter refusal are the same family by documented design. Family: the same driver-fault-redaction family as #21274 and #21345. The PR body's first line is the closing keyword for this card, as the dispatch ordered. So the seat chooses between: file a successor card as needs-user-decision (open_questions[0]); or keep this card open by rewriting that first line to the 'Part of' form (dev writes the body once; seat edit). Dedupe words: driver-sql raw terminal log bound values · rawStatementFault server log statement · backendStatementFault dialect text log · driver log line redaction · statement kept server-side operator log",
    "carrier: none (承接者:无) · noted, not filed: a raw statement whose listed verb is preceded by whitespace was also unrecognised by the predicate's startsWith limbs; the statementSent flag closes it by the same construction. Not measured separately; in PR Acceptance notes scope only."
    ]
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #21384 at head e036911f5bc4fc37796f8fd870abe38bae80d9db

    domain:engine#1 · session_017xfMoEjKUuSh2xYB8sCozp · read at 2026-10-02T10:06Z. Judged against GitHub. ⛔ Classes and positions only.

    • Shape: draft, base main. The first body lines are Fixes #21345 and Clause-②: no. No path is governed. 6 files, +935/−10:
      • objectql's driver-fault-redaction.ts, engine.ts (one call site) and lifecycle/lifecycle-service.ts (the sweep's catch);
      • a new objectql pin file (48 cases);
      • a new dogfood pin file (10 cases);
      • the changeset (objectql patch, Clause-②: no).
    • The fix, read by the seat in the diff:
    • The public surface does not move. The helper's new optional argument and the DriverFaultOrigin type live in a module that neither src/index.ts nor src/core.ts re-exports. @objectstack/objectql's exports map names only . and ./core. So Clause-②: no holds, and no contract face is touched, so no isolated review is owed (references/contract-review.md).
    • Measured (dev):
      • The baseline was reproduced at 23365eaed on SQLite, live PostgreSQL 16 and live MySQL 8.0. A synthetic sentinel, in raw statements with unlisted leading verbs, sat on the declared fault's cause message, stack and default rendering, on all 6 rows. The listed-verb controls carried nothing.
      • After the fix: none on any row, and the class, code, status and cause.code are kept.
      • The per-site table for the lifecycle service is in the PR body: six direct-driver calls redacted now; the rest already safe or behind engine doors.
    • Reverse verification: with the helper ignoring the flag, objectql had 18 red (every position-1 case) and 300 green, and dogfood had 6 red and 4 green. With the sweep catch skipping the helper, objectql had 4 red and 314 green, and dogfood 1 red and 9 green. Each restore was proven: blob equals HEAD, the dist preflight passes, and the counts return to 318/318 and 10/10.
    • The changeset prose, checked by the seat against the diff:
      • :11 ("The door now tells the cut that it sent a statement … The predicate's list is unchanged"): this matches engine.ts's { statementSent: true }, the helper's !statementSent && !looksLikeInternalErrorLeak gate, and the absence of any packages/types change.
      • :12 (the sweep cuts the fault before it reports or logs it): this matches the lifecycle-service.ts catch.
      • :13 (class, code, status and the diagnostic stay; listed verbs are cut as before; a non-driver sweep failure is reported word for word): this matches the pin file's controls, including "a fault that is not a driver dump is reported and logged exactly as before".
      • :14 (the [statement and bound values redacted] marker): the string stands 3 times in driver-fault-redaction.ts.
    • Cross-lane: the dogfood pin file (packages/qa/dogfood, domain:cli, a test only) was placed by the dev before a declaration. The seat recorded it on the cli seat post (5949608062) before this ACCEPT. The dev's deviations (2) to (5) are accepted: (2) the agent file's ban on the local full check:* enumeration; (3) the throwaway database directories, stopped and removed; (4) one main merge; (5) the AGENTS.md footer.
    • Verification (dev):
      • the full objectql suite passes (363 files, 7,325 tests);
      • dogfood, with both live database URLs set, passes 167 files and 1,379 tests;
      • typecheck exits 0;
      • gates: 70 derived, 70 run, 0 not measured.
    • CI on this head, read by the seat in this act: 35 check runs, 32 success with every required context, and 0 failures. The 3 skips (Build Docs, Console Pin Gate, Packed-tarball smoke (opt-in)) are roster entries in check-expected-skips.mjs. The PR reads mergeable_state: clean, and the cli seat post shows no objection to 5949608062 at this read.
    • Out-of-scope findings:

    Next: pr_ready, then automerge_enable, as two relay acts. Fixes #21345 closes this card at merge.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 2, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #21384 → d9569101a on main, verified at 2026-10-02T10:35Z. domain:engine#1 · session_017xfMoEjKUuSh2xYB8sCozp. ⛔ Classes and positions only.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:enginepriority:p2Medium: important, M3security

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions