From 0b9c0284006c8d892c4617c2bfc16869bb6637f0 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 18 Jul 2026 22:38:53 +0800 Subject: [PATCH] fix(backup): align Host110 runtime manifest staging --- agent99-host110-backup-runtime-broker.ps1 | 2 +- .../AWOOOI-AG99-H110-BACKUP-RUNTIME-V8.md | 78 +++++++++++++++++++ ...t_agent99_host110_backup_runtime_broker.py | 44 +++++++++++ 3 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 docs/proposals/AWOOOI-AG99-H110-BACKUP-RUNTIME-V8.md diff --git a/agent99-host110-backup-runtime-broker.ps1 b/agent99-host110-backup-runtime-broker.ps1 index af94e0f79..ad842e7c3 100644 --- a/agent99-host110-backup-runtime-broker.ps1 +++ b/agent99-host110-backup-runtime-broker.ps1 @@ -384,7 +384,7 @@ if (Test-Path -LiteralPath $evidencePath -PathType Leaf) { } $sourceRoot = Join-Path $SourceRootBase "host110-backup-source-$RunId" -$manifestPath = Join-Path $sourceRoot "agent99-host110-backup-runtime-manifest.json" +$manifestPath = Join-Path $sourceRoot "manifest.json" $remoteStage = "/tmp/agent99-host110-backup-runtime-$RunId" $startedAt = [DateTime]::UtcNow $mutex = [Threading.Mutex]::new($false, $TransportMutexName) diff --git a/docs/proposals/AWOOOI-AG99-H110-BACKUP-RUNTIME-V8.md b/docs/proposals/AWOOOI-AG99-H110-BACKUP-RUNTIME-V8.md new file mode 100644 index 000000000..dd1d1df80 --- /dev/null +++ b/docs/proposals/AWOOOI-AG99-H110-BACKUP-RUNTIME-V8.md @@ -0,0 +1,78 @@ +# AWOOOI Agent99 Host110 Backup Runtime V8 + +Status: proposed; owner approval required before any production apply. + +This artifact supersedes V2 through V7. The V7 candidate, revision, artifact, +and diff hashes are void for approval. All V5 hashes remain void. Prior refs +remain immutable audit evidence only; none is production execution authority. + +## Scope + +- Agent99 runtime bundle: 19 files, guarded by one producer/consumer parity + test. +- Host110 backup payload: 18 files, comprising 17 backup scripts and + `backup-health-textfile-exporter.py`. +- Host110 remote stage: 21 unique basenames, comprising the 18 payload files, + the fixed executor, the independent verifier, and `manifest.json`. +- Fixed target: `wooo@192.168.0.110`, dispatched by Windows99 Agent99 from an + exact Gitea main revision and digest manifest. +- Gitea offline backup and container lifecycle changes remain excluded. + `backup-gitea.sh` has no candidate diff. + +## V7 Defect Closed + +V7 created and uploaded +`agent99-host110-backup-runtime-manifest.json`, while the fixed Host110 +executor consumed `$SOURCE_STAGE/manifest.json`. V8 uses `manifest.json` at +the broker source stage, so SCP preserves the exact basename consumed by the +executor. + +The integration test derives all broker SCP basenames from the actual +PowerShell file list and manifest path, requires 21 unique stage files, and +matches the manifest and verifier basenames to the executor's exact +`$SOURCE_STAGE` paths. A hand-built temporary manifest cannot satisfy this +test by itself. + +## Exact Diff Serialization + +Substitute the V8 Gitea live-ref SHA for `` and hash the +exact stdout bytes from this command: + +```sh +git diff --binary --full-index \ + e7e3bcf8a5a8a19c1253d73f51d9dca248af0aa3.. | + shasum -a 256 +``` + +The review message independently binds the Gitea live ref and Git SHA, this +tracked artifact path and bytes hash, and the serialized diff hash. + +## Apply Effects + +- Windows99 Agent99 runtime promotion writes the 19 runtime files plus bounded + manifest and receipt state. +- Host110 Apply writes/replaces the 18 payload paths through one exclusive, + digest-bound filesystem transaction. +- Apply does not run a backup, invoke a Gitea container lifecycle, sync Google + Drive, prune or delete snapshots, restart a VM or service, or change a DB, + network, or firewall. + +## Validation + +- The exact broker/SCP basename to executor path integration test must pass. +- Focused runtime, broker, backup, parity, and Ansible suites must pass. +- Ansible and shell/Python static validation must pass. +- Changed PowerShell must parse on Windows99 with zero errors. +- Central review must independently reproduce the live ref, artifact hash, + diff hash, 19/18/21 counts, and zero `backup-gitea.sh` diff. + +## Rollback + +- Source rollback reverts the exact V8 diff to + `e7e3bcf8a5a8a19c1253d73f51d9dca248af0aa3` without force-pushing. +- Agent99 rollback restores the prior runtime files and manifest and verifies + the prior hashes independently. +- Host110 rollback restores every prior payload's content, SHA-256, uid, gid, + and mode under the exclusive lock. +- Any mismatch terminates as `rollback_unverified` and cannot be reported as a + successful rollback. diff --git a/scripts/reboot-recovery/tests/test_agent99_host110_backup_runtime_broker.py b/scripts/reboot-recovery/tests/test_agent99_host110_backup_runtime_broker.py index 3bea09c33..263aaf327 100644 --- a/scripts/reboot-recovery/tests/test_agent99_host110_backup_runtime_broker.py +++ b/scripts/reboot-recovery/tests/test_agent99_host110_backup_runtime_broker.py @@ -50,6 +50,22 @@ def _sha256(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() +def _powershell_string(source: str, variable: str) -> str: + match = re.search(rf'\${re.escape(variable)}\s*=\s*"([^"]+)"', source) + assert match is not None, variable + return match.group(1) + + +def _powershell_array(source: str, variable: str) -> tuple[str, ...]: + match = re.search( + rf'\${re.escape(variable)}\s*=\s*@\((.*?)\n\)', + source, + re.DOTALL, + ) + assert match is not None, variable + return tuple(re.findall(r'"([^"]+)"', match.group(1))) + + def test_windows99_broker_fetches_exact_gitea_revision_and_is_bounded() -> None: source = BROKER.read_text(encoding="utf-8") @@ -75,6 +91,34 @@ def test_windows99_broker_fetches_exact_gitea_revision_and_is_bounded() -> None: assert 'Invoke-Agent99BoundedScp $sourcePackage.localPaths' in source +def test_broker_scp_basenames_match_executor_source_stage_contract() -> None: + broker = BROKER.read_text(encoding="utf-8") + executor = EXECUTOR.read_text(encoding="utf-8") + + runtime_paths = _powershell_array(broker, "RuntimeRelativePaths") + executor_path = _powershell_string(broker, "ExecutorRelativePath") + verifier_path = _powershell_string(broker, "VerifierRelativePath") + manifest_match = re.search( + r'\$manifestPath\s*=\s*Join-Path\s+\$sourceRoot\s+"([^"]+)"', + broker, + ) + assert manifest_match is not None + manifest_name = manifest_match.group(1) + + staged_basenames = tuple( + Path(path).name + for path in (*runtime_paths, executor_path, verifier_path, manifest_name) + ) + assert len(staged_basenames) == 21 + assert len(set(staged_basenames)) == 21 + assert manifest_name == "manifest.json" + assert '$localPaths += $executorPath, $verifierPath, $ManifestPath' in broker + assert 'Invoke-Agent99BoundedScp $sourcePackage.localPaths $remoteStage' in broker + assert f'$SOURCE_STAGE/{manifest_name}' in executor + assert f'$SOURCE_STAGE/{Path(verifier_path).name}' in executor + assert Path(executor_path).name == "host110-backup-runtime-executor.sh" + + def test_windows99_broker_verify_is_independent_no_write_and_evidence_is_immutable() -> None: source = BROKER.read_text(encoding="utf-8")