Repository navigation
Log-event hashes are keyed with a per-store secret the service keeps outside the store, so a store reader can no longer test guesses at the literals they hide (#4004) - #4020
Merged
Conversation
…outside the store, so a store reader can no longer test guesses at the literals they hide (#4004) raw_line_hash and statement_fingerprint were unkeyed SHA-256 over text a store reader can mostly rebuild, an offline guessing oracle for the literals (a PIN came back in 6 ms). Both are now HMAC-SHA-256 under a 32-byte key, with a separate subkey per identity, at the same 32-hex width. - PgLogHashKey (shared collectors) computes both; the unkeyed PgLogTextRedactor functions and the keyless PgLogEventClassifier.Default are gone, so nothing can hash without the key. The classifier stamps every claimed event; the writer both transports use refuses an unstamped one. - The service loads the key once at start (DarlingLogHashKeyFile): managed Windows beside pg-credential.dpapi as a DPAPI blob with the hardened ACL; compose in the darling-credentials volume, 0600, under #3983's directory and file trust checks; bring-your-own in darling-keys beside darling.json. Generated only when nothing is at the path; an existing key that cannot be trusted, read or parsed is refused, never replaced, and pg_log_events on both routes refuses with a clear message (before querying the target) until an operator fixes or deletes the file. - No surface returns either hash: statement_fingerprint leaves get_pg_log_events, the reader's page and the web log tab. - Old rows keep their unkeyed hashes until retention drops them: a one-time dedupe and fingerprint discontinuity, as #4013 accepted. Lite collects no PostgreSQL logs and hashes no raw log or statement text, so it has no counterpart. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
…ges its exclusions below the repo root (#4004) PgServerLogPathPinTests builds pg_log_events' query, which now refuses without the store's log-hash key, so its context carries one. AuroraOnlySqlIsGatedTests excluded any path containing a .claude directory, which is every file of a checkout that itself lives under .claude/worktrees, so it scanned nothing and failed as "nothing names the surface"; the exclusions now apply to the path below the repo root. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
… so a checkout under .claude/worktrees scans its tree (#4004) Every exclusion matched the absolute path, and the .claude one matched every file of a checkout that itself lives under .claude/worktrees, so the sweep scanned nothing and failed as "the sweep lost the tree". The repo's own .claude folder is still excluded. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
erikdarlingdata
added a commit
that referenced
this pull request
Sep 23, 2026
…ne's worktree (#4021) An isolated lane (#4020's) was refused a write to the coordinator's scratchpad, so its report came back only as its final message. Isolated worktrees also sit under .claude/worktrees/ inside the repo, which two repo-scanning tests excluded (fixed in #4020). Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
erikdarlingdata
added a commit
that referenced
this pull request
Sep 23, 2026
…draft while its review round runs #4020 and #4022 were merged from the UI mid-review on 2026-09-23. A draft can't be merged there, so the coordinator drafts the PR at review dispatch and readies it once the review is resolved. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
1 task done
erikdarlingdata
added a commit
that referenced
this pull request
Sep 23, 2026
…h database first, and recurring CI failures get a census (#4026) * Lane orders: the report goes in the PR body, a failure is re-run on a fresh database before it is called pre-existing, and a recurring CI failure gets a census first - All three of the last lanes were blocked from writing a report file (one outside its worktree, two by a policy that subagents return text), so the full report now lives in the PR body, or an issue comment for a parked item, and the last message is 400 words or fewer. - A lane filed an issue for a sweep-test failure that was already fixed on dev, and called two tests pre-existing that pass on a fresh database. - The coordinator censuses two days of failed first attempts before fixing a flake, and fixes every flake found in one PR. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv * Lane orders: a security PR or irreversible data rewrite is held as a draft while its review round runs #4020 and #4022 were merged from the UI mid-review on 2026-09-23. A draft can't be merged there, so the coordinator drafts the PR at review dispatch and readies it once the review is resolved. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This was referenced Sep 23, 2026
erikdarlingdata
added a commit
that referenced
this pull request
Sep 23, 2026
… one after verifying it (#4033) On 2026-09-23 three lane PRs opened ready (#4020, #4022, #4030) were merged from the UI before the coordinator's verification finished, and #4030 put a regression on dev (every enabled trace flag hidden; fixed in #4032). A draft can't be merged from the UI, so a lane now opens a draft, lists anything it didn't run as an unchecked box, and the coordinator readies it after the checklist read (and a review round for security PRs). Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
erikdarlingdata
added a commit
that referenced
this pull request
Sep 23, 2026
…g_events rows from before #4020 age out per #4004), update the alert-read exemption, and name the keyed-alert tag so the state-key census does not take it for a state key (#4012) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv
This was referenced Sep 23, 2026
erikdarlingdata
added a commit
that referenced
this pull request
Sep 23, 2026
… walks pages to its hourly budget, and a failing stage no longer blocks the rest (#4012) (#4036) * Deadlock re-mask: a missed alert is keyed, never keeps its raw hash; each tick walks pages to its budget; stages run alerts, reports, alert keys, findings and are done only after a clean walk (#4012) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv * Deadlock re-mask: narrow the README claim to what is rewritten (pg_log_events rows from before #4020 age out per #4004), update the alert-read exemption, and name the keyed-alert tag so the state-key census does not take it for a state key (#4012) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv * Deadlock re-mask round 2: runs without a log-hash key (only the alert keys wait), counts each row's outcome, re-masks finding alerts, finds exemplars anywhere on the chain, retries a given-up stage daily, and binds no raw report hash (#4012, #4035) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv * Deadlock re-mask round 2: keep FlattenInto's doc block on FlattenInto, and give TopVictimFingerprint a block body the member scan reads whole (#4012) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv * Deadlock re-mask round-2 fixes: the day-later retry runs, and no alert is cut from its report (#4012) - The worker gates the re-mask on RemaskProgress.Pending (a stage not done, or one that gave up), not on Done, which a stage that gave up satisfies: gated on Done the daily retry never ran (round-2 finding 1). - The reports wait for the alerts to be done (clean or given up), not merely waiting out a failure, and the alert keys wait for a clean report walk, not a report stage that gave up (round-2 finding 2 a/b). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv * Deadlock re-mask round-2 fixes: rebuilt incidents are marked, and a gone report's key is keyed in the alert stage's own write (#4012) - An incident rebuilt from its raw report carries a RemaskRebuilt4012 property, its own member rather than part of the key, so no walk keys it while its report is still raw; the key, dedupe and every reader are unchanged (round-2 finding 2c). - With the key, the alert stage keys an incident whose report is gone in the same write that normalizes its row, so its raw hash is never bound back to the store; only in a row no pass has rewritten, so an unmarked incident #4022's dev pass rebuilt waits for the alert-key stage (round-2 finding 3). - A live test records every value the re-mask's UPDATEs bind, through triggers, and finds no raw hash or literal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv * Deadlock re-mask round-2 fix: a finding alert is rewritten only from the run it was sent for (#4012) The finding alert's exemplar fields other than the SQL are counts and fixed text, so another stored run of the same story, newer but before the alert was recorded, passed as its finding and its exemplars were written over the alert's. The alert's Diagnosis must now read the finding's severity, confidence and window as the formatter rendered them; otherwise the SQL is withheld (round-2 finding 5). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv * Deadlock re-mask round-2 fixes: walks page by alert_time and finding_id, and the finding-alert page skips rows older than #3721 (#4012) - The alert, alert-key and finding-alert walks page config_alert_log by alert_time WITH TIES, and the finding walk pages analysis_findings by finding_id WITH TIES; ctid and xmin are kept only for the write. A row copy that a dismissal, VACUUM FULL or CLUSTER put behind a ctid cursor was never read, and the stage was marked done over it (round-2 finding 4). Each row's callback moves the cursor only once its tie group is finished. - The finding-alert page reads no row recorded before 2026-09-18 (a day before #3721's first commit, for a store that records local time), and that test guards the text tests, so an older row's context is never detoasted (round-2 finding 6). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv * Deadlock re-mask round-2: the moved-row test runs without a key, so only the alert stage can reach the row (#4012) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
erikdarlingdata
added a commit
that referenced
this pull request
Sep 23, 2026
…ntries in their sections (#4080) Adds 42 entries and 42 link refs (#3992, #3995, #3996, #3998, #4001, #4002, #4003, #4007, #4010, #4011, #4013, #4015, #4020, #4022, #4025, #4029, #4030, #4031, #4036, #4038, #4039, #4040, #4044, #4047, #4048, #4049, #4050, #4051, #4055, #4061, #4063, #4064, #4065, #4066, #4067, #4068, #4069, #4070, #4071, #4073, #4074, #4078). Each PR's entry was buffered, and this lands every entry whose PR was merged on origin/dev when it ran. #3989 left 26 entries under bare 'Changed' and 'Fixed' lines above '### Added'. They move into '### Changed' and '### Fixed', below the new entries, and one blank line stays under [Unreleased]. Claude-Session: https://claude.ai/code/session_01Ua31ugERL5DmhFVRtf6keQ Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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 #4004.
Why
collect.pg_log_eventsstored two unkeyed SHA-256 values over text a store reader can mostly rebuild:raw_line_hash, over the raw log entry with its literals;statement_fingerprint, overRedactStatement's output, which leaves$$bodies andDOblocks as written.Anyone who can read the row can enumerate candidate literals offline and test each one against the hash. #3996's review recovered a four-digit PIN from a failed
UPDATEin 6 ms. Erik's ruling: use HMAC-SHA-256 with a per-store key that the service generates and keeps outside the database. New rows use it, and old rows keep their hash until retention drops them. No tool or viewer returns any hash.What changes
PgLogHashKey(shared collectors) computes both identities as HMAC-SHA-256, with a separate subkey for each, truncated to the same 32 hex characters as before, so columns and reads are unchanged.PgLogTextRedactor.Fingerprint/RawLineHashand the keylessPgLogEventClassifier.Defaultare deleted, so the compiler rejects any path that would hash without the key.The key is loaded once, at worker start (
DarlingLogHashKeyFile). The same instance goes to every run that hashes:pg_log_eventson thepg_read_fileroute and on the RDS route.log-hash-key.dpapibesidepg-credential.dpapiDarlingFileSecurityACL (SYSTEM, Administrators, service account; no interactive read)log-hash-keyin thedarling-credentialsvolumedarling-keys/log-hash-keybesidedarling.json(.dpapion Windows)Missing and unreadable keys.
pg_log_eventsthen refuses on both routes withPgLogHashKey.UnavailableMessage, inBuildQuery, before it pulls 4 MB of log from the target.No surface returns a hash.
statement_fingerprintis removed fromget_pg_log_events, from the reader's page record and SQL (the SQL no longer selects it), and from the web log tab.raw_line_hashwas already removed by Log messages show as PostgreSQL wrote them, the SQL in them stays normalized, and one statement's text never decides how another's is read (#3944) #3996. The lock-wait fact still groups on the fingerprint inside the store and emits only the count.Old rows keep their unkeyed hashes until the 30-day retention drops them. The first cycle after the upgrade stores each entry still in the tail's overlap window once more, and each statement shape's fingerprint changes once. This is the one-time discontinuity PostgreSQL deadlock reports store and show their SQL normalized, and no read returns a hash of the raw report (#4005) #4013 accepted.
Hash census, nothing else to key.
plan_hashcovers the stored, redacted plan.deadlock_hashcovers the stored, normalized graph since PostgreSQL deadlock reports store and show their SQL normalized, and no read returns a hash of the raw report (#4005) #4013.text_hashishashtextover the storedquery_text.PayloadDimensions.Digestcovers the stored payload.None of these has a hidden input.
Lite collects no PostgreSQL logs and hashes no raw log or statement text. Its only text hash is DuckDB
hash(query_text)inside aCOUNT(DISTINCT)over stored text, which is never stored or returned. So Lite gets no counterpart.Docs: the README covers the key in its credential-file table and in a new paragraph, and the compose file and Dockerfile comments name it.
In-lane test fixes:
AuroraOnlySqlIsGatedTests(Lite) andFleetIdentifierScrubTests(Darling) judged their.claudeexclusion on the absolute path, so they scanned zero files when the repo was checked out under.claude/worktrees/. Both now judge the path below the repo root, and the repo's own.claudefolder is still excluded.PgServerLogPathPinTestsnow gives its context a key.Test plan
PgLogHashKeyTests(new, 11 tests):BuildQueryrefuses without a key;ToStringprints key material;DarlingFileSecurity, or Unix mode 0600), and the same key comes back after a restart; on Windows the file is a DPAPI blob;Red-watch: with the raw-line hash reverted to unkeyed SHA-256, 3 of the 11 tests fail (the unkeyed-difference, two-stores and PIN-attack tests). Restored, all 11 pass.
The existing no-hash pin in
PgLogEventsPipelineTestsis extended tostatement_fingerprintin the tool, the reader's row type and the web tab.Targeted classes, including live ones on a PG 18.6 + TimescaleDB 2.30.1 rig:
PgLogEventsPipelineTests,PgLogEventMetricsTests,PgTargetBlockingTests,PostgresEngineGateBehaviorTests,DarlingCollectorRunnerTests,ComposeStoreRolesLiveTests,DarlingStoreLoginsTests,DarlingManagedRolesTests,DarlingFileSecurityTests,ServerPageTabsTests,McpPayloadContractCensusTests,DocCommentHygiene.Full
Darling.TestswithDARLING_TEST_PG: 13,025 tests, 16 skipped (environment-gated), 4 failed. None of the four is caused by this change:FleetIdentifierScrubTests(2): the worktree-path exclusion, fixed in this PR.RefreshCeilingStalenessTests.TheSpacingFinding_IsCalledPerLightRefresh_OnTheSameWatch: it expects CRLF inDarlingWorker.cs, and my working copy was briefly LF. The committed content is unaffected.TrendPayloadBudgetLiveTests: a transientException while reading from streamfrom the rig onget_pg_io_trend.Re-run after the fixes, those classes plus the touched ones pass 215/215.
Full
Lite.Tests, since the change touches the shared collectors: 5,173 tests, 4 failed, all inAuroraOnlySqlIsGatedTests(the worktree-path exclusion, fixed in this PR). Re-run 9/9.Both builds report
0 Warning(s).🤖 Generated with Claude Code
https://claude.ai/code/session_01GdmA4ND1wLSqA91ax1m4xv