Skip to content

feat: plugin dependencies wait out the 14-day release quarantine, and CI checks the whole catalog installs together - #133409

Open
ethernet8023 wants to merge 9 commits into
mainfrom
feat/plugin-dep-quarantine
Open

ethernet8023 wants to merge 9 commits into
mainfrom
feat/plugin-dep-quarantine

Conversation

@ethernet8023

@ethernet8023 ethernet8023 commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

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:

[project]
dependencies = ["my-plugin-sdk==1.4.0"]

[tool.uv.exclude-newer-package]
my-plugin-sdk = false

Hermes copies accepted exemptions into the settings uv actually reads. an exemption must be false on 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

problem change
a plugin's pyproject.toml contains only linter/test settings, but Hermes treats it as its package definition read package dependencies from it only when it has [project]; otherwise use the plugin manifest
unused optional features or development tools conflict with Hermes's packages remove plugin extras and development groups from the copy used to resolve installation
existing entries require releases younger than 14 days eight temporary per-package date cutoffs admit the oldest acceptable release, while blocking later uploads

those 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

  • bug fixes
  • dependency policy change
  • tests and CI
  • documentation

Changes Made

  1. PM keeps the global 14-day cutoff and copies accepted plugin exemptions into the generated install settings.
  2. hermes plugins validate rejects unsupported exemptions and dependency ranges that exclude core's locked version.
  3. the catalog-wide job resolves each entry alone, then the combined set; when an entry breaks that set, it identifies an earlier entry involved in the conflict.
  4. the job runs through the plugin_catalog change-detection lane on PRs touching catalog entries, pyproject.toml, uv.lock or 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.
  5. packaging fixes and temporary dated exceptions reduce the failures found by that check.

What remains open

  • Python-version caps and incompatible runtime dependencies still need upstream fixes or catalog pin updates.
  • removing plugin extras means optional plugin features are not part of the install resolution. Hermes does not currently select those extras; this PR does not add that feature.
  • exact pins and release dates are not a review of every dependency's code, and do not freeze the hashes of future files uploaded to a release.

How to Test

scripts/run_tests.sh tests/pm/test_workspace.py tests/pm/test_plugin_declarations.py tests/pm/test_workspace_build_inputs.py tests/hermes_cli/test_plugin_validate.py
python -m pm.build_env --source . --check-lock
scripts/run-in-hermes-env python3 scripts/ci/catalog_resolve_all.py --report "$TMPDIR/catalog-report.json"

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.

verification observed result
targeted tests at c7f8e40919 50 passed
PM's lockfile check passed
scripts/check --only health passed
13 real catalog entries covering tool-config files, extras and torch sources the 8 with Python dependencies resolved together, no failures
8 real entries needing recent releases 7 resolved together; mnemostack still failed because its package depends on Hermes itself

these catalog results prove dependency resolution, not a full installation or working plugin features. the whole-catalog run at c7f8e40919 completed: 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.

verification at 9cfa78a321 observed result
CI test directory 323 passed, 1 Windows-only test skipped on Linux
real offline uv fixtures compatible sets pass; solo and cross-plugin conflicts fail; --baseline is rejected
regression before the change an unsatisfiable baseline-listed plugin returned success with 1 failing, 0 blocking
workflow shell + aggregate replay resolver failure propagates to a failed step and failed aggregate
actionlint catalog workflow clean; unchanged ci.yaml:197 constant-false condition is the same finding on the base
full local blocking checks only pre-existing archives inside other worktrees fail the profile-archives check
full current catalog with strict gate 114 members resolve together; 9 failures; process exits 1

Checklist

Code

  • read the contributing guide
  • conventional commits
  • searched for and credited existing related PRs
  • changes are scoped to plugin dependency installation and its checks
  • CI on strict-gate head 9cfa78a321 — the catalog gate must remain red while any listed plugin fails; the previous green result at c7f8e40919 used baseline exemptions and is not acceptance evidence
  • full python scripts/check is green locally — health passes; local worktree archives blocked the earlier full run
  • added behavior tests and proved regressions
  • tested on Linux / NixOS

Documentation & Housekeeping

  • catalog rule 10 updated in both mirrored copies
  • dependency guide and PM instructions updated
  • temporary dates include removal instructions
  • no new config keys or tool schemas
  • dependency markers retained

Infographic

plugin dependency failures and the fixes included in this PR

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.
@ethernet8023
ethernet8023 requested a review from a team October 5, 2026 17:01
@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown

૮ >ﻌ< ა ci review

ran on 9cfa78a — ci: fail catalog resolution on every broken plugin

❌ Job failures

Plugin catalog resolve / resolve · View job

Job Plugin catalog resolve / resolve failed.


debug info

CI timings

CI timings · View report · View job

Wall time 27m25s vs 26m33s (+3.3%). 24 job(s) slower, 12 faster, 3 unchanged.

  • OS-specific tests / Windows-only tests (arm64): +454.0s
  • Python tests / e2e: +212.0s
  • Python tests / e2e-upgrade (core): -177.0s
  • Python tests / Run tests (2/2): +164.0s
  • OS-specific tests / Windows install + update E2E / Windows install.ps1 + hermes update E2E: +101.0s

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.
@alt-glitch alt-glitch added type/security Security vulnerability or hardening P3 Low — cosmetic, nice to have comp/plugins Plugin system and bundled plugins Plugin Catalog Plugin catalog entries, discovery, metadata, and catalog management sweeper:risk-automation Sweeper risk: may affect CI, automerge, label sync, or maintainer automation labels Oct 5, 2026
… 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.
@ethernet8023
ethernet8023 added this pull request to stack #133452 October 5, 2026 18:47
@ethernet8023
ethernet8023 removed this pull request from stack #133452 October 5, 2026 19:13
@ethernet8023

Copy link
Copy Markdown
Collaborator Author

loreconvo/lancedb-suite blocker: the tested fix keeps lancedb-suite at 0.34.0 and widens loreconvo's package requirement to >=0.30.2,<=0.34.0. the application lock stays at 0.30.2. verified real-table tests across four engine releases and Python 3.10/3.14; a new loreconvo release is still required.

tested database dependency interval

@ethernet8023

Copy link
Copy Markdown
Collaborator Author

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 plugin compatibility fixes

@ethernet8023

ethernet8023 commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator Author

upstream blocker PRs

these source fixes are submitted, not merged. published-package changes still need upstream releases and catalog pin updates.

plugin / fix upstream PR
health — compatibility Infinite-Labs-AI/hermes-health-apollo#17
pubky — compatibility MCarlomagno/hermes-pubky#1
zero — compatibility TxnLab/hermes-zerosignal-provider#1
lancedb — compatibility lancedb/hermes-agent-memory#12
mnemostack — compatibility udjin-labs/hermes-mnemostack#6
birkin-mnemosyne — exact pin + exemption ashmoonori-afk/birkin-mnemosyne#24
cortexlayer — exact pin + exemption Cortex-Layer/cortexlayer-hermes-plugin#1
evalroute — exact pin + exemption keppy/hermes-plugin-evalroute#2
gonogo — exact pin + exemption keppy/hermes-plugin-gonogo#1
loreconvo — exact pin + exemption labyrinth-analytics/loreconvo#3
mnemostack — exact pin + exemption udjin-labs/hermes-mnemostack#7
thomas — exact pin + exemption keppy/hermes-plugin-thomas#1

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.

@ethernet8023
ethernet8023 added this pull request to stack #133482 October 5, 2026 19:27
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.
@Enough1122

Copy link
Copy Markdown

AI code review - automated follow-up for reference; not a maintainer.

Defect: the dependency quarantine validator and the installer pass different core_locked sets, so it approves exemptions PM will refuse

The new quarantine_exemptions(pyproject, core_locked) has two callers in this PR, and they disagree on what "core locks":

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-package keys: 108
  • install-time held: 343 — validator-time held: 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 from uv.lock) → honoured → problems empty → "dependency quarantine" reports True. The entry is admitted.
  • install: held contains hermes-mnemostack (from the core table key) → refused → the exemption is dropped with a logging.warning and core's own cutoff 2026-09-27T19:26:09Z stands.

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 held

then 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, plus pyproject.toml, uv.lock, scripts/ci/*).
  • Confirmed both call sites and the key not in core_locked refusal condition by reading the source at head.
  • Computed the two held sets by parsing the head uv.lock ([package] names) and head pyproject.toml (exclude-newer-package keys) 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 ethernet8023 comments concern upstream package pins/exemptions and the failing catalog entries, not the core-side asymmetry between the two callers of quarantine_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_quarantine and quarantine_exemptions, not from a run — please confirm with a real uv lock against 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] overrides or in a [manifest.members] entry, which locked_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.warning is 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:25Z for evalroute is still correct at review time, I did not evaluate.

@alt-glitch alt-glitch added python:uv Pull requests that update python:uv code blocked Waiting on external dependency or decision sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Oct 6, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

blocked Waiting on external dependency or decision comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have Plugin Catalog Plugin catalog entries, discovery, metadata, and catalog management python:uv Pull requests that update python:uv code sweeper:risk-automation Sweeper risk: may affect CI, automerge, label sync, or maintainer automation sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants