From 5d870e2e7ac17d7f52c73c6790fdc130e8fe4a39 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Wed, 30 Sep 2026 09:24:30 +0000 Subject: [PATCH] Remove historical installer compatibility handling --- deploy/install/README.md | 3 +- deploy/install/install.py | 32 +---------------- deploy/install/oac_cli.py | 6 ++-- deploy/install/test_convert.py | 40 ---------------------- deploy/install/test_install.py | 55 ++++++------------------------ deploy/install/test_rename.py | 26 -------------- docs/getting-started/operations.md | 4 +-- scripts/name-allowlist.json | 15 -------- 8 files changed, 16 insertions(+), 165 deletions(-) delete mode 100644 deploy/install/test_convert.py delete mode 100644 deploy/install/test_rename.py diff --git a/deploy/install/README.md b/deploy/install/README.md index d10383019..378807681 100644 --- a/deploy/install/README.md +++ b/deploy/install/README.md @@ -40,7 +40,7 @@ This directory holds the Core/Web installer, the `oac` command and the node inst ## Versions and the lock -- Install only into an empty directory or over an [incomplete installation](#new-installations), or repair a complete installation of the same source revision. Refuse older formats and different revisions before changing anything; keep their data and direct the operator to install separately. Distributions carry only current installation code: no conversion, migration or binary replacement. Keep the refusal checks and their tests. +- Install only into an empty directory or over an [incomplete installation](#new-installations), or repair a complete installation of the same source revision. Require the current state format and matching source revision before changing installation files or services. Preserve data when those checks fail. - The packaged `oac.pyz` embeds its build revision and refuses a `state.json` whose `source_commit` differs. - The installer and every mutating `oac` command share `.oac.lock`. The installer holds it across creation, payload, native service and launcher repair, and apply, calling the already-locked apply implementation without locking again. Never replace the lock file; its inode must stay stable. Only the cleanup of a new installation, with the directory the installer created, and `oac uninstall` unlink it, last and while holding it. `locked` refuses a lock whose path no longer names the file it locked. @@ -67,7 +67,6 @@ This directory holds the Core/Web installer, the `oac` command and the node inst - `docker` prints its weaker isolation and needs a y/N confirmation or `--accept-docker-risks` before anything is created. - `e2b` needs a non-loopback HTTPS `public_url`, `--e2b-api-key-file` and `--e2b-template`; otherwise the installer refuses before installing anything. - A Docker or microsandbox selection with a loopback `public_url` is saved, but no node can serve it until `public_url` is guest-reachable HTTPS. -- `--sandbox-provider` and `--provider` fail with a message naming `--sandbox`. ## Accounts and permissions diff --git a/deploy/install/install.py b/deploy/install/install.py index c48ff3e37..80701853b 100644 --- a/deploy/install/install.py +++ b/deploy/install/install.py @@ -48,8 +48,6 @@ } SETTING_FLAGS = ("core_only", "web_only", "native_core", *( flag.removeprefix("--").replace("-", "_") for flag in SETTING_ARGUMENTS)) -RETIRED_NODE_FLAGS = ("--sandbox-provider and --provider are retired: use --sandbox docker|microsandbox|e2b|none. " - "The installer no longer adds this host as a node; add it with Add node on the Nodes page in Web.") AVOID = 20 # An omitted Core or Web port moves at most this far above its default. DOCKER_RISKS = """Docker sandboxes isolate less than microsandbox, the default: - Containers share the node's kernel, so a container escape reaches the host; @@ -163,36 +161,18 @@ def arguments(argv=None): parser.add_argument("--e2b-template", help="With --sandbox e2b: the ready template build, template-id:build-uuid") parser.add_argument("--e2b-api-url", help="With --sandbox e2b: compatible service HTTPS API origin") parser.add_argument("--e2b-domain", help="With --sandbox e2b: compatible service data-plane domain") - parser.add_argument("--sandbox-provider", nargs="?", const=True, help=argparse.SUPPRESS) - parser.add_argument("--provider", nargs="?", const=True, help=argparse.SUPPRESS) parser.add_argument("--install-dir", type=Path) for flag, (_, node) in SETTING_ARGUMENTS.items(): value_type = int if node.get("type") == "integer" else public_origin if config_model.annotation(node, "check") == "origin" else str parser.add_argument(flag, type=value_type, help=node["description"]) parser.add_argument("--core-key-file", type=Path, help="Web-only: private file containing the existing Core's Core key") parser.add_argument("--config", type=Path, help="Seed a new installation's config.json from this file") - parser.add_argument("--convert", action="store_true", - help=argparse.SUPPRESS) - parser.add_argument("--yes", action="store_true", help=argparse.SUPPRESS) - parser.add_argument("--admin-token-file", type=Path, help=argparse.SUPPRESS) - parser.add_argument("--status", action="store_true", help=argparse.SUPPRESS) - parser.add_argument("--stop", action="store_true", help=argparse.SUPPRESS) args = parser.parse_args(argv) - if args.convert or args.yes: - parser.error(oac_cli.UNSUPPORTED_VERSION) - args.explicit_install_dir = args.install_dir is not None args.install_dir = args.install_dir or Path.home() / ".oac/core" - if args.admin_token_file: - parser.error("--admin-token-file was renamed; use --core-key-file") - if args.sandbox_provider is not None or args.provider is not None: - parser.error(RETIRED_NODE_FLAGS) - for retired in ("status", "stop"): - if getattr(args, retired): - parser.error(f"--{retired} is retired; run {args.install_dir / 'oac'} {retired}") if not args.install_dir.is_absolute(): parser.error("--install-dir must be absolute") args.given = [name for name, value in vars(args).items() - if name not in ("install_dir", "given", "explicit_install_dir") and value not in (None, False)] + if name not in ("install_dir", "given") and value not in (None, False)] return args @@ -492,8 +472,6 @@ def publish(source): if destination.is_symlink(): raise InstallError("Invalid node payload directory") destination.mkdir(mode=0o700, exist_ok=True) - if (destination / "manifest.json").exists(): - raise InstallError(oac_cli.UNSUPPORTED_VERSION) revision = publish(bundle) pointer = destination / "active.json" if pointer.is_symlink(): @@ -677,10 +655,6 @@ def main(argv=None): if root.is_symlink() or root.resolve() != root: raise InstallError("Installation directory must be canonical and not a symlink") bundle = Path(__file__).resolve().parent - if not args.explicit_install_dir: - old = Path.home() / ".parsar/core" - if (old / "state.json").exists() or (old / "installation.json").exists(): - raise InstallError(oac_cli.UNSUPPORTED_VERSION) # Settings and listeners take seconds to check, so they come before hashing the bundle. prepared = None if layout(root) == "empty": @@ -701,14 +675,10 @@ def main(argv=None): def check_release(root, manifest): - if (root / "installation.json").exists(): - raise InstallError(oac_cli.UNSUPPORTED_VERSION) if (root / "state.json").exists(): state = oac_cli.load_state(root) if state.get("source_commit") != manifest["source_commit"]: raise InstallError(oac_cli.UNSUPPORTED_VERSION) - if (root / "node-payload/manifest.json").exists(): - raise InstallError(oac_cli.UNSUPPORTED_VERSION) def prepare_fresh(args): diff --git a/deploy/install/oac_cli.py b/deploy/install/oac_cli.py index 830afdbca..238f08406 100644 --- a/deploy/install/oac_cli.py +++ b/deploy/install/oac_cli.py @@ -39,7 +39,7 @@ SOURCE_COMMIT = None # Set by the packaged entrypoint from its build revision. -UNSUPPORTED_VERSION = ("This installation version or historical conversion is not supported; " +UNSUPPORTED_VERSION = ("This installation version is not supported; " "preserve its data and reinstall into a new empty directory. Nothing was changed.") INCOMPLETE = ("This installation did not finish installing. Rerun the installer command, which removes what is " "left and installs again, or remove it with oac uninstall.") @@ -144,7 +144,7 @@ def load_config(root): def load_state(root): state = json.loads(read_private(root / "state.json", "state.json")) - if (state.get("format") != 2 or state.get("converted_from") or state.get("renamed_from") + if (state.get("format") != 2 or SOURCE_COMMIT is not None and state.get("source_commit") != SOURCE_COMMIT): raise OacError(UNSUPPORTED_VERSION) return state @@ -1169,8 +1169,6 @@ def uninstall(root, yes=False, interactive=None, out=print): def main(argv=None, root=None, out=print): root = Path(root) if root else Path(sys.argv[0]).resolve().parent - if Path(sys.argv[0]).name == "parsar": - raise OacError(f"parsar was renamed to oac. Run {root / 'oac'} .") parser = argparse.ArgumentParser(prog=str(root / "oac"), description=__doc__.split("\n\n")[0]) commands = parser.add_subparsers(dest="command", required=True) commands.add_parser("status", help="Show service health, addresses and configuration drift") diff --git a/deploy/install/test_convert.py b/deploy/install/test_convert.py deleted file mode 100644 index 2a72bf842..000000000 --- a/deploy/install/test_convert.py +++ /dev/null @@ -1,40 +0,0 @@ -"""Historical layout conversion is refused before any install mutation.""" -import contextlib -import io -import json -from pathlib import Path -import tempfile -import unittest -from unittest import mock - -import install - - -class ConvertTests(unittest.TestCase): - def test_conversion_flags_refuse_without_touching_legacy_state(self): - base = Path.home() / ".oac/tests/convert" - base.mkdir(parents=True, exist_ok=True) - with tempfile.TemporaryDirectory(dir=base) as directory: - root = Path(directory) - for name, data in (("installation.json", '{"format":1}'), - ("core.key", "retained-private-key"), ("history", "retained-history")): - (root / name).write_text(data) - before = {p.name: p.read_bytes() for p in root.iterdir()} - for extra in ([], ["--yes"], ["--public-url", "https://core.example"]): - output = io.StringIO() - with self.subTest(extra=extra), contextlib.redirect_stderr(output), self.assertRaises(SystemExit), \ - mock.patch.object(install, "check_host", side_effect=AssertionError("host touched")): - install.main(["--install-dir", str(root), "--convert", *extra]) - self.assertIn("not supported", output.getvalue()) - self.assertIn("reinstall", output.getvalue()) - self.assertEqual(before, {p.name: p.read_bytes() for p in root.iterdir()}) - - def test_legacy_layout_refuses_without_conversion_flag(self): - with tempfile.TemporaryDirectory() as directory: - root = Path(directory) - (root / "installation.json").write_text(json.dumps({"format": 1})) - before = (root / "installation.json").read_bytes() - with self.assertRaisesRegex(install.InstallError, "not supported;.*reinstall"): - install.check_release(root, {"source_commit": "a" * 40}) - self.assertEqual(before, (root / "installation.json").read_bytes()) - self.assertEqual([p.name for p in root.iterdir()], ["installation.json"]) diff --git a/deploy/install/test_install.py b/deploy/install/test_install.py index 6994e8ddb..7d346f1ac 100644 --- a/deploy/install/test_install.py +++ b/deploy/install/test_install.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""install.sh acceptance behavior: fresh install, repair, retired flags and bundle checks.""" +"""install.sh acceptance behavior: fresh install, repair and bundle checks.""" import contextlib import io @@ -320,7 +320,7 @@ def test_web_repair_refuses_old_paired_core_before_payload_or_state_writes(self) self.install() self.assertEqual(before, self.snapshot()) - def test_different_revision_and_conversion_refuse_before_mutation(self): + def test_different_revision_refuses_before_mutation(self): self.install() state = self.document("state.json") state["source_commit"] = "b" * 40 @@ -329,9 +329,6 @@ def test_different_revision_and_conversion_refuse_before_mutation(self): with self.assertRaisesRegex(install.InstallError, "not supported;.*reinstall"): self.install() self.assertEqual(before, self.snapshot()) - with self.assertRaises(SystemExit): - self.install("--convert") - self.assertEqual(before, self.snapshot()) def test_fresh_install_writes_config_json_and_the_layout(self): previous = os.umask(0) @@ -459,31 +456,17 @@ def test_rerun_reads_config_json_rejects_flags_and_repairs(self): with contextlib.redirect_stdout(self.output): run_installer(install, other, ["--install-dir", self.root]) - def test_retired_flags_and_old_layouts_name_their_replacement(self): - for flag, command in (("--status", "oac status"), ("--stop", "oac stop")): - output = io.StringIO() - with self.subTest(flag=flag), contextlib.redirect_stderr(output), self.assertRaises(SystemExit): - install.arguments(["--install-dir", str(self.root), flag]) - self.assertIn(f"{flag} is retired; run {self.root / command}", output.getvalue()) - for flags in (["--sandbox-provider"], ["--sandbox-provider", "true", "--provider", "docker"], ["--provider", "docker"]): - output = io.StringIO() - with self.subTest(flags=flags), contextlib.redirect_stderr(output), self.assertRaises(SystemExit): - install.arguments(flags) - self.assertIn("are retired: use --sandbox docker|microsandbox|e2b|none. The installer no longer adds this " - "host as a node", output.getvalue()) - output = io.StringIO() - with contextlib.redirect_stderr(output), self.assertRaises(SystemExit): - install.arguments(["--admin-token-file", str(self.key_file())]) - self.assertIn("--core-key-file", output.getvalue()) + def test_unsupported_state_format_refuses_before_lock_creation(self): self.root.mkdir() - (self.root / "installation.json").write_text("{}") - with self.assertRaisesRegex(install.InstallError, "not supported;.*reinstall"): - self.install() - (self.root / "installation.json").unlink() - (self.root / "state.json").write_text('{"format": 1, "generated": {}}') - (self.root / "state.json").chmod(0o600) + install.oac_cli.save_state(self.root, {"format": 1}) + before = self.snapshot() with self.assertRaisesRegex(install.oac_cli.OacError, "not supported;.*reinstall"): self.install() + with self.assertRaisesRegex(install.oac_cli.OacError, "not supported;.*reinstall"): + with install.oac_cli.locked(self.root): + self.fail("Unsupported installation acquired the lock") + self.assertEqual(before, self.snapshot()) + self.assertFalse((self.root / ".oac.lock").exists()) def test_a_live_installation_missing_config_json_is_never_told_to_start_over(self): self.install() @@ -674,24 +657,6 @@ def test_node_payload_exports_only_matched_distribution_files(self): with self.assertRaisesRegex(install.InstallError, "node payload differs"): install.prepare_node_payload(self.root, self.document("state.json"), self.bundle) - def test_legacy_flat_payload_is_refused_without_conversion(self): - import shutil - self.install() - payload = self.root / "node-payload" - old = payload / "releases" / ("a" * 40) - for path in old.rglob("*"): - if path.is_file(): - target = payload / path.relative_to(old) - target.parent.mkdir(parents=True, exist_ok=True) - shutil.copyfile(path, target) - shutil.rmtree(payload / "releases") - (payload / "active.json").unlink() - bundle, _ = make_bundle(self.work / "bundle-b", dict(MANIFEST, source_commit="b" * 40)) - before = self.snapshot() - with self.assertRaisesRegex(install.InstallError, "not supported;.*reinstall"): - install.prepare_node_payload(self.root, self.document("state.json"), bundle) - self.assertEqual(before, self.snapshot()) - def test_bundle_verifies_transferred_bytes_and_checksum_list(self): self.assertEqual(install.verify_bundle(self.bundle)["source_commit"], "a" * 40) for name in ("images/core.tar", "manifest.json", "oac.pyz", "config.schema.json"): diff --git a/deploy/install/test_rename.py b/deploy/install/test_rename.py deleted file mode 100644 index b717328db..000000000 --- a/deploy/install/test_rename.py +++ /dev/null @@ -1,26 +0,0 @@ -"""Current lifecycle commands never resume historical rename/conversion journals.""" -import json -from pathlib import Path -import tempfile -import unittest - -import oac_cli - - -class RenameTests(unittest.TestCase): - def test_old_format_and_all_conversion_journals_refuse_before_lock(self): - base = Path.home() / ".oac/tests/rename" - base.mkdir(parents=True, exist_ok=True) - with tempfile.TemporaryDirectory(dir=base) as directory: - root = Path(directory) - for record in ({"format": 1}, *({"format": 2, key: {"finished": finished}} - for key in ("converted_from", "renamed_from") for finished in (False, True))): - path = root / "state.json" - path.write_text(json.dumps(record)) - path.chmod(0o600) - before = path.read_bytes() - with self.subTest(record=record), self.assertRaisesRegex(oac_cli.OacError, "not supported;.*reinstall"): - with oac_cli.locked(root): - self.fail("Unsupported installation acquired the lock") - self.assertEqual(before, path.read_bytes()) - self.assertFalse((root / ".oac.lock").exists()) diff --git a/docs/getting-started/operations.md b/docs/getting-started/operations.md index 21f867d42..8528e1558 100644 --- a/docs/getting-started/operations.md +++ b/docs/getting-started/operations.md @@ -155,7 +155,7 @@ Nodes on other hosts keep running. To uninstall them the usual way, remove them ## Installation version policy -An installation runs one release for its whole life. In-place version upgrades, downgrades and historical conversions are not supported. Nothing migrates data between releases. +An installation runs one release for its whole life. In-place version upgrades and downgrades are not supported. Nothing migrates data between releases. To move to a new release, install it into a new, empty directory, with its own database, Core key and nodes, and add nodes from its Web. Keep the old installation, its data and its nodes until their work is finished. Nodes run the program of the console that added them and are never upgraded in place; Core accepts only nodes that speak its own node protocol. @@ -175,7 +175,7 @@ The installer and mutating `oac` commands hold the same installation lock, `.oac | `Automatic HTTPS needs ports 80 and 443 …` | Free the port the message names, install without `--public-url` and set up the domain later, or install with `--ingress external` and use your own [reverse proxy](install-options.md#https-and-the-reverse-proxy) | | `Installation directory is not empty …` | Use an empty `--install-dir` | | `This installation is configured by …/config.json …` | Flags only seed a new installation: edit `config.json` and run `oac apply`. To start over with other flags, [uninstall](#uninstall) it first | -| `This installation version or historical conversion is not supported …` | The target directory holds an installation of another release, or a default install found one at `~/.parsar/core`. Keep it, and install into another empty `--install-dir` ([version policy](#installation-version-policy)) | +| `This installation version is not supported …` | The target installation's state format or source revision does not match this release. Keep it, and install into another empty `--install-dir` ([version policy](#installation-version-policy)) | | `generated/ was edited by hand` | Put the change in `config.json`, then `oac apply --discard-edits` | | `config.json has changes that are not applied` | Run `oac apply` | | `Core rejects secrets/core.key …` | Run `oac apply`, which restarts Core with the key's digest | diff --git a/scripts/name-allowlist.json b/scripts/name-allowlist.json index 2810a1f89..0e273f183 100644 --- a/scripts/name-allowlist.json +++ b/scripts/name-allowlist.json @@ -159,16 +159,6 @@ "regex": "AGENTS_API_PUBLIC_URL", "reason": "Historical migration commentary records the environment name used when this migration was written." }, - { - "path": "deploy/install/install.py", - "regex": "\\.parsar/core|\\bparsar\\b", - "reason": "These old default paths and command names are used only by conversion refusal or the explicit retired-command stub." - }, - { - "path": "deploy/install/oac_cli.py", - "regex": "\\.parsar/core|\\bparsar\\b", - "reason": "These old default paths and command names are used only by conversion refusal or the explicit retired-command stub." - }, { "path": "apps/daemon/internal/agent/pi/provider_config.go", "regex": "parsar(?:/[A-Za-z0-9<>_-]+)?|Parsar Anthropic", @@ -239,11 +229,6 @@ "regex": "and Parsar itself", "reason": "These exact phrases refer to the separate Parsar product, its ownership or historical source, not the OpenAgentCore brand." }, - { - "path": "docs/getting-started/operations.md", - "regex": "~/\\.parsar/core", - "reason": "A default install refuses while this path holds an installation of an earlier release; the troubleshooting row names it." - }, { "path": "services/core/tests/official_environment_plugin_mcp.py", "regex": "'PARSAR_'",