Repository navigation
Description
Two deferred asks from the review of PR 1913, and the measured price that kept them out of that PR.
Origin. Threads 4505068035, 4505068046 and 4510300891 on PR 1913, each re-checked at head 44a862d. That PR stands at exactly 500 changed executable lines and 192 selectors against its own merge base 73931fe and the same 500 and 192 against main tip 18861b2 through the clean synthetic merge d906391b849b, advisories 1 on both legs, so it has no room left for source additions.
Ask 1, the handle-relative primitive and the two behaviours it is standing in for (threads 4505068035 and 4505068046; the primitive itself is tracked in #2148). src/utils/safeWriteJson.ts rows 72-76 state that openat and renameat style handle-relative operations are not available from Node's fs API on Windows. Confined writes therefore protect themselves by lstat-and-refuse instead: src/services/file-safety/safeWriteText.ts 326-332 refuses a staging path that is a symlink or not a regular file, 196-208 refuses to publish through a link, and src/utils/safeWriteJson.ts 443-453 throws ConfinedPathEscapeError after staging when the publish target left the confined root. What is still missing is the positive half: recovering a matching backup under the referent lock before rejecting a dangling link, and preserving the Windows security descriptor across the JSON commit rename at src/utils/safeWriteJson.ts:407 the way src/services/file-safety/safeWriteText.ts does at 576-578.
Ask 2, the mode-read retry (thread 4510300891). src/utils/safeWriteJson.ts:587 stats the target before publishing onto it, and 591-592 refuse the write for any failure other than ENOENT or ENOTDIR with no retry, so one transient EIO or ELOOP abandons a final save.
Cost, measured rather than guessed. A probe commit that adds exactly one bounded retry around the stat at 587 (8 lines, probe commit c7de3f00eed8, deliberately kept off the 1913 branch) prices that tree at 506 changed executable lines against 73931fe and 507 against 18861b2 through synthetic merge 1c09a5e96d1b. The 500-line gate is what the mutation-gate runner enforces, so each of these three asks needs its own unit on this chain instead of being trimmed into PR 1913 to fit.
Not in scope here. Nothing in this issue changes PR 1913. Its fail-closed behaviour, the confinement escape check, the identity guard in undoEscapedPublish and the tests that pin them stay as shipped at 44a862d.
Neighbours found by searching open issues before filing: #2392 (Protect Windows DACLs throughout safeWriteText publication), #2402 (safeWriteText reports post-commit failures only through the ...), #2391 (Protect safeWriteText staging entry from replacement through commit). Searches run in this repository before filing, all over open issues, with what they found: no-follow openat 11 hits, openat 13, renameat 14, transient retry publish 39, mode read retry 156.
So this does not name a blank. #2148 (guardedWrite: evaluate descriptor-relative publication, openat style) is already open and already owns the primitive question itself, and #2317 (bind staging and publication to approved directory identities) and #2390 (report orphaned staging links after no-replace publication) sit next to it. #2392, #2402 and #2391 own the DACL restore, the post-commit reporting shape and the staging-entry protection.
What this issue adds on top of those is the two behaviours the review threads asked for and no open issue names: recovering a matching backup under the referent lock before rejecting a dangling link (thread 4505068035), and carrying the Windows security descriptor across the JSON commit rename at src/utils/safeWriteJson.ts:407 the way src/services/file-safety/safeWriteText.ts already does at 576-578 through _restoreDaclWindows (thread 4505068046), together with the measured cost that kept all three out of PR 1913. If a maintainer would rather fold the retry into #2148, this issue's numbers travel with it.
Line check against the tree this describes, read from the working copy rather than from memory: row 74 of src/utils/safeWriteJson.ts reads "* (openat/renameat over a directory fd); this runtime exposes neither, so the", row 587 reads "const stats = await fs.stat(targetPath)", row 591 reads "if (!code || (code !== "ENOENT" && code !== "ENOTDIR")) {", row 592 begins "throw new Error(`Could not read the mode of ${targetPath} before publishin", row 407 reads "await fs.rename(actualTempNewFilePath, resolvedTargetPath)", and row 578 of src/services/file-safety/safeWriteText.ts reads "const restored = await _restoreDaclWindows(restoredDir, daclDumpPath, options?.execFileRunner)", with the DaclRestoreError thrown at its row 587.
Activity