Allow compatible CLI release upgrades
This commit is contained in:
@@ -3,15 +3,17 @@
|
|||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python3 nap_install.py
|
python3 nap_install.py
|
||||||
|
nap init
|
||||||
|
nap status
|
||||||
|
nap doctor
|
||||||
|
nap auth login
|
||||||
nap project create
|
nap project create
|
||||||
nap project status
|
nap project attach
|
||||||
|
nap mcp status
|
||||||
nap gateway start
|
nap gateway start
|
||||||
nap gateway stop
|
nap gateway stop
|
||||||
nap gateway configure
|
|
||||||
nap gateway logs
|
|
||||||
nap status
|
|
||||||
nap agent list
|
|
||||||
nap update
|
nap update
|
||||||
|
nap version
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import ast
|
import ast
|
||||||
@@ -212,6 +214,23 @@ def bundle_manifest():
|
|||||||
return fetch_json("/v1/scripts", "CLI 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):
|
def validated_manifest_items(manifest):
|
||||||
if manifest.get("schema_version") != CLI_BUNDLE_SCHEMA_VERSION:
|
if manifest.get("schema_version") != CLI_BUNDLE_SCHEMA_VERSION:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
@@ -220,12 +239,15 @@ def validated_manifest_items(manifest):
|
|||||||
contract = manifest.get("contract")
|
contract = manifest.get("contract")
|
||||||
if not isinstance(contract, dict):
|
if not isinstance(contract, dict):
|
||||||
raise RuntimeError("CLI bundle manifest is missing contract metadata")
|
raise RuntimeError("CLI bundle manifest is missing contract metadata")
|
||||||
if contract.get("current") != CLI_DISTRIBUTION_CONTRACT_VERSION:
|
current_contract = contract_version_tuple(contract.get("current"))
|
||||||
raise RuntimeError("CLI bundle contract does not match this installer")
|
minimum_contract = contract_version_tuple(contract.get("minimum_supported"))
|
||||||
if contract.get("minimum_supported") != CLI_MINIMUM_CONTRACT_VERSION:
|
installer_contract = contract_version_tuple(CLI_DISTRIBUTION_CONTRACT_VERSION)
|
||||||
raise RuntimeError("CLI bundle minimum contract metadata is inconsistent")
|
if current_contract is None or minimum_contract is None or current_contract < minimum_contract:
|
||||||
if manifest.get("release_version") != CLI_RELEASE_VERSION:
|
raise RuntimeError("CLI bundle contract metadata is invalid")
|
||||||
raise RuntimeError("CLI bundle release version does not match this installer")
|
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")
|
source = manifest.get("source")
|
||||||
if (
|
if (
|
||||||
not isinstance(source, dict)
|
not isinstance(source, dict)
|
||||||
|
|||||||
@@ -282,3 +282,34 @@ def test_manifest_rejects_path_traversal():
|
|||||||
assert "unsafe install_path" in str(exc)
|
assert "unsafe install_path" in str(exc)
|
||||||
else:
|
else:
|
||||||
raise AssertionError("unsafe manifest path must be rejected")
|
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")
|
||||||
|
|||||||
Reference in New Issue
Block a user