* fix(windows): retry atomic file renames on Access is denied / Sharing violation
* test(swarm): add unit tests for rename retry and non-retryable errors
* test(swarm): skip TestMailboxRenameRetry on non-Windows platforms
* fix(sandbox): scrub sensitive provider credentials from sandbox env
* refactor(sandbox): unify environment creation and reduce lines of code
* fix(sandbox): forward workspaceRoot through sandboxEnvironment, gate Windows env on backend
* style(sandbox,swarm): remove stray blank lines flagged by gofmt
* refactor(fsutil): extract shared rename retry helper, derive scrub list from provider catalog
Address CodeRabbit review feedback:
- Add internal/fsutil.RenameWithRetry with an injectable rename override and
move the four duplicated renameWithRetry/isWindowsSharingViolation copies
(cron, sessions, sandbox, swarm) onto it. Mailbox keeps its rename test
seam by passing its field through. Add fsutil tests for the Windows retry
and non-retryable paths.
- Remove the orphaned metadata.json.tmp in cron writeJob when the rename
permanently fails, matching the sibling writers.
- Derive scrubSensitiveEnv keys from providercatalog AuthEnvVars plus a
static list of non-catalog secrets, so new providers are scrubbed
automatically. AWS_PROFILE and GOOGLE_APPLICATION_CREDENTIALS stay
because they are pointers, not secrets.
* fix(sandbox): deny sandboxed reads of cloud credential stores by default
Address CodeRabbit finding that scrubbed env pointers still left the
underlying credential files readable under the read-all workspace posture:
- Add credentialDenyReadPaths: default profile-level deny-read entries for
~/.aws, ~/.config/gcloud, ~/.azure, and the GOOGLE_APPLICATION_CREDENTIALS
target file, wired into both permission profile builders so the seatbelt
and Linux helper backends enforce them. Entries are stat-filtered and a
store nested under a user AllowRead entry is dropped, keeping an explicit
opt-out.
- Keep the defaults out of Policy.DenyRead, whose emptiness gates escalated
execution, and skip Windows so the profile deny list does not flip the
runner off the WRITE_RESTRICTED token path.
- Scrub GOOGLE_APPLICATION_CREDENTIALS from the sandbox env; keep AWS_PROFILE
since the deny-read rule on ~/.aws is the real protection and the SDKs
fall back to the default profile regardless.
- Add credential deny path tests, extend the scrub test, relax the profile
test's exact deny count to containment.
* test(cli): pin HOME in sandbox policy golden test
The default credential-store deny-read entries depend on what exists in the
real home directory (the macOS CI image ships ~/.aws), which leaked host
paths into the policy JSON golden comparison. Point HOME at an empty temp
directory so the golden stays hermetic.
* fix(sandbox): protect GOOGLE_APPLICATION_CREDENTIALS without a home, retry lock violations
Address CodeRabbit review feedback:
- credentialDenyReadPaths no longer returns early when the home lookup
fails; home-based candidates become conditional and the
GOOGLE_APPLICATION_CREDENTIALS target is always considered.
- fsutil.RenameWithRetry also retries ERROR_LOCK_VIOLATION (errno 33),
matching the mcp lock handling; helper renamed to
isWindowsSharingOrLockViolation.
- Extend tests: homeless GOOGLE_APPLICATION_CREDENTIALS case and a lock
violation step in the retry sequence.
* fix(sandbox): tighten env-scrub test assertion and add missing secret keys
TestScrubSensitiveEnv only checked an allowlist plus a length count, so a
duplicate allowed key silently replacing a required one would still pass.
Compare the exact expected key/value map instead.
Also add ZERO_WEBSEARCH_API_KEY and ZERO_DAEMON_REMOTE_TOKEN to
scrubSensitiveEnv's sensitiveKeys list: both are real bearer secrets read
from the environment but were missing from the scrub list, leaving them
readable by sandboxed commands when set in the parent shell.