From 0b3e646c67bd7d5079282ee1c6db762cad15d48f Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 30 Sep 2026 08:57:32 +0000 Subject: [PATCH] Delete pre-rename node refusal and legacy generation handling --- .../agents-api/node-generation-protocol.md | 18 +-- deploy/install/node_generations.py | 3 - deploy/install/node_install.py | 83 ------------ deploy/install/test_node_generations.py | 8 -- deploy/install/test_node_install.py | 19 +-- deploy/install/test_node_legacy.py | 120 ------------------ scripts/name-allowlist.json | 15 --- 7 files changed, 5 insertions(+), 261 deletions(-) delete mode 100644 deploy/install/test_node_legacy.py diff --git a/contracts/agents-api/node-generation-protocol.md b/contracts/agents-api/node-generation-protocol.md index 122463aac..b40e511e2 100644 --- a/contracts/agents-api/node-generation-protocol.md +++ b/contracts/agents-api/node-generation-protocol.md @@ -87,9 +87,6 @@ Neither opener adopts a missing identity, replaces its inode, or erases it after GC. Initialization interrupted before the identity is durable refuses re-adoption; preserve the installation for inspection. A removed identity or an owned replacement 0600 lease still refuses, even when its current fstat/lstat agree. -Historical installations and their `legacy-unfenced` records are retained -for inspection; the current installer does not adopt or upgrade them. Do not -create a new fence to bypass a missing historical lease identity. A dropped generation cannot be prepared or used again; a future rollback would require a new generation and a separate policy. @@ -173,9 +170,7 @@ metadata refuse cleanup. Interruption resumes under the same collection journal. The current node executable, preparer, identity, base provider configuration and manifests remain, so restart can read its enrolled identity and construct a newer retained provider after the original Runtime bytes have gone. Shared native paths -are compared across all retained configurations before removal. A historical -`legacy-unfenced` marker remains a reason to retain its original payload, not -evidence that upgrading or serving the historical installation is supported. +are compared across all retained configurations before removal. New preparation has two distinct records. Before downloads, `.preparing` holds the immutable installation/generation/specification identity, private provider paths @@ -198,10 +193,8 @@ An interrupted download repairs only missing bytes at the original paths. If collection precedes any import attempt, the preparation journal proves that this generation has no imported native image. An older or interrupted generation whose native executable is missing and whose import may have started remains retained; -missing files do not prove native absence. Receipt/store history and a -historical `legacy-unfenced` record are never erased using an empty native -inventory. These retention checks do not authorize historical installation -conversion or adoption. +missing files do not prove native absence. Receipt/store history is never +erased using an empty native inventory. Preparation diagnostics preserve fixed typed causes. Only artifact transfer, checksum or release-provenance failures report `runtime_download_failed`. A private @@ -225,10 +218,7 @@ flow. Reinstallation does not automatically delete or migrate existing data. Current Runtime generation changes operate within an installation of the current node program. They do not upgrade that program or establish compatibility with -historical node installations. A historical `legacy-unfenced` marker records that -older helpers did not share the current lease protocol. Preserve its files and -resources for inspection; do not clear it, recreate its lease, or infer safe -collection from an empty allocation list, process absence or a node restart. +historical node installations. ## Qualification boundary diff --git a/deploy/install/node_generations.py b/deploy/install/node_generations.py index 9857df8bb..0a74896d7 100644 --- a/deploy/install/node_generations.py +++ b/deploy/install/node_generations.py @@ -516,9 +516,6 @@ def collect(args, installer): if (value["installation_id"] != args.installation_id or installer.node_spec.digest(value["provider"], value["specification"]) != args.specification_digest): raise installer.InstallError("Collection grant does not match the local generation") - marker = root / "state/node/generations" / (str(args.generation) + ".legacy-unfenced") - if marker.exists() or marker.is_symlink(): - raise installer.InstallError("The original v1 generation retains unfenced legacy helpers; its local payload is kept") source = value["specification"]["runtime"]["source_commit"] if not re.fullmatch(r"[a-f0-9]{40}", source): raise installer.InstallError("Invalid retained release identity") diff --git a/deploy/install/node_install.py b/deploy/install/node_install.py index 740329356..1d74b90f9 100644 --- a/deploy/install/node_install.py +++ b/deploy/install/node_install.py @@ -453,79 +453,6 @@ def register_node(root, args, token, helper_archive=None): raise InstallError("Registered node identity differs; refusing to replace it") -# These paths are inspected only to refuse an unremoved node from an older -# release. They are never adopted, rewritten or removed by this installer. -LEGACY_RECORDS = Path("/etc/parsar-node") -LEGACY_SERVICE_HOME = Path("/var/lib/parsar-node") - - -def legacy_path_present(path): - """Inspect only metadata, without following links in user-controlled directories.""" - descriptors = [] - try: - descriptors.append(os.open(path.anchor, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)) - for part in path.parts[1:-1]: - descriptors.append(os.open(part, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, - dir_fd=descriptors[-1])) - os.stat(path.name, dir_fd=descriptors[-1], follow_symlinks=False) - return True - except FileNotFoundError: - return False - finally: - for descriptor in reversed(descriptors): - os.close(descriptor) - - -def refuse_legacy_node(args): - """Reject pre-rename resources for this installation, leaving every other one alone.""" - installation = args.installation_id - unit = "parsar-node-" + installation + ".service" - homes = {Path.home()} - if os.geteuid() == 0: - sudo_user = os.environ.get("SUDO_USER", "") - if sudo_user and sudo_user != "root": - try: - home = Path(pwd.getpwnam(sudo_user).pw_dir) - if home.is_absolute(): - homes.add(home) - except KeyError: - pass - paths = [LEGACY_RECORDS / (installation + ".json"), SYSTEM_UNITS / unit] - paths += [home / ".parsar/nodes" / installation for home in homes] - paths += [home / ".config/systemd/user" / unit for home in homes] - # Non-root users cannot inspect the old mode-0700 service home; its matching - # root-owned record and system unit above remain observable without reading it. - if os.geteuid() == 0 or os.access(LEGACY_SERVICE_HOME, os.R_OK | os.X_OK): - paths.append(LEGACY_SERVICE_HOME / ".parsar/nodes" / installation) - found = [] - for path in paths: - try: - if legacy_path_present(path): # A dangling final symlink is still retained state. - found.append(str(path)) - except OSError: - raise InstallError("Cannot inspect possible legacy node state at " + str(path) - + "; check this path before installing." + NOTHING_CHANGED) from None - if not found and shutil.which("systemctl") is not None: - result = subprocess.run(["systemctl", "show", unit, "--property=LoadState", "--value"], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, timeout=10) - if result.returncode == 0 and result.stdout.strip() not in ("", "not-found"): - found.append(unit) - # Inspect only the fixed local engine. A microsandbox user without Docker - # access does not need that unrelated host capability to install its node. - if not found and shutil.which("docker") is not None and DOCKER_SOCKET.exists() and ( - getattr(args, "provider", None) == "docker" or os.access(DOCKER_SOCKET, os.R_OK | os.W_OK)): - network = "parsar-node-" + installation - networks = checked(list(DOCKER) + ["network", "ls", "--format", "{{.Name}}"], - "Cannot inspect legacy node networks; check the local Docker engine." + NOTHING_CHANGED).splitlines() - if network in networks: - found.append("Docker network " + network) - if found: - raise InstallError("This host still has a node for this installation from before the OpenAgentCore rename (" - + ", ".join(found) + "). Remove it on the Nodes page, then uninstall it with the previous " - "release's node-install.pyz --uninstall --installation-id " + installation - + ", or follow \"Remove a node added before the rename\" in the node guide." + NOTHING_CHANGED) - - def prepare_service_node(args, token, helper_archive): """Sudo mode, as the service user: everything but the root-owned system unit.""" root = open_node(args, token) @@ -1052,7 +979,6 @@ def install_system(args, token): """Sudo mode: prepare the host, then run the node as a root-owned system service.""" os.environ["PATH"] = SAFE_PATH host_checks() - refuse_legacy_node(args) # Checks that change nothing run first, so a refusal leaves no trace, not even a lock. record = node_record(args.installation_id) configuration = None @@ -1203,7 +1129,6 @@ def uninstall_system(args): os.environ["PATH"] = SAFE_PATH if shutil.which("systemctl") is None: raise InstallError("systemctl is required." + NOTHING_CHANGED) - refuse_legacy_node(args) record = node_record(args.installation_id) unit = SYSTEM_UNITS / unit_name(args.installation_id) # Only root-owned files decide whether a node is installed; the service home is not read here. @@ -1321,11 +1246,6 @@ def wait_ready(root, args, timeout=60): raise InstallError(detail + "; state and service are retained. Inspect " + journal + ", then rerun the installation command") -# A token in the environment could reach sudo's log (`sudo VAR=... python3`) and every -# program the installer starts; it is refused rather than read. -RETIRED_TOKEN_VARIABLE = "PARSAR_NODE_ENROLLMENT_TOKEN" - - def read_token(args): """The one-time token comes on standard input, never in argv, the environment or a sudo command line.""" if not args.enrollment_token_stdin: @@ -1354,9 +1274,6 @@ def main(argv=None): args = parser.parse_args(argv) if args.no_color: os.environ["NO_COLOR"] = "1" - if RETIRED_TOKEN_VARIABLE in os.environ: - parser.exit(2, RETIRED_TOKEN_VARIABLE + " is retired: pass the enrollment token on standard input with " - "--enrollment-token-stdin.\n") if str(uuid.UUID(args.installation_id)) != args.installation_id: raise InstallError("Installation ID must be a canonical UUID") if args.update: diff --git a/deploy/install/test_node_generations.py b/deploy/install/test_node_generations.py index 851628ae2..ca64cb5a7 100644 --- a/deploy/install/test_node_generations.py +++ b/deploy/install/test_node_generations.py @@ -164,14 +164,6 @@ def test_never_imported_generation_can_collect_missing_executable(self): installer.checked.assert_not_called() self.assertFalse(self.release.exists()) - def test_legacy_unfenced_marker_blocks_before_collection_journal(self): - (self.directory / "1.legacy-unfenced").write_text("retained") - with self.assertRaisesRegex(installer.InstallError, "legacy helpers"): - node_generations.collect(self.args, installer) - installer.checked.assert_not_called() - self.assertFalse((self.directory / "1.collecting").exists()) - self.assertTrue(self.release.exists()) - def micro_fixture(self): self.value["provider"] = "microsandbox" del self.value["docker"] diff --git a/deploy/install/test_node_install.py b/deploy/install/test_node_install.py index 87951f286..87a59bfef 100644 --- a/deploy/install/test_node_install.py +++ b/deploy/install/test_node_install.py @@ -67,8 +67,7 @@ def setUp(self): self.fail_service = False self.fail_registration = False self.register_stderr = None - for patch in (mock.patch.object(installer, "refuse_legacy_node"), - mock.patch.object(installer.Path, "home", return_value=self.home), + for patch in (mock.patch.object(installer.Path, "home", return_value=self.home), mock.patch.object(installer, "preflight"), mock.patch.object(installer, "wait_ready"), mock.patch.object(installer, "open_request", side_effect=self.configuration_response), @@ -115,7 +114,6 @@ def refresh_manifest(self): def checked(self, arguments, failure, **kwargs): self.calls.append((arguments, kwargs)) self.assertNotIn("synthetic-once-token", str(arguments)) - self.assertNotIn("PARSAR_NODE_ENROLLMENT_TOKEN", os.environ) if "register" in arguments: self.assertNotIn("--max-active", arguments) self.assertNotIn("--max-retained", arguments) @@ -988,21 +986,6 @@ def test_token_comes_on_standard_input_only(self): installer.main(arguments) self.assertEqual(install.call_args.args[1], "synthetic-once-token") - def test_the_retired_token_variable_is_refused_in_every_mode(self): - install = ["--source-url", self.args.source_url, "--core-url", self.args.core_url, - "--installation-id", self.args.installation_id, "--enrollment-token-stdin"] - for euid in (1000, 0): - for arguments in (install, install[:-1], ["--uninstall", "--installation-id", self.args.installation_id]): - errors = io.StringIO() - with mock.patch.dict(os.environ, {"PARSAR_NODE_ENROLLMENT_TOKEN": "synthetic-once-token"}), \ - mock.patch.object(installer.os, "geteuid", return_value=euid), mock.patch.object(installer.sys, "stderr", errors), \ - mock.patch.multiple(installer, install_system=mock.DEFAULT, uninstall_system=mock.DEFAULT) as steps, self.assertRaises(SystemExit): - installer.main(arguments) - self.assertEqual(errors.getvalue().splitlines(), ["PARSAR_NODE_ENROLLMENT_TOKEN is retired: pass the enrollment " - "token on standard input with --enrollment-token-stdin."]) - self.assertFalse(any(step.called for step in steps.values())) - - def test_offline_bundle_uses_same_bootstrap_and_verified_artifacts(self): bundle = self.home / "bundle" bundle.mkdir() diff --git a/deploy/install/test_node_legacy.py b/deploy/install/test_node_legacy.py deleted file mode 100644 index d1374f175..000000000 --- a/deploy/install/test_node_legacy.py +++ /dev/null @@ -1,120 +0,0 @@ -"""Legacy-node refusals are read-only and scoped to the requested installation.""" -import argparse -import os -from pathlib import Path -import subprocess -import tempfile -import unittest -from types import SimpleNamespace -from unittest import mock - -import node_install as installer - - -class LegacyNodeTests(unittest.TestCase): - def setUp(self): - base = Path.home() / ".oac/tests/node-legacy" - base.mkdir(parents=True, exist_ok=True) - temporary = tempfile.TemporaryDirectory(dir=base) - self.addCleanup(temporary.cleanup) - self.root = Path(temporary.name) - self.home = self.root / "user" - self.home.mkdir() - self.args = argparse.Namespace(installation_id="94be54a1-138c-4f30-bc87-b13686272dbe", provider="docker") - self.other = "634d97be-e54d-40f0-9468-ae6b62be85bf" - self.unit = "parsar-node-" + self.args.installation_id + ".service" - self.networks = "" - for patch in ( - mock.patch.multiple(installer, LEGACY_RECORDS=self.root / "records", LEGACY_SERVICE_HOME=self.root / "service", - SYSTEM_RECORDS=self.root / "new-records", SYSTEM_UNITS=self.root / "units", - DOCKER_SOCKET=self.root / "docker.sock"), - mock.patch.object(installer.Path, "home", return_value=self.home), - mock.patch.object(installer, "host_checks"), - mock.patch.object(installer.os, "geteuid", return_value=1000), - mock.patch.object(installer.shutil, "which", side_effect=lambda tool: "/usr/bin/" + tool), - mock.patch.object(installer.subprocess, "run", return_value=subprocess.CompletedProcess([], 0, "not-found\n", "")), - mock.patch.object(installer, "checked", side_effect=lambda *a, **k: self.networks), - ): - patch.start() - self.addCleanup(patch.stop) - - def assert_refusal(self): - with mock.patch.object(installer, "open_node") as opened, mock.patch.object(installer, "host_lock") as lock: - with self.assertRaisesRegex(installer.InstallError, "before the OpenAgentCore rename.*Nothing was changed") as failure: - installer.install_system(self.args, "private-token") - self.assertIn(self.args.installation_id, str(failure.exception)) - self.assertNotIn("private-token", str(failure.exception)) - opened.assert_not_called() - lock.assert_not_called() - self.assertFalse((self.home / ".oac/nodes").exists()) - - def test_each_same_installation_path_refuses_without_mutation(self): - paths = [installer.LEGACY_RECORDS / (self.args.installation_id + ".json"), - self.home / ".parsar/nodes" / self.args.installation_id, - self.home / ".config/systemd/user" / self.unit, - installer.SYSTEM_UNITS / self.unit, - installer.LEGACY_SERVICE_HOME / ".parsar/nodes" / self.args.installation_id] - for path in paths: - with self.subTest(path=path): - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text("retained") - self.assert_refusal() - self.assertEqual(path.read_text(), "retained") - path.unlink() - - def test_final_symlink_counts_as_retained_state_without_reading_its_target(self): - path = self.home / ".parsar/nodes" / self.args.installation_id - path.parent.mkdir(parents=True) - path.symlink_to(self.root / "absent-private-target") - self.assert_refusal() - self.assertTrue(path.is_symlink()) - - def test_intermediate_symlink_refuses_without_following_it(self): - target = self.root / "private-target" - target.mkdir() - (self.home / ".parsar").symlink_to(target, target_is_directory=True) - with mock.patch.object(installer, "open_node") as opened: - with self.assertRaisesRegex(installer.InstallError, "Cannot inspect possible legacy node state.*Nothing was changed"): - installer.install_system(self.args, "private-token") - opened.assert_not_called() - self.assertEqual(list(target.iterdir()), []) - - def test_other_installations_and_old_account_alone_do_not_refuse(self): - for path in [installer.LEGACY_RECORDS / (self.other + ".json"), - installer.LEGACY_RECORDS / "account.json", - self.home / ".parsar/nodes" / self.other, - installer.LEGACY_SERVICE_HOME / ".parsar/nodes" / self.other, - installer.SYSTEM_UNITS / ("parsar-node-" + self.other + ".service")]: - path.parent.mkdir(parents=True, exist_ok=True) - path.write_text("foreign") - installer.DOCKER_SOCKET.touch() - self.networks = "parsar-node-" + self.other + "\n" - installer.refuse_legacy_node(self.args) - self.assertTrue(all(p.read_text() == "foreign" for p in self.root.rglob("*") if p.is_file() and p != installer.DOCKER_SOCKET)) - - def test_loaded_unit_and_exact_network_refuse(self): - with mock.patch.object(installer.subprocess, "run", return_value=subprocess.CompletedProcess([], 0, "loaded\n", "")): - self.assert_refusal() - installer.DOCKER_SOCKET.touch() - self.networks = "parsar-node-" + self.args.installation_id + "\n" - self.assert_refusal() - - def test_sudo_invoking_user_legacy_node_refuses_before_account_creation(self): - invoking = self.root / "invoking" - old = invoking / ".parsar/nodes" / self.args.installation_id - old.mkdir(parents=True) - with mock.patch.object(installer.os, "geteuid", return_value=0), \ - mock.patch.dict(os.environ, {"SUDO_USER": "operator"}), \ - mock.patch.object(installer.pwd, "getpwnam", return_value=SimpleNamespace(pw_dir=str(invoking))), \ - mock.patch.object(installer, "host_checks"), \ - mock.patch.object(installer, "prepare_account") as account, \ - mock.patch.object(installer, "node_record") as record: - with self.assertRaisesRegex(installer.InstallError, "before the OpenAgentCore rename"): - installer.install_system(self.args, "private-token") - account.assert_not_called() - record.assert_not_called() - self.assertEqual(list(old.iterdir()), []) - - -if __name__ == "__main__": - unittest.main() diff --git a/scripts/name-allowlist.json b/scripts/name-allowlist.json index 85d4bac33..16b10fe6b 100644 --- a/scripts/name-allowlist.json +++ b/scripts/name-allowlist.json @@ -284,21 +284,6 @@ "regex": "AGENTS_API_PUBLIC_URL", "reason": "Historical migration commentary records the environment name used when this migration was written." }, - { - "path": "deploy/install/node_install.py", - "regex": "parsar-node(?:-)?|\\.parsar(?:/nodes)?|PARSAR_NODE_ENROLLMENT_TOKEN", - "reason": "Only the old node service/state paths and retired enrollment variable are recognized for scoped refusal and uninstall guidance." - }, - { - "path": "deploy/install/test_node_legacy.py", - "regex": "parsar-node(?:-)?|\\.parsar(?:/nodes)?|PARSAR_NODE_ENROLLMENT_TOKEN", - "reason": "Only the old node service/state paths and retired enrollment variable are recognized for scoped refusal and uninstall guidance." - }, - { - "path": "deploy/install/test_node_install.py", - "regex": "parsar-node(?:-)?|\\.parsar(?:/nodes)?|PARSAR_NODE_ENROLLMENT_TOKEN", - "reason": "Only the old node service/state paths and retired enrollment variable are recognized for scoped refusal and uninstall guidance." - }, { "path": "deploy/install/install.py", "regex": "\\.parsar/core|\\bparsar\\b",