Skip to content

Set and report permission modes on Branch file calls (#442) - #445

Open
yifanxuaaa wants to merge 3 commits into
mainfrom
issue442-fs-mode
Open

yifanxuaaa wants to merge 3 commits into
mainfrom
issue442-fs-mode

Conversation

@yifanxuaaa

@yifanxuaaa yifanxuaaa commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Closes #442.

The Branch file calls always created files as 0644 and folders as 0755, silently dropped any mode a caller passed, and fs.stat did not report a mode. This adds modes end to end.

What changes

  • mode on create: fs.write, fs.mkdir, and the write/mkdir changes of fs.apply take an optional integer mode (e.g. 0o755).
    • A new file gets mode, else 0644. An existing file keeps its mode unless mode is given.
    • A new folder gets mode, else 0755. New parent folders get 0755 and an existing folder keeps its mode, as mkdir -p -m does.
  • chmod change: { op: "chmod", path, mode } in fs.apply.
  • fs.stat reports mode, read with read_portable (no object read for inline metadata).
  • Refusals:
    • A mode that is not an integer from 0 to 0o7777 is InvalidInput.
    • A mode LayerFS cannot store (setuid, setgid, or sticky on a file) is InvalidInput with detail { reason: "Mode", path, mode }, as project.init reports it. Sticky on a folder is allowed, as in Init.
    • chmod of a symlink is NotFile.
    • Other unknown change fields are still ignored: fs.write/fs.mkdir copy all their call arguments into the change, so a strict check would break them.
  • JS SDK: the last argument of fs.write and fs.mkdir is a message string, as before, or { message?, mode? }.
  • CLI: --mode <octal> on fs write and fs mkdir.
  • Docs: api/docs/agent.md covers modes, chmod and the options.

No Store format change: every entry already stores a mode, and the Workspace engine already validates modes on create and chmod. Mode-only changes already commit and merge correctly (#352 F-052).

Commits

  • f9f154c8ff feature. Production LOC: 102160 -> 102248 (delta +88): core +75 (layerfs-store-service +57, layerfs-api +18), api +13 (@layerfs/agent +8, @layerfs/cli +5).
  • 20b05510d2 end-to-end test only. Production LOC: 102248 -> 102248 (delta 0).
  • d8d6f54c18 merge of main 3e5f3a627c (layer split D8 · Refactor core/ into core, engine and api layers #463): the edits followed the moves; only the two new test files needed placing. Production LOC: 102248 -> 102250 (+2, main's own); against main this branch adds +88. All checks below were rerun on the merged tree and pass.

Tests

No test deleted or weakened. relink_commit.rs passes None for the new Mutation::write argument. The sdk_methods golden adds mode to its two fs.stat lines.

All runs below are on Linux arm64 via tools/worktree.py, each under the 2-minute limit:

  • New engine/crates/layerfs-api/tests/fs_mode.rs: 2 tests, pass.
  • Every other layerfs-api test that uses these calls (30 binaries, including the slow ones) and layerfs-store-service's own tests: all pass.
  • rustfmt, and Clippy -D warnings on both crates: clean.
  • JS against the fake binding: api/agent/test/calls.mjs (38 cases, 2 new) and api/cli/test/cli.test.mjs (74, including 2 new --mode usage errors): pass.
  • New api/agent/test/verify/fs_mode_real.mjs, through the real native addon: passed once. It covers fs.stat modes after each call, the refusals, stat -c %a in a mounted Workspace (FUSE) for all nine paths, and a Workspace chmod + commit read back through fs.stat.

Not run: the macOS/Windows worker routes. They forward the same SDK calls to a Linux worker and were not changed.

🤖 Generated with Claude Code

yifanxuaaa and others added 3 commits October 24, 2026 19:54
fs.write and fs.mkdir, and the write and mkdir changes of fs.apply, take an
optional integer `mode`. A new file gets it, else 0o644; an existing file
keeps its mode unless one is given. A new folder gets it, else 0o755; new
parents get 0o755 and an existing folder keeps its mode, as `mkdir -p -m`
does. A new `{ op: "chmod", path, mode }` change sets an existing entry's
mode. fs.stat now reports `mode` (read with read_portable: no object read
for inline metadata).

A `mode` that is not an integer from 0 to 0o7777 is InvalidInput. A mode
LayerFS cannot store (set-user-ID or set-group-ID, sticky on a file) is
InvalidInput with detail { reason: "Mode", path, mode }, as project.init
reports it. A symlink has no mode of its own: chmod of one is NotFile.
Before this a supplied mode was silently dropped. Other unknown change
fields are still ignored.

JS SDK: the last argument of fs.write and fs.mkdir is a message string, as
before, or { message?, mode? }. CLI: `--mode <octal>` on fs write and fs
mkdir. api/how-to-use/agent.md documents modes, chmod and the options.

Store format unchanged. No test deleted. relink_commit passes `None` for
the new Mutation::write argument; the sdk_methods golden adds `mode` to
its two fs.stat lines. New test: core/crates/layerfs-api/tests/fs_mode.rs.

Production LOC: 102160 -> 102248 (delta +88)
  core 96788 -> 96863 (+75: layerfs-store-service 10009 -> 10066,
  layerfs-api 2293 -> 2311); api 5372 -> 5385 (+13: @layerfs/agent
  836 -> 844, @layerfs/cli 540 -> 545); reference absent (retired R9c;
  last counted 65,417 at tree 498dd19). Counted with
  tools/production_loc.py --json --root on git archive extracts of the
  parent c77281e and the staged tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
api/agent/sdk/test/verify/fs_mode_real.mjs drives the real native binding
under the E2E launcher: fs.write/fs.mkdir with { message, mode } and with a
plain message, fs.apply write/mkdir modes and chmod, and fs.stat's `mode`.
A mounted Workspace of the Branch shows the same modes through FUSE
(`stat -c %a`, and the new scripts run), and a chmod made in the Workspace
and committed reads back through fs.stat. Refusals (set-user-ID and sticky
on a file with reason Mode, a text mode, a non-object option) commit
nothing.

Passed once on Linux (worktree.py run, bundle
build-linux-ef3909ed42c24038b2b0f4c500de457f, run
ba95a5161f3e4baa94ab028cccb3d00d, 4 s).

Production LOC: 102248 -> 102248 (delta 0)
  Test only. core 96863, api 5385, unchanged; reference absent (retired
  R9c; last counted 65,417 at tree 498dd19). Counted with
  tools/production_loc.py --json --root on git archive extracts of the
  parent f9f154c and the staged tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Main's layer split (#463) moved the crates and packages this PR changes.
Git carried every edit to its new path; the two conflicts were this PR's
new test files, added inside renamed directories. They now live at
engine/crates/layerfs-api/tests/fs_mode.rs and
api/agent/test/verify/fs_mode_real.mjs; the latter's usage comment names
its new path. No source conflict needed a choice.

Checked on the merged tree (Linux arm64, tools/worktree.py, each run under
the 2-minute limit): layerfs-api fs_mode, sdk_method_golden,
write_refusals, method_refusal_order and the 25 other fast tests using
these calls, the moved merge_report, concurrent_commits and lane_merge, the
five slow ones, and layerfs-store-service's tests all pass; rustfmt and
Clippy -D warnings clean; api/agent/test/calls.mjs (38) and
api/cli/test/cli.test.mjs (74) pass; fs_mode_real.mjs passed once through
the rebuilt native bundle (run 23860de121af405ca2e6315b8ef48ee4).

Production LOC: 102248 -> 102250 (delta +2)
  First-parent comparison (20b0551 -> this merge); the +2 is main's.
  Against main 3e5f3a6 (102162) this branch adds +88: engine layerfs-api
  1913 -> 1931, layerfs-store-service 10009 -> 10066; api @layerfs/agent
  836 -> 844, @layerfs/cli 540 -> 545. Merged scopes: api 2646, core
  38241, engine 61363; reference absent (retired R9c; last counted 65,417
  at tree 498dd19). Counted with the merged tree's
  tools/production_loc.py --json --root on git archive extracts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

JS SDK: fs.write / fs.apply cannot set a file's mode (new files are always 0644, new folders always 0755), and fs.stat does not report the mode

1 participant