Repository navigation
fix(audit-trail): close the remaining scope oracles and parity gaps with Ruby [PRD-1269] - #1983
Merged
bexchauveto merged 9 commits intoOct 21, 2026
Conversation
6 new issues
|
|
Coverage Impact This PR will not change total coverage. Modified Files with Diff Coverage (8)
🛟 Help
|
bexchauveto
force-pushed
the
feature/prd-1269-agent-nodejs-the-search-filter-lets-a-non-admin-test-the
branch
2 times, most recently
from
October 7, 2026 15:01
8fdcc29 to
280642a
Compare
bexchauveto
force-pushed
the
feature/prd-1269-agent-nodejs-the-search-filter-lets-a-non-admin-test-the
branch
from
October 8, 2026 13:23
280642a to
153870a
Compare
bexchauveto
force-pushed
the
feature/prd-1269-agent-nodejs-the-search-filter-lets-a-non-admin-test-the
branch
from
October 8, 2026 15:22
153870a to
e13e23e
Compare
bexchauveto
force-pushed
the
feature/prd-1269-agent-nodejs-the-search-filter-lets-a-non-admin-test-the
branch
4 times, most recently
from
October 12, 2026 07:51
13e339b to
ab7d8e5
Compare
bexchauveto
commented
Oct 12, 2026
bexchauveto
left a comment
Member
Author
There was a problem hiding this comment.
Spec (PRD-1269): conforms. Item 1 is closed by the ticket's 9 October note, and both parity gaps and the recreated id are addressed.
bexchauveto
force-pushed
the
feature/prd-1269-agent-nodejs-the-search-filter-lets-a-non-admin-test-the
branch
2 times, most recently
from
October 13, 2026 08:08
e31cef3 to
56ce7f7
Compare
bexchauveto
force-pushed
the
feature/prd-1269-agent-nodejs-the-search-filter-lets-a-non-admin-test-the
branch
4 times, most recently
from
October 14, 2026 09:19
f484511 to
4056d33
Compare
Base automatically changed from
feature/prd-1321-agent-nodejs-carry-the-id-a-primary-key-move-came-from
to
main
October 14, 2026 09:28
bexchauveto
force-pushed
the
feature/prd-1269-agent-nodejs-the-search-filter-lets-a-non-admin-test-the
branch
3 times, most recently
from
October 15, 2026 11:33
f4768ce to
ffc12dc
Compare
bexchauveto
force-pushed
the
feature/prd-1269-agent-nodejs-the-search-filter-lets-a-non-admin-test-the
branch
from
October 15, 2026 11:50
c7f8a07 to
144864d
Compare
bexchauveto
force-pushed
the
feature/prd-1269-agent-nodejs-the-search-filter-lets-a-non-admin-test-the
branch
2 times, most recently
from
October 16, 2026 14:06
acc47a8 to
2e55539
Compare
bexchauveto
force-pushed
the
feature/prd-1269-agent-nodejs-the-search-filter-lets-a-non-admin-test-the
branch
2 times, most recently
from
October 21, 2026 09:17
bd63011 to
2df4f5f
Compare
Merged
4 tasks done
…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>
…D-1269] A primary-key move or an unconfirmed delete frees an id without a confirmed delete, so the latest confirmed create also marks where the current record's life starts. The gone-or-earlier-life decision now lives once in earlier-life.ts, and the SQL and served search matchers share one column list in value-filters.ts, split out of sql-store.ts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…an [PRD-1269] A record created through Forest has its own create as the latest boundary, so every scoped search or fields request on it scanned the whole history to match on served values. A create with nothing filed before it began the id's only life, and no longer counts as a boundary. Same fix as agent-ruby #410. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he audit trail [PRD-1269] The audit trail store is loaded only when `auditTrail` is configured, so an agent without it no longer installs nor loads sequelize and umzug at boot. An agent that enables it without them gets an error naming the packages to install: `npm install sequelize umzug`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1269] Reverts the optional peer dependencies of 144864d: sequelize and umzug are regular dependencies again, and the PostgreSQL driver (pg, pg-hstore) joins them, so the auditTrail option alone turns the audit trail on, whatever the main datasource. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…project timeline [PRD-1323] Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…PRD-1269] Migration 003 adds (collection, record_id, timestamp, id), and the same columns over create and delete rows only. Without them, lifeBoundaryOf's oldest-row lookup walked the whole table's (timestamp, id) index: 534 ms on a 2.1M-row table for a recently created record, 0.04 ms with them. MySQL and MariaDB have no partial index and get the first one only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bexchauveto
force-pushed
the
feature/prd-1269-agent-nodejs-the-search-filter-lets-a-non-admin-test-the
branch
from
October 21, 2026 12:50
4109771 to
3147db0
Compare
bexchauveto
deleted the
feature/prd-1269-agent-nodejs-the-search-filter-lets-a-non-admin-test-the
branch
October 21, 2026 13:50
forest-bot
added a commit
that referenced
this pull request
Oct 21, 2026
## @forestadmin/agent [1.110.4](https://github.com/ForestAdmin/agent-nodejs/compare/@forestadmin/agent@1.110.3...@forestadmin/agent@1.110.4) (2026-10-21) ### Bug Fixes * **audit-trail:** close the remaining scope oracles and parity gaps with Ruby [PRD-1269] ([#1983](#1983)) ([669ef58](669ef58)) ### Dependencies * **@forestadmin/workflow-executor:** upgraded to 1.35.0
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Closes PRD-1269.
Item 1, the timeline
searchoracle: resolved by removing the admin gateThe oracle existed because the timeline blanked values for non-admins while
searchstill matched them. #1911 dropped the admin gate before it shipped: a caller who can read a collection gets its timeline values, the same ones they read record by record. There is nothing withheld left forsearchto probe, so this PR carries no change for it.Item 2, parity gap 1: search escaping and the redaction mask (commit
a0dda89d16)Mirrors agent-ruby's
Sql::TextSearchandsearch_matches?, in both the SQL search and the served-value matcher:15" monitor(stored as15\" monitor) is found, and a bare quote no longer matches the JSON structure;[redacted]is removed from the value text before matching, sosearch=redactedno 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:
recheckRecordVisibilityre-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,/stateand correlation routes all go through it, including the history route's early path for a gone record withsearch/fields.Item 4, a recreated id serves the earlier life's values (commits
54f54904ac,b31f9594db,4f557cf626,f4768ce8c)A live record in the caller's scope says nothing about the rows filed under its id before it took the id. The boundary between the two lives is the id's latest confirmed
deleteorcreate, whichever is later (lifeBoundaryOf, inaudit-trail/earlier-life.ts):deleteis the earlier record's last row, a boundarycreatethe current record's first;createcovers an id freed with no confirmeddelete: a primary-key move, or a delete that was never confirmed;pendingrow marks no boundary, since it may never have landed;createwith nothing filed before it began the id's only life and is no boundary either. Without that, a record created through Forest, whose own create is its latest boundary, sent every scopedsearch/fieldsrequest down the served-value scan.Rows before the boundary go through the gone-record withholding even though a live record holds the id now:
search/fieldsthe matching runs on served values, so count and authors cannot leak them;/state: a reconstruction strictly before the boundary is tested against the scope. At the boundary's own instant the state already includes a replacementcreatesharing it.withholdEarlierLivesholds the gone-or-earlier-life decision once for the history and correlation routes. The lookup 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.
Refactor in the same commits
value-filters.tsis split out ofsql-store.ts: the SQL search and the served-value matcher share one column list. The helpers stay exported frompackages/agent/src/audit-trail.The audit trail's dependencies ship with the agent (commit
0f1864e14)So that turning the audit trail on takes the
auditTrailoption and nothing else, whatever the main datasource:sequelizeandumzugstay regular dependencies (commit144864d65had made them optional peers; it is reverted);pgandpg-hstore, joins them. The audit trail only targets PostgreSQL today, and both are pure JS, so they add no native build to an agent that never enables it.Migration 003: index a record's history (commit
410977157)Item 4's
lifeBoundaryOfruns on every read by a caller with a scope, on a record that still exists. With onlyrecord_idand(timestamp, id)to choose from, its oldest-row lookup (ASC LIMIT 1) walked the whole table's(timestamp, id)index. On a 2.1M-row Postgres table, a hot record created 30 days ago discarded 2.1M rows (534 ms).Migration 003 adds:
(collection, record_id, timestamp, id), which serves every per-record read;create/deleterows only (partial index), for the boundary lookup.Both lookups drop to ~0.05 ms whatever the table or history size; checked with
EXPLAINon Postgres 15, on a database upgraded from 002. MySQL/MariaDB have no partial index and get the first one only. Ruby: ForestAdmin/agent-ruby#416.Tests
redactedmatch nothing; same on the served-value path.createare still withheld; the rows of a record whose primary key moved off the id are withheld; an id never freed is served whole; a scoped search on a record created through Forest stays a single store query;/statebefore the delete withheld, after it served, and at a delete shared with a replacementcreateserved. The boundary tests fail when the check is disabled or<=is used.🤖 Generated with Claude Code
Note
Fix audit-trail scope oracles and close parity gaps with Ruby
Closes leaks where a caller who can read a current record also saw captured values from an earlier record that reused the same id.
auditTrail.lifeBoundaryOffinds the latest confirmed create/delete boundary, andauditTrail.withholdEarlierLiveswithholds pre-boundary rows for live reused idsMacroscope summarized 3147db0.