From 1cedc1b927c22790539043de4de5852f0c328b9d Mon Sep 17 00:00:00 2001 From: HumbleTurtle Date: Thu, 10 Sep 2026 04:10:40 -0300 Subject: [PATCH] fix: make the published stdin bootstrap install the runtime bundle --- resources/scripts/nap_install.py | 4 ++ tests/test_bootstrap_entrypoint.py | 79 ++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 tests/test_bootstrap_entrypoint.py diff --git a/resources/scripts/nap_install.py b/resources/scripts/nap_install.py index e88bcba..6c109aa 100644 --- a/resources/scripts/nap_install.py +++ b/resources/scripts/nap_install.py @@ -1464,6 +1464,10 @@ def version_status(): def main(argv): invoked_as_nap = pathlib.Path(argv[0]).name in {"nap", "nap.cmd"} + # The published /install endpoint is executed as `python3 -`. Keep that + # bootstrap separate from the removed operator-facing `nap install` alias. + if len(argv) == 1 and not invoked_as_nap: + return handle_mcp(["install"]) command = argv[1] if len(argv) > 1 else ("help" if invoked_as_nap else "install") args = argv[2:] if command in HELP_TOKENS: diff --git a/tests/test_bootstrap_entrypoint.py b/tests/test_bootstrap_entrypoint.py new file mode 100644 index 0000000..6916c4a --- /dev/null +++ b/tests/test_bootstrap_entrypoint.py @@ -0,0 +1,79 @@ +"""Exercise the published `curl /install | python3 -` entry point over HTTP.""" +import hashlib +import importlib.util +import json +import os +from pathlib import Path +import subprocess +import sys +import threading +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + + +def test_stdin_bootstrap_installs_verified_runtime_and_preserves_credentials(tmp_path): + source = Path(__file__).resolve().parents[1] / "resources/scripts" + spec = importlib.util.spec_from_file_location("bootstrap_contract", source / "nap_install.py") + installer = importlib.util.module_from_spec(spec) + spec.loader.exec_module(installer) + revision = "f" * 40 + items, payloads = [], {} + for name in installer.SCRIPT_NAMES: + content = (source / installer.INSTALL_PATHS[name]).read_text() + item = {"name": name, "version": installer.CLI_RELEASE_VERSION, + "bytes": len(content.encode()), "sha256": hashlib.sha256(content.encode()).hexdigest(), + "source_repo": "https://github.com/napseer/nap-cli", "source_revision": revision, + "source_revision_status": "resolved", "contract_version": installer.CLI_DISTRIBUTION_CONTRACT_VERSION, + "minimum_contract_version": installer.CLI_MINIMUM_CONTRACT_VERSION, + "install_path": installer.INSTALL_PATHS[name], "mode": "0755"} + items.append(item) + payloads["/v1/scripts/" + name] = {**item, "content": content} + payloads["/v1/scripts"] = {"schema_version": installer.CLI_BUNDLE_SCHEMA_VERSION, + "bundle_id": "synthetic-bootstrap", "release_version": installer.CLI_RELEASE_VERSION, + "contract": {"current": installer.CLI_DISTRIBUTION_CONTRACT_VERSION, "minimum_supported": installer.CLI_MINIMUM_CONTRACT_VERSION}, + "source": {"repo": "https://github.com/napseer/nap-cli", "revision": revision, "revision_status": "resolved"}, "items": items} + + class Provider(BaseHTTPRequestHandler): + def log_message(self, *_args): + pass + + def do_GET(self): + assert self.path in payloads, "bootstrap must only download its declared bundle" + raw = json.dumps(payloads[self.path]).encode() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(raw))) + self.end_headers() + self.wfile.write(raw) + + server = ThreadingHTTPServer(("127.0.0.1", 0), Provider) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + user_data = tmp_path / "user-data" + user_data.mkdir(exist_ok=True) + auth = user_data / "auth.json" + auth.write_text('{"refresh_token":"synthetic-preserved"}') + env = {"PATH": os.environ["PATH"], "NAPSEER_HOME": str(tmp_path / "runtime"), + "NAPSEER_BIN_DIR": str(tmp_path / "bin"), "NAPSEER_USER_DATA_DIR": str(user_data), + "NAPSEER_PROJECT_ROOT": str(tmp_path), "NAPSEER_BASE_URL": f"http://127.0.0.1:{server.server_port}"} + try: + result = subprocess.run([sys.executable, "-"], input=(source / "nap_install.py").read_text(), + cwd=tmp_path, env=env, capture_output=True, text=True, timeout=30) + assert result.returncode == 0, "the advertised stdin bootstrap must install without an explicit subcommand" + assert json.loads(result.stdout)["status"] == "installed" + for name in installer.SCRIPT_NAMES: + installed = tmp_path / "runtime/current" / installer.INSTALL_PATHS[name] + assert installed.read_bytes() == (source / installer.INSTALL_PATHS[name]).read_bytes() + assert auth.read_text() == '{"refresh_token":"synthetic-preserved"}' + # Import the actual activated modules, including the newly added dependency. + env["PYTHONPATH"] = str(tmp_path / "runtime/current") + imported = subprocess.run([sys.executable, "-c", "import napseer_credentials, napseer_mcp_server"], + cwd=tmp_path, env=env, capture_output=True, timeout=10) + assert imported.returncode == 0, "activated runtime must load all dependencies" + result = subprocess.run([sys.executable, str(tmp_path / "bin/nap"), "update"], + cwd=tmp_path, env=env, capture_output=True, text=True, timeout=30) + assert result.returncode == 0 + assert auth.read_text() == '{"refresh_token":"synthetic-preserved"}' + finally: + server.shutdown() + server.server_close() + thread.join(timeout=2)