Repository navigation
feat: plugin dependencies wait out the 14-day release quarantine, and CI checks the whole catalog installs together - #133409
ethernet8023 wants to merge 9 commits into
Conversation
Plugins now follow the policy core already uses for itself: exact-pin a
fresh direct dependency and exempt it with
`[tool.uv] exclude-newer-package = { name = false }`; everything else,
transitive deps included, waits out core's 14-day `exclude-newer`.
Before, _core_release_quarantine moved the cutoff onto core-locked
packages only, so any plugin-only package (and anything it pulled in)
could be a release from yesterday.
uv ignores `exclude-newer-package` on workspace members, so
_release_quarantine lifts each member's exemptions into the generated
root, filtered by quarantine_exemptions: `false` on an exact-pinned
direct dependency that core's lock does not hold. A plugin can let its
own fresh SDK through; it cannot loosen a core or transitive package.
The tests lock against a local PEP 691 index that serves upload-time:
uv never applies exclude-newer to find-links wheels, so the old fixture
could not show the quarantine biting.
`hermes plugins validate` gains a `dependency quarantine` check, built on the same quarantine_exemptions predicate PM uses at install time: - an exclude-newer-package exemption must be `false` on an exact `==` direct dependency that Hermes core does not lock - a dependency core also locks must admit core's locked version, or the plugin can only install by moving core Catalog CI already runs validate at the pinned SHA, so this gates admission and SHA bumps.
scripts/ci/catalog_resolve_all.py fetches each entry at its pinned sha, builds the workspace root with PM's own generator (quarantine, exemptions, core's lock as the seed) and locks it: each plugin alone, then all of them together, bisecting a plugin that breaks the set to name the first partner it conflicts with. plugin-catalog-resolve.yml runs it on catalog PRs, on core dependency or resolver changes, and weekly. PRs compare against a run on their base, so only new failures block, plus any failure of an entry the PR changes; the weekly run has no baseline and goes red on anything broken.
Rule 10 (both mirrored copies) and the developer guide's dependency security policy now describe the quarantine applying to plugin dependencies, the exact-pin + `exclude-newer-package = false` exemption, and the catalog-wide resolve check.
The pm/AGENTS.md paragraph still described plugin deps as exempt from the 14-day quarantine and named the removed _core_release_quarantine.
૮ >ﻌ< ა ci reviewran on 9cfa78a — ci: fail catalog resolution on every broken plugin ❌ Job failuresPlugin catalog resolve / resolve · View jobJob Plugin catalog resolve / resolve failed. debug infoCI timingsCI timings · View report · View jobWall time 27m25s vs 26m33s (+3.3%). 24 job(s) slower, 12 faster, 3 unchanged.
|
The catalog-wide lock clones every listed repo and takes minutes, so it now runs as a ci.yaml sub-workflow behind a new plugin_catalog lane (catalog entries, pyproject.toml, uv.lock, or the check itself) instead of its own pull_request + weekly schedule triggers. It is PR-only: it blocks on failures that are new relative to the PR base, and a push has no base, so it would go red on every plugin that was already broken. The aggregate gate counts it as a PR-only job, so release runs tolerate the skip.
… 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.
…leases 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.
|
upstream blocker fixes prepared and locally verified: Python 3.14 metadata caps, incompatible dependency floors, mnemostack's transitive host dependency, and explicit exact-pin release exemptions. patches do not override resolver conflicts. some require new upstream releases before catalog updates can use them. |
upstream blocker PRsthese source fixes are submitted, not merged. published-package changes still need upstream releases and catalog pin updates.
Already open: loreconvo/lancedb version compatibility and deepgram catalog re-pin. Kanban is submitted to its required pre-release branch: diegomarino/kanban-task-threads#18. CLA authorization received. Mnemosyne submitted: mnemosyne-oss/mnemosyne#1134 (issue #1133, changelog and wrapper docs; maintainer core-version waiver requested). Sugar fork PR to the required develop branch was rejected by GitHub permissions; tested branch and maintainer handoff: roboticforce/sugar#80. No Sugar PR was created. |
Remove baseline exemptions and changed-entry overrides. Every fetch, declaration, solo or combined conflict now fails the catalog gate. Resolve only the current catalog and retain its failure report on red. Exercise real offline uv resolution for compatible sets, core conflicts and cross-plugin conflicts; reject the removed baseline escape hatch.
Defect: the
|
| caller | line (head 9cfa78a) |
set passed as core_locked |
|---|---|---|
| the installer | pm/workspace.py:162 |
set(locked_versions(core_lock)) | {canonicalize_name(name) for name in per_package} — uv.lock packages ∪ core's [tool.uv] exclude-newer-package keys |
| the validator | hermes_cli/plugin_validate.py:620 |
set(core) where core = locked_versions(lock) — uv.lock packages only |
quarantine_exemptions refuses an exemption when key in core_locked (pm/plugin_declarations.py:139). So for any package that appears in core's exclude-newer-package table but is not in uv.lock's [package] list, the two callers return opposite verdicts on the identical plugin.
The gap is 12 packages, and it is exactly this PR's own set
Parsed from uv.lock and pyproject.toml at head, with PEP 503 normalization:
uv.lock[package]names: 331- core
[tool.uv] exclude-newer-packagekeys: 108 - install-time
held: 343 — validator-timeheld: 331 - keys where install refuses the exemption but validate reports it honoured: 12
birkin-mnemosyne = "2026-09-30T10:47:28Z" cortexlayer = "2026-09-25T14:28:59Z"
evalroute = "2026-10-05T04:51:25Z" gonogo-eval = "2026-09-28T21:07:00Z"
hermes-mnemostack = "2026-09-27T19:26:09Z" loreconvo = "2026-10-04T17:35:24Z"
mnemosyne-hermes = "2026-09-23T23:42:51Z" mnemosyne-memory = "2026-09-23T23:41:58Z"
neutts = "2026-07-22T14:54:23Z" maturin = false setuptools-rust = false wheel = false
Eight of those twelve are the catalog-plugin date cutoffs this PR adds in pyproject.toml:790-804 (birkin-mnemosyne, cortexlayer, evalroute, gonogo-eval, hermes-mnemostack, loreconvo, mnemosyne-hermes, mnemosyne-memory). None of the twelve is a [package] entry in uv.lock — that is precisely why they are missing from locked_versions().
Why this is the feature's flagship path, not an edge case
The docs this PR adds tell authors the exemption exists for "usually its own SDK, published alongside the plugin", and the accompanying upstream PRs in the thread are exactly that shape (birkin-mnemosyne, cortexlayer, evalroute, gonogo, loreconvo, mnemostack — "exact pin + exemption"). A plugin that exact-pins hermes-mnemostack==1.0.3 and declares hermes-mnemostack = false in its own pyproject.toml gets:
- validate:
key in exact✓,value is False✓,key not in core_locked✓ (absent fromuv.lock) →honoured→problemsempty →"dependency quarantine"reports True. The entry is admitted. - install:
heldcontainshermes-mnemostack(from the core table key) → refused → the exemption is dropped with alogging.warningand core's own cutoff2026-09-27T19:26:09Zstands.
So the gate whose stated job is "fails exemptions PM would ignore" returns green for the one exemption shape it cannot honour, and the plugin is then held by the quarantine at install time. That is the same class of defect as a fix keyed on the wrong identifier: the validator's predicate is satisfied while the thing it gates keeps failing.
The tests don't catch it because tests/hermes_cli/test_plugin_validate.py:816-821 asserts the pass case on httpx (a real uv.lock package, so both sets agree) and test_plugin_validate.py:824-836 asserts refusals on httpx and on a name (plugin-only-sdk) absent from both sets. Nothing exercises a name present in core's table but absent from uv.lock.
Minimal fix
Compute core_locked once and share it, so the validator cannot drift from the installer. Add to pm/plugin_declarations.py:
def held_quarantine_names(lock: Path, pyproject: Path) -> set[str]:
"""Names the quarantine holds: every package in core's lock, plus every key core
already carries in ``exclude-newer-package`` (install path, ``_release_quarantine``)."""
from packaging.utils import canonicalize_name
held = set(locked_versions(lock))
document = tomllib.loads(pyproject.read_text(encoding="utf-8-sig"))
held |= {canonicalize_name(name)
for name in document.get("tool", {}).get("uv", {}).get("exclude-newer-package", {})}
return heldthen in pm/workspace.py:162 use held = held_quarantine_names(core_lock, <the core pyproject dict's table>) — the install path already has the parsed per_package, so keep its union there — and in hermes_cli/plugin_validate.py:620 replace set(core) with held_quarantine_names(lock, paths.repo_root() / "pyproject.toml"). Add a test case with a package name present in core's table but not in uv.lock (e.g. hermes-mnemostack), asserting the check fails; that assertion is what currently would not hold.
Alternatively, if the intent is that core's table keys should not count as "locked", make the installer match by dropping the {canonicalize_name(name) for name in per_package} half of line 162. Either way the two sides must be derived from one source — please confirm which semantic is intended, because the docs (plugin-catalog/README.md rule 10: "an exact == direct dependency that Hermes itself does not lock") read as "not in uv.lock", while the installer's held reads as "not in uv.lock and not in core's table".
What I verified
- Read the full diff and the four changed implementation files at head
9cfa78a3219b4239e80f2209749df767ea2ee8a6(pm/workspace.py,pm/plugin_declarations.py,hermes_cli/plugin_validate.py, pluspyproject.toml,uv.lock,scripts/ci/*). - Confirmed both call sites and the
key not in core_lockedrefusal condition by reading the source at head. - Computed the two
heldsets by parsing the headuv.lock([package]names) and headpyproject.toml(exclude-newer-packagekeys) with PEP 503 normalization; the 12-package divergence and the eight cutoffs this PR adds are machine-derived, not read off the prose. - Re-read all three existing endpoints before posting (issue comments, reviews, inline comments); the three
ethernet8023comments concern upstream package pins/exemptions and the failing catalog entries, not the core-side asymmetry between the two callers ofquarantine_exemptions. Head SHA re-checked as unchanged immediately before posting.
Unverified / please confirm
- I did not execute anything: no
uv lock, no pytest, no resolver run. The claim that install-time behaviour is "exemption dropped, core's cutoff stands" is read from_release_quarantineandquarantine_exemptions, not from a run — please confirm with a realuv lockagainst a plugin that exact-pins one of the twelve names. - I confirmed none of the twelve is in
uv.lock's[package]list but did not check whether any is pinned in the[manifest] overridesor in a[manifest.members]entry, whichlocked_versions()does not read; if uv treats one of those as held, the divergence shrinks for that name but the two callers still disagree. logging.warningis the only surface for a refused exemption at install — I did not verify whether PM's caller surfaces that warning to a user or swallows it.- The absolute-vs-relative timestamp semantics of the dated cutoffs, and whether
2026-10-05T04:51:25Zforevalrouteis still correct at review time, I did not evaluate.


What does this PR do?
plugin dependencies now get the same 14-day release wait as Hermes's own dependencies. the PR also checks whether all catalog plugins can resolve together, and fixes the packaging mistakes that check found.
a catalog entry pins the plugin's code to a reviewed commit. that does not freeze the packages it downloads. before this change, a plugin-only dependency could pick up a release from yesterday. the 14-day wait now covers those packages and their dependencies too.
when a plugin needs a fresh release
the plugin exact-pins that direct dependency and exempts it in its own
pyproject.toml:Hermes copies accepted exemptions into the settings uv actually reads. an exemption must be
falseon an exact-pinned direct dependency that Hermes does not already lock. a plugin cannot change core's release policy. this replaces the earlier blanket exemption for plugin dependencies in #120231 with an explicit, reviewed exemption.fixes folded in from #133451
pyproject.tomlcontains only linter/test settings, but Hermes treats it as its package definition[project]; otherwise use the plugin manifestthose date cutoffs have removal dates in their comments. they must be removed when the regular 14-day window catches up; leaving a fixed cutoff behind would keep rejecting newer releases. the permanent fix is an upstream plugin pin and explicit exemption.
credit to @fangliquanflq for the same tooling-only pyproject fix in #122164 and @webtecnica for stripping virtual members' test dependencies in #123960.
Related Issue
follows from #120076 / #120231. incorporates the work from #133451.
Type of Change
Changes Made
hermes plugins validaterejects unsupported exemptions and dependency ranges that exclude core's locked version.plugin_catalogchange-detection lane on PRs touching catalog entries,pyproject.toml,uv.lockor the check itself. every fetch, declaration, solo or combined conflict fails the job, including entries already broken on the base. there is no baseline exemption. it is not a scheduled job.What remains open
How to Test
live repro: the new quarantine, unused-dependency and tool-config regressions fail without their corresponding fixes and pass with them. the tests use real uv resolution and local package files; the quarantine fixture supplies actual upload dates through a local index.
c7f8e40919scripts/check --only healththese catalog results prove dependency resolution, not a full installation or working plugin features. the whole-catalog run at
c7f8e40919completed: 114 Python-dependency members resolve together, with 9 failures. those failures are the five Python-version caps, lancedb, sugar, mnemostack, and a newly exposed loreconvo/lancedb-suite conflict. entries without Python dependencies are still fetched and inspected but are not workspace members. the strict gate now reports any remaining failure as blocking; a resolving subset does not satisfy the whole-catalog contract.Update — strict catalog gate
removed the baseline comparison, changed-entry overrides and warning-only path. CI resolves the current catalog once and retains its JSON report even when the job fails. every reported failure blocks; survivors are diagnostics, not a passing catalog.
9cfa78a321--baselineis rejected1 failing, 0 blockingci.yaml:197constant-false condition is the same finding on the baseChecklist
Code
9cfa78a321— the catalog gate must remain red while any listed plugin fails; the previous green result atc7f8e40919used baseline exemptions and is not acceptance evidencepython scripts/checkis green locally — health passes; local worktree archives blocked the earlier full runDocumentation & Housekeeping
Infographic