Files
stack/docs/scratchpads/791-upgrade-config-protection.md
Hermes Agent 0a5e703a70
All checks were successful
ci/woodpecker/pr/ci Pipeline was successful
test(mosaic): gate #797 ledger upgrade-survival + harden manifest parity (#791)
Fold the Mos-elevated #797 Runtime-Ledger survival sentinel into #791 PR1 and
harden the .txt-format parity test per the accepted-format conditions.

- framework-manifest.txt: annotate the existing fleet/run/** operator carve-out
  to name the #797 ledger (fleet/run/sessions/) so it reads as load-bearing.
  The glob already matches the #797 spec exactly — no location divergence.
- HARD GATE (test-upgrade-manifest-guard.sh): seed a populated ledger
  (events.ndjson journal + ledger.json projection, 0600 under 0700) as an
  operator sentinel; assert byte-identical + mtime-unchanged + dir-perms
  unchanged after a keep-mode upgrade. Relabel the prune check as the explicit
  negative control. 48 -> 58 checks.
- Parity (manifest-parity.spec.ts): add format-edge fixtures driven through
  BOTH resolvers via MANIFEST_FILE — comments/blanks/whitespace, duplicate and
  overlapping globs (deny-wins), section/glob-ordering independence, and an
  explicit UNKNOWN->operator negative probe; add ledger probe paths.
- manifest.spec.ts: isolate the carve-out's load-bearing value with a resolver
  red->green — under a hypothetical fleet/** framework glob, the ledger is
  pruned WITHOUT the fleet/run/** carve-out and protected WITH it (deny-wins).

Gates: typecheck, lint, format:check green; mosaic vitest 1069 passed;
HARD GATE 58/58; migration 21/21. Commits forward on 34e55d4a (no rebase).

Part of #791
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 16:11:06 -05:00

9.3 KiB

Scratchpad — #791 Upgrade config protection (ms-791 worker lane)

Lane: web1:ms-791 → reports to MS-LEAD (web1:mosaic-100). Do NOT contact Jason/Mos directly. Worktree: /home/hermes/agent-work/stack-agents-dir-791, branch feat/791-upgrade-config-protection off origin/main 9745bc3f (verified exact head).

Mission prompt (verbatim intent)

Protect operator-owned config under ~/.config/mosaic from framework-upgrade wipes. Ratified combination (Mos-approved, do NOT re-litigate): (b) strict ownership separation [PRIMARY] + (a) transactional pre-update snapshot [safety net] + (d) regeneration-from-SSOT [recovery]. (c) periodic timer DEFERRED. HARD GATE: unit test that an upgrade run touches NO path outside the manifest. Design-first: write design doc, send to MS-LEAD, WAIT for confirmation before impl.

Session 1 (2026-07-16) — Phase 1 design

Evidence gathered (wipe mechanism, file/line)

  • mosaic updateupdate-checker.ts:509 buildReseedCommandbash install.sh (MOSAIC_SYNC_ONLY=1, MOSAIC_INSTALL_MODE=keep).
  • Wipe = packages/mosaic/framework/install.sh:199 rsync -a --delete + PRESERVE_PATHS denylist (install.sh:47). cp-fallback install.sh:223 find ... -exec rm -rf.
  • Denylist gaps → WIPED: agents/*.conf, policy/*.md, *.local.md, harvester/SOP, tools/_lib/credentials.json.
  • Stale comment update-checker.ts:492 claims *.local preserved — PRESERVE_PATHS has no such entry.
  • TS path file-adapter.ts:157file-ops.ts:66 syncDirectory = non-destructive copy-overlay, BUT its preserve list (file-adapter.ts:164) already DRIFTED from install.sh (missing fleet/backlog, fleet/roles.local). Evidence for single shared manifest SSOT.
  • Existing snapshot (install.sh:76) = /tmp, crash-trap only, deleted on success → inadequate; no mosaic restore.
  • fleet-reconciler.ts:93,234 already has regenerate-projections-from-roster phase separate from lifecycle → mosaic fleet regen = thin projection-only wrapper (no restart), no FCM-M4/M5 preemption.

Design decisions

  • (b) Invert to allow-list: shared framework/framework-manifest.json (framework globs + operatorReserved carve-outs); resolve per-path, deny-wins; UNKNOWN ⇒ operator (fail-safe). Mechanism: drop --delete; non-deleting bulk copy + explicit manifest-scoped prune pass (iterate framework globs only → operator/unknown structurally unreachable). Pure prune-planner fn for tests.
  • (a) Snapshot to ~/.local/state/mosaic/backups/pre-update-<ts>/ 0700/0600, retention N=5, post-sync verify+restore, mosaic restore --list/--from. No secret values in output.
  • (d) mosaic fleet regen projection-only, preview-first, never restart.
  • HARD GATE test includes a deliberately-unanticipated operator path to prove fail-safe default.
  • PR split: PR1 manifest+guard (root fix, ships alone) → PR2 snapshot/restore (secrev) → PR3 regen+docs. PR2/PR3 depend on PR1.

Status

Design doc written: docs/design/791-upgrade-config-protection.md. Sent to MS-LEAD.

Session 1 (cont.) — MS-LEAD CONFIRMED → Phase 2 GO

All 4 asks approved. Binding conditions:

  • TDD tests-first, red-first proof per PR; ≥85% new-code; co-located *.spec.ts; never --no-verify.
  • HARD GATE test (§2.4, unanticipated sentinel survives byte-identical + mtime unchanged) = MERGE-BLOCKING for PR1.
  • Manifest-completeness test (§6.2) required.
  • Bash+TS read ONE shared framework-manifest.json; parity test (§6.1) required (closes #631 drift class).
  • UNKNOWN⇒operator (rule 3) non-negotiable. Keep prune-planner PURE.
  • fleet regen: NEVER restart; dry-run default, --write to apply; "never issues restart" test mandatory.
  • Independent review every PR; PR2 dedicated secrev.
  • One PR at a time through DAG. Report PR1 exact head + red→green evidence for review commission.

Now: implementing PR1 (manifest + resolver + non-deleting sync + scoped prune + guard tests).

Session 2 (2026-07-16) — PR1 built, tests-first, red→green proven

Deviation noted to MS-LEAD in PR: manifest is framework-manifest.txt (line-oriented), NOT .json. Rationale: keep the bash installer free of a python3/jq dependency. The "ONE shared file, parity- tested" requirement is honored — manifest-parity.spec.ts drives the bash resolver as a subprocess and asserts byte-identical ownership vs the TS resolver over 34 probe paths spanning every class.

PR1 artifacts

  • SSOT: packages/mosaic/framework/framework-manifest.txt ([framework]/[operator], deny-wins, fail-safe).
  • TS resolver: src/framework/manifest.ts (pure: parse/matchGlob/resolveOwnership/frameworkSubtreeRoots/ planPrune) + manifest.spec.ts (18 tests incl. planPrune property test + §6.2 completeness).
  • Bash resolver: framework/tools/_lib/manifest.sh (compiled globs → fork-free manifest_is_framework; CLI resolve|subtree-roots|classify). Sourced by install.sh.
  • HARD GATE (§2.4): framework/tools/quality/scripts/test-upgrade-manifest-guard.sh — keep-mode reseed, 10 operator sentinels (incl. unanticipated unknown-operator-dir/x, harvester/sop.md, fleet/my-fleet.yaml) survive byte-identical + mtime-unchanged; retired framework file pruned; secret value absent from output. RED=31 fail (orig install.sh) → GREEN=48 pass (fixed).
  • install.sh: keep mode now manifest-driven (sync_framework_keep, no --delete); overwrite unchanged. PRESERVE_PATHS denylist deleted.
  • TS sync: file-ops.syncDirectory gains isOperatorOwned guard; file-adapter.syncFramework derives it from loadManifest — hardcoded (drifted) preservePaths deleted. Fixture uses the REAL manifest.
  • Parity: manifest-parity.spec.ts (§6.1) — bash↔TS agree on 34 paths + subtree roots.
  • Migration matrix test-install-migration.sh: F6 flipped — my-fleet.yaml now MUST survive (fail-safe).
  • CI: new merge-blocking upgrade-guard step (.woodpecker/ci.yml) runs both bash suites (adds rsync).
  • update-checker.ts reseed comment corrected to the manifest model.

Gates (all green)

  • pnpm typecheck ✓ · pnpm lint ✓ · pnpm format:check
  • Full mosaic vitest: 1062 passed (cli-smoke needs pnpm build first — build-artifact dep, not this change).
  • HARD GATE 48/48 · migration 21/21 · parity 3/3 · manifest 18/18 · file-adapter 8/8.

PR opened + reported (2026-07-16)

  • PR #802 #802 — base main@9745bc3f, head 34e55d4a (commit feat(mosaic): manifest-owned upgrade guard…). 15 files, +1160/-142.
  • Reported PR head + red→green evidence to MS-LEAD (web1:mosaic-100); queued (lead busy). Standing by for the independent-review commission at head 34e55d4a.
  • TWO items flagged to MS-LEAD for decision (awaiting reply):
    1. Deviation .txt vs .json — confirm accept (parity-tested) or convert to .json+jq.
    2. pr-create -i 791 appended Fixes #791 → would auto-close the tracking issue on PR1 merge while PR2/PR3 remain. Recommended edit to Part of #791; awaiting go-ahead to patch PR body.
  • DO NOT start PR2/PR3 until PR1 merges (DAG; one PR at a time).

MS-LEAD ruling → #797 ledger-survival sentinel folded into PR1 (2026-07-16)

MS-LEAD ruled both my decisions: (1) .txt format ACCEPTED (parity must be strict/merge-blocking incl. format edge cases + negative probe); (2) trailer Fixes #791Part of #791 APPROVED (patched PR #802 body via Gitea API — tracking issue no longer auto-closes on PR1 merge). Plus Mos-ELEVATED merge-blocker (spec ~/agent-work/planning/epic-796/791-ledger-survival-sentinel-SPEC.md): #797 Runtime Session Ledger must survive upgrade. Two coupled deliverables landed in PR1:

  • (i) Carve-out: fleet/run/** was ALREADY an explicit [operator] entry — glob matches the spec's pinned fleet/run/** EXACTLY, so NO divergence to route back to planner-opus. Strengthened its comment to name the ledger (fleet/run/sessions/ events.ndjson + ledger.json) so it is unmistakably load-bearing.
  • (ii) HARD-GATE sentinel: seeded populated ledger (events.ndjson 3 events + ledger.json node+edge+gen, 0600 under 0700) into test-upgrade-manifest-guard.sh sentinels; asserts byte-identical + mtime-unchanged
    • dir-perms unchanged. Negative control (retired framework file IS pruned) relabeled explicitly. HARD GATE now 58/58 (was 48).
  • Decision-1 parity hardening: format-edge fixtures (comments/blanks/whitespace, duplicate+overlapping globs deny-wins, section/glob-ordering independence) + explicit UNKNOWN→operator negative probe, driven through BOTH resolvers via MANIFEST_FILE override. Parity 7/7 (was 3).
  • RED-FIRST honesty note: the bash ledger sentinel stays GREEN even against the pre-fix installer (the ledger was incidentally safe from the rsync --delete bug; overall pre-fix run 30/58 as expected). The carve-out's TRUE load-bearing value (deny-wins if framework ownership ever broadens to fleet/**) is isolated by a dedicated resolver-seam red→green in manifest.spec.ts: WITHOUT fleet/run/** operator entry + hypothetical fleet/** framework → ledger resolves framework and planPrune DELETES it (RED); WITH the carve-out → deny-wins → operator, unprunable (GREEN). manifest.spec.ts 21/21 (was 18).
  • Gates all green: typecheck ✓ lint ✓ format:check ✓ · full mosaic vitest 1069 passed · HARD GATE 58/58 · migration 21/21. Committing FORWARD on the branch (NOT rebasing 34e55d4a out from under review).