Compare commits
1 commit
main
...
installer/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0644a3d24 |
10 changed files with 263 additions and 14 deletions
|
|
@ -18,7 +18,7 @@ namespace HermesHubSetup
|
|||
// Подставляется сборщиком из фактического git-коммита. Раньше здесь
|
||||
// жил зашитый "8cddc9f", то есть манифест сообщал неправду о том, из
|
||||
// какого кода собран установщик.
|
||||
public const string BuildCommit = "e431e39";
|
||||
public const string BuildCommit = "9e2afe1";
|
||||
public const string MIN_HERMES_VERSION = "0.20.0";
|
||||
public const string MAX_TESTED_HERMES = "0.20.4";
|
||||
|
||||
|
|
@ -86,7 +86,12 @@ namespace HermesHubSetup
|
|||
}
|
||||
}
|
||||
|
||||
string defaultTarget = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"Programs\HermesHub");
|
||||
string localAppData = Environment.GetEnvironmentVariable("LOCALAPPDATA");
|
||||
if (string.IsNullOrEmpty(localAppData))
|
||||
{
|
||||
localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
}
|
||||
string defaultTarget = Path.Combine(localAppData, @"Programs\HermesHub");
|
||||
TargetInstallDir = defaultTarget;
|
||||
|
||||
// Check if already installed
|
||||
|
|
@ -595,6 +600,7 @@ namespace HermesHubSetup
|
|||
|
||||
private static void CreateStartMenuShortcut()
|
||||
{
|
||||
if (Environment.GetEnvironmentVariable("HERMES_HUB_NO_REGISTRY") == "1") return;
|
||||
try
|
||||
{
|
||||
string startMenu = Environment.GetFolderPath(Environment.SpecialFolder.Programs);
|
||||
|
|
@ -641,6 +647,7 @@ namespace HermesHubSetup
|
|||
|
||||
private static void RemoveStartMenuShortcut()
|
||||
{
|
||||
if (Environment.GetEnvironmentVariable("HERMES_HUB_NO_REGISTRY") == "1") return;
|
||||
try
|
||||
{
|
||||
string startMenu = Environment.GetFolderPath(Environment.SpecialFolder.Programs);
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -11,7 +11,9 @@ Strictly checks all criteria before allowing a release build:
|
|||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import ast
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
|
@ -283,7 +285,81 @@ def check_production_update_feed() -> tuple[bool, str]:
|
|||
return True, "Production update feed verified"
|
||||
|
||||
|
||||
def run_release_gate():
|
||||
def check_release_assets(assets_dir: Path | str, min_size: int = 5 * 1024 * 1024) -> tuple[bool, str]:
|
||||
"""Verify release assets in assets_dir (HermesHubSetup.exe and checksums.txt)."""
|
||||
p = Path(assets_dir)
|
||||
exe_path = p / "HermesHubSetup.exe"
|
||||
if not exe_path.is_file():
|
||||
return False, f"HermesHubSetup.exe missing in {assets_dir}"
|
||||
|
||||
checksum_file = p / "checksums.txt"
|
||||
if not checksum_file.is_file():
|
||||
return False, f"checksums.txt missing in {assets_dir}"
|
||||
|
||||
size = exe_path.stat().st_size
|
||||
if size < min_size:
|
||||
return False, f"HermesHubSetup.exe size {size} bytes is suspiciously small (< {min_size})"
|
||||
|
||||
expected_hash: str | None = None
|
||||
try:
|
||||
content = checksum_file.read_text(encoding="utf-8-sig", errors="ignore")
|
||||
for line in content.splitlines():
|
||||
line = line.strip().lstrip("\ufeff")
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
parts = [p.lstrip("\ufeff") for p in line.split()]
|
||||
if len(parts) >= 2:
|
||||
target_name = parts[-1].lstrip("*")
|
||||
if target_name.lower() == "hermeshubsetup.exe":
|
||||
expected_hash = parts[0].strip()
|
||||
break
|
||||
elif len(parts) == 1 and len(parts[0]) == 64:
|
||||
expected_hash = parts[0].strip()
|
||||
break
|
||||
except Exception as exc:
|
||||
return False, f"Failed to read checksums.txt: {exc}"
|
||||
|
||||
if not expected_hash:
|
||||
return False, f"HermesHubSetup.exe SHA-256 hash not found in {checksum_file.name}"
|
||||
|
||||
hasher = hashlib.sha256()
|
||||
with open(exe_path, "rb") as f:
|
||||
while chunk := f.read(65536):
|
||||
hasher.update(chunk)
|
||||
actual = hasher.hexdigest().lower()
|
||||
|
||||
if actual != expected_hash.lower():
|
||||
return False, f"SHA-256 mismatch: expected {expected_hash}, got {actual}"
|
||||
|
||||
size_mb = size / (1024 * 1024)
|
||||
return True, f"Assets verified: HermesHubSetup.exe ({size_mb:.2f} MB), SHA-256 {actual} matches checksums.txt"
|
||||
|
||||
|
||||
def run_assets_gate(assets_dir: str | Path) -> int:
|
||||
print("=" * 70)
|
||||
print(" Hermes Hub - Release Assets Verification Suite")
|
||||
print(f" Target Directory: {assets_dir}")
|
||||
print("=" * 70)
|
||||
|
||||
ok, msg = check_release_assets(assets_dir)
|
||||
if ok:
|
||||
print(f"\n [ASSETS VERIFIED] {msg}")
|
||||
print("\n" + "=" * 70)
|
||||
print(" [RELEASE GATE: PASSED] Release assets verified.")
|
||||
print("=" * 70)
|
||||
return 0
|
||||
else:
|
||||
print(f"\n [FAIL] {msg}")
|
||||
print("\n" + "=" * 70)
|
||||
print(" [RELEASE GATE: FAILED] Release assets verification failed.")
|
||||
print("=" * 70)
|
||||
return 1
|
||||
|
||||
|
||||
def run_release_gate(assets_dir: str | Path | None = None):
|
||||
if assets_dir:
|
||||
sys.exit(run_assets_gate(assets_dir))
|
||||
|
||||
print("=" * 70)
|
||||
print(f" Hermes Hub — Release Gate Verification Suite (Target: v{__version__})")
|
||||
print("=" * 70)
|
||||
|
|
@ -319,5 +395,17 @@ def run_release_gate():
|
|||
sys.exit(1)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Hermes Hub — Automated Release Gate & Verification Engine.")
|
||||
parser.add_argument(
|
||||
"--assets",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Verify release assets in specified directory (e.g. dist)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
run_release_gate(assets_dir=args.assets)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_release_gate()
|
||||
main()
|
||||
|
|
|
|||
|
|
@ -141,8 +141,9 @@ def run_checks() -> int:
|
|||
# Проверяем изоляцию пути, а не побочное создание каталога: запрос пути
|
||||
# каталогов больше не плодит, иначе любая проверка засоряла бы диск
|
||||
# десятком пустых слотов.
|
||||
pdir = get_profile_env_dir("ag-w2")
|
||||
assert "ag-w2" in str(pdir)
|
||||
probe_id = "ag-probe-isolation-test"
|
||||
pdir = get_profile_env_dir(probe_id)
|
||||
assert probe_id in str(pdir)
|
||||
assert "agy_profiles" in str(pdir)
|
||||
assert not pdir.exists(), "запрос пути не должен создавать каталог"
|
||||
print(f" [PASS] Profile directory isolated at {pdir} (не создан)")
|
||||
|
|
|
|||
|
|
@ -330,7 +330,11 @@ class WorkspaceBoundaryGuard:
|
|||
# путём отклонялась. То есть самый естественный способ
|
||||
# написать опасную команду обходил защиту ровно там, ради
|
||||
# чего она и делалась — на каталоге учётных данных.
|
||||
expanded = os.path.expandvars(os.path.expanduser(target_arg))
|
||||
exp_arg = target_arg
|
||||
if "$HOME" in exp_arg and "HOME" not in os.environ:
|
||||
home_dir = os.environ.get("USERPROFILE") or str(Path.home())
|
||||
exp_arg = exp_arg.replace("$HOME", home_dir)
|
||||
expanded = os.path.expandvars(os.path.expanduser(exp_arg))
|
||||
target_path = Path(expanded) if Path(expanded).is_absolute() else (base_cwd / expanded)
|
||||
ok, reason, alt = self.validate_path(target_path, operation="delete")
|
||||
if not ok:
|
||||
|
|
|
|||
|
|
@ -1001,6 +1001,13 @@ class UpdateManager:
|
|||
chosen_url = a_url
|
||||
break
|
||||
|
||||
if not chosen_url:
|
||||
for s_name in ("hermes-hub-setup.sh", "install-linux.sh", "HermesHubSetup.exe"):
|
||||
if s_name in assets:
|
||||
chosen_asset_name = s_name
|
||||
chosen_url = assets[s_name]
|
||||
break
|
||||
|
||||
if not chosen_url and check_result.manifest and check_result.manifest.package_url:
|
||||
chosen_url = check_result.manifest.package_url
|
||||
chosen_asset_name = Path(chosen_url).name or f"hermes-hub-{check_result.latest_version}.zip"
|
||||
|
|
|
|||
|
|
@ -250,10 +250,14 @@ def test_p0_2_sha256_mismatch_aborts_and_sets_failed_status(tmp_path, monkeypatc
|
|||
|
||||
# ── TEST 5: P0-3 Process Isolation stop_running_hub ──
|
||||
@pytest.mark.unit
|
||||
def test_p0_3_stop_running_hub_isolates_user_and_excludes_current_pid():
|
||||
def test_p0_3_stop_running_hub_isolates_user_and_excludes_current_pid(monkeypatch):
|
||||
"""stop_running_hub filters by current UID on Linux and never targets own PID."""
|
||||
current_pid = os.getpid()
|
||||
|
||||
monkeypatch.setattr("antigravity_provider.updater.update_manager.sys.platform", "linux")
|
||||
if not hasattr(os, "getuid"):
|
||||
monkeypatch.setattr(os, "getuid", lambda: 1000, raising=False)
|
||||
|
||||
# Mock subprocess.run for pgrep
|
||||
with patch("subprocess.run") as mock_run:
|
||||
# Simulate pgrep returning other PID and own PID
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
|||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
|
||||
|
|
@ -39,12 +40,22 @@ def test_silent_installer_execution_with_hermes(tmp_path):
|
|||
if not SETUP_EXE.is_file():
|
||||
pytest.skip("HermesHubSetup.exe not built yet")
|
||||
|
||||
# Set up mock Hermes Agent structure in temp home
|
||||
# Set up mock Hermes Agent structure in temp home pointing to the active venv
|
||||
agent_dir = tmp_path / "hermes" / "hermes-agent"
|
||||
venv_scripts = agent_dir / "venv" / "Scripts"
|
||||
venv_scripts.mkdir(parents=True, exist_ok=True)
|
||||
(venv_scripts / "python.exe").touch()
|
||||
(venv_scripts / "hermes.exe").touch()
|
||||
agent_dir.mkdir(parents=True, exist_ok=True)
|
||||
real_venv = Path(sys.prefix)
|
||||
target_venv = agent_dir / "venv"
|
||||
try:
|
||||
import _winapi
|
||||
_winapi.CreateJunction(str(real_venv), str(target_venv))
|
||||
except Exception:
|
||||
try:
|
||||
os.symlink(str(real_venv), str(target_venv), target_is_directory=True)
|
||||
except Exception:
|
||||
venv_scripts = target_venv / "Scripts"
|
||||
venv_scripts.mkdir(parents=True, exist_ok=True)
|
||||
(venv_scripts / "python.exe").touch()
|
||||
(venv_scripts / "hermes.exe").touch()
|
||||
|
||||
env = dict(os.environ)
|
||||
env["HERMES_HOME"] = str(tmp_path / "hermes")
|
||||
|
|
@ -54,7 +65,7 @@ def test_silent_installer_execution_with_hermes(tmp_path):
|
|||
env["HERMES_HUB_NO_REGISTRY"] = "1"
|
||||
|
||||
res = subprocess.run([str(SETUP_EXE), "/silent"], env=env, capture_output=True, text=True)
|
||||
assert res.returncode == 0, f"Expected returncode 0, got {res.returncode}. Stderr: {res.stderr}"
|
||||
assert res.returncode == 0, f"Expected returncode 0, got {res.returncode}. Stderr: {res.stderr}. Stdout: {res.stdout}"
|
||||
|
||||
|
||||
@pytest.mark.installer
|
||||
|
|
|
|||
|
|
@ -17,10 +17,15 @@ from __future__ import annotations
|
|||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
|
||||
SCRIPTS_DIR = Path(__file__).resolve().parent.parent / "scripts"
|
||||
if str(SCRIPTS_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(SCRIPTS_DIR))
|
||||
|
||||
from antigravity_provider.paths import get_hermes_home, get_profile_dir
|
||||
from antigravity_provider.router.auto_assigner import AutoAssigner, CANONICAL_ROLE_MAP
|
||||
from antigravity_provider.router.role_registry import RoleRegistry
|
||||
|
|
@ -461,3 +466,125 @@ def test_s4_secret_scanner_ast_detection(tmp_path):
|
|||
clean_file.write_text('def hello(): return "world"\n', encoding="utf-8")
|
||||
v3 = scan_file_for_secrets(clean_file)
|
||||
assert len(v3) == 0
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# A61: Release Assets Verification Gate (check_release_assets)
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_check_release_assets_valid(tmp_path):
|
||||
"""A61: Verify check_release_assets succeeds on valid HermesHubSetup.exe and checksums.txt."""
|
||||
import hashlib
|
||||
from release_gate import check_release_assets
|
||||
|
||||
assets_dir = tmp_path / "valid_assets"
|
||||
assets_dir.mkdir()
|
||||
|
||||
exe_content = b"MOCK_HERMES_SETUP_EXE_DATA" * (250 * 1024) # ~6.75 MB (> 5 MB)
|
||||
exe_file = assets_dir / "HermesHubSetup.exe"
|
||||
exe_file.write_bytes(exe_content)
|
||||
|
||||
actual_sha = hashlib.sha256(exe_content).hexdigest()
|
||||
checksum_file = assets_dir / "checksums.txt"
|
||||
checksum_file.write_text(f"{actual_sha.upper()} HermesHubSetup.exe\n", encoding="utf-8")
|
||||
|
||||
ok, msg = check_release_assets(assets_dir)
|
||||
assert ok is True
|
||||
assert "Assets verified: HermesHubSetup.exe" in msg
|
||||
assert actual_sha in msg
|
||||
assert "matches checksums.txt" in msg
|
||||
|
||||
# Also verify dist/ if present in repo
|
||||
dist_dir = Path(__file__).resolve().parent.parent / "dist"
|
||||
if (dist_dir / "HermesHubSetup.exe").is_file() and (dist_dir / "checksums.txt").is_file():
|
||||
ok_dist, msg_dist = check_release_assets(dist_dir)
|
||||
assert ok_dist is True
|
||||
assert "Assets verified: HermesHubSetup.exe" in msg_dist
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_check_release_assets_missing_exe(tmp_path):
|
||||
"""A61: Verify check_release_assets rejects missing HermesHubSetup.exe."""
|
||||
from release_gate import check_release_assets
|
||||
|
||||
assets_dir = tmp_path / "no_exe"
|
||||
assets_dir.mkdir()
|
||||
(assets_dir / "checksums.txt").write_text("dummy_hash HermesHubSetup.exe\n", encoding="utf-8")
|
||||
|
||||
ok, msg = check_release_assets(assets_dir)
|
||||
assert ok is False
|
||||
assert "HermesHubSetup.exe missing in" in msg
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_check_release_assets_missing_checksums(tmp_path):
|
||||
"""A61: Verify check_release_assets rejects missing checksums.txt."""
|
||||
from release_gate import check_release_assets
|
||||
|
||||
assets_dir = tmp_path / "no_checksums"
|
||||
assets_dir.mkdir()
|
||||
exe_file = assets_dir / "HermesHubSetup.exe"
|
||||
exe_file.write_bytes(b"X" * (6 * 1024 * 1024))
|
||||
|
||||
ok, msg = check_release_assets(assets_dir)
|
||||
assert ok is False
|
||||
assert "checksums.txt missing in" in msg
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_check_release_assets_too_small(tmp_path):
|
||||
"""A61: Verify check_release_assets rejects suspiciously small installer (< 5 MB)."""
|
||||
import hashlib
|
||||
from release_gate import check_release_assets
|
||||
|
||||
assets_dir = tmp_path / "small_exe"
|
||||
assets_dir.mkdir()
|
||||
small_data = b"TINY_STUB_INSTALLER_100_BYTES"
|
||||
exe_file = assets_dir / "HermesHubSetup.exe"
|
||||
exe_file.write_bytes(small_data)
|
||||
|
||||
actual_sha = hashlib.sha256(small_data).hexdigest()
|
||||
(assets_dir / "checksums.txt").write_text(f"{actual_sha} HermesHubSetup.exe\n", encoding="utf-8")
|
||||
|
||||
ok, msg = check_release_assets(assets_dir)
|
||||
assert ok is False
|
||||
assert "suspiciously small" in msg
|
||||
assert str(len(small_data)) in msg
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_check_release_assets_hash_mismatch(tmp_path):
|
||||
"""A61: Verify check_release_assets rejects SHA-256 mismatch."""
|
||||
from release_gate import check_release_assets
|
||||
|
||||
assets_dir = tmp_path / "hash_mismatch"
|
||||
assets_dir.mkdir()
|
||||
exe_file = assets_dir / "HermesHubSetup.exe"
|
||||
exe_file.write_bytes(b"DATA" * (2 * 1024 * 1024)) # 8 MB
|
||||
|
||||
expected_bad_sha = "0" * 64
|
||||
(assets_dir / "checksums.txt").write_text(f"{expected_bad_sha} HermesHubSetup.exe\n", encoding="utf-8")
|
||||
|
||||
ok, msg = check_release_assets(assets_dir)
|
||||
assert ok is False
|
||||
assert "SHA-256 mismatch" in msg
|
||||
assert f"expected {expected_bad_sha}" in msg
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_check_release_assets_hash_not_found(tmp_path):
|
||||
"""A61: Verify check_release_assets rejects checksums.txt missing hash for HermesHubSetup.exe."""
|
||||
from release_gate import check_release_assets
|
||||
|
||||
assets_dir = tmp_path / "hash_not_found"
|
||||
assets_dir.mkdir()
|
||||
exe_file = assets_dir / "HermesHubSetup.exe"
|
||||
exe_file.write_bytes(b"DATA" * (2 * 1024 * 1024)) # 8 MB
|
||||
|
||||
(assets_dir / "checksums.txt").write_text("abc123def456 SomeOtherFile.zip\n", encoding="utf-8")
|
||||
|
||||
ok, msg = check_release_assets(assets_dir)
|
||||
assert ok is False
|
||||
assert "SHA-256 hash not found" in msg
|
||||
|
|
|
|||
Loading…
Reference in a new issue