From 336db61b780012725ac4122645b73235064062c1 Mon Sep 17 00:00:00 2001 From: ethernet Date: Mon, 5 Oct 2026 13:21:31 -0400 Subject: [PATCH 1/2] fix(pm): plugin extras, dev groups and lint-only pyprojects no longer block installs The catalog-wide lock found 12 plugins that could not install for reasons unrelated to their real dependencies: - 6 ship a pyproject.toml holding only ruff/pytest settings, with no [project] table. PM treated any pyproject as the package definition, invented a [project] table with just a name, and uv refused it for lacking a version. A pyproject without [project] now leaves the plugin manifest in charge of dependencies. - 4 pin pytest/ruff/ty in a dev extra or dev group, conflicting with core's own dev group; one pins transformers in an optional extra; two route torch to different PyTorch indexes through extras. Hermes never installs a plugin's extras, and uv syncs a member's default dev group into Hermes's environment, so both are dropped from the workspace copy. --- pm/plugin_declarations.py | 10 +++++---- pm/workspace.py | 20 +++++++++++++++++ tests/pm/test_workspace.py | 45 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 4 deletions(-) diff --git a/pm/plugin_declarations.py b/pm/plugin_declarations.py index 169b7136437b8..8d38d636155f5 100644 --- a/pm/plugin_declarations.py +++ b/pm/plugin_declarations.py @@ -170,12 +170,14 @@ def read_python_declaration(plugin_dir: Path) -> PythonDeclaration: text = None if text is not None: files.append(project) - if "GENERATED by pm" not in text: - document = tomllib.loads(text) - specs = document.get("project", {}).get("dependencies", []) + document = tomllib.loads(text) if "GENERATED by pm" not in text else {} + # A pyproject with no [project] table only configures tools (ruff, pytest); + # the manifest still declares the dependencies. + if "project" in document: + specs = document["project"].get("dependencies", []) if not isinstance(specs, list) or any(not isinstance(v, str) for v in specs): raise ValueError(f"invalid project.dependencies: {project}") - requires_python = document.get("project", {}).get("requires-python") + requires_python = document["project"].get("requires-python") if requires_python is not None and not isinstance(requires_python, str): raise ValueError(f"invalid project.requires-python: {project}") return PythonDeclaration(tuple(files), project, tuple(specs), manifest, requires_python) diff --git a/pm/workspace.py b/pm/workspace.py index 2f54fcf43b221..82c12eb8763c4 100644 --- a/pm/workspace.py +++ b/pm/workspace.py @@ -248,6 +248,25 @@ def _member_key(identity: Path) -> str: return f"{name}-{digest}" +def _installable_project(document: dict) -> bool: + """Drop what Hermes never installs from a plugin's pyproject; True when it changed. + + Hermes never installs a plugin's extras, and uv syncs a member's default ``dev`` + group into Hermes's own environment. Resolving them anyway made a plugin's + pytest/ruff pins conflict with core's. + """ + project = document.get("project", {}) + changed = False + if "optional-dependencies" in project and "optional-dependencies" not in project.get("dynamic", []): + del project["optional-dependencies"] + changed = True + for table, key in ((document, "dependency-groups"), (document.get("tool", {}).get("uv", {}), "dev-dependencies")): + if key in table: + del table[key] + changed = True + return changed + + def _workspace_member(plugin_dir: Path, root: Path, *, identity: Path) -> Path: """Keep workspace members with their generation, not a temporary install clone.""" import json @@ -272,6 +291,7 @@ def _workspace_member(plugin_dir: Path, root: Path, *, identity: Path) -> Path: changed = declaration.install_requirements != declaration.requirements if changed: document["project"]["dependencies"] = list(declaration.install_requirements) + changed = _installable_project(document) or changed for sources in document.get("tool", {}).get("uv", {}).get("sources", {}).values(): for spec in sources if isinstance(sources, list) else [sources]: if not isinstance(spec, dict) or "path" not in spec: diff --git a/tests/pm/test_workspace.py b/tests/pm/test_workspace.py index 21406f3819aeb..6c88679a02607 100644 --- a/tests/pm/test_workspace.py +++ b/tests/pm/test_workspace.py @@ -60,6 +60,51 @@ def test_missing_explicit_seed_cannot_silently_resolve_new_versions(layout): assert not (tmp / "env").exists() +def _plugin_pyproject(tmp: Path, name: str, body: str) -> Path: + directory = tmp / name + directory.mkdir() + (directory / "pyproject.toml").write_text(body, encoding="utf-8") + (directory / "plugin.yaml").write_text(f"name: {name}\n", encoding="utf-8") + return directory + + +def test_plugin_extras_and_dev_groups_never_constrain_hermes(layout): + """Hermes installs neither a plugin's extras nor its dev group, so their pins (here an + unsatisfiable one against core's ``base-dep==1.0``) must not make the plugin uninstallable.""" + import tomllib + + tmp, core, _, _ = layout + plugin = _plugin_pyproject(tmp, "pinned-dev", ( + '[project]\nname = "pinned-dev"\nversion = "1"\nrequires-python = ">=3.11"\n' + 'dependencies = ["member-dep==1.0"]\n' + '[project.optional-dependencies]\ndev = ["base-dep==9.9"]\n' + '[dependency-groups]\ndev = ["other-dep==9.9"]\n[tool.uv]\npackage = false\n')) + root = tmp / "workspace" + ws.lock_and_sync([plugin], [], root=root, source=core, seed_lock=core / "uv.lock", + environment=managed_environment(tmp / "env")) + locked = {p["name"]: p["version"] for p in tomllib.loads((root / "uv.lock").read_text())["package"]} + assert locked["base-dep"] == "1.0" and locked["member-dep"] == "1.0" + + +def test_tool_config_pyproject_leaves_the_manifest_in_charge_of_dependencies(layout): + """A pyproject holding only tool settings (ruff, pytest) is not a package definition: + the plugin's manifest dependencies still install, instead of uv refusing a [project] + table PM had to invent.""" + import tomllib + + tmp, core, _, _ = layout + plugin = tmp / "lint-only" + plugin.mkdir() + (plugin / "pyproject.toml").write_text("[tool.ruff]\nline-length = 100\n", encoding="utf-8") + (plugin / "plugin.yaml").write_text("name: lint-only\npython_dependencies:\n - member-dep==1.0\n", + encoding="utf-8") + root = tmp / "workspace" + ws.lock_and_sync([plugin], [], root=root, source=core, seed_lock=core / "uv.lock", + environment=managed_environment(tmp / "env")) + locked = {p["name"] for p in tomllib.loads((root / "uv.lock").read_text())["package"]} + assert "member-dep" in locked + + class _TimedIndex: """A PEP 691 JSON index with per-file ``upload-time``. From c7f8e40919c5267fc3d03503af1f2d65cb1ad3a9 Mon Sep 17 00:00:00 2001 From: ethernet Date: Mon, 5 Oct 2026 13:21:31 -0400 Subject: [PATCH 2/2] build: temporary quarantine cutoffs for 8 catalog plugins on fresh releases Eight catalog plugins require a release younger than the 14-day window (their own SDKs, mostly). Each gets a per-package exclude-newer cutoff at the upload time of the oldest release the plugin accepts, so exactly that release gets through and anything newer still waits. Every line names its plugins and the date it becomes dead (cutoff + 14 days); the lasting fix is upstream, an exact pin plus the plugin's own exemption. uv.lock is relocked with the pinned uv 0.12.3, which writes the options table in its own order. --- pyproject.toml | 22 +++++++ uv.lock | 174 ++++++++++++++++++++++++++----------------------- 2 files changed, 113 insertions(+), 83 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a16898701ee23..8f8781aa8d16d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -787,6 +787,28 @@ uvicorn = false vercel = false websockets = false youtube-transcript-api = false +# Catalog plugins floored on a release younger than the 14-day window +# (temporary). Each cutoff is the upload time of the OLDEST release the +# plugin accepts, so exactly that release gets through and anything newer +# still waits. A line is dead once its date is 14 days old: delete it then. +# The lasting fix is upstream (exact pin + the plugin's own exemption, see +# the developer guide's "Dependency security policy"). +# birkin-mnemosyne 0.4.0: catalog birkin-mnemosyne; dead after 2026-10-14 +birkin-mnemosyne = "2026-09-30T10:47:28Z" +# cortexlayer 0.1.2: catalog cortexlayer; dead after 2026-10-09 +cortexlayer = "2026-09-25T14:28:59Z" +# evalroute 0.9.0: catalog evalroute; dead after 2026-10-19 +evalroute = "2026-10-05T04:51:25Z" +# gonogo-eval 0.3.0: catalog gonogo, thomas; dead after 2026-10-12 +gonogo-eval = "2026-09-28T21:07:00Z" +# hermes-mnemostack 1.0.3: catalog mnemostack; dead after 2026-10-11 +hermes-mnemostack = "2026-09-27T19:26:09Z" +# loreconvo 0.10.15: catalog loreconvo; dead after 2026-10-18 +loreconvo = "2026-10-04T17:35:24Z" +# mnemosyne-hermes 0.7.3: catalog mnemosyne; dead after 2026-10-07 +mnemosyne-hermes = "2026-09-23T23:42:51Z" +# mnemosyne-memory 4.0.0b3: catalog mnemosyne; dead after 2026-10-07 +mnemosyne-memory = "2026-09-23T23:41:58Z" [tool.setuptools] # Root single-file modules are derived by setup.py at build time from the diff --git a/uv.lock b/uv.lock index 5389dde571a2d..3b0d67814667b 100644 --- a/uv.lock +++ b/uv.lock @@ -14,106 +14,114 @@ exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for exclude-newer-span = "P14D" [options.exclude-newer-package] -agent-client-protocol = false +elevenlabs = false +langfuse = "2026-09-09T16:01:26Z" +setuptools = false +modal = false +pyyaml = false +dingtalk-stream = false +maturin = false +numpy = false +pvporcupine = false +google-auth = false aiohttp = false -aiohttp-socks = false -aiosqlite = false -alibabacloud-dingtalk = false +starlette = false +mistralai = false +firecrawl-py = false +parallel-web = false +markdown = false +opentelemetry-sdk = false +prompt-toolkit = false +fastapi = false anthropic = false +pathspec = false +ruff = false +defusedxml = false +websockets = false +exa-py = false +mcp = false +python-olm = false +wheel = false +pytest = false +pillow = false +setuptools-rust = false +tenacity = false +sherpa-onnx-core = "2026-09-10T15:58:47Z" +pyjwt = false +slack-bolt = false +fal-client = false asyncpg = false -azure-identity = false boto3 = false -brotlicffi = false -browser-harness = false +ruamel-yaml = false +sherpa-onnx = "2026-09-10T17:00:04Z" +huggingface-hub = false +pytest-asyncio = false +google-cloud-pubsub = false +slack-sdk = false +discord-py = false +rich = false +faster-whisper = false certifi = false -concurrent-log-handler = false -croniter = false -cryptography = false -daytona = false -ddgs = "2026-08-26T21:52:34Z" debugpy = false -defusedxml = false -dingtalk-stream = false -discord-py = false +ddgs = "2026-08-26T21:52:34Z" +packaging = false +unpaddedbase64 = false +youtube-transcript-api = false +h2 = false +birkin-mnemosyne = "2026-09-30T10:47:28Z" +gonogo-eval = "2026-09-28T21:07:00Z" +mnemosyne-memory = "2026-09-23T23:41:58Z" edge-tts = false -elevenlabs = false -exa-py = false -fal-client = false -fastapi = false -faster-whisper = false -fire = false +microsoft-teams-apps = false +aiohttp-socks = false +sentencepiece = false +opentelemetry-exporter-otlp-proto-http = false +croniter = false +browser-harness = false +azure-identity = false +lark-oapi = false firecrawl-anydoc = false -firecrawl-py = false google-api-python-client = false -google-auth = false -google-auth-httplib2 = false -google-auth-oauthlib = false -google-cloud-pubsub = false -h2 = false -httplib2 = false -httpx = false -httpx2 = false -huggingface-hub = false -jinja2 = false -langfuse = "2026-09-09T16:01:26Z" -lark-oapi = false -markdown = false -maturin = false -mautrix = false -mcp = false +brotlicffi = false mem0ai = false -microsoft-teams-apps = false -mistralai = false -modal = false +qrcode = false +soundfile = "2026-06-06T08:58:48Z" +vercel = false +hermes-mnemostack = "2026-09-27T19:26:09Z" +evalroute = "2026-10-05T04:51:25Z" +aiosqlite = false +tzdata = false +python-dotenv = false +daytona = false +uvicorn = false +loreconvo = "2026-10-04T17:35:24Z" +mnemosyne-hermes = "2026-09-23T23:42:51Z" nemo-relay = false -neutts = "2026-07-22T14:54:23Z" -numpy = false -openai = false -opentelemetry-exporter-otlp-proto-http = false -opentelemetry-sdk = false -packaging = false -parallel-web = false -pathspec = false -pillow = false +requests = false +httplib2 = false +httpx = false piper-tts = "2026-09-04T16:47:32Z" -prompt-toolkit = false -psutil = false -pvporcupine = false pyasn1 = false +snowballstemmer = false +concurrent-log-handler = false +cryptography = false pydantic = false -pyjwt = false -pytest = false -pytest-asyncio = false -python-dotenv = false +agent-client-protocol = false +fire = false +httpx2 = false +google-auth-oauthlib = false python-multipart = false -python-olm = false +ty = false +openai = false +google-auth-httplib2 = false +neutts = "2026-07-22T14:54:23Z" +mautrix = false +psutil = false python-telegram-bot = false -pyyaml = false -qrcode = false -requests = false -rich = false -ruamel-yaml = false -ruff = false -sentencepiece = false -setuptools = false -setuptools-rust = false -sherpa-onnx = "2026-09-10T17:00:04Z" -sherpa-onnx-core = "2026-09-10T15:58:47Z" -slack-bolt = false -slack-sdk = false -snowballstemmer = false +jinja2 = false sounddevice = false -soundfile = "2026-06-06T08:58:48Z" -starlette = false -tenacity = false -ty = false -tzdata = false -unpaddedbase64 = false -uvicorn = false -vercel = false -websockets = false -wheel = false -youtube-transcript-api = false +alibabacloud-dingtalk = false +cortexlayer = "2026-09-25T14:28:59Z" [manifest] overrides = [