From ced2cc2f6736402dc36d2f7de59287d1c33975c1 Mon Sep 17 00:00:00 2001 From: john-the-dev <52230987+john-the-dev@users.noreply.github.com> Date: Mon, 6 Jul 2026 20:49:57 -0700 Subject: [PATCH 1/2] fix(sync-workspace): allow pre-wsId flat branch past init guard (closes #1961 more) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _assert_sync_initialized rejected workspaces that had a flat `host/` branch (no wsId segment) because they lacked both init sentinels (.git/info/exclude marker and .sutando-vault/ws-id). This caused the existing Test 27 to abort before _migrate_flat_branch could fire — the function designed to upgrade exactly this state. Add a third sentinel: if HEAD is on `host/` with no wsId segment, the workspace was initialized under the pre-#1459 scheme. Allow the sync to proceed; _host_ws_segment writes .sutando-vault/ws-id later in the same run, before push-only sees the guard again. sync-workspace.test.sh: 88/88 pass (Test 27 was the pre-existing failure). Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01QB3qZQrwponfum2JFNvHeh --- scripts/sync-workspace.sh | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/scripts/sync-workspace.sh b/scripts/sync-workspace.sh index 7e9029f1f8..b224d65ff3 100755 --- a/scripts/sync-workspace.sh +++ b/scripts/sync-workspace.sh @@ -637,12 +637,14 @@ _snapshot_per_host_config() { # v0.8 migration report (2026-06-06): plain `bash scripts/sync-workspace.sh` # silent-committed an uninitialized state. # -# Sentinels we accept as proof of a real init (either suffices): +# Sentinels we accept as proof of a real init (any one suffices): # 1. `.git/info/exclude` contains the generator marker from # `_compose_exclude_content()` — proves we wrote the whitelist. # 2. `.sutando-vault/ws-id` — proves _init_impl reached its ws-id step (PR #1459). +# 3. HEAD is on a flat `host/` branch (no wsId segment) — pre-wsId init +# that _migrate_flat_branch will upgrade; ws-id is written later in the same run. # -# If neither is present, refuse with a clear error pointing to --init. +# If none is present, refuse with a clear error pointing to --init. # Override: `SUTANDO_SYNC_SKIP_INIT_GUARD=1` for an operator who knows what # they're doing (e.g. resurrecting a pre-marker init from before this fix). _assert_sync_initialized() { @@ -657,6 +659,14 @@ _assert_sync_initialized() { if [ -f "$_wsid" ]; then return 0 fi + # Pre-wsId flat branch: HEAD is `host/` with no wsId segment. + # _migrate_flat_branch will carry history into the wsId branch and _host_ws_segment + # will write .sutando-vault/ws-id before push-only runs — so this is safe to allow. + local _cur_branch + _cur_branch="$(git -C "$WORKSPACE_DIR" symbolic-ref --short HEAD 2>/dev/null || true)" + if [ "$_cur_branch" = "host/$(_host)" ]; then + return 0 + fi die "${_caller}: $WORKSPACE_DIR has .git but sync was never initialized (no whitelist marker in .git/info/exclude and no .sutando-vault/ws-id). Refusing to push — git add -A here would commit the WHOLE workspace tree with NO carrier-set filter. Run: bash scripts/sync-workspace.sh --init (or set SUTANDO_SYNC_SKIP_INIT_GUARD=1 to bypass at your own risk)" } From b5b0870775bedd9e4ada0725fe41610c7842ef0e Mon Sep 17 00:00:00 2001 From: john-the-dev <52230987+john-the-dev@users.noreply.github.com> Date: Thu, 9 Jul 2026 14:22:29 -0700 Subject: [PATCH 2/2] fix(sync-workspace): require a real whitelist on the pre-wsId branch fast path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review fix (qingyun-wu, PR #1997): the branch-name fast path in _assert_sync_initialized returned 0 whenever HEAD was on host/, without checking any init marker. A half-initialized repo — HEAD parked on host/ but lacking both .git/info/exclude and .sutando-vault/ws-id — could slip past the guard into `git add -A`, the "commit the whole workspace" failure mode the guard exists to prevent. Tighten the fast path: still require a real whitelist to exist first — either the .git/info/exclude generator marker OR the pre-#1459 in-tree .gitignore `*` deny-all (that era's whitelist, which a genuine pre-wsId workspace carries — exactly what _migrate_flat_branch upgrades). If no whitelist is present, fall through to die(). This preserves the legitimate pre-wsId migration (Test 27, still 88/88) while closing the half-init hole. Updated the uninit-guard inline mirror to match and added T12 (flat branch + whitelist passes) / T13 (flat branch, no whitelist, blocked — the reviewer's exact case). Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01DFp2p6XUSAQ1eAKKZeiZPt --- scripts/sync-workspace.sh | 19 ++++++++- tests/sync-workspace-uninit-guard.test.sh | 49 +++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/scripts/sync-workspace.sh b/scripts/sync-workspace.sh index b224d65ff3..e76784ace7 100755 --- a/scripts/sync-workspace.sh +++ b/scripts/sync-workspace.sh @@ -643,6 +643,10 @@ _snapshot_per_host_config() { # 2. `.sutando-vault/ws-id` — proves _init_impl reached its ws-id step (PR #1459). # 3. HEAD is on a flat `host/` branch (no wsId segment) — pre-wsId init # that _migrate_flat_branch will upgrade; ws-id is written later in the same run. +# GUARDED: this fast path ALSO requires a real whitelist to exist first (either +# the `.git/info/exclude` marker OR the pre-#1459 in-tree `.gitignore` deny-all), +# so a half-initialized repo — HEAD parked on `host/` but with NO +# carrier-set filter — cannot ride the branch name past the guard into `git add -A`. # # If none is present, refuse with a clear error pointing to --init. # Override: `SUTANDO_SYNC_SKIP_INIT_GUARD=1` for an operator who knows what @@ -653,7 +657,9 @@ _assert_sync_initialized() { local _exclude="$WORKSPACE_DIR/.git/info/exclude" local _wsid="$WORKSPACE_DIR/.sutando-vault/ws-id" + local _has_exclude_marker=0 if [ -f "$_exclude" ] && grep -q "Generated by sync-workspace.sh" "$_exclude" 2>/dev/null; then + _has_exclude_marker=1 return 0 fi if [ -f "$_wsid" ]; then @@ -662,10 +668,21 @@ _assert_sync_initialized() { # Pre-wsId flat branch: HEAD is `host/` with no wsId segment. # _migrate_flat_branch will carry history into the wsId branch and _host_ws_segment # will write .sutando-vault/ws-id before push-only runs — so this is safe to allow. + # + # BUT only when a real whitelist already exists — otherwise a half-initialized + # repo (HEAD parked on `host/` but no carrier-set filter) would ride the + # branch name straight into `git add -A` and commit the WHOLE workspace tree. + # The exclude marker is checked above (returns 0 when present); here we also accept + # the pre-#1459 in-tree `.gitignore` deny-all, which was that era's whitelist and is + # exactly what a genuine pre-wsId workspace carries (see _migrate_flat_branch). local _cur_branch _cur_branch="$(git -C "$WORKSPACE_DIR" symbolic-ref --short HEAD 2>/dev/null || true)" if [ "$_cur_branch" = "host/$(_host)" ]; then - return 0 + local _legacy_gitignore="$WORKSPACE_DIR/.gitignore" + if [ "$_has_exclude_marker" = "1" ] || \ + { [ -f "$_legacy_gitignore" ] && grep -qE '^\*[[:space:]]*$' "$_legacy_gitignore" 2>/dev/null; }; then + return 0 + fi fi die "${_caller}: $WORKSPACE_DIR has .git but sync was never initialized (no whitelist marker in .git/info/exclude and no .sutando-vault/ws-id). Refusing to push — git add -A here would commit the WHOLE workspace tree with NO carrier-set filter. Run: bash scripts/sync-workspace.sh --init (or set SUTANDO_SYNC_SKIP_INIT_GUARD=1 to bypass at your own risk)" } diff --git a/tests/sync-workspace-uninit-guard.test.sh b/tests/sync-workspace-uninit-guard.test.sh index d64226a696..7c2db5a737 100755 --- a/tests/sync-workspace-uninit-guard.test.sh +++ b/tests/sync-workspace-uninit-guard.test.sh @@ -64,17 +64,32 @@ TMP_E2E="$(mktemp -d -t sutando-sync-uninit-e2e.XXXXXX)" # Re-implement the helper inline. Mirror of scripts/sync-workspace.sh. # Kept in lockstep with the script — if these diverge, structural tests # 5+6 still catch missing marker/sentinel grep in the script. +_host() { echo "${SUTANDO_HOST_OVERRIDE:-testhost}"; } _assert_sync_initialized() { local _caller="${1:-sync}" [ "${SUTANDO_SYNC_SKIP_INIT_GUARD:-0}" = "1" ] && return 0 local _exclude="$WORKSPACE_DIR/.git/info/exclude" local _wsid="$WORKSPACE_DIR/.sutando-vault/ws-id" + local _has_exclude_marker=0 if [ -f "$_exclude" ] && grep -q "Generated by sync-workspace.sh" "$_exclude" 2>/dev/null; then + _has_exclude_marker=1 return 0 fi if [ -f "$_wsid" ]; then return 0 fi + # Pre-wsId flat branch fast path — but only when a real whitelist exists, + # so a half-init repo (branch set, no filter) can't ride the branch name + # into git add -A. Mirror of scripts/sync-workspace.sh. + local _cur_branch + _cur_branch="$(git -C "$WORKSPACE_DIR" symbolic-ref --short HEAD 2>/dev/null || true)" + if [ "$_cur_branch" = "host/$(_host)" ]; then + local _legacy_gitignore="$WORKSPACE_DIR/.gitignore" + if [ "$_has_exclude_marker" = "1" ] || \ + { [ -f "$_legacy_gitignore" ] && grep -qE '^\*[[:space:]]*$' "$_legacy_gitignore" 2>/dev/null; }; then + return 0 + fi + fi echo "ERR: ${_caller}: $WORKSPACE_DIR has .git but sync was never initialized" return 1 } @@ -117,6 +132,40 @@ if [ "$?" != "0" ]; then echo " FAIL: T11 — SUTANDO_SYNC_SKIP_INIT_GUARD=1 did NOT bypass guard"; fail=1 fi +# Test 12: pre-wsId flat branch WITH a whitelist (.gitignore deny-all) → passes. +# Genuine pre-#1459 layout: HEAD on host/, in-tree .gitignore carries the +# `*` deny-all whitelist. _migrate_flat_branch upgrades it; ws-id written later. +mkdir -p "$TMP_E2E/flat-ok" +( + cd "$TMP_E2E/flat-ok" + git init -q + git symbolic-ref HEAD "refs/heads/host/$(SUTANDO_HOST_OVERRIDE=flathost; echo flathost)" + printf '*\n!notes/\n' > .gitignore +) +WORKSPACE_DIR="$TMP_E2E/flat-ok" +if ! SUTANDO_HOST_OVERRIDE=flathost _assert_sync_initialized "test" >/dev/null 2>&1; then + echo " FAIL: T12 — pre-wsId flat branch with .gitignore whitelist rejected"; fail=1 +fi + +# Test 13 (reviewer qingyun-wu, PR #1997): half-initialized repo — HEAD parked on +# host/ but NO whitelist (no .git/info/exclude marker, no in-tree .gitignore). +# The branch-name fast path MUST NOT let this ride past into `git add -A`. +mkdir -p "$TMP_E2E/flat-halfinit" +( + cd "$TMP_E2E/flat-halfinit" + git init -q + git symbolic-ref HEAD "refs/heads/host/flathost" + # deliberately no .gitignore, no .git/info/exclude marker, no ws-id +) +WORKSPACE_DIR="$TMP_E2E/flat-halfinit" +out13="$(SUTANDO_HOST_OVERRIDE=flathost _assert_sync_initialized "test" 2>&1)" +rc13=$? +if [ "$rc13" = "0" ]; then + echo " FAIL: T13 — half-init flat branch (no whitelist) slipped past the guard"; fail=1 +fi +echo "$out13" | grep -q "never initialized" \ + || { echo " FAIL: T13 — half-init error message missing 'never initialized'"; fail=1; } + # Cleanup rm -rf "$TMP_E2E"