Skip to content

fix(audit-trail): close the remaining scope oracles and parity gaps with Ruby [PRD-1269] - #1983

Open
bexchauveto wants to merge 3 commits into
feature/prd-1321-agent-nodejs-carry-the-id-a-primary-key-move-came-fromfrom
feature/prd-1269-agent-nodejs-the-search-filter-lets-a-non-admin-test-the
Open

bexchauveto wants to merge 3 commits into
feature/prd-1321-agent-nodejs-carry-the-id-a-primary-key-move-came-fromfrom
feature/prd-1269-agent-nodejs-the-search-filter-lets-a-non-admin-test-the

Conversation

@bexchauveto

@bexchauveto bexchauveto commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Stacked on #1946 (itself stacked on #1911). Its base retargets as those merge.

Closes PRD-1269.

Item 1, the timeline search oracle: resolved by removing the admin gate

The oracle existed because the timeline blanked values for non-admins while search still matched them. #1911 now drops the admin gate: a caller who can read a collection gets its timeline values, the same ones they read record by record. There is nothing withheld left for search to probe, so the identitySearch commit this PR used to carry is gone.

Item 2, parity gap 1: search escaping and the redaction mask (commit 35375d0de)

Mirrors agent-ruby's Sql::TextSearch and search_matches?, in both the SQL search and the served-value matcher:

  • the term is JSON-escaped for the value columns, so 15" monitor (stored as 15\" monitor) is found, and a bare quote no longer matches the JSON structure;
  • [redacted] is removed from the value text before matching, so search=redacted no longer confirms which rows hold a masked value. Same as Ruby, a real value containing that literal loses it for matching.

Item 3, parity gap 2: re-reading a record already gone (same commit)

Aligned with Ruby: recheckRecordVisibility re-reads whenever a scope applies, even for a record gone at the first check, and gone at either read means gone. An id since taken by a record the caller cannot read answers 404; one taken by an in-scope record does not release the earlier values. The history, /state and correlation routes all go through it, including the history route's early path for a gone record with search / fields.

Item 4, a recreated id serves the earlier life's values (commits e0b310857, e13e23e66)

Rows filed at or before the id's last confirmed delete belong to the earlier record, so they go through the gone-record withholding even though a live record holds the id now:

  • history route: those rows are withheld, and with search / fields the matching runs on served values, so count and authors cannot leak them;
  • correlation lookups: same row rule;
  • /state: a reconstruction strictly before that delete is tested against the scope. At the delete's own instant the state already includes a replacement create sharing it.

A pending delete frees nothing. The lookup (lastDeleteOf, in audit-trail/earlier-life.ts) runs only for a scoped caller on a live record. Ruby still needs this: PRD-1258.

These are permission-scope withholdings, independent of the admin level, so they stay after the gate removal.

Tests

  • Search: a quote and a backslash are found; a bare quote and redacted match nothing; same on the served-value path.
  • Re-read: an id gone then taken out of scope answers 404; taken in scope keeps withholding.
  • Recreated id: earlier rows withheld and current kept; a search cannot find the earlier values; a pending delete is not a boundary; an id never freed is served whole; /state before the delete withheld, after it served, and at a delete shared with a replacement create served. The boundary tests fail when the check is disabled or <= is used.

760 tests pass across test/audit-trail and test/routes; lint and typecheck clean.

🤖 Generated with Claude Code

Note

Fix audit-trail id-reuse scope oracles and close search parity gaps with Ruby

  • Adds earlier-life boundary logic via lastDeleteOf and belongsToEarlierLife in earlier-life.ts. Audit rows at or before an id's latest confirmed delete are treated as a prior record life and withheld using earlier-life permission scope, not the current record's scope. Pending deletes do not establish a boundary.
  • Applies this to history, correlation, and state routes: audit-trail.ts and audit-trail-correlation.ts withhold earlier-life rows, match value filters against the values actually served, and apply a visibility recheck (removing the early return in recheckRecordVisibility) so an id reused by an out-of-scope record returns 404.
  • Fixes JSON-value search in searchCondition and matchesServedValues: terms are JSON-escaped (jsonEscaped), so quotes and backslashes match literally, and the redaction marker is stripped from stored values before matching.
  • Updates tests in sql-store.test.ts, audit-trail.test.ts, and audit-trail-correlation.test.ts for id reuse, pending deletes, recheck races, and escaped/redacted value searches.
  • Behavioral Change: scoped audit history, correlation, and state requests now perform an extra visibility read and a latest-delete lookup, and earlier-life values no longer appear in search results or author lists; ids reused by inaccessible records now 404 instead of serving earlier rows.

Macroscope summarized 13e339b.

@linear-code

linear-code Bot commented Oct 7, 2026

Copy link
Copy Markdown

PRD-1269

@bexchauveto
bexchauveto added this pull request to stack #1915 October 7, 2026 14:00
@bexchauveto bexchauveto changed the title fix(audit-trail): keep a non-admin's timeline search off the withheld values [PRD-1269] fix(audit-trail): close the remaining value oracles and parity gaps [PRD-1269] Oct 7, 2026
@qltysh

qltysh Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

6 new issues

Tool Category Rule Count
qlty Structure Function with high complexity (count = 11): withhold 3
qlty Structure Function with many returns (count = 5): withhold 2
qlty Structure Function with many parameters (count = 4): scanServedValues 1

Comment thread packages/agent/src/routes/access/audit-trail.ts Outdated
Comment thread packages/agent/src/audit-trail/sql-store.ts
@qltysh

qltysh Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

Qlty


Coverage Impact

Unable to calculate total coverage change because base branch coverage was not found.

Modified Files with Diff Coverage (6)

RatingFile% DiffUncovered Line #s
New file Coverage rating: A
packages/agent/src/audit-trail/index.ts100.0%
New file Coverage rating: A
packages/agent/src/routes/access/audit-trail-correlation.ts100.0%
New file Coverage rating: A
packages/agent/src/audit-trail/record-visibility.ts100.0%
New file Coverage rating: A
packages/agent/src/routes/access/audit-trail.ts100.0%
New file Coverage rating: A
packages/agent/src/audit-trail/earlier-life.ts100.0%
New file Coverage rating: A
packages/agent/src/audit-trail/sql-store.ts100.0%
Total100.0%
🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

@bexchauveto
bexchauveto force-pushed the feature/prd-1269-agent-nodejs-the-search-filter-lets-a-non-admin-test-the branch 2 times, most recently from 8fdcc29 to 280642a Compare October 7, 2026 15:01
@bexchauveto
bexchauveto force-pushed the feature/prd-1269-agent-nodejs-the-search-filter-lets-a-non-admin-test-the branch from 280642a to 153870a Compare October 8, 2026 13:23
@bexchauveto
bexchauveto force-pushed the feature/prd-1269-agent-nodejs-the-search-filter-lets-a-non-admin-test-the branch from 153870a to e13e23e Compare October 8, 2026 15:22
@bexchauveto bexchauveto changed the title fix(audit-trail): close the remaining value oracles and parity gaps [PRD-1269] fix(audit-trail): close the remaining scope oracles and parity gaps with Ruby [PRD-1269] Oct 8, 2026
@bexchauveto
bexchauveto force-pushed the feature/prd-1269-agent-nodejs-the-search-filter-lets-a-non-admin-test-the branch 2 times, most recently from 67f6d5d to 381a38b Compare October 9, 2026 12:33
bexchauveto and others added 3 commits October 9, 2026 14:43
…n mask, and re-read a gone record [PRD-1269]

Parity with agent-ruby on two gaps.

Search: the values are matched as serialized JSON, where a quote or a
backslash sits escaped, so the term is now escaped the same way. That
finds '15" monitor' and keeps a bare quote off the document's
structure. The [redacted] mask is removed before matching, so
search=redacted no longer confirms which rows hold a masked value. Same
change on the served-value path.

Re-read: the record is read again after the audit read even when it was
already gone at the first check, and gone at either read means gone. An
id taken since by a record the caller cannot read answers 404, and one
taken by an in-scope record does not release the earlier life's values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e id [PRD-1269]

A delete frees the id and a later record can take it. Both reads then
see a live record in scope, so the route served the whole history as
the current record's, including values the caller's scope would have
withheld on the record that held the id before.

Rows filed at or before the id's last confirmed delete now go through
the gone-record withholding: on the history route (search and fields
matched against the served values, so count and authors cannot leak
them), on the correlation lookups, and on /state for a reconstruction
at or before that delete. A pending delete frees nothing.

The history route's early path for a record gone at the first check
also re-reads it now, so an id since taken by a record the caller
cannot read answers 404 there too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e earlier record's [PRD-1269]

At the delete's own instant the state already reflects every row there,
including a replacement create sharing that timestamp. Testing it
against the scope as an earlier life withheld the replacement whenever
the scope read a column the capture never kept, although the caller had
just been shown to read it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bexchauveto
bexchauveto force-pushed the feature/prd-1269-agent-nodejs-the-search-filter-lets-a-non-admin-test-the branch from 381a38b to 13e339b Compare October 9, 2026 12:43

This branch has not been deployed

No deployments
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