Skip to content

fix(#457): opencode rename pre-create newDir at 0700 before cpSync (close umask 022 timing window) - #483

Merged
vansin merged 1 commit into
mainfrom
fix/457-opencode-rename-umask
Jul 29, 2026
Merged

fix(#457): opencode rename pre-create newDir at 0700 before cpSync (close umask 022 timing window)#483
vansin merged 1 commit into
mainfrom
fix/457-opencode-rename-umask

Conversation

@vansin

@vansin vansin commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #457 — under standard umask 022 (Docker/CI/most shells), anet node rename on any opencode-cli node fails PHASE 1 with:

rename PHASE 1 failed: OpenCode preset refuses node workDir at .../<new-alias>:
directory mode must be 0700 — rolling back

Root cause (agent-network/bin/cli.ts:5911): cpSync(oldDir, newDir, {recursive:true}) creates the top-level newDir with default umask permissions (0755 under umask 022). The subsequent opencode-preset predjection (assertPrivateDirectory at opencode-preset.ts:190-191) rejects any mode other than 0o700 — throw and rollback.

Fix (+8/-0 lines, one-hunk): pre-create newDir with mkdirSync(newDir, {mode:0o700, recursive:false}) before cpSync. Node 20/22 fs.cp preserves the mode of an existing dest directory (validated empirically; see evidence). No post-cpSync chmod (that pattern would be TOCTOU-flavored and defeat the预检's purpose).

Test plan

Verify 1 — witnessed_red → green (cpSync semantics)

Verify 2 — 5 fast + 5 slow (bash -x) iterations

  • BASELINE: 0/5 fast pass_700, 0/5 slow pass_700 (all 0755, consistent, no timing window)
  • FIXED: 5/5 fast pass_700, 5/5 slow pass_700 (all 0700, deterministic)

Verify 6 — 0755 source directory (real-world scenario for pre-fix nodes)

  • Src=0755 + fix → dst=0700 across 5 iterations ✓
  • Sub-file modes (config.json 0600, session.jsonl 0644) preserved
  • Src unchanged (0755)

Verify — rollback safety (mkdirSync EEXIST)

  • Pre-plant stale newDir → mkdirSync throws EEXIST → PHASE 1 try/catch rolls back
  • Src untouched, stale dst preserved for operator inspection

Verify — non-opencode regression (analytical + semantic)

  • grep -rn 0o755 on agent-network src returns only descriptive comments (no code path asserts newDir mode == 0755)
  • Sub-file/subdir modes byte-identical to baseline for any runtime
  • 0700 = owner rwx (same anet-managed daemon owner) — functionally equivalent for claude-code-cli / codex-sdk / codex-app-server / grok-build-acp
  • Security-positive change (tighter perms), not regression

Verify 3 — test384 full L8 ⚠️ NOT ATTEMPTED

Verify 4 — Docker only

  • All evidence generated inside p457-baseline:test (test384 Dockerfile), never touched host. Umask 022 explicitly printed in evidence.

Evidence

Evidence tar.gz: /tmp/p457-evidence-20260729T131528Z.tar.gz
sha256: 668e7fa3fc8a45824e8e2f758f0a3cb3a6a78758b056df5f9bfb998f806f7ffb

Contents:

  • 00-provenance-manifest.md — HEAD SHA, tree hash, status--porcelain=0, image sha256, test switches (none)
  • cpsync-semantics.log — direct Node fs.cpSync mode preservation proof
  • iterations-fast-slow.log — 5+5 iteration matrix baseline vs fixed
  • rollback-test.log — EEXIST rollback safety
  • regression-non-opencode.log — non-opencode paths unaffected
  • rename-repro.sh / rename-repro-with-hub.sh — full-flow repros (hub setup flake noted)

Timing window note

The issue reports "run1/run3 fail, run2 (bash -x) pass" — my BASELINE cpSync repro shows deterministic 0755 (5/5 fast + 5/5 slow all fail same way). The reported inconsistency was likely anet-side (a race between the failed rename and OTHER chmod code paths); my structural fix eliminates any race by making the dir 0o700 at creation atomically. Fix is race-free by design.

AWAITING INDEPENDENT VALIDATION

Do NOT merge without 通信龙 sign. Fork does not have merge authority. Independent validator (通信龙 route, ~30min turnaround per commitment) will run full E2E in their environment; PR merges only after their PASS + coordinator co-sign.

Closes #457.

🤖 Generated with Claude Code

Under standard umask 022, `cpSync(oldDir, newDir, {recursive:true})`
creates the top-level newDir with default umask permissions (0755),
which subsequently fails the opencode-preset 0700 predjection at PHASE 3
wiring with "directory mode must be 0700 — rolling back".

Node 20/22 fs.cp preserves the mode of a pre-existing dest directory
(verified: `mkdirSync(dst,{mode:0o700})` + `cpSync(src,dst)` leaves dst
at 0700 even when src is 0755). Pre-creating newDir at 0700 is a
structural fix — the directory is born at 0700 in a single atomic
mkdirSync call, with no TOCTOU window between creation and the
predjection.

No post-cpSync chmod: that pattern would be TOCTOU-flavored and defeat
the预检's purpose (identity-bound fchmod via opened fd is the only safe
chmod path, and it's already gated by the preset's own `created=true`
branch which callers deliberately opt into).

Non-copresence rename paths (claude-code-cli, codex-sdk, etc.) are
unaffected — those flows do not invoke the opencode-preset predjection,
and mkdirSync at 0700 is functionally equivalent to letting cpSync
create the dir at umask default (all files/subdirs continue to be
copied recursively as before, only the top-level mode differs).

Closes #457.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bd483b2857

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread agent-network/bin/cli.ts
// Node 20.20.0: `mkdirSync(dst,{mode:0o700})` + `cpSync(src,dst)` leaves
// dst at 0700 even when src is 0755. Structural fix, no post-cpSync chmod
// (would be TOCTOU vs the预检's own identity-bound fchmod branch).
mkdirSync(newDir, { mode: 0o700, recursive: false });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Run the OpenCode rename Docker E2E before merging

This permission change directly affects the OpenCode rename path exercised by test384 L8, but the commit contains no test/report update and the existing docs/tests/report-test384.txt predates this change and stops after L7. Without running the full Docker flow, the actual anet node rename interaction with profile validation, binding migration, and rollback remains unverified; run the relevant Docker E2E suite and record its passing result before merging.

AGENTS.md reference: AGENTS.md:L16-L16

Useful? React with 👍 / 👎.

@vansin

vansin commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

独立复核结论:PASS

复核方逐条核实,未采信作者描述:

  1. baseline 失败是确定性的,不是 race —— 用 Node 直接复核 cpSync 语义:baseline 顶层 755、fixed 700,子文件/子目录 mode 保持源样。我派单时给的「时序窗口」前提是错的 —— 原始 run2「慢速跑通过」更可能是环境/状态污染(目标目录已被预创建为 0700、源/目标非 fresh、或观察点不是同一个 failing precheck),没有真实偶发窗口。结构修同时覆盖两种情形。
  2. test384 L5 是存量失败 —— 用同一镜像重跑复现同形失败(transient OpenCode root survived cleanup),发生在 L8 之前且 baseline 同样失败,与本改动无关。L8 未跑记录为「被存量 L5 阻塞」,不计入本 PR。
  3. EEXIST 不是破坏性变更 —— rename 前置已有 resolveNodeRef(newName) + existsSync(newDir) 双重拒绝,合法路径本就不支持「重名覆盖」。新增的 EEXIST 只在预检之后出现竞争/脏状态时 fail-closed 回滚。
  4. 非 opencode runtime 可接受 —— 顶层目录 0755 → 0700 属安全正向;同 owner 的 claude-code-cli / codex-sdk 仍可访问,子项 mode 不变;静态扫描未见依赖顶层必须 0755 的路径。
  5. 磁盘纪律执行到位:已 docker rmi p457-baseline:test,未跑任何 prune/批量删除,临时 worktree 已移除。

复核笔记留存于 /tmp/p483-review-notes.md(按要求边做边写,防 vendor 超时丢证据)。

合入后 #457 关闭,发版门禁的这一项解除。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug][runtime][P1] opencode node rename unusable under umask 022 — rename 自建 workDir 过不了自家 0700 preset 预检(PHASE 1 自拒回滚)

2 participants