fix(agent99): deploy backup freshness exporter atomically

This commit is contained in:
Your Name
2026-07-18 21:33:50 +08:00
parent 68dcc6bbd4
commit 5c4f22f7da
5 changed files with 122 additions and 41 deletions

View File

@@ -214,6 +214,8 @@ $host110BackupRuntimeContract = [bool](
$host110BackupRuntimeBroker.Contains('"merge-base", "--is-ancestor", $SourceRevision, $sourceHead') -and
$host110BackupRuntimeBroker.Contains('executorSha256 = $executorDigest') -and
$host110BackupRuntimeBroker.Contains('verifierSha256 = $verifierDigest') -and
$host110BackupRuntimeBroker.Contains('"scripts/ops/backup-health-textfile-exporter.py"') -and
$host110BackupRuntimeBroker.Contains('$ExpectedFileCount = 18') -and
$host110BackupRuntimeBroker.Contains('StrictHostKeyChecking=yes') -and
$host110BackupRuntimeBroker.Contains('$process.WaitForExit($TimeoutSeconds * 1000)') -and
$host110BackupRuntimeBroker.Contains('taskkill.exe /PID $process.Id /T /F') -and

View File

@@ -20,8 +20,8 @@ $IdentityFile = Join-Path $ExpectedAgentRoot "keys\agent99_ed25519"
$EvidenceRoot = Join-Path $ExpectedAgentRoot "evidence\host110-backup-runtime"
$SourceRootBase = Join-Path $ExpectedAgentRoot "deploy"
$TransportMutexName = "Global\WoooAgent99Host110BackupRuntimeV2"
$ExpectedFileCount = 17
$ExpectedRemoteStageFileCount = 20
$ExpectedFileCount = 18
$ExpectedRemoteStageFileCount = 21
$RuntimeRelativePaths = @(
"scripts/backup/common.sh",
"scripts/backup/backup-all.sh",
@@ -39,7 +39,8 @@ $RuntimeRelativePaths = @(
"scripts/backup/sync-offsite-backups.sh",
"scripts/backup/backup-offsite-readiness-gate.sh",
"scripts/backup/verify-offsite-full-sync.sh",
"scripts/backup/enforce-latest-only-retention.sh"
"scripts/backup/enforce-latest-only-retention.sh",
"scripts/ops/backup-health-textfile-exporter.py"
)
$ExecutorRelativePath = "scripts/backup/host110-backup-runtime-executor.sh"
$VerifierRelativePath = "scripts/backup/verify-host110-backup-runtime.py"
@@ -281,7 +282,9 @@ function Get-Agent99ExecutorResult {
[string]$parsed.mode -ne $ExpectedMode.ToLowerInvariant() -or
-not [bool]$parsed.ok -or
[string]$parsed.sourceRevision -ne $SourceRevision -or
[int]$parsed.fileCount -ne $ExpectedFileCount
[int]$parsed.fileCount -ne $ExpectedFileCount -or
[int]$parsed.backupScriptFileCount -ne 17 -or
-not [bool]$parsed.backupHealthExporterIncluded
) {
throw "host110_${ExpectedMode}_receipt_contract_failed"
}
@@ -313,6 +316,8 @@ function Get-Agent99VerifierResult {
[string]$parsed.sourceRevision -ne $SourceRevision -or
[string]$parsed.runId -ne $RunId -or
[int]$parsed.fileCount -ne $ExpectedFileCount -or
[int]$parsed.backupScriptFileCount -ne 17 -or
-not [bool]$parsed.backupHealthExporterIncluded -or
[bool]$parsed.remoteWritePerformed -or
[string]$parsed.verifierSha256 -ne [string]$SourcePackage.verifierSha256
) {
@@ -324,7 +329,7 @@ function Get-Agent99VerifierResult {
function Invoke-Agent99ReadOnlyPreflight {
param([object]$SourcePackage)
$command = 'test "$(id -un)" = wooo && hostname -I | tr " " "\n" | grep -qx 192.168.0.110 && test -d /backup/scripts && test ! -L /backup/scripts && command -v timeout >/dev/null && command -v flock >/dev/null && command -v python3 >/dev/null && printf HOST110_BACKUP_PREFLIGHT_OK=1'
$command = 'test "$(id -un)" = wooo && hostname -I | tr " " "\n" | grep -qx 192.168.0.110 && test -d /backup/scripts && test ! -L /backup/scripts && test -d /home/wooo/scripts && test ! -L /home/wooo/scripts && command -v timeout >/dev/null && command -v flock >/dev/null && command -v python3 >/dev/null && printf HOST110_BACKUP_PREFLIGHT_OK=1'
$transport = Invoke-Agent99BoundedSsh $command 45
if (-not $transport.ok -or ([string]$transport.stdout).Trim() -ne "HOST110_BACKUP_PREFLIGHT_OK=1") {
throw "host110_read_only_preflight_failed"
@@ -335,6 +340,8 @@ function Invoke-Agent99ReadOnlyPreflight {
sourceRevision = $SourceRevision
sourceHead = [string]$SourcePackage.sourceHead
fileCount = $ExpectedFileCount
backupScriptFileCount = 17
backupHealthExporterIncluded = $true
remoteWritePerformed = $false
}
}
@@ -342,7 +349,7 @@ function Invoke-Agent99ReadOnlyPreflight {
function Invoke-Agent99NoWriteVerifier {
param([object]$SourcePackage)
$command = "timeout --signal=TERM --kill-after=5s 60s python3 - --manifest-base64 $($SourcePackage.manifestBase64) --destination /backup/scripts --source-revision $SourceRevision --run-id $RunId --expected-verifier-sha256 $($SourcePackage.verifierSha256)"
$command = "timeout --signal=TERM --kill-after=5s 60s python3 - --manifest-base64 $($SourcePackage.manifestBase64) --destination /backup/scripts --exporter-destination /home/wooo/scripts/backup-health-textfile-exporter.py --source-revision $SourceRevision --run-id $RunId --expected-verifier-sha256 $($SourcePackage.verifierSha256)"
$transport = Invoke-Agent99BoundedSsh $command 90 ([string]$SourcePackage.verifierPath)
return Get-Agent99VerifierResult $transport $SourcePackage
}

View File

@@ -7,6 +7,8 @@ umask 077
readonly EXPECTED_HOST_IP="192.168.0.110"
readonly EXPECTED_USER="wooo"
readonly DEST_ROOT="/backup/scripts"
readonly EXPORTER_ROOT="/home/wooo/scripts"
readonly EXPORTER_FILE="backup-health-textfile-exporter.py"
readonly STATUS_ROOT="/backup/status"
readonly STAGE_ROOT="/backup/.agent99-backup-runtime-stage"
readonly ROLLBACK_ROOT="/backup/.agent99-backup-runtime-rollback"
@@ -31,6 +33,7 @@ readonly -a RUNTIME_FILES=(
verify-offsite-full-sync.sh
enforce-latest-only-retention.sh
)
readonly -a PAYLOAD_FILES=("${RUNTIME_FILES[@]}" "$EXPORTER_FILE")
MODE=""
SOURCE_REVISION=""
@@ -46,13 +49,14 @@ APPLY_COMPLETE=0
ROLLBACK_VERIFIED=0
declare -A FILE_EXISTED=()
declare -A PREVIOUS_DIGEST=()
declare -A PREVIOUS_METADATA=()
declare -A EXPECTED_DIGEST=()
usage() {
printf '%s\n' \
"Usage: $0 --mode check|apply|verify --source-revision SHA --run-id ID --source-stage PATH" \
"The executor is fixed to host 192.168.0.110, an Agent99 run-bound source stage," \
"and the 17-file /backup/scripts transaction."
"and the 18-file transaction: 17 /backup/scripts files plus the backup health exporter."
}
fail() {
@@ -93,7 +97,7 @@ case "$MODE" in check|apply|verify) ;; *) fail "invalid_mode" ;; esac
expected_source_stage="/tmp/agent99-host110-backup-runtime-${RUN_ID}"
[ "$SOURCE_STAGE" = "$expected_source_stage" ] || fail "invalid_source_stage"
for command_name in awk bash cp flock grep hostname id install mv pgrep python3 readlink rm sha256sum stat timeout tr; do
for command_name in awk bash cp dirname flock grep hostname id install mv pgrep python3 readlink rm sha256sum stat timeout tr; do
command -v "$command_name" >/dev/null 2>&1 || fail "required_command_missing_${command_name}"
done
@@ -102,8 +106,9 @@ current_user="$(id -un)"
hostname -I 2>/dev/null | tr ' ' '\n' | grep -qx "$EXPECTED_HOST_IP" || fail "unexpected_executor_host"
[ -d "$SOURCE_STAGE" ] && [ ! -L "$SOURCE_STAGE" ] || fail "canonical_source_stage_unavailable"
[ -d "$DEST_ROOT" ] && [ ! -L "$DEST_ROOT" ] || fail "backup_runtime_destination_unavailable"
[ -d "$EXPORTER_ROOT" ] && [ ! -L "$EXPORTER_ROOT" ] || fail "backup_exporter_destination_unavailable"
manifest_rows="$(python3 - "$SOURCE_STAGE/manifest.json" "$SOURCE_REVISION" "$RUN_ID" "${RUNTIME_FILES[@]}" <<'PY'
manifest_rows="$(python3 - "$SOURCE_STAGE/manifest.json" "$SOURCE_REVISION" "$RUN_ID" "${PAYLOAD_FILES[@]}" <<'PY'
import json
import re
import sys
@@ -184,7 +189,7 @@ source_digest() {
validate_source() {
local name source_path expected_digest actual_digest resolved
for name in "${RUNTIME_FILES[@]}"; do
for name in "${PAYLOAD_FILES[@]}"; do
source_path="$SOURCE_STAGE/$name"
[ -f "$source_path" ] && [ ! -L "$source_path" ] || return 65
resolved="$(readlink -f "$source_path")"
@@ -196,10 +201,19 @@ validate_source() {
done
}
destination_path() {
local name="$1"
if [ "$name" = "$EXPORTER_FILE" ]; then
printf '%s/%s\n' "$EXPORTER_ROOT" "$name"
else
printf '%s/%s\n' "$DEST_ROOT" "$name"
fi
}
verify_destination() {
local name dest_path expected_digest actual_digest
for name in "${RUNTIME_FILES[@]}"; do
dest_path="$DEST_ROOT/$name"
for name in "${PAYLOAD_FILES[@]}"; do
dest_path="$(destination_path "$name")"
[ -f "$dest_path" ] && [ ! -L "$dest_path" ] || return 68
expected_digest="$(source_digest "$name")"
actual_digest="$(working_digest "$dest_path")"
@@ -227,7 +241,9 @@ document = {
"sourceRevision": sys.argv[3],
"sourceHead": sys.argv[4],
"runId": sys.argv[5],
"fileCount": 17,
"fileCount": 18,
"backupScriptFileCount": 17,
"backupHealthExporterIncluded": True,
"rollbackPerformed": sys.argv[6] == "1",
"rollbackVerified": sys.argv[6] == "1",
"verifierSha256": sys.argv[7],
@@ -248,23 +264,26 @@ PY
}
record_prestate() {
local name dest_path digest
local name dest_path digest metadata
: > "$ROLLBACK_DIR/prestate.tsv"
chmod 600 "$ROLLBACK_DIR/prestate.tsv"
for name in "${RUNTIME_FILES[@]}"; do
dest_path="$DEST_ROOT/$name"
for name in "${PAYLOAD_FILES[@]}"; do
dest_path="$(destination_path "$name")"
if [ -f "$dest_path" ] && [ ! -L "$dest_path" ]; then
[ "$(stat -c '%U:%G' "$dest_path")" = "wooo:wooo" ] || return 71
FILE_EXISTED[$name]=1
digest="$(working_digest "$dest_path")"
metadata="$(stat -c '%u:%g:%a' "$dest_path")"
PREVIOUS_DIGEST[$name]="$digest"
printf '%s\t1\t%s\n' "$name" "$digest" >> "$ROLLBACK_DIR/prestate.tsv"
PREVIOUS_METADATA[$name]="$metadata"
printf '%s\t1\t%s\t%s\n' "$name" "$digest" "$metadata" >> "$ROLLBACK_DIR/prestate.tsv"
cp -p -- "$dest_path" "$ROLLBACK_DIR/$name"
[ "$(working_digest "$ROLLBACK_DIR/$name")" = "$digest" ] || return 72
elif [ ! -e "$dest_path" ]; then
FILE_EXISTED[$name]=0
PREVIOUS_DIGEST[$name]="-"
printf '%s\t0\t-\n' "$name" >> "$ROLLBACK_DIR/prestate.tsv"
PREVIOUS_METADATA[$name]="-"
printf '%s\t0\t-\t-\n' "$name" >> "$ROLLBACK_DIR/prestate.tsv"
else
return 73
fi
@@ -272,14 +291,15 @@ record_prestate() {
}
rollback_transaction() {
local name dest_path backup_path temporary failed=0
local name dest_path destination_parent backup_path temporary failed=0
set +e
for name in "${RUNTIME_FILES[@]}"; do
for name in "${PAYLOAD_FILES[@]}"; do
[ -n "${FILE_EXISTED[$name]+x}" ] || { failed=1; continue; }
dest_path="$DEST_ROOT/$name"
dest_path="$(destination_path "$name")"
if [ "${FILE_EXISTED[$name]}" = "1" ]; then
backup_path="$ROLLBACK_DIR/$name"
temporary="$DEST_ROOT/.${name}.agent99-rollback-${RUN_ID}"
destination_parent="$(dirname "$dest_path")"
temporary="$destination_parent/.${name}.agent99-rollback-${RUN_ID}"
cp -p -- "$backup_path" "$temporary" \
&& mv -f -- "$temporary" "$dest_path" \
|| failed=1
@@ -287,11 +307,12 @@ rollback_transaction() {
rm -f -- "$dest_path" || failed=1
fi
done
for name in "${RUNTIME_FILES[@]}"; do
dest_path="$DEST_ROOT/$name"
for name in "${PAYLOAD_FILES[@]}"; do
dest_path="$(destination_path "$name")"
if [ "${FILE_EXISTED[$name]:-}" = "1" ]; then
[ -f "$dest_path" ] && [ ! -L "$dest_path" ] \
&& [ "$(working_digest "$dest_path" 2>/dev/null)" = "${PREVIOUS_DIGEST[$name]:-invalid}" ] \
&& [ "$(stat -c '%u:%g:%a' "$dest_path" 2>/dev/null)" = "${PREVIOUS_METADATA[$name]:-invalid}" ] \
|| failed=1
elif [ "${FILE_EXISTED[$name]:-}" = "0" ]; then
[ ! -e "$dest_path" ] || failed=1
@@ -328,13 +349,13 @@ cleanup() {
validate_source || fail "source_validation_failed"
if [ "$MODE" = "check" ]; then
printf '{"schemaVersion":"agent99_host110_backup_runtime_executor_v1","mode":"check","ok":true,"sourceRevision":"%s","sourceHead":"%s","fileCount":17,"remoteWritePerformed":false}\n' "$SOURCE_REVISION" "$SOURCE_HEAD"
printf '{"schemaVersion":"agent99_host110_backup_runtime_executor_v1","mode":"check","ok":true,"sourceRevision":"%s","sourceHead":"%s","fileCount":18,"backupScriptFileCount":17,"backupHealthExporterIncluded":true,"remoteWritePerformed":false}\n' "$SOURCE_REVISION" "$SOURCE_HEAD"
exit 0
fi
if [ "$MODE" = "verify" ]; then
verify_destination || fail "destination_verification_failed"
printf '{"schemaVersion":"agent99_host110_backup_runtime_executor_v1","mode":"verify","ok":true,"sourceRevision":"%s","sourceHead":"%s","fileCount":17,"remoteWritePerformed":false}\n' "$SOURCE_REVISION" "$SOURCE_HEAD"
printf '{"schemaVersion":"agent99_host110_backup_runtime_executor_v1","mode":"verify","ok":true,"sourceRevision":"%s","sourceHead":"%s","fileCount":18,"backupScriptFileCount":17,"backupHealthExporterIncluded":true,"remoteWritePerformed":false}\n' "$SOURCE_REVISION" "$SOURCE_HEAD"
exit 0
fi
@@ -362,7 +383,7 @@ trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM HUP
for name in "${RUNTIME_FILES[@]}"; do
for name in "${PAYLOAD_FILES[@]}"; do
install -m 700 "$SOURCE_STAGE/$name" "$STAGE_DIR/$name"
[ "$(working_digest "$STAGE_DIR/$name")" = "$(source_digest "$name")" ] || fail "stage_identity_failed"
validate_file "$STAGE_DIR/$name" || fail "stage_validation_failed"
@@ -374,16 +395,19 @@ APPLY_STARTED=1
# common.sh is promoted first. Every managed mutating backup entrypoint then
# takes the shared side of LOCK_PATH before doing work, while this process holds
# the exclusive side until the independent verifier has completed.
for name in "${RUNTIME_FILES[@]}"; do
temporary="$DEST_ROOT/.${name}.agent99-${RUN_ID}"
for name in "${PAYLOAD_FILES[@]}"; do
dest_path="$(destination_path "$name")"
destination_parent="$(dirname "$dest_path")"
temporary="$destination_parent/.${name}.agent99-${RUN_ID}"
install -m 755 "$STAGE_DIR/$name" "$temporary"
mv -f -- "$temporary" "$DEST_ROOT/$name"
mv -f -- "$temporary" "$dest_path"
done
verify_destination || fail "post_apply_verification_failed"
verifier_result="$(BACKUP_RUNTIME_DEPLOY_CONTEXT=1 python3 "$SOURCE_STAGE/verify-host110-backup-runtime.py" \
--manifest "$SOURCE_STAGE/manifest.json" \
--destination "$DEST_ROOT" \
--exporter-destination "$EXPORTER_ROOT/$EXPORTER_FILE" \
--source-revision "$SOURCE_REVISION" \
--run-id "$RUN_ID" \
--expected-verifier-sha256 "$VERIFIER_DIGEST")" || fail "independent_verifier_failed"
@@ -400,7 +424,9 @@ if not (
and row.get("ok") is True
and row.get("sourceRevision") == sys.argv[2]
and row.get("runId") == sys.argv[3]
and row.get("fileCount") == 17
and row.get("fileCount") == 18
and row.get("backupScriptFileCount") == 17
and row.get("backupHealthExporterIncluded") is True
and row.get("remoteWritePerformed") is False
and row.get("selfIdentityVerified") is True
):
@@ -423,7 +449,9 @@ print(json.dumps({
"ok": True,
"sourceRevision": sys.argv[1],
"sourceHead": sys.argv[2],
"fileCount": 17,
"fileCount": 18,
"backupScriptFileCount": 17,
"backupHealthExporterIncluded": True,
"rollbackPerformed": False,
"receipt": sys.argv[3],
"verifier": json.loads(sys.argv[4]),

View File

@@ -16,8 +16,9 @@ from pathlib import Path
EXPECTED_DESTINATION = Path("/backup/scripts")
EXPECTED_EXPORTER_DESTINATION = Path("/home/wooo/scripts/backup-health-textfile-exporter.py")
RUNTIME_LOCK = Path("/tmp/agent99-host110-backup-runtime.lock")
EXPECTED_FILES = (
EXPECTED_BACKUP_FILES = (
"common.sh",
"backup-all.sh",
"backup-host188-products.sh",
@@ -36,6 +37,8 @@ EXPECTED_FILES = (
"verify-offsite-full-sync.sh",
"enforce-latest-only-retention.sh",
)
EXPORTER_FILE = "backup-health-textfile-exporter.py"
EXPECTED_FILES = EXPECTED_BACKUP_FILES + (EXPORTER_FILE,)
def fail(reason: str) -> None:
@@ -78,6 +81,7 @@ def main() -> None:
manifest_source.add_argument("--manifest")
manifest_source.add_argument("--manifest-base64")
parser.add_argument("--destination", required=True)
parser.add_argument("--exporter-destination", required=True)
parser.add_argument("--source-revision", required=True)
parser.add_argument("--run-id", required=True)
parser.add_argument("--expected-verifier-sha256", required=True)
@@ -90,6 +94,9 @@ def main() -> None:
destination = Path(args.destination)
if destination != EXPECTED_DESTINATION or destination.is_symlink() or not destination.is_dir():
fail("invalid_destination")
exporter_destination = Path(args.exporter_destination)
if exporter_destination != EXPECTED_EXPORTER_DESTINATION:
fail("invalid_exporter_destination")
runtime_lock = acquire_runtime_read_lock(destination)
if not re.fullmatch(r"[0-9a-f]{64}", args.expected_verifier_sha256):
@@ -131,11 +138,14 @@ def main() -> None:
root = destination.resolve(strict=True)
for name in EXPECTED_FILES:
path = destination / name
path = exporter_destination if name == EXPORTER_FILE else destination / name
if path.is_symlink() or not path.is_file():
fail("runtime_file_type_failed")
resolved = path.resolve(strict=True)
if resolved.parent != root:
if name == EXPORTER_FILE:
if resolved != exporter_destination:
fail("runtime_file_boundary_failed")
elif resolved.parent != root:
fail("runtime_file_boundary_failed")
stat = resolved.stat()
if stat.st_uid != os.getuid() or stat.st_gid != os.getgid() or stat.st_mode & 0o777 != 0o755:
@@ -149,7 +159,10 @@ def main() -> None:
"sourceRevision": args.source_revision,
"runId": args.run_id,
"fileCount": len(EXPECTED_FILES),
"backupScriptFileCount": len(EXPECTED_BACKUP_FILES),
"backupHealthExporterIncluded": True,
"destination": str(EXPECTED_DESTINATION),
"exporterDestination": str(EXPECTED_EXPORTER_DESTINATION),
"remoteWritePerformed": False,
"secretValuesRead": False,
"selfIdentityVerified": self_identity_verified,

View File

@@ -64,6 +64,7 @@ def test_windows99_broker_fetches_exact_gitea_revision_and_is_bounded() -> None:
assert '"checkout", "--quiet", $SourceRevision, "--"' in source
assert 'executorSha256 = $executorDigest' in source
assert 'verifierSha256 = $verifierDigest' in source
assert '"scripts/ops/backup-health-textfile-exporter.py"' in source
assert "/home/wooo/awoooi" not in source
assert '"StrictHostKeyChecking=yes"' in source
assert '"NumberOfPasswordPrompts=0"' in source
@@ -98,6 +99,9 @@ def test_host110_executor_is_manifest_bound_atomic_and_fail_closed() -> None:
assert 'readonly EXPECTED_HOST_IP="192.168.0.110"' in source
assert 'readonly EXPECTED_USER="wooo"' in source
assert 'readonly DEST_ROOT="/backup/scripts"' in source
assert 'readonly EXPORTER_ROOT="/home/wooo/scripts"' in source
assert 'readonly EXPORTER_FILE="backup-health-textfile-exporter.py"' in source
assert 'readonly -a PAYLOAD_FILES=("${RUNTIME_FILES[@]}" "$EXPORTER_FILE")' in source
assert '--source-stage' in source
assert 'document.get("executorSha256"' in source
assert 'document.get("verifierSha256"' in source
@@ -105,16 +109,20 @@ def test_host110_executor_is_manifest_bound_atomic_and_fail_closed() -> None:
assert 'verifier_identity_failed' in source
assert 'flock -x -w 30 9' in source
assert 'legacy_backup_runtime_active' in source
assert source.index("for name in \"${RUNTIME_FILES[@]}\"") < source.index("mv -f -- \"$temporary\"")
assert source.index("for name in \"${PAYLOAD_FILES[@]}\"") < source.index("mv -f -- \"$temporary\"")
assert 'rollback_unverified' in source
assert 'failed_rolled_back' in source
assert 'working_digest "$dest_path"' in source
assert 'PREVIOUS_METADATA' in source
assert "stat -c '%u:%g:%a'" in source
assert 'os.link(temporary, path)' in source
assert 'run_identity_receipt_exists' in source
assert 'run_identity_stage_exists' in source
assert 'run_identity_rollback_exists' in source
assert 'verify-host110-backup-runtime.py' in source
assert 'selfIdentityVerified' in source
assert '"backupScriptFileCount": 17' in source
assert '"backupHealthExporterIncluded": True' in source
assert '"productionServiceRestarted": False' in source
assert '"secretValuesRead": False' in source
assert "sudo " not in source
@@ -139,12 +147,16 @@ def test_independent_verifier_accepts_exact_bundle_without_writing(tmp_path, mon
verifier = _load_verifier()
destination = tmp_path / "scripts"
destination.mkdir()
exporter_destination = tmp_path / verifier.EXPORTER_FILE
rows = []
for name in verifier.EXPECTED_FILES:
for name in verifier.EXPECTED_BACKUP_FILES:
path = destination / name
path.write_text(f"runtime:{name}\n", encoding="utf-8")
path.chmod(0o755)
rows.append({"name": name, "sha256": _sha256(path)})
exporter_destination.write_text("exporter\n", encoding="utf-8")
exporter_destination.chmod(0o755)
rows.append({"name": verifier.EXPORTER_FILE, "sha256": _sha256(exporter_destination)})
verifier_digest = _sha256(VERIFIER)
source_revision = "a" * 40
run_id = "unit-exact"
@@ -159,8 +171,12 @@ def test_independent_verifier_accepts_exact_bundle_without_writing(tmp_path, mon
}
manifest_path = tmp_path / "manifest.json"
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
before = {path.name: (path.stat().st_mtime_ns, _sha256(path)) for path in destination.iterdir()}
before = {
path.name: (path.stat().st_mtime_ns, _sha256(path))
for path in [*destination.iterdir(), exporter_destination]
}
monkeypatch.setattr(verifier, "EXPECTED_DESTINATION", destination)
monkeypatch.setattr(verifier, "EXPECTED_EXPORTER_DESTINATION", exporter_destination)
monkeypatch.setattr(
sys,
"argv",
@@ -170,6 +186,8 @@ def test_independent_verifier_accepts_exact_bundle_without_writing(tmp_path, mon
str(manifest_path),
"--destination",
str(destination),
"--exporter-destination",
str(exporter_destination),
"--source-revision",
source_revision,
"--run-id",
@@ -182,8 +200,14 @@ def test_independent_verifier_accepts_exact_bundle_without_writing(tmp_path, mon
verifier.main()
result = json.loads(capsys.readouterr().out)
after = {path.name: (path.stat().st_mtime_ns, _sha256(path)) for path in destination.iterdir()}
after = {
path.name: (path.stat().st_mtime_ns, _sha256(path))
for path in [*destination.iterdir(), exporter_destination]
}
assert result["ok"] is True
assert result["fileCount"] == 18
assert result["backupScriptFileCount"] == 17
assert result["backupHealthExporterIncluded"] is True
assert result["remoteWritePerformed"] is False
assert result["selfIdentityVerified"] is True
assert before == after
@@ -193,13 +217,17 @@ def test_independent_verifier_rejects_drifted_bundle(tmp_path, monkeypatch, caps
verifier = _load_verifier()
destination = tmp_path / "scripts"
destination.mkdir()
exporter_destination = tmp_path / verifier.EXPORTER_FILE
rows = []
for name in verifier.EXPECTED_FILES:
for name in verifier.EXPECTED_BACKUP_FILES:
path = destination / name
path.write_text(f"runtime:{name}\n", encoding="utf-8")
path.chmod(0o755)
rows.append({"name": name, "sha256": _sha256(path)})
(destination / verifier.EXPECTED_FILES[0]).write_text("drifted\n", encoding="utf-8")
exporter_destination.write_text("exporter\n", encoding="utf-8")
exporter_destination.chmod(0o755)
rows.append({"name": verifier.EXPORTER_FILE, "sha256": _sha256(exporter_destination)})
(destination / verifier.EXPECTED_BACKUP_FILES[0]).write_text("drifted\n", encoding="utf-8")
verifier_digest = _sha256(VERIFIER)
source_revision = "d" * 40
run_id = "unit-drift"
@@ -214,6 +242,7 @@ def test_independent_verifier_rejects_drifted_bundle(tmp_path, monkeypatch, caps
}
encoded = base64.b64encode(json.dumps(manifest).encode()).decode()
monkeypatch.setattr(verifier, "EXPECTED_DESTINATION", destination)
monkeypatch.setattr(verifier, "EXPECTED_EXPORTER_DESTINATION", exporter_destination)
monkeypatch.setattr(
sys,
"argv",
@@ -223,6 +252,8 @@ def test_independent_verifier_rejects_drifted_bundle(tmp_path, monkeypatch, caps
encoded,
"--destination",
str(destination),
"--exporter-destination",
str(exporter_destination),
"--source-revision",
source_revision,
"--run-id",