Repository navigation
feat(file-safety): guarded text publish module + specs (A4, #1375) #1395
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
7680812
ab32342
615d39c
301e5b2
7462774
9085459
01a607d
c97302d
8ab5e69
46146d3
27ea6df
9b9fa4c
db365be
203b283
19a5aa8
a12a5fc
7c39035
5d2327a
0e9d3cf
e39f007
4205fe3
080e223
1ee3607
6a90327
62bf8db
b19940f
f12b207
f0a3a75
b1efbe1
0903d24
faaf143
e3203e8
b3cf2d9
02fe9ac
9a449c1
9010617
784a586
b27221c
782dc5a
de2f868
7d62bfa
faf666d
0390c1f
9f3eab7
a53cbc1
63a20bf
5f75502
f02a358
5bdedd2
78ff00a
d7f4ca1
9ff5d5a
f881e08
a56e566
99b8b90
ea34e2d
80754a7
22c651f
daee64d
99664d0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import * as vscode from "vscode" | ||
| import * as path from "path" | ||
| import * as fs from "fs/promises" | ||
| import { constants as fsConstants } from "fs" | ||
| import * as diff from "diff" | ||
| import stripBom from "strip-bom" | ||
| import delay from "delay" | ||
|
|
@@ -18,6 +19,11 @@ import { arePathsEqual, getReadablePath } from "../../utils/path" | |
| import { formatResponse } from "../../core/prompts/responses" | ||
| import { diagnosticsToProblemsString, getNewDiagnostics } from "../diagnostics" | ||
| import { Task } from "../../core/task/Task" | ||
| import { | ||
| assertPublishWithinAuthorizedRoot, | ||
| isPublishCommittedError, | ||
| safeWriteText, | ||
| } from "../../services/file-safety/safeWriteText" | ||
|
|
||
| import { DecorationController } from "./DecorationController" | ||
|
|
||
|
|
@@ -1155,9 +1161,44 @@ export class DiffViewProvider { | |
| // Get diagnostics before editing the file | ||
| this.preDiagnostics = vscode.languages.getDiagnostics() | ||
|
|
||
| // Reject an escape through an intermediate symlink before anything is | ||
| // created on disk: createDirectoriesForFile follows symlinks, so an | ||
| // approved inside path whose intermediate link points outside the | ||
| // workspace would otherwise create the outside directory before | ||
| // safeWriteText's authorizedRoot check rejected the publish. The same | ||
| // canonical check runs inside safeWriteText; running it here first keeps | ||
| // the allowlist decision free of filesystem side effects. | ||
| await assertPublishWithinAuthorizedRoot(this.cwd, absolutePath, absolutePath) | ||
|
|
||
| // Write the content directly to the file | ||
| await createDirectoriesForFile(absolutePath) | ||
| await fs.writeFile(absolutePath, content, "utf-8") | ||
| // safeWriteText publishes with rename, which only needs write permission on the | ||
| // DIRECTORY. The previous fs.writeFile required it on the file itself, so a target | ||
| // the user marked read-only used to fail with EACCES; keep that contract instead of | ||
| // silently renaming over it. A not-yet-existing target stays creatable. | ||
| try { | ||
| await fs.access(absolutePath, fsConstants.W_OK) | ||
| } catch (error: unknown) { | ||
| const code = | ||
| typeof error === "object" && error !== null && "code" in error | ||
| ? (error as { code?: string }).code | ||
| : undefined | ||
| if (code !== "ENOENT") throw error | ||
| } | ||
| try { | ||
| await safeWriteText(absolutePath, content, { authorizedRoot: this.cwd }) | ||
| } catch (error) { | ||
| // Both errors are raised after the atomic rename committed the content: they | ||
| // describe a durability or access-rights condition the tool layer cannot fix by | ||
| // writing again. Treat the save as completed so the tool's bookkeeping matches | ||
| // the disk (and content already on disk is not retried), and surface the | ||
| // condition as a warning instead. | ||
| if (isPublishCommittedError(error)) { | ||
| console.warn(`saveDirectly: content committed with a warning: ${error.message}`) | ||
|
Comment on lines
+1196
to
+1197
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| } else { | ||
| throw error | ||
| } | ||
| } | ||
|
|
||
| // Open the document to ensure diagnostics are loaded | ||
| // When openFile is false (PREVENT_FOCUS_DISRUPTION enabled), we only open in memory | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,8 +18,23 @@ vi.mock("fs/promises", () => ({ | |
| readFile: vi.fn().mockResolvedValue("file content"), | ||
| writeFile: vi.fn().mockResolvedValue(undefined), | ||
| access: vi.fn().mockResolvedValue(undefined), | ||
| mkdir: vi.fn().mockResolvedValue(undefined), | ||
| rename: vi.fn().mockResolvedValue(undefined), | ||
| unlink: vi.fn().mockResolvedValue(undefined), | ||
| })) | ||
|
|
||
| // Mock safeWriteText (used by saveDirectly). The real module's error classes stay | ||
| // available through importOriginal, so a test can hand the caller an actual | ||
| // DaclRestoreError instead of a look-alike. | ||
| vi.mock("../../../services/file-safety/safeWriteText", async (importOriginal) => { | ||
| const actual = await importOriginal<typeof import("../../../services/file-safety/safeWriteText")>() | ||
| return { | ||
| ...actual, | ||
| safeWriteText: vi.fn().mockResolvedValue(undefined), | ||
| assertPublishWithinAuthorizedRoot: vi.fn().mockResolvedValue(undefined), | ||
| } | ||
| }) | ||
|
|
||
| // Mock utils | ||
| vi.mock("../../../utils/fs", () => ({ | ||
| createDirectoriesForFile: vi.fn().mockResolvedValue([]), | ||
|
|
@@ -29,6 +44,8 @@ vi.mock("../../../utils/fs", () => ({ | |
| vi.mock("path", () => ({ | ||
| resolve: vi.fn((cwd, relPath) => `${cwd}/${relPath}`), | ||
| basename: vi.fn((path) => path.split("/").pop()), | ||
| dirname: vi.fn((path) => path.split("/").slice(0, -1).join("/") || "/"), | ||
| join: (...args: string[]) => args.join("/"), | ||
| })) | ||
|
|
||
| // Mock vscode | ||
|
|
@@ -770,6 +787,153 @@ describe("DiffViewProvider", () => { | |
| }) | ||
|
|
||
| describe("saveDirectly method", () => { | ||
| it("rejects an escaped target before creating directories for it", async () => { | ||
| const { assertPublishWithinAuthorizedRoot, safeWriteText } = | ||
| await import("../../../services/file-safety/safeWriteText") | ||
| const { createDirectoriesForFile } = await import("../../../utils/fs") | ||
| vi.mocked(createDirectoriesForFile).mockClear() | ||
| vi.mocked(safeWriteText).mockClear() | ||
| vi.mocked(assertPublishWithinAuthorizedRoot).mockClear() | ||
| const escape = new Error("safeWriteText: refusing to publish outside the authorized root") | ||
| vi.mocked(assertPublishWithinAuthorizedRoot).mockRejectedValueOnce(escape) | ||
|
|
||
| await expect(diffViewProvider.saveDirectly("linkdir/new/secret.txt", "x", false, false, 0)).rejects.toThrow( | ||
| "refusing to publish", | ||
| ) | ||
|
|
||
| // The allow-list decision must precede the filesystem mutation: | ||
| // createDirectoriesForFile follows symlinks, so running it first would | ||
| // already have created the outside directory the check exists to protect. | ||
| expect(createDirectoriesForFile).not.toHaveBeenCalled() | ||
| expect(safeWriteText).not.toHaveBeenCalled() | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| // The stub rejects whatever it is handed, so without pinning the arguments this test | ||
| // would still pass if the guard were given the workspace root instead of the target. | ||
| expect(assertPublishWithinAuthorizedRoot).toHaveBeenCalledWith( | ||
| mockCwd, | ||
| `${mockCwd}/linkdir/new/secret.txt`, | ||
| `${mockCwd}/linkdir/new/secret.txt`, | ||
| ) | ||
| vi.mocked(assertPublishWithinAuthorizedRoot).mockResolvedValue(undefined) | ||
| }) | ||
| it("rejects a save over a read-only target instead of renaming onto it", async () => { | ||
| const fs = await import("fs/promises") | ||
| const { safeWriteText } = await import("../../../services/file-safety/safeWriteText") | ||
| vi.mocked(safeWriteText).mockClear() | ||
| const eacces = Object.assign(new Error("EACCES: permission denied"), { code: "EACCES" }) | ||
| vi.mocked(fs.access).mockRejectedValueOnce(eacces) | ||
|
|
||
| await expect(diffViewProvider.saveDirectly("test.ts", "new content", true, true, 200)).rejects.toThrow( | ||
| "EACCES", | ||
| ) | ||
|
|
||
| // The old fs.writeFile path failed the same way; safeWriteText must not run. | ||
| expect(safeWriteText).not.toHaveBeenCalled() | ||
| vi.mocked(fs.access).mockResolvedValue(undefined) | ||
| }) | ||
|
|
||
| it("still writes when the target does not exist yet", async () => { | ||
| const fs = await import("fs/promises") | ||
| const { safeWriteText } = await import("../../../services/file-safety/safeWriteText") | ||
| vi.mocked(safeWriteText).mockClear() | ||
| vi.mocked(fs.access).mockRejectedValueOnce(Object.assign(new Error("ENOENT"), { code: "ENOENT" })) | ||
|
|
||
| const result = await diffViewProvider.saveDirectly("test.ts", "new content", true, true, 200) | ||
|
|
||
| expect(safeWriteText).toHaveBeenCalledWith(`${mockCwd}/test.ts`, "new content", { authorizedRoot: mockCwd }) | ||
| // Call counts alone would still pass if the publish ran but the caller reported | ||
| // nothing about the content it wrote. | ||
| expect(result.finalContent).toBe("new content") | ||
| expect(result.userEdits).toBeUndefined() | ||
| vi.mocked(fs.access).mockResolvedValue(undefined) | ||
| }) | ||
|
|
||
| it("checks write permission on the absolute target before publishing", async () => { | ||
| const fs = await import("fs/promises") | ||
| const { safeWriteText } = await import("../../../services/file-safety/safeWriteText") | ||
| vi.mocked(safeWriteText).mockClear() | ||
| vi.mocked(fs.access).mockClear() | ||
|
|
||
| await diffViewProvider.saveDirectly("test.ts", "new content", true, true, 200) | ||
|
|
||
| // safeWriteText publishes with rename, which only needs write permission on the | ||
| // DIRECTORY. The contract this unit preserves requires it on the FILE, so the guard | ||
| // must be handed the absolute target - not the relative path the caller passed. | ||
| const calls = vi.mocked(fs.access).mock.calls | ||
| const accessIndex = calls.findIndex((call) => String(call[0]).endsWith("test.ts")) | ||
| expect(accessIndex).toBeGreaterThanOrEqual(0) | ||
| expect(calls[accessIndex][0]).toBe(`${mockCwd}/test.ts`) | ||
|
|
||
| // An existence-only check would not catch a read-only target: the guard has to be handed | ||
| // the write bit, otherwise the read-only contract this unit preserves is silently gone. | ||
| const { constants } = await import("fs") | ||
| expect(calls[accessIndex][1]).toBe(constants.W_OK) | ||
|
|
||
| // The check is only meaningful before the publish; after it, an EACCES target would | ||
| // already have been renamed over. | ||
| expect(vi.mocked(fs.access).mock.invocationCallOrder[accessIndex]).toBeLessThan( | ||
| vi.mocked(safeWriteText).mock.invocationCallOrder[0], | ||
| ) | ||
| }) | ||
|
|
||
| it("reports a post-commit DACL restore failure as a warning instead of a failed save", async () => { | ||
| const { DaclRestoreError, safeWriteText } = await import("../../../services/file-safety/safeWriteText") | ||
| vi.mocked(safeWriteText).mockClear() | ||
| const warn = vi.spyOn(console, "warn").mockImplementation(() => {}) | ||
| // The primitive reports a lost DACL after the commit rename as DaclRestoreError: | ||
| // the content is already on disk, so re-writing cannot change the access rights. | ||
| // The tool layer must still record the write as done, or it keeps stale state and | ||
| // retries content that is already published. | ||
| vi.mocked(safeWriteText).mockRejectedValueOnce(new DaclRestoreError(`${mockCwd}/test.ts`, null)) | ||
|
|
||
| const result = await diffViewProvider.saveDirectly("test.ts", "new content", true, true, 200) | ||
|
|
||
| expect(result.finalContent).toBe("new content") | ||
| expect(warn).toHaveBeenCalledWith(expect.stringContaining("content committed with a warning")) | ||
| const publishCalls = vi.mocked(safeWriteText).mock.calls.filter(function (call) { | ||
| return call[0] === `${mockCwd}/test.ts` | ||
| }) | ||
| expect(publishCalls).toHaveLength(1) | ||
| warn.mockRestore() | ||
| }) | ||
|
|
||
| it("reports an unconfirmed-durability publish as a warning instead of a failed save", async () => { | ||
| const { PublishNotDurableError, safeWriteText } = | ||
| await import("../../../services/file-safety/safeWriteText") | ||
| vi.mocked(safeWriteText).mockClear() | ||
| const warn = vi.spyOn(console, "warn").mockImplementation(() => {}) | ||
| // The rename landed before the durability failure was observed, so the content is | ||
| // committed: a re-write would duplicate the work and skip the save bookkeeping. | ||
| vi.mocked(safeWriteText).mockRejectedValueOnce( | ||
| new PublishNotDurableError(`${mockCwd}/test.ts`, "directory fsync failed"), | ||
| ) | ||
|
|
||
| const result = await diffViewProvider.saveDirectly("test.ts", "new content", true, true, 200) | ||
|
|
||
| expect(result.finalContent).toBe("new content") | ||
| expect(warn).toHaveBeenCalledWith(expect.stringContaining("content committed with a warning")) | ||
| warn.mockRestore() | ||
| }) | ||
|
|
||
|
Comment on lines
+878
to
+916
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a test for a non-committed Both new tests cover only committed-write errors, Add a case that rejects
|
||
| it("rethrows an ordinary publish failure instead of reporting a committed write", async () => { | ||
| const { safeWriteText } = await import("../../../services/file-safety/safeWriteText") | ||
| vi.mocked(safeWriteText).mockClear() | ||
| vi.mocked(vscode.window.showTextDocument).mockClear() | ||
| const warn = vi.spyOn(console, "warn").mockImplementation(() => {}) | ||
| // Only the committed-publish classes are tolerated here. An ordinary failure means the | ||
| // bytes never landed, so the tool layer must reject rather than record the save, and | ||
| // must not open a document for content that was never published. | ||
| const ordinary = new Error("EACCES: ordinary publish failure") | ||
| vi.mocked(safeWriteText).mockRejectedValueOnce(ordinary) | ||
|
|
||
| await expect(diffViewProvider.saveDirectly("test.ts", "new content", true, true, 200)).rejects.toBe( | ||
| ordinary, | ||
| ) | ||
|
|
||
| expect(vscode.window.showTextDocument).not.toHaveBeenCalled() | ||
| expect(warn).not.toHaveBeenCalledWith(expect.stringContaining("content committed with a warning")) | ||
| warn.mockRestore() | ||
| }) | ||
|
|
||
| beforeEach(() => { | ||
| // Mock vscode functions | ||
| vi.mocked(vscode.window.showTextDocument).mockResolvedValue(makeTextEditor()) | ||
|
|
@@ -782,9 +946,9 @@ describe("DiffViewProvider", () => { | |
|
|
||
| const result = await diffViewProvider.saveDirectly("test.ts", "new content", true, true, 2000) | ||
|
|
||
| // Verify file was written | ||
| const fs = await import("fs/promises") | ||
| expect(fs.writeFile).toHaveBeenCalledWith(`${mockCwd}/test.ts`, "new content", "utf-8") | ||
| // Verify file was written via safeWriteText | ||
| const { safeWriteText } = await import("../../../services/file-safety/safeWriteText") | ||
| expect(safeWriteText).toHaveBeenCalledWith(`${mockCwd}/test.ts`, "new content", { authorizedRoot: mockCwd }) | ||
|
|
||
| // Verify file was opened without focus | ||
| expect(vscode.window.showTextDocument).toHaveBeenCalledWith( | ||
|
|
@@ -805,9 +969,9 @@ describe("DiffViewProvider", () => { | |
| it("should not open file when openWithoutFocus is false", async () => { | ||
| await diffViewProvider.saveDirectly("test.ts", "new content", false, true, 1000) | ||
|
|
||
| // Verify file was written | ||
| const fs = await import("fs/promises") | ||
| expect(fs.writeFile).toHaveBeenCalledWith(`${mockCwd}/test.ts`, "new content", "utf-8") | ||
| // Verify file was written via safeWriteText | ||
| const { safeWriteText } = await import("../../../services/file-safety/safeWriteText") | ||
| expect(safeWriteText).toHaveBeenCalledWith(`${mockCwd}/test.ts`, "new content", { authorizedRoot: mockCwd }) | ||
|
|
||
| // Verify file was NOT opened | ||
| expect(vscode.window.showTextDocument).not.toHaveBeenCalled() | ||
|
|
@@ -820,9 +984,9 @@ describe("DiffViewProvider", () => { | |
|
|
||
| await diffViewProvider.saveDirectly("test.ts", "new content", true, false, 1000) | ||
|
|
||
| // Verify file was written | ||
| const fs = await import("fs/promises") | ||
| expect(fs.writeFile).toHaveBeenCalledWith(`${mockCwd}/test.ts`, "new content", "utf-8") | ||
| // Verify file was written via safeWriteText | ||
| const { safeWriteText } = await import("../../../services/file-safety/safeWriteText") | ||
| expect(safeWriteText).toHaveBeenCalledWith(`${mockCwd}/test.ts`, "new content", { authorizedRoot: mockCwd }) | ||
|
|
||
| // Verify delay was NOT called | ||
| expect(mockDelay).not.toHaveBeenCalled() | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Script executed:
Repository: Zoo-Code-Org/Zoo-Code
Length of output: 21041
Script executed:
Repository: Zoo-Code-Org/Zoo-Code
Length of output: 48922
Web query:
Node.js v22 fs.access Windows W_OK ACL does not check access control list documentationResult:
Web query:
Microsoft Windows rename replace file permissions DELETE FILE_DELETE_CHILD directory deny FILE_WRITE_DATA fileResult:
Web query:
Node.js v22.23.4 bundled libuv Windows fs rename MoveFileExW MOVEFILE_REPLACE_EXISTING sourceResult:
Web query:
Microsoft icacls save DACL READ_CONTROL permission security descriptor required fileResult:
Script executed:
Repository: Zoo-Code-Org/Zoo-Code
Length of output: 20220
Web query:
site:raw.githubusercontent.com/nodejs/node/v22.23.4/deps/uv/src/win/fs.c "fs__rename" "MoveFileExW"Result:
Script executed:
Repository: Zoo-Code-Org/Zoo-Code
Length of output: 6512
Script executed:
Repository: Zoo-Code-Org/Zoo-Code
Length of output: 9667
Check the target’s Windows write authorization before publishing.
When the target’s DACL denies file writes but permits replacement,
fs.access(..., W_OK)can succeed and the rename can overwrite the file. The previousfs.writeFilerejected that write. Check write authorization without truncating the target before publishing, and add a Windows DACL regression test.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not changed in this push - this row needs a decision I should not make alone, so I am asking rather than editing production code on a guess.
What I verified: the probe at
DiffViewProvider.ts:1179-1187isfs.access(absolutePath, fsConstants.W_OK), carried over from the oldfs.writeFilepath, andDiffViewProvider.spec.ts:842-868already pins that it is handed the absolute target with the write bit and that it runs beforesafeWriteText. Your point is a different one and it stands: for a rename-based publish, a DACL that denies file writes but permits directory replacement can letaccess(..., W_OK)pass and the rename then overwrite, which the truncate-and-write path rejected.Why it is not in this commit: the fix is production code in
src/integrations/editor/DiffViewProvider.ts(blobf5ef407514f9at 39d4b95, this PR), and the candidate shapes do not promise the same thing - queryicaclsfor the write ACE, or stage-and-verify, or drop the pre-publish shortcut and let the publish report the failure. Budget is not the reason: Measured here: the gate's ownselectFromGitagainst base 73931fe returns 359 counted executable lines of the 500 cap and the same 128 selectors (selectorsHash bd67c22397aded1e) as the previous head, so the mutation-diff inputs are identical. There are 141 counted lines of headroom.Named deferral: @edelauna (whose CHANGES_REQUESTED review 5646298552 has stood on this file family since 2026-10-26) to say which check
saveDirectlyshould own. With that answer the change is that blob plus arunIf(process.platform === "win32")regression case; I have not quoted a counted-line cost for it because none of that code exists yet.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Answer by decision, read off 39d4b95 at 2026-11-11T00:28:37.574Z. The ask was not to treat an access check with the write bit as Windows DACL write authorization, and to add a Windows regression case where the DACL denies file writes but permits replacement.
What exists at this head: the probe at 1179 to 1187 falls through only on a missing-file code, and the comment at 1175 to 1178 states the deliberate contract, that publishing by rename needs write permission on the directory while the write path it replaces needed it on the file, so the probe exists to preserve the read-only contract rather than to rename over a read-only target silently. The spec covers the probe with a case at 818 that rejects a save over a read-only target under a mocked permission error and a case at 850 that asserts at 861 to 869 that the write bit is the bit handed to the check. Only two lines in the whole 1,923-line spec mention a DACL and both belong to a different case about post-commit warnings, so the real-filesystem Windows case this finding asks for does not exist.
The decision, which this lane declined to make alone and which is now made: the real-filesystem Windows case is to be added in the unit that owns the write path, which under the merge order declared below is 2385, gated on the platform with an explicit guard rather than a blanket skip, because a DACL outcome is the class of fact that this repository's own test-placement guidance puts at the layer that touches the real filesystem and that a mock cannot prove. No ACL outcome is asserted here from the machine that wrote this reply, and none should be read into it.
This thread stays open until that case lands.
Check facts at this read time. This head carries three mutation-diff rows: 129284510370 completed and success from 2026-11-09T16:16:51Z to 16:20:21Z, 129318606292 completed and success from 17:15:31Z to 17:19:01Z, and 129318302860 completed and cancelled from 17:15:00Z to 17:15:29Z, whose log contains no gate sentence and whose only error annotation is that the operation was canceled during checkout. So across the 74 rows on this head the only row that is not completed and success belongs to a superseded run, and the mutation-diff state at this head is success: the success log measures the extension package at 359 lines against a cap of 500 and raises one advisory, 428 mutants against a limit of 400, which the script treats as advisory rather than blocking. The copy of the script inside this pull's tree is 30,108 bytes at sha256 16 c332971815aecfaf, and CI runs the copy inside each pull's tree.
Line numbers quoted here are as of the head named above. This file has grown since the thread was filed, so a pinned line number can land a reader somewhere other than the code under discussion. At this head line 1180 is inside the probe at 1179 to 1187, so the pinned line lands on the code the finding is about.
This campaign states no position on which instrument governs where REST and GraphQL report different review fields.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Analysis chain
Script executed:
Length of output: 12547
@easonLiangWorldedtech, the PR head is still39d4b95128. I confirmed thatsaveDirectlyusesfs.access(..., W_OK)and that the cited tests mock the access check and publisher. I did not run a Windows DACL test.The test is planned for
#2385, which is open. The test should establish that file writes are denied while replacement is permitted, then verify that the save rejects and leaves the content unchanged. The production authorization fix also remains unverified. This thread stays open.The API now lists 80 check runs for the head: 78 successful, one skipped, and one cancelled. That includes the three mutation-diff runs you identified. I did not inspect their logs or rerun CI. Separately, the last saved CodeRabbit gate was blocking on security and persistence findings; I did not refresh it.
Would you like a follow-up issue for the production authorization fix?
Learnings used
You are interacting with an AI system.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Restated at the current head 1395 daee64d rather than corrected: the answer above was written against 39d4b95 and every file it quotes is the identical blob at both heads, DiffViewProvider.ts f5ef407514f9, its spec c35387b5f997, fileLock.ts d8bdbf910b30, its spec c8f08732fd9f, safeWriteText.ts 571d345098c3, cache-manager.ts b1062de55688 and WorkspacePathResolver.ts 10e425c8ec52, measured with git rev-parse on the two revisions, and the 107 files where the two heads differ are main-carried content with none of those seven among them. So the coordinates quoted above still land on the same code and nothing in this thread needs a coordinate correction.
The remedy is an edit this campaign has not made, so the thread stays open under the standing rule that a thread whose remedy is an edit this campaign still owes stays open until that edit lands. Under the declared merge order that edit is owed by 2385, the unit that owns the write path: a Windows regression case on a real filesystem where the DACL denies file writes but permits directory replacement, showing that the access check with the write bit passes and that the publish decision which follows it is the behaviour under discussion, gated on the platform with an explicit guard rather than a blanket skip. Which check saveDirectly should own is recorded as a maintainer decision for edelauna on this pull rather than a call this campaign makes alone.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@easonLiangWorldedtech Thanks for the clarification. The Windows regression test is planned for
#2385. The choice of authorization check forsaveDirectlyremains with edelauna.The thread should remain open. The test can establish the DACL conditions and observe the publish decision, but it does not by itself fix the authorization concern. After the maintainer decision, verify that a denied write cannot replace the target and that its content remains unchanged.
I have not independently checked the blob comparison or run a Windows DACL test.
Learnings used
You are interacting with an AI system.