Skip to content

fix(cli): preserve permissions when replacing settings - #13293

Merged
wenshao merged 7 commits into
mainfrom
codex/preserve-settings-permissions
Oct 6, 2026
Merged

wenshao merged 7 commits into
mainfrom
codex/preserve-settings-permissions

Conversation

@doudouOUC

@doudouOUC doudouOUC commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator

What this PR does

Preserve an existing regular settings file's ordinary POSIX permission bits during atomic replacement, including under a stricter umask. Apply the captured mode before publication and exclude setuid, setgid and sticky bits. New files, targets missing at the permission probe, and replacements of symlinked settings retain default 0666 filtered by umask; replacement changes the link entry while leaving its referent intact. Links to directories still refuse. When chmod reports ENOSYS, ENOTSUP or EPERM, publication is allowed only if the staged file grants no ordinary permission bits beyond the captured mode. Broader staged modes and unrelated errors refuse the save before publication.

Why it's needed

Startup migration and ordinary saves can turn private 0600/0640 settings into 0644 under umask 022 while retaining private environment values. Under umask 077, an existing 0640 file instead loses intentional group-read access. Capturing permissions only for regular entries also avoids inheriting a symlink referent's broader mode. The fix preserves existing replacement, backup and recovery behavior. The paired English and Chinese design notes describe the boundary.

Reviewer Test Plan

How to verify

  • Trigger startup normalization with isolated User/Workspace settings and fake values under umask 022 and 077. Existing regular 0600/0640/0666 files should retain their modes and complete values, with version metadata added.
  • Delete an existing settings target after its initial inspection and before the permission probe. The save should still publish complete new content at the default umask-filtered mode, with no leftover staging artifacts.
  • Save settings or restore a transaction snapshot. Complete content and existing ordinary permissions should survive replacement; independent policy readers should see the committed policy while publication proceeds.
  • Create new settings or replace settings links to /dev/null and owned 0644/0777 files. Staged and final permissions should be 0644 under 022 and 0600 under 077; links should become regular files and referents stay unchanged. Directory links should refuse without replacement or staging artifacts.
  • Inject ENOSYS/ENOTSUP/EPERM during chmod: equal or narrower staged modes should publish complete content and clean staging; broader modes should refuse before backup/publication. EPERM should allow direct settings saves and startup version normalization. Inject EACCES, I/O, read-only or unclassified errors: refuse before publication, retaining original bytes/mode. Set special permission bits on a POSIX fixture and confirm the replacement drops them.
  • Run native Windows replacement/sharing/crash tests separately. POSIX test skips provide no Windows ACL or read-only acceptance.

Evidence (Before & After)

Independent native macOS reproduction confirms private files losing their existing mode and a missing-target regression at the new permission probe. R4 source-bound verification has four actual built CLI startup migrations with deterministic real deletion pass under User/Workspace and umask 022/077; complete fake values and version metadata publish at default 0644/0600 without chmod, backup-copy or leftover staging. Two regular-file startup migration controls retain 0666/0777; two injected compiled-helper EACCES/EIO controls propagate errors and preserve original bytes/inode/mode. The three selected normal tests pass; removing the missing-entry option fails the new deletion case and clamping write bits fails both mode cases at the added 0666 row. Unselected tests are distinct from platform skips. These native controls exercise startup migration; direct save commands are covered by caller tests separately.

At clean commit 4c8a8e1, build, typecheck, bundle and the six affected/caller test files passed once (351 passed, 0 failed, 4 native Windows skips). Independent CLI observations preceded commit, with all five observed source blobs identical to this commit. Two clean self-audits and independent review bind the same source blobs. Detailed scoped evidence and limitations are in the E2E report. The separate 221-assertion Linux sandbox report belongs to parent 37b137e; it is neither whole-suite coverage nor a run of this commit.

R6 at e477bac: a new chmod EPERM save regression was independently reproduced on owned macOS fixtures using controlled errno injection. After the fix, 23 actual built CLI scenarios and 2 built-helper checks pass: direct project saves and startup normalization accept equal/narrower modes, all three recognized errors refuse broader staging before backup/rename, EACCES/EIO preserve the target, and native mode/new-file/symlink controls pass. Frozen normal tests pass 12/12; the reviewed old writer fails 5 selected rows and removing the ceiling guard fails 3 broader-mode rows (38 deselected per run). The four observed source blobs match this commit; observations preceded commit and are not claimed as a new-SHA rerun. The clean-commit build/typecheck/bundle/six-file gate passed once (356 passed, 0 failed, 4 native Windows skips). Two clean self-audits and independent source/evidence review found no new in-scope Critical. Maintainer native Linux CIFS and macOS evidence covers the prior head and their candidate separately; our local errno simulation is not a native CIFS run of this commit. R6 CLI uses Node 22.14.0; build and final checks use Node 24.19.0.

Current HEAD after normal main synchronization is 9fa9393. This incorporates the newer lint baseline; the five permission-related PR files remain identical to R6. The actual clean merge commit passed build/typecheck/bundle and six affected/caller test files once (356 passed, 0 failed, 4 native Windows skips). Four representative cases ran against its final CLI 0.25.0 after commit: EPERM project save, startup version persistence, broader-mode refusal retaining original bytes/inode/0640, and native exact 0640 under umask 077. Source and final binary identities stayed stable. CLI observations use Node 22.14.0 on owned macOS fixtures; injected EPERM/staging modes are distinct from native CIFS evidence. Two clean self-audits and independent scoped merge review are recorded. Earlier R6 observations retain their original binding; this is a separate actual-HEAD integration subset, not a rerun of all older observations.

Tested on

OS Status
macOS arm64: real CLI, native filesystem controls and affected/caller tests
Windows Not executed locally; native suite remains platform-gated
Linux No local native run of the latest fix; CI evidence is separate

Environment (optional)

Node 24.19.0 on macOS arm64; owned isolated fixtures and fake values, with umasks restored and fixtures cleaned. Startup uses the built CLI without provider authentication. Caller tests run with QWEN_HOME unset; independent environment-sensitive failures remain tracked in #13288.

Risk & Scope

Preservation is symmetric: already group- or world-writable regular files retain those bits; the previous implicit narrowing to 0666 & ~umask no longer occurs. Exact ordinary mode preservation requires filesystem support. On ENOSYS/ENOTSUP/EPERM, the staged file’s reported ordinary bits are checked against the captured ceiling; equal or narrower permissions may publish. Exact preservation remains dependent on filesystem support. Owner/group, ACLs, extended attributes, native FAT/exFAT behavior, hostile replacement races, concurrent external chmod and power-loss durability are outside this fix. Startup failure diagnostics and native Windows read-only interaction remain separate follow-ups. There are no settings schema/API or sandbox authorization changes.

Linked Issues

Fixes #13287. Refs #12417.

中文说明

本 PR 的改动

原子替换时保留已有普通设置文件的普通 POSIX 权限位,包括更严格的 umask;发布前恢复权限,不复制 setuid、setgid、sticky 位。新文件、权限探测时已缺失的目标和符号链接位置的替换文件继续使用默认 0666 与 umask 共同决定的权限;替换链接本身,引用目标不变。目录链接仍拒绝。chmod 返回 ENOSYS、ENOTSUP 或 EPERM 时,仅在暂存文件的普通权限位不超出已捕获 mode 时发布;更宽的暂存权限和其他错误仍在发布前拒绝保存。

原因

启动迁移和普通保存会在 umask 022 下把私有 0600/0640 设置变成 0644,并保留私有环境值;umask 077 又会让已有 0640 丢失有意设置的组读权限。只对普通目录条目继承权限,还可避免复制链接引用目标的更宽权限。保持现有替换、备份和恢复行为,双语设计说明同步记录该边界。

审查者测试计划

如何验证

  • 使用隔离 User/Workspace 设置和假值,在 umask 022/077 下触发启动迁移。已有普通 0600/0640/0666 文件应保留权限与完整值,并添加版本信息。
  • 在初始检查后、权限探测前删除已有设置目标。保存仍应以默认 umask 权限发布完整新内容,且无暂存遗留。
  • 保存或恢复事务快照,完整内容和原有普通权限应保留;发布过程中独立策略读取者应看到已提交策略。
  • 创建新设置,或替换指向 /dev/null、自有 0644/0777 文件的设置链接。暂存和最终权限应分别为 022 下 0644、077 下 0600;链接变成普通文件,引用目标不变。目录链接应拒绝,且无替换或暂存遗留。
  • chmod 注入 ENOSYS/ENOTSUP/EPERM 时,相同或更窄的暂存权限应完整发布并清理;更宽权限应在备份/发布前拒绝。EPERM 下直接保存与启动版本规范化应成功。注入 EACCES、I/O、只读或无分类错误时,应在发布前拒绝并保留原字节和权限。POSIX 特殊权限位应在替换时丢弃。
  • Windows 原生替换、共享句柄、进程中断测试单独执行。POSIX 测试跳过不代表 Windows ACL 或只读验收。

证据(前后)

独立 macOS 原生复现确认私有文件丢失原权限,以及新权限探测点的缺失目标回归。R4 源码绑定复验有 4 次实际构建后 CLI 启动迁移通过,覆盖 User/Workspace 与 umask 022/077 下的真实受控删除;完整假值和版本信息按默认 0644/0600 发布,无 chmod、备份复制或暂存遗留。两项普通文件启动迁移控制保留 0666/0777;两项注入 EACCES/EIO 的编译 helper 控制仍传播错误,保留原字节、inode 和权限。3 项选中正常测试通过;移除缺失条目选项使新增删除用例失败,收窄写权限使两项 mode 用例在新增 0666 行失败。未选中测试与平台跳过单独计数。原生 CLI 控制验证启动迁移;直接保存调用方由单独的调用方测试覆盖。

干净提交 4c8a8e1 上 build、typecheck、bundle 和 6 个受影响/调用方测试文件各通过一次(351 passed, 0 failed, 4 native Windows skips)。独立 CLI 观察发生在提交前,5 个观察源码 blob 与本提交完全相同。连续两轮自审与独立审查绑定相同源码;完整分域证据与限制见独立 E2E 报告。221 项断言的 Linux 沙箱报告明确属于父提交 37b137e,不代表全套覆盖或本次提交执行。

R6 提交 e477bac:独立使用自有 macOS 夹具和受控 errno 注入复现新增 chmod EPERM 保存回归。修复后 23 项实际构建 CLI 场景及 2 项编译 helper 检查通过:直接项目保存和启动规范化接受相同/更窄权限,三种容忍错误均在更宽暂存权限下拒绝备份/发布,EACCES/EIO 保留原目标,原生 mode/新文件/符号链接控制通过。冻结正常测试 12/12 通过,旧 writer 在 5 个选中行失败,移除权限上限守卫后 3 个更宽权限行失败(每次 38 项未选中)。4 个观察源码 blob 与本提交一致;观察发生在提交前,不称为新 SHA 再跑。干净提交最终 build/typecheck/bundle/6 文件检查各通过一次(356 通过、0 失败、4 项原生 Windows 跳过)。连续两轮自审及独立源码/证据审查无新增范围内 Critical。维护者的原生 Linux CIFS/macOS 报告绑定先前 HEAD 与其候选,属于外部证据;本地 errno 注入不代表本提交的原生 CIFS 运行。R6 CLI 使用 Node 22.14.0,构建和最终检查使用 Node 24.19.0。

正常合并 main 后的当前 HEAD 为 9fa9393,纳入更新的 lint 基线;5 个权限相关 PR 文件仍与 R6 相同。实际干净合并提交最终检查各跑一次(356 通过、0 失败、4 项原生 Windows 跳过)。提交后针对最终 CLI0.25.0 执行4项代表性验证:EPERM 项目保存、启动版本持久化、更宽权限拒绝且原字节/inode/0640 保留、原生 umask077 下精确保留0640。源码与最终产物身份稳定;CLI使用自有 macOS 夹具与 Node22.14.0,注入 EPERM/暂存 mode 不代表原生 CIFS。连续两轮自审和独立合并审查完成,旧 R6 证据保持原绑定;此次是实际 HEAD 的独立集成子集,未机械重跑全部旧观察。

平台与环境

macOS arm64、Node 24.19.0,真实 CLI、原生文件系统和定向/调用方测试;自有隔离夹具、假值,umask 恢复且夹具清理。未本地执行最新修复的原生 Linux/Windows。无需模型认证;调用方测试清空 QWEN_HOME,环境敏感失败由 #13288 独立追踪。

风险与范围

权限保留是对称的:已有组写或其他用户写权限的普通文件仍保留这些位,此前隐式收窄到 0666 & ~umask 的行为不再发生。精确保留普通权限需要文件系统支持。chmod 返回 ENOSYS/ENOTSUP/EPERM 时,会核对暂存文件报告的普通权限位与捕获上限;相同或更窄权限可发布。精确保留仍取决于文件系统支持。owner/group、ACL、扩展属性、原生 FAT/exFAT、敌对替换竞争、外部并发 chmod 和断电持久性不在本次范围。启动失败诊断、Windows 原生只读交互单独跟进;不改变设置 schema/API 或沙箱授权。

关联 Issue

修复 #13287,关联 #12417。

@github-actions github-actions Bot added the review/self-reported The linked issue was opened by the PR author (self-reported) label Oct 3, 2026
@doudouOUC

doudouOUC commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator Author

[codex] Settings permission preservation — scoped E2E report

Independent test-engineer verification binds to writer blob ec85ed424d80d89b69a118a797ee2d8d38440239, which exactly matches commit b30d1cdc73dbd8aab0bf406cbc8c94b89bdf8c97. Test execution preceded the commit; no source changes occurred between verification/review and committing.

Evidence Result
Baseline: global qwen 0.24.6 offline startup 8 actual User/Workspace migration startups:022 widens0600/0640 to0644;077 narrows0640 to0600. All exit0 and retain fake token/version metadata.
Baseline: exact-main source controls 16 actual writer/JSONC calls at576689d reproduce the same mode changes.
Built CLI after fix 8 node dist/cli.js --list-extensions startups preserve0600/0640 under022/077, exit0, add version4, retain fake token and complete settings.
Compiled writer/editor controls 16 existing-file observations preserve exact modes;4 new-file controls retain022→0644 and077→0600. Total post-fix28 observations passed.
Negative regression oracle Replacing only the writer with exact-main source in an ignored Vite transform makes both new mode cases and staging-chmod-failure refusal fail; the2 new-file controls stay green. No production files were reverted.

Environment: macOS arm64, UID501, Node 24.19.0; owned temporary User/Workspace/settings/runtime fixtures and fake credentials only. Umask restored to022 and fixtures cleaned. Compiled source identity was checked with parser-derived leaf tokens; three initial harness prechecks stopped before CLI execution and were corrected before the recorded28-case verification.

This verifies offline startup migration and actual compiled save helpers. It does not claim native Linux/Windows, ACL/owner identity, cross-user reads, full TUI/authenticated-provider sessions or new concurrency/failure-injection E2E runs. Parent-owned unit tests separately cover atomic-reader policy, overlapping writers, recovery and chmod failure; final exact-commit build/typecheck/bundle/test gate is recorded in the PR description. Four native Windows tests are local skips, not passed native acceptance.

中文说明

独立测试工程师复验绑定writer blob ec85ed424d80d89b69a118a797ee2d8d38440239,与提交 b30d1cdc73dbd8aab0bf406cbc8c94b89bdf8c97 完全一致。测试先于提交执行,复验/审查至提交之间没有源码变化。

基线全局qwen 0.24.6完成8个真实User/Workspace启动,精确main源码再完成16个writer/JSONC对照:022下0600/0640变0644,077下0640变0600;exit0、假值/版本元数据保留。修复后8个真实构建CLI离线启动、16个编译writer/editor已有文件对照保留所有mode;4个新文件控制保持022→0644、077→0600,共28个观察全部通过。忽略目录下的Vite transform只替换为旧writer时,新权限保留与chmod失败拒绝的3项回归变红,2个新文件控制仍绿;未回退生产文件。

环境:macOS arm64、UID501、Node 24.19.0,仅自有隔离设置/runtime夹具与假凭证。umask恢复022、夹具清理。编译源码用parser叶子token核对;最初3次harness预检在CLI运行前停止,修正后才执行记录中的28例。

范围是离线启动迁移与实际编译保存调用,不宣称原生Linux/Windows、ACL/owner、跨用户读取、完整TUI/认证模型会话或新增并发/故障注入E2E。主代理单测分别覆盖原子策略读取、交错writer、恢复及chmod失败;最终精确提交build/typecheck/bundle/test记在PR说明。4项原生Windows测试在本地跳过,不算原生验收。


[codex] Round 1 compatibility verification — 37b137ee7c0c7a5cdce9b131b3344ec5fe9543d0

Independent observer verification recorded 44 effective observations and 117 passing checks: 22 actual built User/Workspace startups (seven injected error conditions and four normal mode/umask controls per scope), 14 compiled writer/editor error controls and eight real-file special-bit controls. ENOSYS/ENOTSUP publish complete version-normalized settings; EPERM/EACCES/EIO/EROFS and code-less errors retain the exact old bytes/inode/mode and do not copy/rename. Startup still exits 0 on a refused normalization write; exit status is not used as proof of saving. Compiled helpers preserve the original error object. Real 04000/02000/01000 and combined 07640 fixtures drop special bits under both masks.

The tested working writer blob d1d7df0fbf14387e646111c28a1f639c995affeb matches this commit. Execution preceded committing; the entry bundle hash was f3f569d394960db907dbea3b8cb77ad9b69917754001a0606063f701f58e71f7. An initial Workspace injector missed /var versus /private/var; it is recorded only as an ordinary save and excluded from injection success. Corrected realpath-bound cases supply the actual injection evidence; completed User cases were not repeated. The final build/typecheck/bundle and 339 focused tests passed once on the clean committed SHA (four native Windows skips).

These are real built-CLI/compiled-API executions with narrowly injected chmod errors and native macOS positive mode controls. No native FAT/exFAT failure, Linux/Windows, ACL/owner or authenticated-provider guarantee is inferred. The attempted ExFAT image command failed at creation because its harness arguments were invalid; no image was mounted. Owned fixtures were cleaned and fs/umask restored.

中文:本轮44个有效观察、117项检查通过:22个真实User/Workspace启动、14个编译API错误对照、8个真实特殊位对照。不支持错误码可发布;权限/I/O/只读/无code错误保留原字节、inode、mode且不copy/rename;启动拒绝写入仍exit0,不把退出码当保存成功。特殊位真实存在于原文件且在两种umask下丢弃。复验writer blob与提交一致,观察先于提交。首次Workspace注入未命中仅记普通保存;修正realpath后取得有效证据,已完成User不重复。干净提交上的最终 build/typecheck/bundle 与339项定向测试已各通过一次(4项Windows跳过)。注入不代表原生FAT/exFAT或其他平台验收;镜像创建命令参数无效,未挂载。夹具及fs/umask已清理恢复。


[codex] Round 2 test-only verification - bc2d5837aa6fd37ef3b40da63b535558cfb3f117

The independent observer first pinned parent 37b137e and recorded original writer/M2 suites both green at 33/33, plus 16 real-file observations showing four actual widening cells when only creation mode is removed (ENOSYS/ENOTSUP, umask 022, original 0600/0640 -> 0644). No production file was mutated.

With the added four POSIX code-by-mask tests, the unchanged writer passes 37/37. The same isolated loader mutant passes 35 and fails only the two new 022 tests on actual staging mode 0644 versus expected 0600; its first fixture fails before the subsequent 0640 iteration, whose widening evidence comes from the separate original matrix. Original 33 and new 077 controls remain green. Real staged/target/final modes, actual chmod arguments, complete new content and cleanup are asserted. Umask is restored in finally.

Test blob 461b5eed886a4e22fec2c2604bdd7e1ae7446dad and unchanged writer blob d1d7df0fbf14387e646111c28a1f639c995affeb match the committed source. Runs preceded committing; each normal/mutant suite ran once. A raw-name property mistake in result aggregation was corrected from the existing outputs, with no rerun. Two clean self-audits and independent review passed. This is owned macOS fixture injection and regression-oracle evidence, not native FAT/exFAT/Windows or new CLI acceptance. The separate Linux container report at parent 37b137e supplies its own 221 passing assertions.

Final gate: build, typecheck and all 37 affected writer tests passed once on the clean committed SHA bc2d5837aa6fd37ef3b40da63b535558cfb3f117; reviewed/tested blobs and tree remained unchanged.

Round 3 — symlink permission boundary (47fc0cf)

Before editing, independent native macOS reproduction compared exact base576689d and reviewed bc2d writer bytes:18 real-writer observations plus2 installed-global CLI startup controls. The reviewed writer published0666 for /dev/null links and0777 for links to owned0777 referents, instead of the base's default0644/0600 under022/077. Owned referents remained unchanged. Two optional source-startup loader calls failed before product import; the old namespace rename probe produced no stage receipts, so that reproduction only claims final-mode evidence.

The fix inherits mode only for a regular entry checked without following its final link. Following directory refusal remains, and other links keep default umask publication. Independent source-bound candidate verification ran6 actual built CLI startups (User links→owned0644 / Workspace links→owned0777 under both masks, plus ordinary0600/0640 controls) and5 compiled writer native-filesystem controls (/dev/null links and new files under both masks, plus directory-link refusal). All pass with complete fake data/version4, correct staged/final modes, unchanged referents and no settings-write artifacts. The4 successful compiled controls capture native pre-rename staging; no device was modified.

Frozen candidate tests select only7 new link cases. Normal7/0; following-stat mutant3/4; omitted-regular-gate mutant1/6. The37 other tests are unselected, not native-platform skip acceptance. Most failures assert actual staged-mode broadening;0644/022 has matching default bits and instead detects the forbidden chmod attempt. Only the advertised writer gate changes in mutant arms, with test bytes frozen. Original aggregator false positives (ordinary User extension directories and Vitest unselected status) are retained and corrected from raw evidence without CLI/test reruns.

Observed writer blob278820c262dd2c03caf14b1b5aed102eece18a2f and test blob69177fca8eb86b41cc2f2038fe14f6c6cf1c5dc7, plus both designs and unchanged settings mock, match this committed fix. Source/compiled tokens, reachable bundled functions and bundle byte manifest bind the pre-commit execution; committing changed no source. Final clean-commit build/typecheck/bundle and six affected/caller test files ran once: 350 passed, 0 failed, 4 native Windows skips. ESLint, two self-audits and independent source/evidence review pass. This is native macOS arm64 evidence, not native Linux/Windows/FAT/exFAT, ACL/owner preservation, hostile races, power-loss or authenticated TUI acceptance. Earlier221 sandbox assertions belong to parent37b.

中文:第三轮修复符号链接权限边界,提交 47fc0cf。独立复现18个真实writer观察+2次全局CLI启动,确认旧bc2d会把引用目标0666/0777继承到替换文件。只对不跟随链接的普通条目继承权限,链接仍按默认umask发布,目录链接仍拒绝。复验6次实际构建后CLI启动+5个编译writer原生控制全过,完整假值/版本、暂存/最终权限、引用目标不变和清理均验证。新增7项测试正常全绿;跟随stat变异3过4失败,移除普通文件门禁1过6失败,37项仅未选择。聚合误判保留原始记录并据已有raw纠正,未重跑。所审/所测blob与本提交一致,干净提交最终检查各跑一次(350 passed, 0 failed, 4 native Windows skips);ESLint、两次自审和独立源码/证据审查通过。证据限macOS arm64,Windows/Linux/FAT/exFAT、ACL/owner、敌对竞争、断电和认证TUI仍未验收;221项沙箱报告归父提交37b。


R4 — missing-target regression and symmetric preservation

Commit 4c8a8e1, tree 67d30b7b0befb707157265894482f9e72149d658; source writer 7f93f0154ab895e253d632d4c49d459512a13d2f, test 48c780586cdb92ab35a059e018e894a3c2c8dce5.

Independent reproduce-first work on native macOS arm64 used 20 bounded observations: two installed global CLI baselines, byte-faithful git-source arms at pre-PR 576689d / pre-R3 bc2d583 / reviewed 47fc0cf, and initially-missing controls. Actual deletion immediately after native staging-directory creation made reviewed 47fc0cf throw native lstat ENOENT and remove staging without publication; earlier arms succeeded. Existing regular modes 0666/0777/0664 are symmetrically retained by the reviewed writer, whereas pre-PR incidental umask narrowing remains a baseline difference. This supports the paired documentation decision, not a clamp.

Independent post-fix verification ran six actual built CLI startup migrations in isolated owned fixtures with fake values and no provider authentication. Four preload-instrumented cases performed a real deletion at that same staging boundary (User/Workspace × umask 022/077); native lstat returned absent, and complete values plus $version 4 published at default 0644/0600. No staged chmod, target backup-copy or leftover settings artifacts occurred. Two ordinary regular-file controls retained User 0666 / Workspace 0777 under 022. These are startup migration via --list-extensions, not dynamic calls to a saveSettings command. Original normal-control labels were preserved locally and corrected only from existing raw observations; executions were not repeated.

Two compiled-writer helper controls injected EACCES/EIO at the new lstat probe: the exact error objects propagated, while original bytes, inode and mode remained unchanged and owned staging was cleaned. These injections establish failure policy, not native access-denial/I/O-failure acceptance.

Frozen test oracle Passed Failed Deliberately unselected
Normal candidate: new missing-entry case + two mode loops 3 0 42
Remove missing-entry option: new deletion case 0 1 (native ENOENT) 44
Clamp group/other write bits: two mode loops 0 2 (new 0666 row staged at 0644) 43

Loader traces hold test bytes fixed and mutate only the intended production expression in memory. Old 0600/0640/0644 values are unaffected by the write-bit clamp; both failures occur at the added 0666 row. Unselected tests are distinct from platform skips. Parent-run candidate writer coverage was separately 45/45. Source tokens match the compiled writer and bundled functions; the writer chunk is reachable from the actual CLI entry. All 656 recorded bundle files stayed stable. CLI entry SHA256 6009d7c65e5b0ae57b2fbb34d4bf8f12fa99517c7a17f689748f0c673d649502. All five observed/reviewed source blobs exactly match this commit. Those independent observations occurred before commit and are bound by source identity, not represented as repeated post-commit execution.

After two consecutive clean self-audits and independent source/evidence review, the clean pushed commit ran build, typecheck, bundle and six affected/caller test files exactly once: 351 passed, 0 failed, 4 native Windows platform skips. Normal writer tests include the prior unsupported chmod, symlink, directory-link, overlap, recovery and complete-byte assertions. Detailed ignored local artifacts: .qwen/e2e-tests/pr-13293-r4-reproduction.md/json, pr-13293-r4-verification.md/json, and .qwen/pr-reviews/pr-13293-r4-final-gate.json, pr-13293-r4-commit-binding.json, pr-13293-r4-independent-review.md/json.

Limits: native macOS only; deterministic scheduling proves the specific added-probe window, not field race frequency or earlier exists/stat race freedom. Error injection and loader mutation remain distinct from real native platform acceptance. No native Linux/Windows/FAT/exFAT, owner/ACL/xattr, hostile replacement/stale-inode, durability, authenticated provider session or full TUI claim. Prior Linux 221-assertion evidence is bound to 37b137e, not approval or this SHA. The documented symmetric ordinary-bit policy remains intact; unrelated shared errno refactoring and diagnostic/native-platform follow-ups remain outside this repair.

中文:R4 已复现并修复新权限探测点的缺失文件回归,双语记录已有宽权限的对称保留,并补充真实删除及普通 0666 断言。原生 macOS 4 次受控删除启动迁移、2 次普通文件启动迁移和 2 项编译 helper 错误注入均通过;三组冻结测试的正常/移除缺失保护/收窄写权限结果分别为 3/0、0/1、0/2,未选中项不是平台跳过。5 个提交源码 blob 与提交前独立观察完全一致,实际推送提交最终检查仅运行一次,351 通过、0 失败、4 项原生 Windows 跳过。普通 CLI 控制明确验证启动迁移,未动态调用直接保存命令;标签更正仅来自原始证据,无重复执行。保留自然竞态频率、原生 Linux/Windows/FAT/ACL 等未验证边界。

R6 — guarded EPERM fallback (e477bac)

Independent reproduction at 4c8a8e1 used global qwen 0.24.6 first, then exact reviewed source: controlled staged chmod EPERM made direct project saves exit 1 without changing the target, while startup exited 0 without persisting version metadata. Exact base writer controls saved without invoking chmod. All errno failures were injected on owned local macOS fixtures, not a native CIFS mount.

The actual built candidate CLI then passed 23 scenarios: direct saves and startup under equal/narrower EPERM modes; ENOSYS/ENOTSUP equivalents; all three recognized errors refusing broader staging before backup/rename; refusal against the original captured ceiling even after an injected target-mode change; EACCES/EIO propagation; and native exact ordinary modes, special-bit removal, default new-file/symlink behavior and unchanged referents. Two built-helper controls also preserve the exact injected EACCES/EIO objects. Source, entry/chunk/helper identities were stable during observation and fixtures removed. The final gate regenerated the Git version stamp; two in-memory bundle diagnostics reproduce the observed and final entry hashes solely by changing that stamp. The final entry reaches the byte-identical writer chunk and compiled helper; no post-commit CLI execution is claimed. These observations preceded commit; four observed source blobs exactly match this commit, without claiming execution of the new SHA.

The writer file passed 50/50. Frozen normal selection passed 12/12; the old writer failed 5 rows and removal of the ceiling guard failed 3 broader-mode rows, with 38 deselected tests each (not platform skips). The exact clean-commit final gate ran once: build/typecheck/bundle and six affected/caller files passed (356 passed, 0 failed, 4 native Windows skips). Two clean self-audits and independent source/evidence review bind the same blobs.

Local environment: macOS arm64/uid501/local temporary filesystem; CLI Node 22.14.0, build/final checks Node 24.19.0. Full local evidence: .qwen/e2e-tests/pr-13293-r6-cifs-chmod.md, reproduction/verification JSON, and .qwen/pr-reviews/pr-13293-r6-commit-binding.json, final-gate/final-tests/mutation-summary/self-audit/independent-review artifacts.

Maintainer native Linux CIFS, second-user and FAT/macOS observations bind the previous head and their candidate as external evidence. No local native Linux CIFS, Windows, macOS SMB, NFS, ACL/xattr or natural-race acceptance is claimed. Owner/group and symlink-managed settings follow-ups remain deferred; forced-owner vfat already failed in base.

中文:R6 在自有 macOS 夹具中通过受控 errno 注入复现 EPERM 回归;实际构建候选 CLI 的 23 项场景与 2 项 helper 检查通过。只在暂存普通权限不超出原捕获上限时容忍 ENOSYS/ENOTSUP/EPERM,更宽权限及其他错误仍拒绝发布。观察发生于提交前,4 个 blob 与提交相同,不称为新 SHA 或原生 CIFS 再跑。最终干净提交检查各执行一次(356 通过、0 失败、4 项原生 Windows 跳过)。维护者的原生平台报告为独立外部证据;身份、链接隐私、并发与额外平台验收保持延期。

Main synchronization — actual HEAD 9fa9393

Normal merge of R6 e477bac with pinned main 8648508; tree 08be8015ca4f7797ed4ae93993b6d3c578433d1e. This incorporates the lint freshness baseline that the prior run lacked. All five main-relative PR files retain the exact R6 blobs/modes, with zero incoming-path overlap; all 501 inherited paths match pinned main. Latest main at the bounded freshness recheck was 85ea235, and all eight gate files' latest changes are ancestors of this merge. That local containment proof is separate from fresh remote CI execution.

The actual clean merge commit ran its final gate once: build, typecheck, bundle and six affected/caller test files pass — 356 passed, 0 failed, 4 native Windows skips (360 total). Two clean full-diff self-audits and an independent scoped source/ownership review found no merge issue. The commit hook's re-staging step reported an ignored upstream tracked .qwen path; no tree change resulted, and this hook is not counted as a successful final gate. The independently reviewed preview and actual tree are identical.

Independent test-engineer verification executed four representative cases against the final built CLI 0.25.0 after this actual commit: controlled EPERM project save and startup version persistence succeed at 0640; controlled broader 0644 staging under EPERM refuses before backup/rename, preserving original bytes/inode/0640; native direct saving under umask 077 restores exact 0640. Successful cases retain complete fake values and publish with one rename; all settings directories contain only settings.json. Fixture directories were removed. The four scenarios are pass/fail assertions, including the expected refusal exit 1; they are not four successful publications.

Generated commit metadata 9fa9393, exact source blobs and binary hashes remained stable. Actual caller stacks bind execution to the final writer-containing chunk. CLI entry SHA256 dc8f4213d728196e00e20b0fad129fbf4baf8fa8a8689d84ca42236adafae94f; chunk-KMA5RTTK.js SHA256 c2419b9fe2e266b3daed1be6ac3c748a2ca8298b8ce7cd013dff83903248a8de; compiled helper SHA256 0a03ffdee8cd2fa366e2f0bfa0b337555b8b34b35c1e1678347e0a591499b0a6. This new observation is bound to 9fa9393; earlier R6 observations are retained as their original precommit source-bound evidence and are not retroactively labelled as runs of this merge.

Environment: owned macOS arm64/uid501 local temporary fixtures with fake values; actual CLI Node 22.14.0; final build/checks Node 24.19.0. EPERM and altered staging are controlled local injections, not native CIFS/SMB acceptance. No new native Linux/Windows/FAT/ACL, owner/group, concurrent or hostile replacement, full TUI or provider-authentication claim is made. The earlier 23-case R6 verification and old final gates were not mechanically rerun.

Local artifacts: .qwen/e2e-tests/pr-13293-main-sync-verification.md/json and .qwen/pr-reviews/pr-13293-main-sync-final-gate.json, final-tests.json, commit-binding.json, self-audit.md/json, independent-review.md/json, lint-freshness-proof.json.

中文:已正常合并 main,实际 HEAD 9fa9393/tree08be8015 保留 R6 的全部 5 个 PR 文件,上游 501 个路径精确匹配 main,无重叠。实际提交最终检查各跑一次,356 通过、0 失败、4 项原生 Windows 跳过。4 项实际最终 bundle 验证通过:EPERM 直接保存、启动版本持久化、更宽暂存权限拒绝且原字节/inode/0640 不变,以及原生 umask077 下保留0640;拒绝场景的 exit1 是预期结果,不把4项都称成功发布。源码、生成版本戳、入口/chunk/helper 身份稳定,夹具已清理;注入错误与维护者原生 CIFS 证据分开,旧 R6 观察不改称本 SHA 执行。两轮自审与独立合并审查完成,既有延期边界保持。

@doudouOUC

doudouOUC commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator Author

[codex] Fixed the conditional ENOSYS/ENOTSUP regression in 37b137ee7, addressing Stage 2 and the review. Only these two chmod error codes are tolerated. EPERM/EACCES/EIO/EROFS and unclassified errors still preserve the original and abort before backup/publication.

Added real staged/final checks for dropping setuid/setgid/sticky, and error-code regression coverage. The two unsupported-code tests fail on b30d1cd and pass after the fix. Independent built-CLI/compiled-helper verification has 44 effective observations and 117 checks pass; the final clean-commit build/typecheck/bundle and 339 focused tests passed once, with four native Windows skips. Both self-audits and independent five-file review passed, and all reviewed/tested source blobs match the pushed commit. Injection establishes the error-code contract, not native FAT/exFAT behavior.

Retaining the creation mode is useful when chmod is unsupported, avoiding a fallback to default creation permissions. Automatic tightening of legacy permissive files and writer consolidation remain separate maintainer policy/scope decisions. This PR retains the existing-permissions contract and adds no ACL/owner or sandbox authorization changes.

中文:条件性不支持错误回归已修复并推送;仅放行 ENOSYS/ENOTSUP,其他权限、I/O、只读和无分类错误继续保全原件。补充特殊位丢弃与错误边界测试;独立44个有效观察、117检查和干净提交上的最终门禁均通过。注入不代表原生FAT/exFAT验收;保留创建mode,历史宽权限自动收紧与writer合并另由维护者决定。

@doudouOUC

doudouOUC commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

[codex] Review round 1 is pushed as 37b137ee7. Please reassess the current HEAD; the earlier CHANGES_REQUESTED reviewed b30d1cd.

Feedback Action
Unsupported chmod blocks a previously allowed save Fixed: only ENOSYS/ENOTSUP are tolerated, matching the existing core predicate; other failures retain the original and abort before publication.
Missing ordinary-bit mask oracle Fixed: real source, staged and published mode checks cover setuid/setgid/sticky removal.
Creation mode appears redundant Retained: it prevents default-mode creation when chmod is unsupported.
Preserve vs automatically tighten legacy settings Deferred: maintain the explicit preserve-existing contract; automatic healing is a separate maintainer policy decision.
Consolidate atomic writers Deferred: outside this targeted permissions fix.
Native/platform evidence New HEAD CI required; injected errors and macOS controls do not establish native FAT/exFAT or Windows ACL behavior.

Validation: 23 baseline injection observations; two unsupported-code tests red on b30d1cd, then green. Independent verification: 44 effective observations/117 checks pass, including 22 actual User/Workspace startups, 14 compiled writer/editor controls and eight actual special-bit fixtures. Startup exit 0 is not evidence of a successful save: refusal is verified from unchanged bytes/inode/mode and absent copy/rename. Final build, typecheck, bundle and 339 focused tests passed once on the clean pushed commit; four native Windows tests skipped. Two clean self-audits and independent full five-file review passed, with exact source-blob binding. The E2E report has the current round appended.

Post-reply query at the pushed HEAD: 0 inline threads total, 0 unresolved; resolved 0/0 because feedback was top-level. The older CHANGES_REQUESTED is awaiting fresh assessment. No CI retry was used. Fixes #13287. Refs #12417; #13288/#13290 remain separate.

中文:第一轮已推送,申请基于当前HEAD复审。修复不支持chmod的窄例外并补特殊位真实断言;保留创建mode和已有权限策略,自动收紧与writer合并延期。44个有效观察/117检查、自审和独立审查通过,干净提交上一次最终build/typecheck/bundle及339项测试通过(4项Windows跳过)。原生FAT/exFAT、新HEAD平台CI和Windows ACL仍未由本地注入证明;没有使用CI重跑预算。

CI diagnostic after the push: Assign PR owner run 37127785761 fails at checkout, before owner-assignment code. The trusted base 576689d workflow requests file paths in sparse-checkout while default cone mode treats them as directories: .github/issue-owners.json is not a directory (exit 128). None of this workflow/config is changed by this PR. This is a deterministic trusted-base CI configuration failure, not a transient chmod/test failure; no rerun or unrelated workflow change was made. The bounded post-push snapshot has 9 passed, 14 pending and this one failed check; the previous-SHA requested-changes review is awaiting the requested fresh triage.

Hourly check at 2026-10-03 16:24 UTC: the requested re-triage job failed to produce a review because the model API connection timed out (ETIMEDOUT / ENETUNREACH). The direct job log identifies a network/API infrastructure failure; it does not report a new code finding. The workflow verify job completed successfully, but its report publication is still queued, so that success is not being treated as a published acceptance report. The separate automatic PR review is currently running. Current HEAD remains 37b137e with 0 unresolved inline threads. The owner-assignment checkout failure remains the same known unrelated configuration issue. No source changes, duplicate review replies, final-gate reruns or workflow retries were made; the watcher has not recommended a retry and the per-SHA retry budget remains 0/3. Hourly follow-up continues.

@doudouOUC

doudouOUC commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator Author

[codex] Fixed F1 in the published verification report. Added a POSIX regression oracle for ENOSYS/ENOTSUP under umask 022/077 with explicit 0600/0640 targets, checking the staged and published permissions. The production writer already kept the creation mode; this test-only round pins that fallback. Fixed in bc2d5837a.

F2 is valid as a startup observability concern: migration persistence catches the write failure and the debug logger returns without an active session. Deferring that product behavior to a separate follow-up because this PR preserves permission bits and deliberately propagates genuine chmod failures; explicit settings saves already rethrow. F3 remains an unverified native Windows read-only interaction, with no observed new defect to fix here. F4 is an incomplete-mock condition, not a production Stats value; the existing mock fidelity change is retained. No owner/ACL/xattr or sandbox authorization changes are proposed.

Independent oracle: normal 37/37 passed; removing only creation mode leaves the original 33 green but makes exactly the two added 022 cases red (real staged 0644 versus expected 0600). The 077 cases remain correct controls. Both self-audits and independent review passed; reviewed/tested blobs match the commit. The earlier report-only aggregation error was corrected from saved raw evidence without repeating tests.

Final gate: build, typecheck and all 37 affected writer tests passed once on the clean committed SHA bc2d5837aa6fd37ef3b40da63b535558cfb3f117; reviewed/tested blobs and tree remained unchanged.

@doudouOUC

doudouOUC commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator Author

[codex] Review round 2: the permission-ceiling test gap from the published sandbox report is addressed in bc2d5837aa6fd37ef3b40da63b535558cfb3f117.

Feedback Action
F1: creation permissions were not pinned when chmod is unsupported Fixed in tests: ENOSYS/ENOTSUP × umask 022/077 with real 0600/0640 files, actual staging/old-target/published mode checks, content and cleanup. Production is unchanged.
F2: refused startup migration can be silent before a debug session exists Deferred to a separate diagnostics follow-up. The existing migration catch and no-session logger explain it; this test batch retains the permission/refusal contract.
F3: native Windows read-only interaction Kept as an unverified coverage limit. The report supplies no measured new regression; general native Windows checks do not establish this scenario.
F4: incomplete mocked Stats without a mode No production fix needed: actual stat supplies a numeric mode. Existing mock fidelity remains intact.

Independent evidence: original suite 33/33 green with both real writer and mode-removal mutant; 16 real-file observations independently expose four mutant 022 widening cells. Added tests: unchanged writer 37/37 green, same isolated mutant 35/37 with exactly the two new 022 tests red on real staged 0644 versus expected 0600. The two 077 controls and original 33 remain green. The failing mutant tests stop at their first 0600 fixture; the separate original matrix supplies the 0640 widening witnesses. A report postprocessor field error was corrected from the saved raw outputs without repeating either suite.

Two clean self-audits and independent five-file review bind to the committed test blob 461b5eed886a4e22fec2c2604bdd7e1ae7446dad; writer blob d1d7df0fbf14387e646111c28a1f639c995affeb is unchanged. The earlier Linux container report's 221 assertions concern parent 37b137e, not a new run of this test-only commit, and are advisory evidence rather than approval. Injected chmod errors do not claim native FAT/exFAT or Windows read-only/ACL acceptance. Fixes #13287. Refs #12417; #13288/#13290 remain separate.

中文:第二轮只补不支持 chmod 时的权限上限回归测试。正常 37 项全过,移除创建 mode 的变异恰好使新增两个 022 场景失败,原 33 项及 077 控制仍通过;真实 0640 放宽证据由独立原始矩阵提供。生产代码与双语设计不变,启动诊断延期、Windows 只读场景仍未验证、mock 缺 mode 不是生产输入。两轮自审和独立审查通过,源码绑定精确提交;Linux 221 项断言明确归于父提交,不当作新提交批准。

Final gate: build, typecheck and all 37 affected writer tests passed once on the clean committed SHA bc2d5837aa6fd37ef3b40da63b535558cfb3f117; reviewed/tested blobs and tree remained unchanged.

Post-reply query on the pushed HEAD: 0 inline threads total, 0 unresolved; resolved 0/0 because this report was top-level. No flaky retry used. New-commit CI is being checked once before the next hourly follow-up.

@doudouOUC

doudouOUC commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator Author

[codex] Review batch 3: fixed the symlink permission regression in 47fc0cf.

Feedback Action
Critical R1-1 / discussion4174970096: symlink referent modes widened the replacement Fixed. Only a non-following regular-file check qualifies for mode inheritance. Symlink replacement retains default0666 filtered by umask, and referents stay unchanged. Existing directory/link-to-directory rejection and ordinary-file preservation stay intact.
Suggested separate regularity/link predicates and removal tests Used one non-following regular-file predicate, which supplies both properties. Verification challenges following-stat and omitted-gate variants; the two-symlink fixture alone cannot independently kill an isFile removal while a separate !isLink predicate still excludes every link.
Full native Windows/macOS unit-suite gaps in review5402862709 Acknowledged. Local macOS focused coverage and built CLI evidence are scoped separately; native Windows permission/readonly/FAT/ACL behavior is not certified by skips.
Review suite totals differ from the earlier221 assertion report The221 figure refers only to the independent parent37b sandbox report5969990910, not the whole repository suite or this new commit. Keep those source identities/counts separate.

Validation: focused44/44 and ESLint passed; independent native macOS observations and exact-source oracle evidence are summarized below. Both designs now state the ordinary-file/symlink boundary. Existing R2 F2 diagnostics deferral, F3 Windows readonly uncertainty and F4 mock-only disposition are unchanged.

Independent native macOS verification passed six built CLI startups and five compiled writer/filesystem controls, with unchanged referents and correct actual staged/final modes. The seven new link tests pass normally; following-stat mutation gives 3 passes/4 failures and omitting the regular-file gate gives 1 pass/6 failures (37 other tests unselected). Raw aggregation corrections required no CLI/test reruns.

The clean committed build/typecheck/bundle and affected/caller test gate ran once: 350 passed, 0 failed, 4 native Windows skips. Source blobs reviewed and observed before committing match this commit. This change does not add directory permissions, link following writes, owner/ACL/xattr preservation, adversarial race protection, or sandbox authorization. The original broader-mode difference is confirmed;0755 traversal alone does not establish permission to plant directory entries.

Threads: resolved 1/1 addressed inline thread; 0 unresolved threads remain.

@doudouOUC

doudouOUC commented Oct 4, 2026 •

Copy link
Copy Markdown
Collaborator Author

[codex] R4 feedback addressed and pushed in 4c8a8e1.

Feedback Action
Missing target at the new mode probe Fixed: a missing non-following entry uses default new-file permissions. Other filesystem errors still propagate. Regression uses a real deletion; removing the option produces native ENOENT.
Symmetric mode preservation Documented in both languages and pinned with ordinary 0666 staged/final assertions under umask 022 and 077. A write-bit clamp fails both cases at the added row.
Shared errno-policy refactor Remains explicitly deferred, as requested by this review; outside the permissions repair.

Independent native macOS verification: four actual built CLI startup migrations with deterministic real deletion (User/Workspace × umask 022/077) publish complete values and version metadata at 0644/0600, without chmod, backup-copy or leftover staging. Two regular-file startup migration controls retain 0666/0777. Two compiled-helper EACCES/EIO injections still propagate the exact errors and preserve original bytes/inode/mode. Those two normal controls exercise startup migration, not a direct saveSettings command; the initial labels are preserved locally and were corrected from existing raw data without reruns.

The three selected normal tests pass; removing the missing-entry option fails the new deletion case, and clamping write bits fails the two mode loops at 0666. Unselected cases are not platform skips. Prior unsupported-chmod and symlink fixes remain intact. After two consecutive clean audits and independent source/evidence review, the clean commit passed build, typecheck, bundle and six affected/caller files exactly once: 351 passed, 0 failed, 4 native Windows skips. All five observed/reviewed source blobs match this commit; independent CLI observations preceded commit and were not rerun on the new SHA.

Scope remains ordinary POSIX mode preservation, including already permissive regular files; no owner/ACL/xattr, stale-inode/hostile race, sandbox authorization or unrelated settings fixture changes. No native Windows/Linux/FAT/ACL acceptance is claimed. The earlier 221-assertion Linux sandbox report belongs to 37b137e and is not approval or a run of this SHA. Existing diagnostic and native read-only follow-ups remain separate. New CI/review must still finish; no automatic merge.

Thread handling: resolved 2/2 addressed threads; 0 unresolved threads remain, confirmed by a complete GraphQL snapshot.

中文:已批量修复新增探测点的缺失文件回归,补齐宽权限对称保留的双语决策和断言。原生 macOS 受控删除与启动迁移验证通过;最终提交检查 351 项通过、0 失败、4 项 Windows 原生测试跳过。独立观察发生在提交前,源码 blob 与本提交完全一致,未冒充新 SHA 重跑。保留 Fixes #13287 与 Refs #12417,#13288/#13290 独立跟进;继续每小时核对新 CI 与评审。

@doudouOUC

doudouOUC commented Oct 4, 2026 •

Copy link
Copy Markdown
Collaborator Author

[codex] R5 review disposition at 4c8a8e12b1cb038d4ff854a9cf1a419798c2c064. This batch records three Suggestions from review 5406624403. After approximately five feedback rounds, repository policy limits further changes to Critical fixes. Independent source review and bounded observations found no new Critical within the explicitly accepted scope, so these items are deferred separately below.

Feedback Judgment and disposition
4178029476 — historical design Agree that the old atomic-save rationale about not inheriting permissions is stale. Defer a bilingual amendment and reciprocal design links to a documentation follow-up. The current permission design and regression tests describe the shipped behavior; default new-file umask semantics and rename retry behavior remain unchanged.
4178029480 — metadata snapshots Partially agree. Independently reproduced symlink-0777 to regular-0644 replacement during staging: the reviewed writer publishes 0777 while the merge-base publishes 0644. External chmod and late-appearing-file controls match the baseline. These require concurrent replacement or permission changes expressly excluded by the contract. Defer a consistent-snapshot policy and its tests to a concurrency follow-up; a single lstat would not pin the entry through publication.
4178029487 — group access Partially agree. An owned macOS fixture changed gid 12 to 20 in both arms; mode became 0644 on the baseline and stayed 0660 on this HEAD. Identical mode bits do not promise identical reader identities. Defer group restoration, identity policy and separate-principal/setgid acceptance to a follow-up because owner/group preservation is outside this PR. Preserve the privacy fix without a world-readable fallback.

Evidence is eight native owned-fixture observations over exact Git source: six deterministic staging-window A/B observations and two gid/mode observations. No second UID performed a read or startup; EACCES/exit-52 consequences are source-based, conditional inferences, not executed acceptance. Parent gid and egid were both 20, so this fixture does not distinguish their inheritance mechanisms. No natural race frequency, attacker exploit, Linux/Windows/FAT/ACL behavior or whole-platform suite is claimed.

Source, tests and tracked designs are unchanged. The existing once-only final gate for this exact commit remains 351 passed, 0 failed and 4 native-Windows skips; it was not rerun. Earlier unsupported-chmod, stable-symlink and missing-at-probe fixes remain present. The previously recorded errno-helper and optional probe-comment Suggestions remain deferred. The review runner's platform/integration gaps and unattributed settings-fixture failures are not promoted to new PR defects or acceptance evidence.

Thread handling: all three newly replied Suggestion threads are resolved with explicit deferral records (3/3); remaining unresolved threads: 0. Resolution records disposition, not a code fix or review approval.


中文:本批次为第五轮反馈处置,源码、测试与已跟踪设计均未修改,未重复最终检查。三条建议分别延期:旧设计的双语修订与互链;并发替换/改权限时的元数据快照契约;组身份及多用户访问策略。后两项属于已明确排除的并发修改与 owner/group 范围,独立源码审查未确认新的范围内 Critical。

独立原生 macOS 观察共 8 项,仅使用自有假数据夹具;确认了替换窗口中的 mode 差异和 gid/mode 组合变化。未由第二用户执行读取或启动,也未测自然竞态频率、攻击利用或其他原生平台,因此不把条件推导写成实测 EACCES/exit-52 验收。原有私有权限修复、默认新文件策略和显式边界保持。CI 无失败,旧 CHANGES_REQUESTED 仍等待维护者新的审查结论;不会自动合并。

@wenshao

wenshao commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification: real-environment A/B at 4c8a8e12b1

Verdict: the core fix works as described on macOS and on native Linux, including against a real second local user. I found one new regression outside the covered matrix: on Linux CIFS/SMB shares mounted root-owned (no uid=), head refuses settings saves that base completes (F1). A small tested candidate fix is below. I recommend fixing F1 before merge. R3-3 and the symlink residual are reasonable follow-ups.

Both arms are real npm run bundle builds of the same checkout and differ only in packages/cli/src/utils/write-with-backup.ts, which I confirmed in the bundled chunk. base uses the merge-base 576689d073 writer. head is 4c8a8e12b1. Each case runs the shipped CLI through two entry points:

  • qwen --list-extensions: startup normalization, which adds $version.
  • qwen mcp add -s user|project: the direct saveSettings path. The PR's own evidence covered only the startup path.

Fixtures contain only a fake env.MY_TOKEN.

Results

Area Runs Outcome
macOS 26.6 APFS: umask 022/077 × existing 0600/0640/0660/0666 × User/Workspace × startup/save 40 ✅ head keeps every existing mode. base widens to 0644 (022) or narrows to 0600 (077).
macOS: new file, setuid 04640, symlink → 0600/0777 referent 16 ✅ New-file defaults are unchanged, setuid is dropped, and referents are untouched. Symlink residual below.
macOS FAT32/exFAT volumes (hdiutil, fskit) 8 ✅ chmod is a silent no-op, and saves complete in both arms.
Linux 6.8: real users alice (owner), bob (other), carol (group devs) 18 ✅ In base, bob reads the secret after a normal startup. In head, bob gets Permission denied.
Linux vfat: uid=0,umask=000 / …,quiet / uid=alice 12 ➖ No difference between arms. Forced-owner vfat already fails in base (below).
Linux CIFS (SMB 3.0): uid=0,file_mode=0777 / uid=alice 8 (+12 with candidate arm, +6 screenshot run) ❌ F1: head refuses where base saves.
Unit tests: 6 affected/caller files at head (QWEN_HOME unset) 378 pass, 4 Windows skips ✅
Trial merge with main dd82140bcd (clean, rebuilt), same 6 files 378 pass, 4 Windows skips ✅
CI at head 29 pass, 27 skipped, 0 fail ✅

macOS matrix

Linux second user

F1: Root-owned CIFS/SMB mounts reject every settings write (new regression)

Setup. A Linux project sits on an SMB share mounted the common fstab way, without uid= (file_mode=0777,dir_mode=0777). Files appear owned by root. The user can create and rename them. However, chmod on a file the user just created returns EPERM, even when the mode is unchanged.

What happens:

  • qwen mcp add -s project …: base exits 0. Head exits 1 with EPERM: operation not permitted, chmod '…/settings.json.write-XXXX/settings.json.tmp', and the settings stay unchanged.
  • Startup normalization: head exits 0 but never writes $version. The error only goes to the debug log, so the write is retried on every launch.
  • Control: with uid=alice on the same share, both arms pass.

I reproduced this three times: two matrix runs and the screenshot run.

Why base works. fs.copyFileSync (libuv uv_fs_copyfile) also fchmods its destination. On Linux, libuv deliberately ignores that EPERM when the destination is on SMB/SMB2/CIFS (libuv v1.52.1 src/unix/fs.c L1340-L1353); f_type here is 0xFE534D42 (smb2). The new chmodSync tolerates only ENOSYS/ENOTSUP, so the save now fails one step before the backup copy, which would have succeeded.

CIFS

Candidate fix (tested). On ENOSYS/ENOTSUP/EPERM, publish only when the staged file grants nothing beyond the captured mode. The staged file is created with mode, so it gets mode & ~umask; on POSIX filesystems the check therefore always holds. A filesystem that ignores the creation mode and reports broader bits still refuses. This also makes the unsupported-chmod path verify the "never broader than the existing file" property instead of assuming it.

Candidate results:

  • On root-owned CIFS, mcp add exits 0 and startup writes $version, matching base.
  • All 32 macOS cases and all 15 Linux head cases give identical results to head, so the privacy fix is kept.
  • The PR suite passes 46/46 with the test update below.
  • Mutation checks: dropping the subset guard fails only the new "refuses EPERM when broader" test. Head source with the new tests fails only the EPERM publish row.
  • eslint, prettier and tsc --noEmit (cli) are clean.
Candidate patch (source + tests)
diff --git a/packages/cli/src/utils/write-with-backup.ts b/packages/cli/src/utils/write-with-backup.ts
@@ -92,11 +92,15 @@ export function writeWithBackupSync(
       try {
         fs.chmodSync(tempPath, mode);
       } catch (error) {
-        // Filesystems without POSIX permissions may not support chmod.
+        // Filesystems without POSIX permissions may not support chmod, and
+        // root-owned CIFS/SMB mounts reject it with EPERM (libuv's copyfile
+        // tolerates that too). Publish only when the staged entry grants
+        // nothing beyond the existing permissions.
+        const code =
+          error instanceof Error && 'code' in error ? error.code : undefined;
         if (
-          !(error instanceof Error) ||
-          !('code' in error) ||
-          (error.code !== 'ENOSYS' && error.code !== 'ENOTSUP')
+          (code !== 'ENOSYS' && code !== 'ENOTSUP' && code !== 'EPERM') ||
+          (fs.statSync(tempPath).mode & 0o777 & ~mode) !== 0
         ) {
           throw error;
         }
diff --git a/packages/cli/src/utils/write-with-backup.test.ts b/packages/cli/src/utils/write-with-backup.test.ts
@@ -87,8 +87,8 @@ describe('writeWithBackup', () => {
-  it.each(['ENOSYS', 'ENOTSUP'])(
-    'publishes when chmod is unsupported (%s)',
+  it.each(['ENOSYS', 'ENOTSUP', 'EPERM'])(
+    'publishes when chmod is unsupported or refused (%s)',
@@ -265,7 +265,29 @@ describe('writeWithBackup', () => {
-    it.each([undefined, 'EPERM', 'EACCES', 'EIO', 'EROFS'])(
+    it('refuses EPERM when the staged file is broader than the target', () => {
+      nativeFs.writeFileSync(targetPath, 'old');
+      nativeFs.chmodSync(targetPath, 0o600);
+      // Simulates a filesystem that ignores the requested creation mode.
+      vi.mocked(fs.writeFileSync).mockImplementation((file, data, options) => {
+        nativeFs.writeFileSync(file, data, options);
+        nativeFs.chmodSync(file as string, 0o644);
+      });
+      const failure = Object.assign(new Error('chmod failed'), {
+        code: 'EPERM',
+      });
+      vi.mocked(fs.chmodSync).mockImplementation(() => {
+        throw failure;
+      });
+
+      expect(() => writeWithBackupSync(targetPath, 'new')).toThrow(failure);
+      expect(fs.readFileSync(targetPath, 'utf8')).toBe('old');
+      expect(fs.statSync(targetPath).mode & 0o777).toBe(0o600);
+      expect(fs.readdirSync(tempDir)).toEqual(['settings.json']);
+      expect(fs.renameSync).not.toHaveBeenCalled();
+    });
+
+    it.each([undefined, 'EACCES', 'EIO', 'EROFS'])(
       'preserves the target when staging chmod fails (%s)',

Other observations

  • R3-3 now confirmed with a real second user (not previously executed). The file is 0640 alice:devs in a plain directory, umask 022.

    • base republishes it as 0644 alice:alice. carol keeps access only because everyone can now read the file.
    • head republishes it as 0640 alice:alice, and carol (in devs) gets Permission denied.
    • In a setgid directory, head keeps alice:devs and carol can read. Under umask 077, base also locks carol out.

    This seems an acceptable trade: the only access lost came from world-readability. It deserves one sentence in the design doc but is not blocking.

  • Symlinked (dotfile-managed) settings still become world-readable after a save, in both arms. A link to a 0600 referent is replaced by a regular 0644 file that contains the secret, on both macOS and Linux; on Linux, bob reads it. This is the documented default for links and not a regression. It does mean fix(cli): preserve private settings permissions during rewrites #13287's privacy goal does not cover dotfile-managed settings. A possible follow-up: inherit the referent's mode capped at the default (referent & 0o666 & ~umask). That keeps 0600 private without reopening R1-1's widening.

  • Forced-owner vfat already fails in base. On vfat uid=0,umask=000, base fails at the backup copy (Failed to backup existing file: EPERM … copyfile, because libuv ignores the error only on CIFS/SMB) and head fails at chmod. Startup normalization silently fails in both arms. quiet and uid=alice mounts pass in both. This is not a regression from this PR.

  • macOS FAT32/exFAT (fskit msdos/exfat, noowners): chmod succeeds without effect and new files report 0700. Behavior does not change.

Not covered

  • Native Windows. PR CI does not run the Windows leg. From reading the source, the code there only toggles the read-only attribute, and a read-only target fails rename in both arms; I did not execute this.
  • The macOS SMB client (smbfs), NFS root_squash/all_squash, and ACL/xattr preservation.

Merge note

reviewDecision is CHANGES_REQUESTED because of the bot's rounds on b30d1cdc / bc2d5837. Only an APPROVE from the bot itself, a dismissal of those reviews, or a human approval clears it.

Environment and method
  • macOS: 26.6.2 arm64, Node 24.18.1. FAT32/exFAT volumes were created with hdiutil and mounted -nobrowse.
  • Linux: colima Ubuntu kernel 6.8.0-117 aarch64, node:24-bookworm (Node 24.19.0), privileged container. Users were created inside the container and fixtures live on tmpfs.
  • vfat: a 64 MB FAT32 image formatted with newfs_msdos and loop-mounted with the options shown.
  • CIFS: Samba 4.19.5 serving a guest share, mounted with -o sec=none,vers=3.0,file_mode=0777,dir_mode=0777. The control mount used uid=1001,gid=1001,file_mode=0644,dir_mode=0755. For the screenshot run, smbd ran inside a --network none container.
  • Each case:
    • Setup: fresh HOME and workspace; QWEN_CODE_SYSTEM_SETTINGS_PATH/QWEN_CODE_SYSTEM_DEFAULTS_PATH redirected; umask set in the child shell.
    • Checks: exit code; lstat type, mode and owner; existing values kept; $version or probe-srv written; no settings.json.write-* leftovers. On Linux, also su bob|carol -c cat.
  • Arms: the head bundle contains the lstat-gated chmodSync(tempPath, mode); the base bundle has neither. The candidate arm is head plus the patch above.
中文版

维护者验证:4c8a8e12b1 真实环境 A/B

结论:核心修复在 macOS 和原生 Linux 上与描述一致,并用另一个真实本地用户验证过。在 PR 已覆盖的矩阵之外,我发现一个新回归:Linux 上以 root 属主挂载(不带 uid=)的 CIFS/SMB 共享里,base 能完成的设置保存,head 会拒绝(F1)。下面附有一个经过测试的小候选修复。建议合并前修掉 F1;R3-3 和符号链接遗留问题可以作为后续跟进。

两个臂都是同一 checkout 真实 npm run bundle 出来的构建,只有 packages/cli/src/utils/write-with-backup.ts 不同,我已在打包后的 chunk 中核对过。base 使用 merge-base 576689d073 的 writer,head 是 4c8a8e12b1。每个用例都通过发布形态的 CLI 走两个入口:

  • qwen --list-extensions:启动规范化,会写入 $version。
  • qwen mcp add -s user|project:直接走 saveSettings。PR 自己的证据只覆盖了启动路径。

夹具里只有假的 env.MY_TOKEN。

截图与候选补丁见上方英文部分。

结果

范围 次数 结论
macOS 26.6 APFS:umask 022/077 × 已有 0600/0640/0660/0666 × User/Workspace × 启动/保存 40 ✅ head 保持每个原有 mode。base 在 022 下放宽成 0644,在 077 下收窄成 0600。
macOS:新文件、setuid 04640、指向 0600/0777 的符号链接 16 ✅ 新文件默认值不变,setuid 被去掉,链接目标未改动。符号链接遗留问题见下文。
macOS FAT32/exFAT 卷(hdiutil,fskit) 8 ✅ chmod 静默无效,两臂都能保存。
Linux 6.8:真实用户 alice(属主)、bob(其他人)、carol(devs 组) 18 ✅ base 下,普通启动后 bob 能读到假密钥。head 下 bob 得到 Permission denied。
Linux vfat:uid=0,umask=000 / …,quiet / uid=alice 12 ➖ 两臂无差异。强制属主的 vfat 在 base 就已失败(见下文)。
Linux CIFS(SMB 3.0):uid=0,file_mode=0777 / uid=alice 8(+ 含候选臂 12 次,+ 截图 6 次) ❌ F1:base 能保存,head 拒绝。
单测:head 上 6 个受影响/调用方测试文件(未设 QWEN_HOME) 378 通过,4 个 Windows 跳过 ✅
与 main dd82140bcd 试合并(无冲突,重新构建),同样 6 个文件 378 通过,4 个 Windows 跳过 ✅
head 的 CI 29 通过,27 跳过,0 失败 ✅

F1:root 属主的 CIFS/SMB 挂载上,所有设置写入都被拒(新回归)

场景。 Linux 项目放在 SMB 共享上,挂载方式是常见的 fstab 写法,不带 uid=(file_mode=0777,dir_mode=0777)。文件显示属主为 root。用户可以创建和 rename 文件,但对自己刚创建的文件 chmod 会返回 EPERM,即使 mode 不变也一样。

现象:

  • qwen mcp add -s project …:base exit 0。head exit 1,报 EPERM: operation not permitted, chmod '…/settings.json.write-XXXX/settings.json.tmp',设置保持不变。
  • 启动规范化:head exit 0,但始终不写 $version。错误只进 debug 日志,所以每次启动都会重试。
  • 对照:同一共享改用 uid=alice 挂载时,两臂都通过。

我复现了三次:两次矩阵运行,加一次截图运行。

base 为什么能成功。 fs.copyFileSync(libuv uv_fs_copyfile)也会对目标 fchmod。在 Linux 上,目标位于 SMB/SMB2/CIFS 时 libuv 会有意忽略这个 EPERM(见上方 libuv 源码链接);这里的 f_type 是 0xFE534D42(smb2)。新增的 chmodSync 只容忍 ENOSYS/ENOTSUP,所以保存在备份复制之前一步就失败了,而备份复制本来能成功。

候选修复(已测)。 遇到 ENOSYS/ENOTSUP/EPERM 时,只有暂存文件的权限位不超出捕获的 mode 才发布。暂存文件用 mode 创建,得到的是 mode & ~umask,所以在 POSIX 文件系统上这个检查总是成立。对于忽略创建 mode、报告更宽权限位的文件系统,仍然拒绝。这样「不支持 chmod」那条路径也从「假定不会更宽」变成了「实际检查不比原文件更宽」。

候选修复的结果:

  • root 属主 CIFS 上,mcp add exit 0,启动会写入 $version,与 base 一致。
  • macOS 32 例、Linux 15 例的结果都与 head 完全一致,隐私修复得以保留。
  • 加上测试改动后,PR 测试套件 46/46 通过。
  • 变异检查:去掉子集守卫后,只有新增的「broader 时拒绝 EPERM」测试失败;head 源码配新测试,只有 EPERM 发布那一行失败。
  • eslint、prettier、cli 的 tsc --noEmit 均通过。

其他观察

  • R3-3 首次用真实第二用户确认(此前未执行)。 文件是普通目录中的 0640 alice:devs,umask 022。

    • base 重新发布为 0644 alice:alice,carol 仍能读,只是因为现在所有人都能读。
    • head 重新发布为 0640 alice:alice,devs 组的 carol 得到 Permission denied。
    • 在 setgid 目录中,head 保持 alice:devs,carol 可以读。umask 077 下 base 也会把 carol 拒之门外。

    这个取舍看起来可以接受:失去的访问权只来自「所有人可读」。值得在设计文档里补一句,但不阻塞合并。

  • 通过符号链接管理(dotfiles)的设置,保存后在两臂中都会变成所有人可读。 指向 0600 文件的链接会被替换为一个含密钥的 0644 普通文件,macOS 和 Linux 都如此;Linux 上 bob 能读到。这是文档写明的链接默认行为,不是回归。但这意味着 fix(cli): preserve private settings permissions during rewrites #13287 的隐私目标没有覆盖 dotfiles 管理的设置。可以后续跟进:继承链接目标的 mode,但以默认值为上限(referent & 0o666 & ~umask)。这样 0600 能保持私有,也不会重新引入 R1-1 的放宽问题。

  • 强制属主的 vfat 在 base 就已失败。 在 vfat uid=0,umask=000 上,base 在备份复制处失败(Failed to backup existing file: EPERM … copyfile,因为 libuv 只在 CIFS/SMB 上忽略这个错误),head 在 chmod 处失败。两臂的启动规范化都会静默失败。quiet 和 uid=alice 挂载两臂都通过。这不是本 PR 引入的回归。

  • macOS FAT32/exFAT(fskit msdos/exfat,noowners):chmod 成功但不生效,新文件显示 0700。行为不变。

未覆盖

  • 原生 Windows。PR 的 CI 不跑 Windows 腿。按源码阅读,Windows 上这段代码只会切换只读属性,只读目标在两臂中都会 rename 失败;我没有实际执行。
  • macOS 的 SMB 客户端(smbfs)、NFS root_squash/all_squash,以及 ACL/xattr 的保留。

合并说明

reviewDecision 处于 CHANGES_REQUESTED,来自 bot 在 b30d1cdc / bc2d5837 上的评审。只有 bot 自己 APPROVE、dismiss 这些评审,或由人工批准,才能解除。

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

[codex] @wenshao Fixed F1 in e477bac. The chmod fallback now accepts ENOSYS/ENOTSUP/EPERM only when the staged file’s reported ordinary bits are a subset of the originally captured mode; broader stages and unrelated errors still refuse before backup/publication.

Independent reproduction confirmed the EPERM save/startup regression at 4c8a8e1 using owned macOS fixtures and controlled errno injection. The actual candidate bundle passed 23 CLI scenarios (including direct project saves and startup normalization) plus 2 built-helper error-identity controls. Your native Linux CIFS observations remain separately attributed external evidence; these local observations preceded commit, with the same four source blobs, and are not described as a new-SHA native CIFS run. The exact clean-commit build/typecheck/bundle/six-file gate passed once: 356 passed, 0 failed, 4 native Windows skips. Frozen tests also detect the old EPERM path and removal of the mode ceiling. Two clean self-audits and independent source/evidence review found no further in-scope Critical.

The confirmed group-reader identity and symlink-managed settings residuals remain follow-ups as recorded in R5; no chown or broader-readable fallback is added. Historical design clarification and concurrent metadata policy also remain deferred. Forced-owner vfat already failing in base, silent chmod on FAT/exFAT, and native Windows/macOS SMB/NFS/ACL acceptance are not claimed fixed or locally verified here.

Reply to #13293 (comment).

@doudouOUC

doudouOUC commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator Author

[codex] R6: address the maintainer’s in-scope EPERM save regression in e477bac.

Feedback Action Result
F1: CIFS/SMB chmod EPERM aborts ordinary saves and version normalization Fixed Recognized errors can publish equal/narrower reported staged permissions; broader modes and unrelated errors refuse before backup/rename.
Unsupported chmod could rely on creation mode without checking reported bits Fixed in the same batch The ceiling applies to ENOSYS/ENOTSUP as well as EPERM.
Group-reader identity, symlink-managed settings privacy, historical design and concurrent metadata policy Deferred Existing R5 follow-up disposition remains; no ownership, ACL/xattr or race-contract expansion.
Forced-owner vfat failure and native platform gaps No code change Baseline failure and external/native coverage remain separately classified.

Validation: actual built CLI 23 scenarios + 2 helper checks, 50 writer tests; frozen normal 12 pass, old-head 5 expected failures, omitted guard 3 expected failures (38 deselected each). Exact final commit gate: build/typecheck/bundle and six affected/caller files, 356 passed, 0 failed, 4 native Windows skips. Two clean self-audits and independent source/evidence review complete. CLI evidence was observed before commit; all four source blobs match. Native Linux CIFS is maintainer evidence, not a local run of this commit. R6 came through a top-level comment, so there are no new inline threads to resolve; previous 6/6 threads remain resolved, 0 unresolved.

CI will be checked once after the push. A clean local gate and our response do not replace maintainer approval; the old CHANGES_REQUESTED disposition still requires a new approval or dismissal. Existing deferred items are recorded in the R5 summary.

Post-push bounded snapshot: OPEN/MERGEABLE/BLOCKED, old CHANGES_REQUESTED awaiting new maintainer approval; all 6 threads resolved, 0 unresolved. The full rollup has 8 success, 8 skipped, 18 pending, 1 failure. The Lint job failed at “Check lint gate freshness”, before dependency install/ESLint/Prettier: main's lint gate commit 6b878e1a04ec is not incorporated in this branch. The API containment compare confirms divergence. This is deterministic and no flaky retry was recommended or spent (0/3). Per the one-round schedule, the next hourly run will safely synchronize main and validate the resulting commit. This is separate from the passed R6 local gate above.

中文:推送后完整快照为 8 成功、8 跳过、18 等待、1 失败,PR 开放且可合并,仍等待新批准,6/6 线程已解决、剩余 0。Lint 在实际 lint 前被基线规则过期门禁拒绝;下一小时安全同步 main 后重新验证实际提交。未消耗重跑预算,仍为 0/3。

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

[codex] Main synchronization pushed in 9fa9393 (parents: R6 e477bac and main 8648508). The lint freshness failure occurred before dependency installation or actual lint execution; this normal merge incorporates the updated gate without changing settings-permission logic or bypassing lint rules.

Item Disposition
Run 37326669711 / job 111819271220: stale lint gate Merged main; current main gate-file commits are contained by this HEAD. New remote CI must validate the result. No flaky rerun budget used.
PR scope and R6 F1 The same five permission-related PR files remain byte/mode identical to e477; all 501 incoming paths match pinned main, with zero overlaps. Existing F1 reply is retained rather than duplicated.
Validation of the actual merge commit Build, typecheck and bundle each passed once; six affected/caller test files: 356 passed, 0 failed, 4 native Windows skips. Two clean self-audits and independent scoped review found no merge issue.
Actual built CLI 0.25.0 Four representative cases passed after this commit: EPERM project save, startup metadata persistence, broader-mode refusal preserving original bytes/inode/0640, and native 0640 preservation under umask 077. Entry/chunk/helper and source identities remained stable.
Deferred boundaries Owner/group, ACL/xattr, concurrency and earlier recorded Suggestions remain deferred; no new permissions contract is added.

The four CLI cases ran on owned macOS fixtures using Node 22.14.0; errno/mode changes were controlled simulations, not native CIFS acceptance. Final build/tests used Node 24.19.0. Prior R6 observations retain their original precommit/source binding and are not reclassified as runs of this merge. Detailed evidence is appended to the existing E2E report.

Threads: no new inline discussions; the existing 6/6 remain resolved, 0 unresolved. CI completion and our comments do not replace maintainer approval; the old CHANGES_REQUESTED review still needs a new approval or dismissal.

中文:已正常合并 main 并推送 9fa9393,修复实际 lint 前的基线过期门禁,不手改 lint 规则。5 个 PR 文件与 R6 完全一致,上游 501 个路径均匹配 main、无重叠;实际合并提交最终检查各跑一次,356 通过、0 失败、4 项原生 Windows 跳过。4 项实际构建后 CLI 代表性验证通过,错误注入与原生 CIFS 证据分开记录。旧 F1 回复不重复,现有 6/6 讨论已解决、剩余 0;原延期项保持延期,等待新 HEAD 的 CI 和维护者批准。

@doudouOUC

Copy link
Copy Markdown
Collaborator Author

[codex] R7 disposition for review 5419097471. No new in-scope Critical was verified; both new Suggestions are deferred under the repository's convergence rule. HEAD remains 9fa93938de90e19fb69e68c815882547dc067c1d (tree 08be8015ca4f7797ed4ae93993b6d3c578433d1e). There is no source/test edit, commit, push, gate repetition, or CI rerun in this batch.

Item Disposition Basis and follow-up
4187525663 — failed staged-mode probe diagnostic Deferred; reply The stat error supersedes chmod EPERM. The two owned observations preserve the target and refuse before backup/rename. Original-error reporting and its regression test remain a diagnostic follow-up; unreadable/missing stage permissions must still refuse publication.
4187525686 — settings/JSONC permission assertions Deferred; reply Caller tests lack these assertions, while current JSONC delegates to the protected writer. Add caller-level permission/guarded-EPERM coverage in follow-up; removing only the type gate need not fail an ordinary 0600 oracle, so symlink/default behavior needs a separate oracle.
Review's read-only creation-mode fixture note Existing reviewer deferral retained Coverage/fixture work, not a demonstrated new product failure or a request for this round. Prior documented concurrency, owner/group and design/errno deferrals also remain unchanged.

The two new observations used the existing exact-bound compiled helper on macOS arm64/Node 22.14.0/uid501: controlled chmod EPERM followed by real staged unlink/native stat ENOENT, and controlled chmod EPERM followed by controlled staged-stat EIO with the stage retained. In both, the exact stat error escapes, original bytes/inode/0640 remain unchanged, no backup/rename occurs, and staging is removed. This is diagnostic-boundary evidence, not native CIFS acceptance, a CLI run, or a code fix.

The existing 356 passed / 4 native Windows skips result belongs to the six affected/caller test files run once on clean 9fa93938; the prior four actual final-CLI observations retain their original binding. They neither claim whole-repository coverage nor close the missing caller-unit assertions. The reviewer's differently scoped suite counts, skipped test legs and inconclusive probe are separate evidence, not our final gate or approval.

This round's complete GH rollup snapshot has 30 SUCCESS, 27 SKIPPED, 0 waiting and 0 failed; the earlier watcher separately reported 29 passed/0 waiting/0 failed. The PR is OPEN/MERGEABLE/BLOCKED with the prior CHANGES_REQUESTED awaiting a new maintainer approval or dismissal. No flaky retry was used (current SHA 0/3, history preserved). Resolving these discussions records their explicit deferral, not a claim that the gaps were fixed. Hourly bounded monitoring continues; the PR will not be auto-merged.

中文:R7 两条新建议已逐条核实并回复延期:暂存权限探测的原异常诊断,以及 settings/JSONC 层的权限回归断言。未确认新的范围内 Critical;仅补充两项自有 macOS helper 诊断边界观察,原文件内容/inode/0640 保持、未备份或发布、暂存清理;这是受控注入及真实 unlink 后的原生 ENOENT,非原生 CIFS、CLI 验收或代码修复。已有六文件 356 通过/4 Windows 跳过及四项真实最终 CLI 观察保持原绑定,与复审者不同范围的测试结果分开。本轮不改代码、不重跑 gate/CI,线程解决仅记录延期;完整快照 CI 无失败,仍待维护者新的批准/撤销旧 CHANGES_REQUESTED,继续每小时有界检查,不自动合并。

@wenshao

wenshao commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator

Maintainer re-verification (R2) at 9fa93938de

Verdict: F1 is fixed. I verified it natively on the same root-owned CIFS mount. A chmodSync trace shows the new fallback branch working as designed, including a native refusal of a broader staged mode on vfat. Every R1 matrix reproduces case-for-case, and I found no new issue. From a verification standpoint this is ready to merge. The deferred suggestions (R3-3, symlinked settings, R4-1, R4-2) remain follow-ups.

What changed since R1:

  • e477bac4b9:
    • When chmod fails with ENOSYS/ENOTSUP/EPERM, the writer now publishes only if the staged file's ordinary permission bits are within the captured mode (R1 candidate semantics).
    • New tests cover the EPERM rows and broader-stage refusal for all three codes.
    • Both design notes are updated.
  • 9fa93938de: merges main. git show --remerge-diff is empty, and the five PR files are byte-identical to e477bac4b9.

The arms are built the same way as in R1. Both are real npm run bundle builds of 9fa93938de (CLI 0.25.0). base swaps in the merge-base 864850816e writer, which is identical to R1's base writer. head is unmodified. I confirmed that the head chunk contains the EPERM code and the & ~mode guard, and that base has no chmodSync.

Results

Check Runs Result
F1: CIFS SMB3 uid=0,file_mode=0777, startup + mcp add -s project 4 + 4 traced ✅ Fixed. Head writes $version and saves, same as base. Trace: chmodSync → EPERM, staged 777 within 777 → tolerated.
CIFS uid=alice control 4 ✅ Both arms pass.
CIFS, existing file with the DOS read-only attribute (0555) 4 + 2 traced ✅ Head saves and keeps read-only: the stage is created with mode 0555, so the share marks it read-only. Base saves but publishes 0777.
vfat uid=0,umask=000 (fails in base already) 4 + 2 traced ➖ Both arms now fail identically at the backup copy (EPERM … copyfile). Head first tolerates chmod EPERM (777 within 777).
vfat, existing read-only file (0555) 4 traced ✅ Native refusal: staged 777 is broader than 555, so head rethrows the original chmod EPERM. The file is unchanged and no staging is left. Base fails later, at the copy.
macOS 26.6 matrix (APFS 022/077 × modes × User/Workspace × startup/save, new file, setuid, symlinks, FAT32/exFAT) 64 ✅ Every case matches R1 (exit, type, mode, content, leftovers).
Linux real-user matrix (second-UID privacy, R3-3 group cases, symlink, vfat variants) 30 ✅ Every case matches R1.
Unit tests: 6 affected/caller files at head (QWEN_HOME unset) 383 pass, 4 Windows skips ✅
Mutation checks on the writer 3 mutants ✅ Each is killed by 3 tests: drop EPERM from the codes; drop the subset guard; compare against 0o777 instead of the captured mode.
Trial merge with current main 69d5db2ff2 (clean, rebuilt), same 6 files 383 pass, 4 Windows skips ✅
CI at head 29 pass, 27 skipped, 0 fail ✅

R2 CIFS/vfat with chmod trace

The R1 figures (macOS matrix, Linux second user) still describe this head exactly, because every case repeated with identical results.

Notes

  • The fix also applies the subset check to ENOSYS/ENOTSUP, which earlier assumed the creation mode was honored. That makes the "never broader than the existing file" property checked rather than assumed, and the new refusal rows pin it.
  • R4-1 (a failing staged-mode stat masks the chmod error): agree it is non-blocking. The path still fails closed, and in every CIFS/vfat run here the probe succeeded. Reporting the original error is a diagnostics follow-up.
  • R4-2 (no permission assertions at the settings layer): agree it is a follow-up. The end-to-end runs above exercise both settings-layer callers (startup normalization and saveSettings via mcp add) on real filesystems, but they don't replace unit coverage there.
  • R3-3 (group identity) and the symlinked-settings residual are unchanged from R1 and remain deferred as recorded.

Not covered

Same as R1: native Windows, the macOS SMB client (smbfs), NFS squash modes, and ACL/xattr preservation.

Merge note

The remaining blocker is procedural. reviewDecision is still CHANGES_REQUESTED from the bot's rounds on b30d1cdc / bc2d5837. It needs a human approval, or a dismissal of those reviews.

Method
  • Environment:
    • macOS 26.6.2 arm64 with Node 24.18.1.
    • Linux: colima kernel 6.8.0-117 aarch64, node:24-bookworm (Node 24.19.0), privileged containers with fixtures on tmpfs.
    • Samba 4.19.5 ran via chroot inside a --network none container, so the share was reachable only from inside that container.
    • Mounts: -o sec=none,vers=3.0,file_mode=0777,dir_mode=0777 (root-owned) and uid=1001,gid=1001,file_mode=0644,dir_mode=0755 (control). vfat was a loop-mounted FAT32 image.
  • Read-only fixtures: root ran chmod 0555 on the existing settings file. On CIFS this sets the DOS read-only attribute; on vfat it sets ATTR_RO.
  • Trace: NODE_OPTIONS=--require trace-chmod.cjs wraps fs.chmodSync and calls module.syncBuiltinESMExports(). It logs each failure's code and the staged file's mode, then rethrows unchanged.
  • Each case:
    • Setup: fresh HOME and workspace, with the system-settings paths redirected.
    • Checks: exit code; lstat mode/owner; content; $version or probe-srv; leftover settings.json.write-* directories; and whether the original bytes survived refused writes.
中文版

维护者复验(R2):9fa93938de

结论:F1 已修复,我在同一个 root 属主的 CIFS 挂载上实测确认。chmodSync 跟踪显示新的容错分支完全按设计工作,包括在 vfat 上真实触发「暂存权限更宽 → 拒绝」。R1 的全部矩阵逐例复现、结果一致,没有发现新问题。从验证角度看可以合并。已延期的建议(R3-3、符号链接设置、R4-1、R4-2)仍作为后续跟进。

自 R1 以来的变化:

  • e477bac4b9:
    • chmod 报 ENOSYS/ENOTSUP/EPERM 时,只有暂存文件的普通权限位不超出捕获的 mode 才发布(与 R1 候选补丁语义一致)。
    • 新增测试覆盖三种错误码的 EPERM 行和「暂存更宽时拒绝」。
    • 中英文设计文档同步更新。
  • 9fa93938de:合并 main。git show --remerge-diff 为空,5 个 PR 文件与 e477bac4b9 逐字节一致。

两个臂的构建方式与 R1 相同,都是 9fa93938de 的真实 npm run bundle 构建(CLI 0.25.0)。base 换回 merge-base 864850816e 的 writer,与 R1 的 base writer 完全相同。head 未做改动。我核对过:head 的 chunk 含 EPERM 和 & ~mode 守卫,base 中没有 chmodSync。

结果

检查 次数 结果
F1:CIFS SMB3 uid=0,file_mode=0777,启动 + mcp add -s project 4 + 4(带跟踪) ✅ 已修复。head 写入 $version 并保存成功,与 base 一致。跟踪:chmodSync → EPERM,暂存 777 未超出 777 → 容忍。
CIFS uid=alice 对照 4 ✅ 两臂都通过。
CIFS 上带 DOS 只读属性(0555)的已有文件 4 + 2(带跟踪) ✅ head 保存成功并保留只读:暂存文件用 mode 0555 创建,共享随之把它标为只读。base 能保存,但发布成 0777。
vfat uid=0,umask=000(base 本来就失败) 4 + 2(带跟踪) ➖ 两臂现在都在备份复制处以相同错误失败(EPERM … copyfile)。head 会先容忍 chmod EPERM(777 未超出 777)。
vfat 上的只读已有文件(0555) 4(带跟踪) ✅ 真实拒绝:暂存 777 宽于 555,head 重新抛出原始 chmod EPERM。原文件不变,没有暂存残留。base 在稍后的复制步骤失败。
macOS 26.6 矩阵(APFS 022/077 × 各种 mode × User/Workspace × 启动/保存、新文件、setuid、符号链接、FAT32/exFAT) 64 ✅ 每个用例都与 R1 一致(退出码、类型、mode、内容、残留)。
Linux 真实用户矩阵(第二用户隐私、R3-3 组用例、符号链接、各种 vfat 挂载) 30 ✅ 每个用例都与 R1 一致。
单测:head 上 6 个受影响/调用方测试文件(未设 QWEN_HOME) 383 通过,4 个 Windows 跳过 ✅
writer 变异检查 3 个变异体 ✅ 每个都被 3 个测试杀死:去掉 EPERM;去掉子集守卫;改为与 0o777 而非捕获 mode 比较。
与当前 main 69d5db2ff2 试合并(无冲突,已重新构建),同样 6 个文件 383 通过,4 个 Windows 跳过 ✅
head 的 CI 29 通过,27 跳过,0 失败 ✅

R1 的两张图(macOS 矩阵、Linux 第二用户)仍然准确描述这个 head,因为每个用例重复运行后结果完全相同。

说明

  • 修复也对 ENOSYS/ENOTSUP 应用了子集检查;此前这条路径假定创建 mode 一定生效。这样「不比原文件更宽」从假定变成了实际检查,新增的拒绝用例把它钉住了。
  • R4-1(暂存 mode 的 stat 探针失败时,会掩盖 chmod 错误):同意不阻塞合并。该路径仍然失败即关闭,而且这里所有 CIFS/vfat 运行中探针都成功了。报出原始错误属于诊断类后续改进。
  • R4-2(settings 层缺少权限断言):同意作为后续。上面的端到端运行在真实文件系统上覆盖了 settings 层的两个调用方(启动规范化,以及经 mcp add 的 saveSettings),但不能代替那一层的单测覆盖。
  • R3-3(组身份)和符号链接设置的遗留问题与 R1 相同,仍按记录延期。

未覆盖

与 R1 相同:原生 Windows、macOS 的 SMB 客户端(smbfs)、NFS 的 squash 模式、ACL/xattr 保留。

合并说明

剩下的阻塞只是流程问题:reviewDecision 仍是 bot 在 b30d1cdc / bc2d5837 上留下的 CHANGES_REQUESTED,需要人工批准,或 dismiss 那些评审。

@wenshao

wenshao commented Oct 6, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@wenshao
wenshao enabled auto-merge October 6, 2026 03:53
@wenshao
wenshao added this pull request to the merge queue Oct 6, 2026
Merged via the queue into main with commit 85eacd0 Oct 6, 2026
177 checks passed
@doudouOUC
doudouOUC deleted the codex/preserve-settings-permissions branch October 6, 2026 04:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/self-reported The linked issue was opened by the PR author (self-reported)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(cli): preserve private settings permissions during rewrites

2 participants