Repository navigation
feat(audit-trail): record the id a primary key moved from [PRD-1321] - #1946
bexchauveto wants to merge 1 commit into
Conversation
…each side by it An update carries two states but files under one id, so a writable primary key that is also redacted left the previous side with nothing to answer a permission scope: #1909 withheld it rather than judging it by the id the record ended up with. Correct, and lossy — a caller squarely in scope saw nothing. The row now carries `previous_record_id`, written on every confirmed update, and each side is judged against the id it actually had. Written whether or not the key moved, which is where this departs from agent-ruby#394: that table had never shipped, so a null there can only mean the key held still. Here a null has to keep meaning "written before this column existed", or a row from an older agent whose key did move would be judged by the id it moved to — the leak #1909 closed. For the same reason the column arrives as its own migration rather than an edit to 001. A pending update's new side still answers with what it captured: the row is filed under the id the record had before the write, which says nothing about the state it was moving to. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 new issue
|
| // Idempotent for the same reason 001 is: a process losing a concurrent-boot race retries. | ||
| if (existing.has('previous_record_id')) return; | ||
|
|
||
| await context.queryInterface.addColumn( |
There was a problem hiding this comment.
🟠 High audit-trail/migrations.ts:218
Concurrent upgrades on SQLite/MySQL/MariaDB/MSSQL can fail agent startup: two processes can both observe previous_record_id as absent, then the loser’s unguarded addColumn rejects with a duplicate-column error and Umzug marks migration 002 as failed. Make the column addition tolerate that dialect-specific duplicate-column race (or otherwise serialize the migration) so the losing process treats it as already applied.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/agent/src/audit-trail/migrations.ts around line 218:
Concurrent upgrades on SQLite/MySQL/MariaDB/MSSQL can fail agent startup: two processes can both observe `previous_record_id` as absent, then the loser’s unguarded `addColumn` rejects with a duplicate-column error and Umzug marks migration `002` as failed. Make the column addition tolerate that dialect-specific duplicate-column race (or otherwise serialize the migration) so the losing process treats it as already applied.
| recordId: { type: DataTypes.TEXT, allowNull: true }, | ||
| // Set on every confirmed update, so a null distinguishes a row older than the column from | ||
| // one whose key held still. TEXT for the same reason as `recordId`. | ||
| previousRecordId: { type: DataTypes.TEXT, allowNull: true }, |
There was a problem hiding this comment.
🟡 Medium audit-trail/sql-store.ts:42
SQL persistence drops PendingAuditRecord.previousRecordId, so a subsequent confirm that omits the optional field leaves the row NULL; withholdOutsidePermissionScope then treats the confirmed update as pre-migration and withholds its previous side. Add previousRecordId to the toRow serialization so the SQL store preserves it like the in-memory store.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @packages/agent/src/audit-trail/sql-store.ts around line 42:
SQL persistence drops `PendingAuditRecord.previousRecordId`, so a subsequent `confirm` that omits the optional field leaves the row `NULL`; `withholdOutsidePermissionScope` then treats the confirmed update as pre-migration and withholds its previous side. Add `previousRecordId` to the `toRow` serialization so the SQL store preserves it like the in-memory store.
|
Coverage Impact This PR will not change total coverage. Modified Files with Diff Coverage (3)
🛟 Help
|

Closes PRD-1321. The Ruby twin is agent-ruby#394, already merged.
What was lossy
An
updatecarries two states but is filed under one id. When a primary key is both writable (so the capture records it) and redacted, both sides hold the placeholder, the packed id is the only value left — and it speaks for the side the row was filed under, not the other one.#1909 chose to withhold rather than answer the previous side from the id the record ended up with. That is correct, and it costs a caller squarely in scope the values they should have seen:
/stateloses the most, since it rebuilds from adeleterow carrying the whole writable column set.What this does
previous_record_idon the audit row, written on every confirmed update, andwithhold.tsjudges the previous side against it.Written whether or not the key moved — this is the one place it departs from agent-ruby#394. That table had never shipped, so a null there can only mean the key held still. This one has: a null has to keep meaning written before this column existed, or a row from an older agent whose key did move would be judged by the id it moved to, which is exactly the leak #1909 closed. For the same reason the column arrives as
002-add-previous-record-idrather than an edit to001, which deployed databases have already recorded as applied.Ported from Ruby unchanged:
Tests
442 tests pass across
test/audit-trailandtest/routes/access.Two things the existing tests caught while I wrote this, both worth knowing: an absent column read as known until I normalised
undefinedto null (which released a previous side it should have withheld), and decoding per side rather than per row made a bad id warn twice.🤖 Generated with Claude Code
Note
Record the previous primary-key id in audit-trail update entries
Confirmed update audit entries now store the packed record id from before the write, alongside the resulting id. This applies to both key-changing and key-stable updates.
previous_record_idfield toAuditRecord, persisted via a new migration 002 that adds the column to existing audit tables and skips if already present; rollback removes it (migrations.ts, sql-store.ts, types.ts).nullfor the previous id.📊 Macroscope summarized 05cba83. 7 files reviewed, 3 issues evaluated, 1 issue filtered, 2 comments posted
🗂️ Filtered Issues
packages/agent/src/audit-trail/withhold.ts — 0 comments posted, 1 evaluated, 1 filtered
previousRecordId, including a no-op primary key update where it equalsrecordId; lines 168 and 169-172 calldecodePrimaryKeystwice for that same malformed/unpackable id. This emits two identical warning logs for each such audit row, despite the intended single warning per id. [ Out of scope (post-validation triage) ]