fix(sandbox): scope exclusive Add File creation to UnixLocal - #4893
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f36d84ba6b
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
seratch
left a comment
There was a problem hiding this comment.
The data-loss case is worth fixing, and the expected-error tracing change is correct. The remaining problem is that the absence check and write are separate operations. A concurrent sandbox command can create the file after the read, and the unconditional write then destroys that content. Reading also does not establish that a dangling symlink entry is absent. Please enforce create-if-absent at the backend write boundary, preserving the bound user and path policy, and cover an intervening creator whose contents must survive. Existing files should continue to use update_file.
|
The two Root cause: the span assertion test I added needs The unix-only imports stay inside the test body on purpose. At module scope the Nothing in One thing I cannot confirm from my side: the workflow run for d68c465 is sitting at |
d68c465 to
6ecc682
Compare
|
Done in 6ecc682. You were right that the probe was not good enough, and the dangling symlink half turned out to be sharper than I gave it credit for. Where the enforcement moved. New Why a method rather than a flag on The symlink case was worse than a missing check. Both paths now validate the parent through the normal policy, so grants, symlinked parents and the bound user are unchanged, and leave the final component unresolved. Coverage. In I checked the shell recipe directly rather than assuming it: Existing files still have to go through |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6ecc68255e
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
All three are fixed in 72ede53, and two of them were real. The wrapper bypass was the important one. The split write is gone. Instead of creating an empty file and then writing the payload, the payload goes to a staging name first and The dash point was correct, and I had actually mis-tested it. I validated the recipe with So a collision on dash surfaced as The umask point is right too. Full stack is clean locally. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 72ede535aa
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
Fixed in 3356606. The leaf-resolution one was the real defect and it was live through the actual tool path, not just theoretical. Preserving the leaf name. The create path now passes the unresolved path. My earlier symlink test called The directory case. Bare Local staging. The Unix-local override claimed the name with Staging cleanup and parent ownership. The staging write moved inside the cleanup scope so a failed or cancelled upload cannot leak The Full stack clean locally. I am conscious this diff has grown well past where it started. If you would rather have the primitive landed on its own, or scoped to UnixLocal only with the remote backends following separately, say the word and I will split it. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3356606ee8
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Security Review
Here are some automated security review suggestions for this pull request.
Reviewed commit: 3356606ee8
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
|
Two of these were live and are fixed in 4ebc93c. Three are reposts of findings already addressed in 3356606. One is a blocker I do not think I should decide alone. Fixed: the login shell. I had used Fixed: collision classification. A parent that is a regular file makes Already addressed in the previous commit: the The blocker: hard links. The object-storage mount point is correct and it breaks the shared implementation rather than needing another condition. A writable rclone, Mountpoint or Blobfuse destination does not provide POSIX hard links, so I do not think I can fix that generically. So the honest position is that the shared shell implementation should go, and enforcement should be backend-native. UnixLocal has one that works. The other seven backends need their own, and I cannot write or meaningfully test those. How would you like to proceed?
I am happy either way, and I would rather close it than keep adding conditions to a shape that cannot hold. Four review rounds is past the point where I should be choosing the scope myself. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4ebc93ca86
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
Rumbo follow-up: PR #4930 hardens this implementation for two current P2 findings — fixed-length staging basenames and early observable-target collision classification, while retaining the atomic claim for races. #4930 is intentionally based on this PR rather than presenting the work as unrelated. |
4ebc93c to
9820c3f
Compare
|
Thanks for flagging #4930, and for basing it on this branch openly instead of presenting the work as unrelated. On the two findings: Fixed-length staging basename is real, and it is my regression. The staging name is Early collision classification I read as an improvement rather than a correctness bug. Today the payload is staged before the collision is detected, so Add File onto an existing name uploads bytes that are then discarded, which is wasteful on remote backends. That seems right to do, as long as the preflight stays additive and the atomic claim is still what decides races, which is what your description says. One practical note: #4930 shows as closed, opened 11:49:38Z and closed 11:54:17Z, so as things stand neither fix has a home. I would rather not absorb your work. If you reopen it or push a replacement, I will leave both to you and keep this branch as the base. If you would rather not carry it, say so and I will fold the staging-name fix in here with credit to you. For whoever picks this up: there is an open question further up this thread about whether the shared shell implementation should exist at all, because hard links are not available on writable object-storage mounts. Both of these findings apply to that shared path, so the answer to that one probably decides where they land. The branch is rebased onto current |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9820c3fa1e
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
@fscfede-beep I said a few hours ago that I would leave both fixes to you, and I have gone back on half of that. Explaining rather than doing it quietly. I fixed the staging basename in 8049c24. The reasoning: #4930 is closed, so there is nothing for the fix to stack on, and this is a regression I introduced in this branch rather than a general improvement, so leaving it sitting in an open PR while waiting was the wrong call. The staging basename is now constant at 52 characters instead of being derived from the destination. There is a regression test using a 254 character name that first confirms the platform accepts that name, so it fails for the right reason, and it fails without the fix with the same Finding credit is yours and it is in the commit message. Your second point, preflighting an observable target before staging payload bytes, I have deliberately not touched. That one is an improvement rather than a fix to something I broke, and it is yours to land if you reopen. If you would rather I carried it too, say so and I will. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8049c24122
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
Status after working every open review thread. Fixed and resolved
Answered and left open on purpose Two threads describe the shared hard-link implementation failing on supported configurations rather than missing a condition: @seratch the behaviour you asked for is implemented and covered: the name is claimed at the backend write boundary, an intervening creator keeps its content, and a dangling symlink at the target name is rejected rather than followed. What I cannot settle myself is scope. Either this lands scoped to the UnixLocal primitive, which is the one I can actually verify, with each remaining backend adopting its own later, or the shared shell path comes out and the design goes to you. I would rather stop hardening a path that may be removed. Happy with either, including closing this in favour of a maintainer-owned version. Branch is rebased on current |
|
#4931 landed and I think it answers the scope question I asked above, so flagging it rather than quietly rebasing. That change gives UnixLocal a That is the primitive this PR was missing, and it is better than what I built. Exclusive create becomes an Doing it that way removes the machinery the open threads are about, rather than hardening it:
My branch still merges cleanly onto current So my proposal, if you agree: I rewrite the UnixLocal side on Nothing pushed for this; it is a design note. I have not rebased because I would rather agree the shape first than resolve a rework of the same file twice. |
bd47f64 to
93cb1b3
Compare
|
Rebuilt on #4931 as proposed, in 93cb1b3. Net effect is 135 insertions against 326 deletions, so this is mostly removal. What it does now. What it deletes. The shell script, the staging file, the hard link, and the staging cleanup. That removes the causes behind the two threads I left open rather than adding conditions to them: with no hard link there is nothing to fail on writable object-storage mounts or across owners under The shared base now documents what it actually guarantees: it rejects an occupied target and leaves the existing content alone, it is explicitly not atomic, and a backend that can claim a name should override it. If you want a guaranteed claim on every backend, that is per-backend work I cannot verify for the provider adapters, and I would rather you scope it than have me guess. One thing you should look at. Verification, stated precisely. Affected files all pass: Branch is rebased on |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 93cb1b3046
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 304545645f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
@seratch this is ready for another look when you have time. Every review thread is resolved, and the shape changed enough since your review that a re-read is probably cheaper than following the thread history. Where it landed. Exclusive create is now That deleted the shell script, the staging file, the hard link, and the staging cleanup that my earlier attempts needed. The two findings I had left open, cross-owner hard links under Source side is 153 lines across four files: 31 in The behaviour you asked for is covered: the name is claimed at the backend write boundary, an intervening creator keeps its content, and a dangling symlink at the target name is rejected rather than followed. Two things I would rather you decided than me. The shared base default rejects an occupied target but is explicitly not atomic, and its docstring says so. Backends that can claim a name should override it, as UnixLocal now does. If you want a guaranteed claim on every provider adapter, that is per-backend work I cannot verify, and I would rather you scoped it than have me guess. I edited one of your tests. CI still shows nothing because the workflow run sits at |
06cec0a to
010eb64
Compare
The previous read-then-write check left a window: a concurrent sandbox command could create the file after the probe, and the unconditional write then destroyed that content. Reading also did not establish that a dangling symlink entry was absent, because the unix_local path policy resolves symlinks and the write landed on the link target. Add BaseSandboxSession.write_new_file(), which claims the target name before the payload is written and raises FileExistsError when the name is already taken. The shared implementation uses a shell noclobber redirection, so the redirect itself is the O_EXCL attempt; UnixLocal overrides it with os.open(O_CREAT|O_EXCL) for its direct path. Both validate the parent through the normal policy, preserving grants and the bound user, and leave the final component unresolved so a symlink at that name is rejected rather than followed. apply_patch create_file now uses it and drops the probe, so the tracing workaround for the probe read is no longer needed. Existing files still have to go through update_file.
Three problems with the previous commit.
SandboxSession did not forward write_new_file, so a session built by
SandboxClient fell back to the shared implementation and never reached
the UnixLocal os.open override. Forward it.
The shared implementation created an empty file and then wrote the
payload in a separate step, so a concurrent writer could be overwritten
and a failed upload left an empty file holding the name. Write the
payload under a staging name first, then claim the target with ln, which
fails when the name is taken. The content is complete before the name
exists, and a failed create leaves only the staging entry, which is
removed.
The script used ':' for the noclobber redirection. ':' is a POSIX special
builtin, so on dash a redirection failure ended the shell before the exit
mapping ran and a collision surfaced as a generic write error instead of
FileExistsError. ln is a regular command, and a test now runs the script
through sh, dash and bash so this cannot regress silently.
Also create local files with 0o666 so the process umask decides the final
mode, matching Path.open("wb") on the ordinary write path.
WorkspaceEditor normalizes the destination before dispatching, and UnixLocal resolves leaf symlinks, so create_file handed the primitive the link target. Add File on a dangling link.txt created missing.txt and reported success. Pass the unresolved path for create. Stage the payload locally too, then os.link it into place. os.link fails with EEXIST for a file, a directory or a dangling symlink, and a write that fails partway now leaves only the staging entry instead of a file holding the name. Reject a name held by a directory in the shared script. Bare ln treats an existing directory as a target directory and would have linked the staging file inside it while reporting the directory as created. Move the staging write inside the cleanup scope so a failed upload cannot leak the staging entry, and create the parent as the bound user so a fresh nested path is owned the way the previous write path owned it. The new tests drive session.apply_patch() rather than the primitive, which is the path that was actually broken.
Use sh -c instead of sh -lc for the exclusive create. This path runs for a filesystem-only capability set, so it must not source shell startup files that live in the workspace it is editing. A parent that is a regular file makes mkdir raise FileExistsError, and the broad handler reported that as a collision on the requested name, telling the model to use update_file for a target that does not exist. Only the os.link call can report a collision now; parent and staging failures are wrapped as write errors.
The staging name was derived from the destination, so it was always longer than the destination itself. A filename that fits the filesystem's component limit, and that the ordinary write path accepts, could then fail to stage: a 254 character name raised WorkspaceArchiveWriteError where a plain write succeeded before this branch. The staging basename is now constant at 52 characters regardless of the destination. Reported by fscfede-beep in #4930.
The staging write ran before the link script could classify the target, so a target inside an executable but non-writable parent failed on the staging write and the caller saw WorkspaceArchiveWriteError instead of the ApplyPatchDiffError that points it at update_file. It also meant an Add File onto an occupied name uploaded a payload that was then discarded. Probe the target first in both paths, then keep the atomic claim to decide real races. A creator that wins between the probe and the link still loses the name, and its staging entry is still cleaned up. Reported by Codex and by fscfede-beep in #4930.
openai#4931 gave UnixLocal a dir_fd plus O_NOFOLLOW file-ops layer and routed the bound-user path through a Python worker running that same module. That is the primitive this change was approximating, so use it instead. write_new is the existing write with O_TRUNC replaced by O_EXCL, which claims the name in the same syscall that creates the file and fails with EEXIST when the name is taken by a file, a directory, or a dangling symlink. The worker exits with a distinct status for an existing target so the caller can report FileExistsError rather than a generic write error. This deletes the machinery the previous approach needed: the shell script, the staging file, the hard link, and the staging cleanup. With no hard link there is nothing to fail on writable object-storage mounts or across owners under fs.protected_hardlinks, and with no staging entry there is nothing for concurrent work to replace. The shared base now documents what it actually guarantees: it rejects an occupied target but is not atomic, and a backend that can claim a name should override it. test_parent_swap_after_validation_cannot_access_outside[patch] injected at session.normalize_path, which the create path no longer calls, so its swap stopped firing. Re-pointed the patch case at the file-ops authorize boundary the create path does traverse, keeping the coverage rather than letting it pass vacuously. Verified separately that a parent symlinked outside the workspace is still refused, with the outside file untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two problems with the previous commit. Passing the whole path into the file ops unresolved made a supported internal symlink parent fail. The ordinary write path resolves those safe aliases, and test_safe_symlinks_grants_and_listing_paths_remain_supported establishes the support, but a create through "internal -> real" opened "internal" with O_NOFOLLOW | O_DIRECTORY and failed. Resolve the parent the way write does and keep only the leaf name unresolved, so a dangling symlink at the target name is still rejected without its target being created. The shared default probed the target with read(), which eagerly fetches the whole payload on the remote backends that inherit it. An Add File aimed at an existing large file would download it before reporting the collision, and an existing file the bound user cannot read reported a read failure instead of the collision. List the parent instead, which needs no payload and no read permission on the target. Both regressions have a test that fails on the unfixed source, including one asserting the rejected create issues no read at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Listing the parent to decide a collision failed when the parent did not exist. BaseSandboxSession.ls() raises ExecNonZeroError in that case, which the probe did not catch, so a nested create such as newdir/file.txt aborted before write() on every backend inheriting the default, while succeeding on UnixLocal. Those backends' write paths create missing parents. Use a bare existence test instead. It needs only execute permission on the parent, never reads the target, and reports absent when the parent is missing so the create still reaches write(). Only a positive result rejects; any other probe outcome falls through rather than blocking a valid create. The test is also more accurate than the listing: it sees a file inside an executable but unreadable parent, which listing could not, so an existing file there is no longer silently overwritten. Validated the exact shipped script under sh, bash and dash for an existing file, a dangling symlink, an absent name, a missing parent, and a file in a 0311 parent. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… working Two problems with the previous commit. The probe treated any unexpected status as "absent" so a valid create would not be blocked. That was wrong: exit 0 already means absent, including when the parent does not exist, so the permissive fallback only covered the case where the probe itself did not run. On provider sessions whose write() uses a separate upload API, the write then succeeds and overwrites an existing target precisely when the precondition could not be checked. Only 0 may proceed now; 13 stays the collision and every other status is a write error. scripted_sandbox_session() broke. The inherited default reaches for exec, and a script that configures only file steps hides exec, so a previously valid mkdir-and-write script failed with AttributeError. Give the scripted session an implementation that consumes the same mkdir and write pair, and restore the explicit parent creation in the default so the observable call sequence matches what the create path did before this branch. Both have a test that fails on the unfixed source. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The create path hands write_new_file an unresolved path on purpose, so a symlink at the target name is rejected rather than followed. The scripted session forwarded that path straight into its compatibility mkdir and write pair, so a script using the documented match callback saw "." and "notes.txt" where it previously saw the workspace paths, and failed with SandboxCallMatcherError. Normalize before emitting the pair. Verified against the previous commit: with a custom manifest root the calls were "sub" and "sub/notes.txt" and are now "/custom/root/sub" and "/custom/root/sub/notes.txt", matching what this surface recorded before this branch. The earlier test only asserted method names, which is why it could not catch this. It now asserts the recorded paths too and fails on the unfixed source.
57f8ae3 to
c9a7a6a
Compare
|
This PR is stale because it has been open for 10 days with no activity. |
|
@seratch The requested create-if-absent behavior is now enforced at the UnixLocal write boundary, with bound-user and path-policy handling preserved. Controlled concurrent-creator tests verify that the intervening file survives through direct, wrapped and bound-user sessions. The generic probe has been removed; the PR explicitly limits the exclusive-create guarantee to UnixLocal and preserves other providers’ existing behavior. For failures after claiming the destination, the maintainer-selected contract preserves filesystem compatibility and reports a non-retryable partial-write error with explicit inspection/recovery guidance. Tests cover copy and close failures, concurrent replacement preservation and deliberate recovery. Merged current main to resolve the test conflict. All 21 GitHub checks pass on 5479144, including native macOS and Windows tests. The complete local verification stack passed with 11,299 tests, and both independent authority/lifecycle reviews are clean. Existing feedback threads are resolved and the affected threads now include implementation details. Ready for re-review. |
markstuart-oai
left a comment
There was a problem hiding this comment.
Reviewed all 12 changed files at 5479144, including the direct, wrapped and bound-user create paths, prior feedback, and the surrounding path/worker contracts. No blocking correctness or structural findings.
The private backend hook keeps UnixLocal’s exclusive claim and payload on the same descriptor while preserving other providers’ normalized mkdir/write behavior. Parent symlinks remain supported without resolving away the requested leaf. Post-claim copy/close failures retain the destination and report non-retryable inspection/recovery guidance, avoiding unsafe pathname cleanup. The focused regressions cover concurrent creators, dangling links, partial writes, replacement preservation and explicit recovery.
All 21 hosted checks passed on this exact commit, including Windows and native macOS. This was a source review; I did not run repository workloads or independently exercise live sudo/provider environments.
|
Resolved the latest main merge conflict in 7f83445, retaining both the new UnixLocal snapshot imports/tests and this PR’s apply-patch imports/tests. Both independent integration reviews are clean. The full local verification stack passed with 11,305 tests, and all 21 GitHub checks pass on the pushed commit, including Windows and native macOS. The PR is mergeable and ready for the requested maintainer re-review. |
jbeckwith-oai
left a comment
There was a problem hiding this comment.
Maintainer decision: need demonstrated; merge-worthy as-is for the stated UnixLocal scope. Repository readiness: CI or review pending.
Reviewed the complete 12-file diff at 7f83445, from merge base 99f8d77 against current main, including surrounding caller paths and previous review discussion.
The underlying problem merits an SDK fix: in v0.22.3 and the base implementation, Add File reaches the ordinary truncating write, so an existing file—or a file created by another operation before the write—can lose its contents while the operation reports success. A separate existence check cannot guarantee create-if-absent. Existing update_file and write remain the supported choices for intentional overwrites; they do not solve this safety requirement.
The current implementation puts the claim at the right boundary. UnixLocal uses O_EXCL and writes through the claimed descriptor, including through SandboxSession and the bound-user worker. Supported parent symlinks, write authorization, nested parents and umask semantics remain intact. Copy/close failures preserve the destination and report non-retryable inspection/recovery guidance, with no pathname cleanup that could delete another operation’s replacement. Other providers retain their normalized mkdir/write behavior without a new exec or hard-link requirement. This is a proportionate fix; generic provider atomic creation or transactional staging would be separate work.
No actionable defects found after two clean rounds, each with two independent fresh-context reviewers covering correctness/security and architecture/ownership. The inspected regressions exercise intervening creators, dangling leaf links, partial writes, close failures, replacement preservation, explicit recovery and scripted-provider compatibility. The earlier findings addressed by removed probes/staging or the current forwarding/recovery implementation are not repeated here. Bounded duplicate searches found no competing open implementation; #4930 is closed, and #4890 addresses update/rename rather than Add File.
Validation limits: source and test inspection only; no PR code, tests, imports or runtime probes were executed for this review. All 21 hosted checks report success on this exact head, including Windows and native macOS. Live sudo identities and external provider environments were not independently exercised.
Next action: maintainer re-review of this head to resolve the outstanding changes-requested review. GitHub reports the PR mergeable but still blocked by review state. This assessment requests no additional code changes.
This pull request fixes UnixLocal Add File overwriting an existing destination, including a file created concurrently after path resolution. Direct, wrapped and bound-user sessions claim the destination with exclusive creation while retaining supported parent symlinks and filesystem authorization. Other sandbox providers retain their existing normalized mkdir/write behavior.
If copying or closing fails after the destination is claimed, the SDK preserves the file and reports a non-retryable error with explicit inspect/update/remove recovery guidance. It does not require hard links or automatically delete a path that another operation may have replaced.
Regression tests cover concurrent creation, partial writes, close failures, replacement preservation and deliberate recovery across direct, wrapped and bound-user paths. The full formatting, lint, typecheck and test stack passes locally: 11,305 passed and 66 skipped. All 21 GitHub checks pass on 7f83445, including native macOS sandbox tests, Windows tests, Python 3.10–3.14, packaged compatibility contracts and CodeQL.