fix: verify complete offline assets before replacing staged runtime

This commit is contained in:
karl committed 2026-10-12 21:30:10 +02:00
1 parent 01d6c3bc18
commit 99c423c0fd
4 files changed
+81 -10

No files matched your search

+3
View File
@@ -49,6 +49,9 @@ Status: implementation started; no production release yet. Windows VM tests use
- [ ] Isolated extraction workers with timeouts and resource ceilings.
## 5. Release gates
- [x] Preflight native license and the complete offline Google model pair before
modifying staged resources; corrupted/missing assets preserve existing worker.
- [ ] Atomic resource staging and interrupted-copy recovery for release bundles.
- [x] Shared monotonic Basic/Debug search metrics, granular bounded engine traces,
actual worker query diagnostics, and headless JSON performance output.
- [ ] Verify detailed performance UI interaction on native desktop platforms;
+6
View File
@@ -7,6 +7,12 @@ and `SEARCHI_FORCE_CPU=1`.
## Current integration checkpoint (2026-10-12)
Offline staging now verifies the native license and every bundled Google asset
before modifying resources. Two Python regressions verify missing-model rejection
and same-size corruption in the second model leaving the existing staged worker
untouched. Actual CPU worker staging passed. This preflight does not prove atomic
recovery from interruptions during the subsequent copies; that gate remains open.
Gitea Actions were disabled for **karl/searchi** at Karl's explicit request;
the API read-back confirmed `has_actions:false`. Fifteen old stuck runs and
the subsequent repair run were cancelled. Diagnosis found unavailable native
+24 -10
View File
@@ -9,6 +9,20 @@ import shutil
ROOT = Path(__file__).resolve().parents[2]
def verify_bundle(root, manifest):
"""Verify the complete offline model pair before modifying staged resources."""
artifacts = []
for spec in manifest["files"]:
artifact = root / ".searchi/models/embeddinggemma2" / spec["path"]
if artifact.stat().st_size != spec["bytes"]:
raise RuntimeError("Model size mismatch")
with artifact.open("rb") as stream:
if hashlib.file_digest(stream, "sha256").hexdigest() != spec["sha256"]:
raise RuntimeError("Model checksum mismatch")
artifacts.append(artifact)
return artifacts
def main():
parser = argparse.ArgumentParser(description="Stage Searchi runtime resources")
parser.add_argument("--backend", choices=["cpu", "auto", "metal", "vulkan", "cuda"], default="cpu")
@@ -21,23 +35,23 @@ def main():
source /= name
if not source.is_file():
raise RuntimeError("Build the pinned native worker before staging")
license_path = ROOT / ".searchi/references/llama.cpp/LICENSE"
if not license_path.is_file():
raise RuntimeError("Native worker license is missing")
artifacts = []
if args.bundle_model:
manifest = json.loads(Path(__file__).with_name("model-manifest.json").read_text())
artifacts = verify_bundle(ROOT, manifest)
destination = ROOT / "apps/desktop/src-tauri/resources"
destination.mkdir(parents=True, exist_ok=True)
shutil.copy2(source, destination / name)
# Include the upstream runtime license with the statically linked helper.
shutil.copy2(ROOT / ".searchi/references/llama.cpp/LICENSE", destination / "LLAMA-LICENSE.txt")
shutil.copy2(license_path, destination / "LLAMA-LICENSE.txt")
if args.bundle_model:
manifest = json.loads(Path(__file__).with_name("model-manifest.json").read_text())
model_destination = destination / "embeddinggemma2"
model_destination.mkdir(exist_ok=True)
for spec in manifest["files"]:
artifact = ROOT / ".searchi/models/embeddinggemma2" / spec["path"]
if artifact.stat().st_size != spec["bytes"]:
raise RuntimeError("Model size mismatch")
with artifact.open("rb") as stream:
if hashlib.file_digest(stream, "sha256").hexdigest() != spec["sha256"]:
raise RuntimeError("Model checksum mismatch")
shutil.copy2(artifact, model_destination / spec["path"])
for artifact in artifacts:
shutil.copy2(artifact, model_destination / artifact.name)
print(f"Staged native worker: {destination / name}")
@@ -0,0 +1,48 @@
"""Offline installation regressions; no model downloads or inference."""
import hashlib
import os
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
import stage_runtime
class StagingTests(unittest.TestCase):
def test_corrupt_second_model_preserves_existing_staged_worker(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
name = "searchi-gemma-worker.exe" if os.name == "nt" else "searchi-gemma-worker"
runtime = root / ".searchi/runtime/cpu"
if os.name == "nt":
runtime /= "Release"
runtime.mkdir(parents=True)
(runtime / name).write_bytes(b"new worker")
license_path = root / ".searchi/references/llama.cpp/LICENSE"
license_path.parent.mkdir(parents=True)
license_path.write_text("license")
destination = root / "apps/desktop/src-tauri/resources"
destination.mkdir(parents=True)
(destination / name).write_bytes(b"previous worker")
models = root / ".searchi/models/embeddinggemma2"
models.mkdir(parents=True)
specs = []
for filename, content in [("model.gguf", b"verified"), ("projector.gguf", b"original")]:
(models / filename).write_bytes(content)
specs.append({"path": filename, "bytes": len(content), "sha256": hashlib.sha256(content).hexdigest()})
(models / "projector.gguf").write_bytes(b"corrupt!")
with patch.object(stage_runtime, "ROOT", root), patch("sys.argv", ["stage_runtime", "--bundle-model"]), patch.object(stage_runtime.json, "loads", return_value={"files": specs}):
with self.assertRaisesRegex(RuntimeError, "checksum mismatch"):
stage_runtime.main()
self.assertEqual((destination / name).read_bytes(), b"previous worker")
self.assertFalse((destination / "embeddinggemma2").exists())
def test_missing_model_rejects_complete_bundle(self):
with tempfile.TemporaryDirectory() as directory:
with self.assertRaises(FileNotFoundError):
stage_runtime.verify_bundle(Path(directory), {"files": [{"path": "missing.gguf", "bytes": 1, "sha256": "unused"}]})
if __name__ == "__main__":
unittest.main()