From ddbaa22b7622c2a5caf9ec9279cfe4be8e12a131 Mon Sep 17 00:00:00 2001 From: Tiago Peixoto Date: Fri, 18 Sep 2026 03:31:56 -0300 Subject: [PATCH 1/2] fix: stop quarantining ordinary shared-captain source updates --- .../skills/secondmate-provisioning/SKILL.md | 3 +- bin/fm-config-inherit-lib.sh | 118 +++++++++-- bin/fm-remote-inherit.sh | 20 +- tests/fm-shared-captain-inheritance.test.sh | 184 +++++++++++++++++- 4 files changed, 304 insertions(+), 21 deletions(-) diff --git a/.agents/skills/secondmate-provisioning/SKILL.md b/.agents/skills/secondmate-provisioning/SKILL.md index f716d5e960c..5872f36b5e6 100644 --- a/.agents/skills/secondmate-provisioning/SKILL.md +++ b/.agents/skills/secondmate-provisioning/SKILL.md @@ -119,9 +119,10 @@ Explicit per-spawn `--backend` and `FM_BACKEND` remain stronger than every home' `data/captain-shared.md` is main-authoritative in the primary home and read-only in secondmate homes. Its primary file header must state that the file is main-authoritative, read-only in secondmate homes, must not be edited there, and that new captain-preference discoveries are routed to the main firstmate through marked status or a document pointer. Every propagation point converges the secondmate copy to the primary bytes; when the primary file is absent, any existing secondmate copy is quarantined and removed so absence converges too. +Both the local helper and the remote receiver compare the destination against the generation each last published there, so an untouched inherited copy is replaced quietly instead of being reported as drift. +A destination matching neither the primary bytes nor that recorded generation is quarantined to a collision-safe private dated sibling file before replacement, with a `SECONDMATE_SYNC:` diagnostic naming the home and quarantine artifact on the local route, so genuine local edits and interrupted publication keep a recovery copy. The helper rejects unsafe directories, symlinked or nonordinary source or destination artifacts, and hardlinked destination files. Between propagation runs, the secondmate copy is filesystem read-only; the helper may make its owned destination writable only around a guarded update and restores read-only mode on success, unchanged bytes, and recoverable failure paths. -Before replacing divergent secondmate bytes, the helper hash-compares source and destination, quarantines the secondmate-local version to a collision-safe private dated sibling file, and emits a `SECONDMATE_SYNC:` diagnostic naming the home and quarantine artifact. Never copy any secondmate `data/captain-shared.md` back into the primary. Keep each home's `data/captain.md` domain-local. After first propagation to an existing home, trim that home's local `data/captain.md` by hand to domain-specific content plus pointers to `data/captain-shared.md`; do not automate or silently delete private content. diff --git a/bin/fm-config-inherit-lib.sh b/bin/fm-config-inherit-lib.sh index 79ff10605c2..1d33edbae40 100644 --- a/bin/fm-config-inherit-lib.sh +++ b/bin/fm-config-inherit-lib.sh @@ -23,6 +23,14 @@ # It also pushes # the one primary-authoritative shared captain-preference file, # data/captain-shared.md, into each secondmate home's data/ as a read-only copy. +# Shared-captain convergence records the SHA-256 of the last successfully +# published destination generation beside that copy. A destination whose bytes +# still match that receipt is replaced quietly when the primary source advances. +# A destination that differs from the receipt, or that has no usable receipt, is +# quarantined before replacement so genuine local edits and interrupted +# publication keep a recovery copy, and primary absence always quarantines +# before removing. The receipt is written only after the destination file +# matches the intended generation. # # Usage: . bin/fm-config-inherit-lib.sh (no FM_* setup required) # @@ -129,13 +137,16 @@ fm_inherit_file_link_count() { } fm_inherit_sha256() { + local digest if command -v shasum >/dev/null 2>&1; then - shasum -a 256 "$1" 2>/dev/null | awk '{print $1}' + digest=$(shasum -a 256 "$1" 2>/dev/null | awk '{print $1}') elif command -v sha256sum >/dev/null 2>&1; then - sha256sum "$1" 2>/dev/null | awk '{print $1}' + digest=$(sha256sum "$1" 2>/dev/null | awk '{print $1}') else return 1 fi + [ -n "$digest" ] || return 1 + printf '%s\n' "$digest" } copy_inheritable_file() { @@ -252,6 +263,69 @@ restore_shared_captain_readonly() { chmod "$FM_SHARED_CAPTAIN_MODE" "$dest" 2>/dev/null || return 1 } +shared_captain_inherited_receipt_path() { + printf '%s/.%s.inherited\n' "$1" "$FM_SHARED_CAPTAIN_FILE" +} + +# Prints the recorded SHA-256 when the receipt is a safe ordinary file containing +# exactly one 64-hex digest. Returns 1 for every other receipt state, which the +# callers treat as "no usable receipt" and answer by quarantining first. +shared_captain_read_inherited_hash() { + local parent=$1 path hash + path=$(shared_captain_inherited_receipt_path "$parent") + if [ ! -e "$path" ] && [ ! -L "$path" ]; then + return 1 + fi + shared_captain_file_safe_existing "$path" || return 1 + hash=$(awk ' + NR == 1 { digest = $0; next } + { extra = 1 } + END { if (extra || NR != 1) exit 1; print digest } + ' "$path" 2>/dev/null) || return 1 + case "$hash" in + *[!a-f0-9]*) return 1 ;; + esac + [ "${#hash}" -eq 64 ] || return 1 + printf '%s\n' "$hash" +} + +shared_captain_write_inherited_hash() { + local parent=$1 hash=$2 path tmp + shared_captain_dir_safe "$parent" || return 1 + path=$(shared_captain_inherited_receipt_path "$parent") + tmp=$(mktemp "$parent/.fm-captain-shared-inherited.XXXXXX" 2>/dev/null) || return 1 + if ! printf '%s\n' "$hash" > "$tmp"; then + rm -f "$tmp" 2>/dev/null || true + return 1 + fi + chmod 0600 "$tmp" 2>/dev/null || { rm -f "$tmp" 2>/dev/null || true; return 1; } + shared_captain_file_safe_existing "$tmp" || { rm -f "$tmp" 2>/dev/null || true; return 1; } + if mv -f -- "$tmp" "$path" 2>/dev/null; then + shared_captain_file_safe_existing "$path" || return 1 + return 0 + fi + rm -f "$tmp" 2>/dev/null || true + return 1 +} + +shared_captain_remove_inherited_receipt() { + local parent=$1 path + path=$(shared_captain_inherited_receipt_path "$parent") + [ -e "$path" ] || [ -L "$path" ] || return 0 + shared_captain_file_safe_existing "$path" || return 1 + rm -f -- "$path" 2>/dev/null +} + +# Record hash after the destination already matches that generation. Skip a +# rewrite when the receipt already names the same digest. +shared_captain_record_inherited_hash() { + local parent=$1 hash=$2 current + if current=$(shared_captain_read_inherited_hash "$parent" 2>/dev/null); then + [ "$current" = "$hash" ] && return 0 + fi + shared_captain_write_inherited_hash "$parent" "$hash" +} + shared_captain_quarantine_existing_for_hash() { local parent=$1 hash=$2 artifact artifact_hash for artifact in "$parent"/."$FM_SHARED_CAPTAIN_FILE".quarantine.*."$hash" "$parent"/."$FM_SHARED_CAPTAIN_FILE".quarantine.*."$hash".[0-9]*; do @@ -324,7 +398,8 @@ copy_shared_captain_file() { } propagate_shared_captain_preferences() { - local src_data=$1 dest_data=$2 src dest src_hash dest_hash dest_parent dest_home quarantine reason rc + local src_data=$1 dest_data=$2 src dest src_hash dest_hash dest_parent dest_home + local quarantine inherited_hash reason rc [ -n "$src_data" ] || return 1 [ -n "$dest_data" ] || return 1 src="$src_data/$FM_SHARED_CAPTAIN_FILE" @@ -366,12 +441,14 @@ propagate_shared_captain_preferences() { restore_shared_captain_readonly "$dest" || true return 1 } + inherited_hash=$(shared_captain_read_inherited_hash "$dest_parent" 2>/dev/null) || inherited_hash= if [ "$src_hash" = "$dest_hash" ]; then - if restore_shared_captain_readonly "$dest"; then + if restore_shared_captain_readonly "$dest" \ + && shared_captain_record_inherited_hash "$dest_parent" "$dest_hash"; then record_inheritable_config_result "$FM_SHARED_CAPTAIN_REL" unchanged "" return 0 fi - reason="failed to restore read-only mode" + reason="failed to restore read-only mode or record inherited generation" warn_inheritable_config_error "$FM_SHARED_CAPTAIN_REL" "$dest" "$reason" record_inheritable_config_result "$FM_SHARED_CAPTAIN_REL" error "$reason" return 1 @@ -383,14 +460,16 @@ propagate_shared_captain_preferences() { restore_shared_captain_readonly "$dest" || true return 1 fi - if ! quarantine=$(quarantine_shared_captain_dest "$dest" "$dest_parent"); then - reason="failed to quarantine divergent destination" - warn_inheritable_config_error "$FM_SHARED_CAPTAIN_REL" "$dest" "$reason" - record_inheritable_config_result "$FM_SHARED_CAPTAIN_REL" error "$reason" - restore_shared_captain_readonly "$dest" || true - return 1 + if [ "$dest_hash" != "$inherited_hash" ]; then + if ! quarantine=$(quarantine_shared_captain_dest "$dest" "$dest_parent"); then + reason="failed to quarantine divergent destination" + warn_inheritable_config_error "$FM_SHARED_CAPTAIN_REL" "$dest" "$reason" + record_inheritable_config_result "$FM_SHARED_CAPTAIN_REL" error "$reason" + restore_shared_captain_readonly "$dest" || true + return 1 + fi + printf 'SECONDMATE_SYNC: secondmate home %s: quarantined %s drift at %s\n' "$dest_home" "$FM_SHARED_CAPTAIN_REL" "$quarantine" fi - printf 'SECONDMATE_SYNC: secondmate home %s: quarantined %s drift at %s\n' "$dest_home" "$FM_SHARED_CAPTAIN_REL" "$quarantine" elif ! shared_captain_dir_safe "$dest_parent"; then reason="unsafe destination directory" warn_inheritable_config_error "$FM_SHARED_CAPTAIN_REL" "$dest_parent" "$reason" @@ -398,10 +477,17 @@ propagate_shared_captain_preferences() { return 1 fi if copy_shared_captain_file "$src" "$dest"; then - if [ -n "${quarantine:-}" ]; then - record_inheritable_config_result "$FM_SHARED_CAPTAIN_REL" pushed "quarantined local drift at $quarantine" + if shared_captain_record_inherited_hash "$dest_parent" "$src_hash"; then + if [ -n "${quarantine:-}" ]; then + record_inheritable_config_result "$FM_SHARED_CAPTAIN_REL" pushed "quarantined local drift at $quarantine" + else + record_inheritable_config_result "$FM_SHARED_CAPTAIN_REL" pushed "" + fi else - record_inheritable_config_result "$FM_SHARED_CAPTAIN_REL" pushed "" + reason="failed to record inherited generation" + warn_inheritable_config_error "$FM_SHARED_CAPTAIN_REL" "$dest" "$reason" + record_inheritable_config_result "$FM_SHARED_CAPTAIN_REL" error "$reason" + rc=1 fi else reason="failed to copy" @@ -424,6 +510,7 @@ propagate_shared_captain_preferences() { return 1 fi if quarantine=$(quarantine_shared_captain_dest "$dest" "$dest_parent"); then + shared_captain_remove_inherited_receipt "$dest_parent" || true printf 'SECONDMATE_SYNC: secondmate home %s: quarantined %s drift at %s\n' "$dest_home" "$FM_SHARED_CAPTAIN_REL" "$quarantine" record_inheritable_config_result "$FM_SHARED_CAPTAIN_REL" pushed "mirrored primary absence after quarantining local copy at $quarantine" else @@ -434,6 +521,7 @@ propagate_shared_captain_preferences() { rc=1 fi else + shared_captain_remove_inherited_receipt "$dest_parent" || true record_inheritable_config_result "$FM_SHARED_CAPTAIN_REL" unchanged "" fi return "$rc" diff --git a/bin/fm-remote-inherit.sh b/bin/fm-remote-inherit.sh index 15bb0d4cb1c..57f90231fcd 100755 --- a/bin/fm-remote-inherit.sh +++ b/bin/fm-remote-inherit.sh @@ -6,8 +6,9 @@ # fm-remote-inherit.sh absent 0 # # Only the inherited-material allowlist is writable or removable. Writes are -# atomic ordinary-file replacements. Divergent data/captain-shared.md bytes are -# quarantined before replacement or removal and its converged copy is read-only. +# atomic ordinary-file replacements. data/captain-shared.md bytes are quarantined +# before removal, or before replacement when they differ from the last published +# generation, and its copy is read-only. set -eu FM_HOME=${FM_HOME:?FM_HOME is required} @@ -78,6 +79,9 @@ GENERATION_FILE="$PARENT_REAL/.fm-inherit-$BASE.generation" fm_lock_acquire_wait "$LOCK" || die "cannot lock inherited destination" TMP= GENERATION_TMP= +# Digest this receiver last published to DEST, captured before commit_generation +# overwrites the record. Empty when no put generation has been committed here. +LAST_PUBLISHED_HASH= cleanup() { [ -z "$TMP" ] || rm -f -- "$TMP" [ -z "$GENERATION_TMP" ] || rm -f -- "$GENERATION_TMP" @@ -102,6 +106,7 @@ commit_generation() { case "$existing_hash" in ''|*[!A-Fa-f0-9]*) die "inheritance generation record is malformed" ;; esac [ "${#existing_hash}" -eq 64 ] || die "inheritance generation record is malformed" case "$existing_command" in put|absent) ;; *) die "inheritance generation record is malformed" ;; esac + [ "$existing_command" != put ] || LAST_PUBLISHED_HASH=$(printf '%s' "$existing_hash" | tr 'A-F' 'a-f') if [ "$existing_generation" -gt "$GENERATION" ]; then die "inheritance write generation is superseded" fi @@ -122,6 +127,15 @@ commit_generation() { GENERATION_TMP= } +# True when the destination still holds the bytes this receiver last published, +# so replacing it is ordinary convergence rather than destination drift. +dest_matches_last_published() { + local actual + [ -n "$LAST_PUBLISHED_HASH" ] && [ -f "$DEST" ] || return 1 + actual=$(sha256_file "$DEST") || return 1 + [ "$actual" = "$LAST_PUBLISHED_HASH" ] +} + quarantine_shared() { local reason=$1 quarantine stamp base n=0 [ "$REL" = data/captain-shared.md ] && [ -f "$DEST" ] || return 0 @@ -152,7 +166,7 @@ case "$COMMAND" in printf 'unchanged: %s\n' "$REL" exit 0 fi - quarantine_shared replaced + dest_matches_last_published || quarantine_shared replaced chmod 600 "$TMP" || die "cannot secure inherited material" mv -f -- "$TMP" "$DEST" || die "cannot publish inherited material" TMP= diff --git a/tests/fm-shared-captain-inheritance.test.sh b/tests/fm-shared-captain-inheritance.test.sh index efd61dd804f..82cda599cc8 100755 --- a/tests/fm-shared-captain-inheritance.test.sh +++ b/tests/fm-shared-captain-inheritance.test.sh @@ -67,7 +67,7 @@ assert_secondmate_write_fails() { } test_first_copy_readonly_and_local_files_preserved() { - local rec primary second report out + local rec primary second report out qcount rec=$(new_home_pair first-copy) primary=${rec%%|*} second=${rec#*|} @@ -90,7 +90,136 @@ test_first_copy_readonly_and_local_files_preserved() { [ -z "$out" ] || fail "unchanged convergence should stay quiet: $out" assert_grep $'data/captain-shared.md\tunchanged\t' "$report" "unchanged bytes should report unchanged" assert_shared_readonly "$second/data/captain-shared.md" - pass "shared captain first copy converges, is read-only, and preserves local captain/learnings files" + + write_shared "$primary/data/captain-shared.md" "shared v2" + : > "$report" + out=$(FM_CONFIG_INHERIT_REPORT="$report" propagate_secondmate_inheritance "$primary" "$second") + [ -z "$out" ] || fail "source-only edit should not emit a quarantine diagnostic: $out" + cmp -s "$primary/data/captain-shared.md" "$second/data/captain-shared.md" \ + || fail "source-only edit did not converge secondmate shared preferences" + qcount=$(find "$second/data" -name '.captain-shared.md.quarantine.*' | wc -l | tr -d ' ') + [ "$qcount" -eq 0 ] || fail "source-only edit quarantined an untouched inherited destination" + assert_grep $'data/captain-shared.md\tpushed\t' "$report" "source-only edit should report pushed" + assert_not_contains "$(cat "$report")" "quarantined local drift" \ + "source-only edit should not report local drift" + assert_shared_readonly "$second/data/captain-shared.md" + pass "shared captain first copy, unchanged copy, and source-only edit stay quiet" +} + +test_true_divergence_after_inherit_still_quarantines() { + local rec primary second report out diag qpath qcount + rec=$(new_home_pair true-divergence) + primary=${rec%%|*} + second=${rec#*|} + write_shared "$primary/data/captain-shared.md" "shared v1" + report="$TMP_ROOT/true-divergence.report" + out=$(FM_CONFIG_INHERIT_REPORT="$report" propagate_secondmate_inheritance "$primary" "$second") + [ -z "$out" ] || fail "setup inherit should stay quiet: $out" + + chmod u+w "$second/data/captain-shared.md" + write_shared "$second/data/captain-shared.md" "local edit after inherit" + chmod "$FM_SHARED_CAPTAIN_MODE" "$second/data/captain-shared.md" + write_shared "$primary/data/captain-shared.md" "shared v2" + : > "$report" + out=$(FM_CONFIG_INHERIT_REPORT="$report" propagate_secondmate_inheritance "$primary" "$second") + diag=$(printf '%s\n' "$out" | grep '^SECONDMATE_SYNC: secondmate home ' || true) + [ -n "$diag" ] || fail "edited destination should emit a SECONDMATE_SYNC diagnostic" + qpath=${diag##* at } + assert_grep "local edit after inherit" "$qpath" "true-divergence quarantine lost the edited bytes" + cmp -s "$primary/data/captain-shared.md" "$second/data/captain-shared.md" \ + || fail "true-divergence convergence did not install primary bytes" + qcount=$(find "$second/data" -name '.captain-shared.md.quarantine.*' | wc -l | tr -d ' ') + [ "$qcount" -eq 1 ] || fail "true-divergence should leave exactly one quarantine artifact" + assert_grep $'data/captain-shared.md\tpushed\tquarantined local drift at '"$qpath" "$report" \ + "true-divergence push should name the quarantine artifact" + pass "shared captain true divergence after inherit is still quarantined" +} + +test_interrupted_publication_matching_source_does_not_quarantine() { + local rec primary second report out qcount + rec=$(new_home_pair interrupted-pub) + primary=${rec%%|*} + second=${rec#*|} + write_shared "$primary/data/captain-shared.md" "shared v1" + report="$TMP_ROOT/interrupted-pub.report" + out=$(FM_CONFIG_INHERIT_REPORT="$report" propagate_secondmate_inheritance "$primary" "$second") + [ -z "$out" ] || fail "setup inherit should stay quiet: $out" + + write_shared "$primary/data/captain-shared.md" "shared v2" + chmod u+w "$second/data/captain-shared.md" + cp "$primary/data/captain-shared.md" "$second/data/captain-shared.md" + chmod "$FM_SHARED_CAPTAIN_MODE" "$second/data/captain-shared.md" + + : > "$report" + out=$(FM_CONFIG_INHERIT_REPORT="$report" propagate_secondmate_inheritance "$primary" "$second") + [ -z "$out" ] || fail "destination already matching the new source should not quarantine: $out" + qcount=$(find "$second/data" -name '.captain-shared.md.quarantine.*' | wc -l | tr -d ' ') + [ "$qcount" -eq 0 ] || fail "interrupted publication matching source created a quarantine artifact" + assert_grep $'data/captain-shared.md\tunchanged\t' "$report" \ + "destination already matching source should report unchanged" + assert_shared_readonly "$second/data/captain-shared.md" + + write_shared "$primary/data/captain-shared.md" "shared v3" + : > "$report" + out=$(FM_CONFIG_INHERIT_REPORT="$report" propagate_secondmate_inheritance "$primary" "$second") + [ -z "$out" ] || fail "healed receipt should accept a later source-only edit quietly: $out" + cmp -s "$primary/data/captain-shared.md" "$second/data/captain-shared.md" \ + || fail "later source-only edit after healed receipt did not converge" + qcount=$(find "$second/data" -name '.captain-shared.md.quarantine.*' | wc -l | tr -d ' ') + [ "$qcount" -eq 0 ] || fail "later source-only edit after healed receipt quarantined" + pass "interrupted publication that already matches source heals without quarantine" +} + +# The remote secondmate route reaches the same destination through +# bin/fm-remote-inherit.sh, so it owes the same answer: an untouched inherited +# copy is ordinary convergence, a locally edited one is drift worth keeping. +remote_put_shared() { + local home=$1 payload=$2 generation=$3 bytes hash + bytes=$(LC_ALL=C wc -c < "$payload" | tr -d ' ') + hash=$(fm_inherit_sha256 "$payload") || fail "cannot hash remote inheritance payload" + PATH="$BASE_PATH" FM_HOME="$home" "$ROOT/bin/fm-remote-inherit.sh" \ + put data/captain-shared.md "$bytes" "$hash" "$generation" < "$payload" 2>&1 +} + +remote_quarantine_count() { + find "$1/data" -name 'captain-shared.md.remote-quarantine-*' | wc -l | tr -d ' ' +} + +test_remote_receiver_accepts_source_only_edit_without_quarantine() { + local home source out qpath + home="$TMP_ROOT/remote-receiver/home" + source="$TMP_ROOT/remote-receiver/source.md" + mkdir -p "$home/data" "$home/config" "$TMP_ROOT/remote-receiver" + + write_shared "$source" "shared v1" + out=$(remote_put_shared "$home" "$source" 1) || fail "remote first inherit failed: $out" + assert_contains "$out" "pushed: data/captain-shared.md" "remote first inherit did not publish" + assert_shared_readonly "$home/data/captain-shared.md" + + write_shared "$source" "shared v2" + out=$(remote_put_shared "$home" "$source" 2) || fail "remote source-only edit failed: $out" + assert_not_contains "$out" "quarantined:" \ + "remote source-only edit quarantined an untouched inherited copy" + [ "$(remote_quarantine_count "$home")" -eq 0 ] \ + || fail "remote source-only edit left a recovery copy for an untouched destination" + cmp -s "$source" "$home/data/captain-shared.md" \ + || fail "remote source-only edit did not converge the destination" + assert_shared_readonly "$home/data/captain-shared.md" + + chmod u+w "$home/data/captain-shared.md" + write_shared "$home/data/captain-shared.md" "remote local edit" + chmod "$FM_SHARED_CAPTAIN_MODE" "$home/data/captain-shared.md" + write_shared "$source" "shared v3" + out=$(remote_put_shared "$home" "$source" 3) || fail "remote divergent inherit failed: $out" + assert_contains "$out" "quarantined:" "remote edited destination was replaced without a recovery copy" + [ "$(remote_quarantine_count "$home")" -eq 1 ] \ + || fail "remote divergence should leave exactly one recovery copy" + qpath=$(find "$home/data" -name 'captain-shared.md.remote-quarantine-*') + assert_grep "remote local edit" "$qpath" "remote quarantine lost the edited bytes" + cmp -s "$source" "$home/data/captain-shared.md" \ + || fail "remote divergent inherit did not install the primary bytes" + assert_shared_readonly "$home/data/captain-shared.md" + pass "remote receiver accepts a source-only edit quietly and still quarantines real drift" } test_drift_quarantine_collision_and_repeated_convergence() { @@ -189,6 +318,20 @@ test_unsafe_artifacts_and_failure_restore_readonly_mode() { assert_grep "unsafe destination" "$err" "unsafe destination hardlink error should be explicit" rm -f "$second/data/captain-shared.md" "$other" + # Root reads a mode-000 file regardless, which would make this case vacuous. + if [ "$(id -u)" != 0 ]; then + write_shared "$second/data/captain-shared.md" "unreadable local bytes" + chmod 000 "$second/data/captain-shared.md" + err="$TMP_ROOT/unreadable-dest.err" + propagate_secondmate_inheritance "$primary" "$second" >/dev/null 2>"$err"; rc=$? + chmod 600 "$second/data/captain-shared.md" + [ "$rc" -ne 0 ] || fail "an unhashable destination should not converge silently" + assert_grep "failed to hash destination" "$err" "unhashable destination error should be explicit" + assert_grep "unreadable local bytes" "$second/data/captain-shared.md" \ + "unhashable destination was replaced without keeping its bytes" + rm -f "$second/data/captain-shared.md" + fi + write_shared "$second/data/captain-shared.md" "permission drift" chmod "$FM_SHARED_CAPTAIN_MODE" "$second/data/captain-shared.md" before_mode=$(file_mode "$second/data/captain-shared.md") @@ -370,6 +513,39 @@ EOF pass "fm-config-push convergence point updates changed shared captain source bytes from FM_DATA_OVERRIDE" } +test_config_push_source_only_edit_after_inherit_stays_quiet() { + local rec w root home sm data_override out + rec=$(new_git_world config-push-source-only) + IFS='|' read -r w root home sm < "$home/state/sm.meta" + write_shared "$data_override/captain-shared.md" "inherited shared bytes" + PATH="$BASE_PATH" FM_HOME="$home" FM_ROOT_OVERRIDE="$root" \ + FM_DATA_OVERRIDE="$data_override" \ + "$ROOT/bin/fm-config-push.sh" >/dev/null 2>&1 + write_shared "$data_override/captain-shared.md" "updated shared bytes" + + out=$(PATH="$BASE_PATH" FM_HOME="$home" FM_ROOT_OVERRIDE="$root" \ + FM_DATA_OVERRIDE="$data_override" \ + "$ROOT/bin/fm-config-push.sh" 2>/dev/null) + + assert_contains "$out" "data/captain-shared.md: pushed" \ + "config-push should report the shared file source-only update" + assert_not_contains "$out" "quarantined local drift" \ + "config-push source-only edit after inherit should not report drift" + cmp -s "$data_override/captain-shared.md" "$sm/data/captain-shared.md" \ + || fail "config-push source-only edit after inherit did not converge" + assert_shared_readonly "$sm/data/captain-shared.md" + pass "fm-config-push source-only edit after inherit stays quiet" +} + test_session_start_digest_labels_shared_file_and_read_once_rule() { local rec w root home _sm fakebin out contract rec=$(new_git_world session-start-label) @@ -393,12 +569,16 @@ EOF } test_first_copy_readonly_and_local_files_preserved +test_true_divergence_after_inherit_still_quarantines +test_interrupted_publication_matching_source_does_not_quarantine +test_remote_receiver_accepts_source_only_edit_without_quarantine test_drift_quarantine_collision_and_repeated_convergence test_missing_source_mirrors_absence_without_losing_local_bytes test_unsafe_artifacts_and_failure_restore_readonly_mode test_spawn_convergence_point_copies_shared_file test_bootstrap_convergence_point_copies_shared_file test_config_push_convergence_point_updates_changed_source +test_config_push_source_only_edit_after_inherit_stays_quiet test_session_start_digest_labels_shared_file_and_read_once_rule echo "# all fm-shared-captain-inheritance tests passed" From b562df82aa454f7d7ee30828b4d530c6da12c342 Mon Sep 17 00:00:00 2001 From: Tiago Peixoto Date: Fri, 18 Sep 2026 03:45:19 -0300 Subject: [PATCH 2/2] no-mistakes(document): Rewrap remote inherit header so usage prints fully --- bin/fm-remote-inherit.sh | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bin/fm-remote-inherit.sh b/bin/fm-remote-inherit.sh index 57f90231fcd..3e5b047aae4 100755 --- a/bin/fm-remote-inherit.sh +++ b/bin/fm-remote-inherit.sh @@ -6,9 +6,8 @@ # fm-remote-inherit.sh absent 0 # # Only the inherited-material allowlist is writable or removable. Writes are -# atomic ordinary-file replacements. data/captain-shared.md bytes are quarantined -# before removal, or before replacement when they differ from the last published -# generation, and its copy is read-only. +# atomic ordinary-file replacements. data/captain-shared.md is read-only and is +# quarantined before removal or before replacing bytes not last published here. set -eu FM_HOME=${FM_HOME:?FM_HOME is required}