Skip to content

fix(cli): mds init writes through atomic_write_file — a symlink at the target is refused, --force replaces by rename (#386) - #391

Merged
dean0x merged 3 commits into
mainfrom
fix/386-init-atomic-write
Sep 15, 2026
Merged

dean0x merged 3 commits into
mainfrom
fix/386-init-atomic-write

Conversation

@dean0x

@dean0x dean0x commented Sep 15, 2026

Copy link
Copy Markdown
Owner

Summary

mds init now writes the starter file through the same replace-by-rename primitive as every other CLI write (output::atomic_write_file, Durability::RenameOnly). A symlink at the target path — live under --force, or dangling without it — is refused with cannot write <path>: refusing to replace a symlink (exit 1) and neither the link nor its target is touched. Previously a dangling link was written through (creating its target) and --force truncated a live link's target in place. The init entry is gone from the tests/write_funnel.rs allow-list, so the CLI's only remaining raw write is the test-only readiness marker in watch.rs.

RenameOnly because the starter is a fixed public template a re-run reproduces; exit stays 1 because every existing atomic_write_file caller's refusal already exits 1 (build -o, fmt, lint --fix).

Behaviour contract

target without --force with --force
absent created, 0666 & !umask (unchanged) same
regular file "already exists (use --force)", exit 1 (unchanged) replaced by rename, mode preserved, no .mds-tmp- residue, exit 0
live symlink exists() follows → "already exists", exit 1, nothing touched (unchanged) cannot write <p>: refusing to replace a symlink, exit 1; link and target untouched (was: target truncated)
dangling symlink same refusal, exit 1; target not created (was: created through the link) same
parent dir missing exit 1; wording is now the primitive's cannot create temp file for <p>: No such file or directory (os error 2) … same

The parent-dir-missing wording change is recorded here deliberately; it is not a CHANGELOG item.

Commits

  1. test(cli): RED — three #[cfg(unix)] tests in tests/cli_commands.rs (live symlink under --force, dangling symlink without it, --force on a 0o600 regular file as a pin). RED evidence: 6 tests run: 4 passed, 2 failed (exit Some(0) vs expected Some(1)).
  2. fix(cli)run_init writes via output::atomic_write_file(&filename, starter, output::Durability::RenameOnly); the .. check and the exists() && !force check are byte-identical. Intermediate positive control with the allow-list entry still present: allow-list expects 1 raw \fs::write(` in main.rs, found 0` (2 of 3 write_funnel tests failed); after deleting the tuple: 3 passed.
  3. docs — CHANGELOG ### Fixed bullet, "two justified sites" → "one justified site", spec §7.2 writer list + §7.6, SECURITY.md writer list, KB site counts (mds-fmt, mds-lint).

Gates (local, on a65fa99)

  • cargo fmt --all -- --check clean; cargo clippy --workspace --all-targets -- -D warnings clean; cargo clippy -p mds-cli --all-targets --features startup-race-probe -- -D warnings clean
  • cargo nextest run -p mds-core -p mds-cli: 2355 passed (mds-core 1465, mds-cli 890; +3 vs main)
  • cargo test --doc -p mds-core: 53 passed; cargo +1.88 check -p mds-core -p mds-cli clean
  • npm run test:gates: 212 pass / 0 fail; node scripts/verify-no-control-bytes.mjs ✓ (563 files)
  • Added-lines ledger-id check and release-surface diff check: both empty, each with a planted positive control
  • git grep -c 'fs::write(' crates/mds-cli/src/main.rs → 0

Snyk code scan unavailable (MCP ENOENT); the security/snyk CI check is SCA-only.

Not a release-surface PR (no release.yml, .github/actions/**, crates/mds-napi/**, crates/mds-python/**, verify-napi-names.mjs, or musl-load-probe.cjs changes).

Related Issues

Closes #386

@dean0x
dean0x merged commit f892e15 into main Sep 15, 2026
46 checks passed
@dean0x
dean0x deleted the fix/386-init-atomic-write branch September 15, 2026 21:04
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.

mds init follows a symlink at the target path, and --force truncates it in place

1 participant