Repository navigation
Fix manifest read errors treated as absent (#998, #1063, #1123) - #1776
Conversation
Adds regression tests: .socket as a plain file and a self-referencing manifest symlink must fail every command with manifest_unreadable (#998), and a directory at the manifest path must get the same verdict whether the path is the default, spelled with --manifest-path (relative or absolute) or set through SOCKET_MANIFEST_PATH (#1123). Adds rollback to the manifest load error matrix and a core manifest_exists probe that treats only NotFound as absence. Assisted-by: Claude Code:claude-opus-5-5
apply, apply --check and vendor treated any error stating .socket/manifest.json as "no manifest" and exited 0 with noManifest, so an unsearchable .socket/, .socket checked in as a file or a symlink loop silently skipped every patch. repair, remove and rollback said manifest_not_found. All six now ask core's manifest_exists, which keeps read_manifest's rule (only NotFound is absence), and refuse with manifest_unreadable before taking a lock (#998). scan's GC passes read an unloadable manifest as missing too. They now report the manifest_invalid / manifest_unreadable warning instead of a clean summary (#1063). A --manifest-path naming a directory exited 2 with no --json output when the path was spelled out, but 1 with manifest_unreadable under the default spelling. Argument validation no longer judges it, so every spelling gets manifest_unreadable (#1123). Un-ignores apply_with_unreadable_socket_dir_fails_closed. Assisted-by: Claude Code:claude-opus-5-5
The contract's manifest_unreadable row now says a path that cannot be stated is unreadable, never noManifest or manifest_not_found, and that the verdict does not depend on how the path is spelled. The ENOTDIR matrix case accepts vendor --check refusing the vendor ledger under the same unreadable .socket, which it reads first. Assisted-by: Claude Code:claude-opus-5-5
On Windows a path under a plain file reports as not found, so the .socket-as-a-file cases are Unix-only tests (read_manifest and manifest_exists agree there either way). manifest_exists now wraps tokio::fs::try_exists. The exit-2 contract row no longer lists a --manifest-path that names a directory. apply --check keeps its "treating it as drift" message whether the manifest fails to stat or to load, repair records failure telemetry for the early refusal like its load failure, and the GC warning skips a throwaway envelope. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 1f3e0ba. Configure here.
|
Ready for review at
Generated by Claude Code |
Final review briefWhat it does. Several commands decided "no manifest" with their own loose probe ( Risk: low. Every change moves a fail-open path to fail-closed; a genuine Look here
Verified
Changes I made: none. Open questions: none that block merging. Auto-merge is armed, so approving sends it straight to the merge queue. Generated by Claude Code |
LLM Description written by Claude Code:claude-opus-5-5
Fixes #998
Fixes #1063
Fixes #1123
Summary
A manifest that exists but can't be read is now refused as
manifest_unreadable(exit 1, with the--jsonenvelope) on every command and under every spelling of its path. Before, several commands decided "no manifest" with their own looser probes:apply,apply --checkandvendorreportednoManifestand exited 0 when stating.socket/manifest.jsonfailed for any reason (.socketchecked in as a file, a symlink loop, an unsearchable.socket/), so install hooks and CI read "every patch skipped" as success.repair,removeandrollbacksaidmanifest_not_found.scan's GC passes (wet and--dry-runpreview) read an unloadable manifest as missing and returned a clean summary.--manifest-pathnaming a directory exited 2 with no--jsondocument when the path was spelled out (./.socket/manifest.json, an absolute path,SOCKET_MANIFEST_PATH), but 1 withmanifest_unreadableunder the default spelling.Root cause (shared)
Core's
read_manifesthas one rule: onlyNotFoundmeans absent. The callers above each decided presence another way:metadata().is_err()(#998),_ => no manifestafterread_manifest(#1063), and a directory check keyed on the flag's text (#1123).Fix
socket_patch_core::manifest::operations::manifest_existskeepsread_manifest's rule (a thin wrapper overtokio::fs::try_exists).apply(incl.--check),vendor,repair,removeandrollbackuse it and refuse a stat error withmanifest_unreadablebefore building a client or taking a lock. A realNotFoundkeeps every existing no-manifest path (noManifest, hosted/vendored fallbacks).scanGC: an unloadable manifest under the lock now yields amanifest_invalid/manifest_unreadablewarning next to the vendored half, and the pre-lock gate no longer treats an unstatable manifest as absent. (Agent-modescanalready failed closed at its top-level load since An unparseable .socket/manifest.json is reported under five different --json error codes depending on the command #931; this closes the GC passes themselves.)GlobalArgs::validate_pathsno longer refuses a directory manifest (the issue's option A).read_manifestrefuses it the same way for every spelling. The remaining "project directory does not exist" message prints a.-free path (no more././.socket/…).CLI_CONTRACT.md: themanifest_unreadablerow and the exit-2 row describe the new rule..socketas a file still reads as "no manifest" for bothread_manifestandmanifest_exists(consistent). Those test cases are#[cfg(unix)].Per-issue tests (red on main → green here)
manifest_load_error_matrix::socket_dir_as_file_is_manifest_unreadable_on_every_command(ENOTDIR, runs as root)apply:noManifest, exit 0)manifest_load_error_matrix::manifest_symlink_loop_is_manifest_unreadable_on_every_command(ELOOP)apply_invariants::apply_with_unreadable_socket_dir_fails_closedun-ignored (chmod 000; self-skips as root)#[ignore = "RED: …"]test_manifest_exists_*scan::gc::tests::apply_gc_reports_an_unloadable_manifest(invalid JSON + directory, wet + preview)warnings: []), verified by reverting only the GC armsmanifest_load_error_matrix::directory_manifest_verdict_does_not_depend_on_spelling(flag relative, flag absolute, env)The matrix now covers
rollbacktoo.cli_parse_listandcli_path_flags_validatedpinned the old exit-2 directory verdict and are updated to the new one.Local verification
cargo fmt --all -- --check: cleancargo clippy --workspace --all-features -- -D warnings: cleancargo test --workspace --all-features --no-fail-fast: everything this PR touches passes (core lib, CLI lib 936, apply, repair, rollback, remove, matrix, cli_parse_list, cli_path_flags_validated). Remaining local failures are environmental and also fail on unmodifiedmainin this container: chmod-based write/delete-failure tests that can't fail as root (repair_cleanup_failure_*,repair_*_lock_file_unremovable, threein_process_redirectwrite-failure tests, four core lib permission tests) ande2e_bun_lockb(Nightly e2e_bun_lockb red: offline takeover test predates #1648 #1770, fix in CI: update bun.lockb offline takeover test (#1770) #1771).npm/,pypi/,gem/only dispatch the binary./code-review high
Fixed: Windows ENOTDIR→NotFound (tests now Unix-only), stale exit-2 contract row, GC comment accuracy,
try_existsreuse,apply --checkmessage parity, repair telemetry, GC message cleanup.Not fixed, by choice:
emit_error_envelope,emit_rollback_error,emit_eject_refusal,error_envelope, apply's refusal), and each call is the existing load-failure mapping,manifest_load_error, plus that emitter. A helper would save a line per site.scanGC'shas_ledgerprobes--cwdinstead of the manifest's project, and vendor-ledger presence probes use the samemetadata().is_ok()pattern. Both predate this change and are about the vendor ledger, not the manifest. Left as follow-ups so this PR stays one root cause.https://claude.ai/code/session_011f9aeR3v4Q36DgE4agLMEK
Generated by Claude Code