From 39adc19510d46820e5428d035e1176af5699bb73 Mon Sep 17 00:00:00 2001 From: HumbleTurtle Date: Sat, 1 Aug 2026 04:15:09 -0300 Subject: [PATCH] Allow compatible CLI release upgrades --- resources/scripts/nap_install.py | 44 ++++++++++++++++++++++++-------- tests/test_cli_command_system.py | 31 ++++++++++++++++++++++ 2 files changed, 64 insertions(+), 11 deletions(-) diff --git a/resources/scripts/nap_install.py b/resources/scripts/nap_install.py index 42ac4aa..ba35fd2 100644 --- a/resources/scripts/nap_install.py +++ b/resources/scripts/nap_install.py @@ -3,15 +3,17 @@ Usage: python3 nap_install.py + nap init + nap status + nap doctor + nap auth login nap project create - nap project status + nap project attach + nap mcp status nap gateway start nap gateway stop - nap gateway configure - nap gateway logs - nap status - nap agent list nap update + nap version """ import ast @@ -212,6 +214,23 @@ def bundle_manifest(): return fetch_json("/v1/scripts", "CLI bundle manifest") +def contract_version_tuple(value): + parts = str(value or "").split("-") + if len(parts) != 3 or any(not part.isdigit() for part in parts): + return None + year, month, day = (int(part) for part in parts) + if year < 2000 or not 1 <= month <= 12 or not 1 <= day <= 31: + return None + return year, month, day + + +def release_version_tuple(value): + parts = str(value or "").split(".") + if len(parts) != 3 or any(not part.isdigit() for part in parts): + return None + return tuple(int(part) for part in parts) + + def validated_manifest_items(manifest): if manifest.get("schema_version") != CLI_BUNDLE_SCHEMA_VERSION: raise RuntimeError( @@ -220,12 +239,15 @@ def validated_manifest_items(manifest): contract = manifest.get("contract") if not isinstance(contract, dict): raise RuntimeError("CLI bundle manifest is missing contract metadata") - if contract.get("current") != CLI_DISTRIBUTION_CONTRACT_VERSION: - raise RuntimeError("CLI bundle contract does not match this installer") - if contract.get("minimum_supported") != CLI_MINIMUM_CONTRACT_VERSION: - raise RuntimeError("CLI bundle minimum contract metadata is inconsistent") - if manifest.get("release_version") != CLI_RELEASE_VERSION: - raise RuntimeError("CLI bundle release version does not match this installer") + current_contract = contract_version_tuple(contract.get("current")) + minimum_contract = contract_version_tuple(contract.get("minimum_supported")) + installer_contract = contract_version_tuple(CLI_DISTRIBUTION_CONTRACT_VERSION) + if current_contract is None or minimum_contract is None or current_contract < minimum_contract: + raise RuntimeError("CLI bundle contract metadata is invalid") + if installer_contract is None or installer_contract < minimum_contract: + raise RuntimeError("CLI bundle requires a newer bootstrap installer") + if release_version_tuple(manifest.get("release_version")) is None: + raise RuntimeError("CLI bundle release version is invalid") source = manifest.get("source") if ( not isinstance(source, dict) diff --git a/tests/test_cli_command_system.py b/tests/test_cli_command_system.py index 50352f1..d935da0 100644 --- a/tests/test_cli_command_system.py +++ b/tests/test_cli_command_system.py @@ -282,3 +282,34 @@ def test_manifest_rejects_path_traversal(): assert "unsafe install_path" in str(exc) else: raise AssertionError("unsafe manifest path must be rejected") + + +def test_manifest_accepts_a_future_compatible_release(): + module = load_installer() + manifest, _payloads = fake_bundle(module) + manifest["release_version"] = "0.2.2" + manifest["contract"]["current"] = "2026-08-02" + for item in manifest["items"]: + item["version"] = "0.2.2" + item["contract_version"] = "2026-08-02" + + items = module.validated_manifest_items(manifest) + + assert len(items) == len(module.SCRIPT_NAMES) + + +def test_manifest_rejects_a_minimum_contract_newer_than_installer(): + module = load_installer() + manifest, _payloads = fake_bundle(module) + manifest["contract"]["current"] = "2026-08-02" + manifest["contract"]["minimum_supported"] = "2026-08-02" + for item in manifest["items"]: + item["contract_version"] = "2026-08-02" + item["minimum_contract_version"] = "2026-08-02" + + try: + module.validated_manifest_items(manifest) + except RuntimeError as exc: + assert "newer bootstrap installer" in str(exc) + else: + raise AssertionError("incompatible minimum contract must be rejected")