Open
#214
Enhancement
Third-party domain plugins: entry_points discovery, install/uninstall UX, upgrade durability, and marketplace distribution
0
Anchors
—
Blast radius
—
Churn 30d
0
Proposals
No description provided.
Activity1
Why
Every domain today — code, midi, mist, todo, chess (Episode 22) — requires editing
muse/plugins/registry.pydirectly and shipping it inside muse itself. That's correct for domains muse wants to bundle for everyone, but it's the wrong model for someone else's domain (genomics, circuit design, a hobby chess plugin) — they shouldn't need a PR into muse to use their own VCS domain, and we definitely don't want to ship every third party's domain to every muse user.Raised by gabriel after Episode 22; this issue is the concrete plan, grounded in a full architecture read (not guessed):
muse/plugins/registry.py's_REGISTRYis a static dict, eagerly instantiated at import time, with one env-var-gated precedent (MUSE_ENABLE_MIDI) and one private cross-module import (domains.py:73reaches into_REGISTRYdirectly for the dashboard).importlib.metadata/entry_points/pkg_resourcesanywhere in muse, musehub, or agentception today. No[plugins]config section, no.muse/plugins/convention. This is a from-scratch mechanism, not a wire-up of something half-built.muse(~/.local/share/muse/venv, real non-editable install) andmuse-dev(~/.local/share/muse-dev/venv, editable, points at~/ecosystem/muse).importlib.metadata.entry_points()only sees whatever's installed in the currently running interpreter's venv — a plugin installed into one is invisible to the other. Any install UX has to say which venv it targeted, not assume one.install.sh/uninstall.sh(generated by MuseHub atmusehub/api/routes/musehub/install.py, served at/install.sh) fully recreate/destroy~/.local/share/muse/venvon every install and upgrade. Anythingpip installed into that venv outside of muse itself — i.e. any third-party plugin — gets silently wiped the next time someone runs the installer. This has to be solved, not just noted.muse domains publish/update/ delete/infoandMusehubDomain/MusehubDomainInstalltables (musehub/db/musehub_domain_models.py) with real metadata (capabilities, viewer_type, install_count) and an explicit docstring anticipating@alice/genomics-style third-party domains. But there is no package name, download URL, or distribution artifact field anywhere in that schema — the publish/discover half exists; the fetch-and-install half is completely missing. This issue is what completes it.muse init --domain <anything>never validates against the registry (muse/core/validation.py::validate_domain_nameis a pure regex/format check, never consults_REGISTRY) — it silently writes an unregistered domain torepo.json. Every subsequent command then raisesMuseCLIErrorfromregistry.py::resolve_plugin(), butgrep -rn "except MuseCLIError" muse/returns zero hits anywhere in the codebase, so it surfaces as a raw, unhandled Python traceback on every single command, including--jsoninvocations (which get a traceback on stderr and no JSON envelope on stdout at all).muse/cli/commands/domains.py::_scaffold_new_domain(the existingmuse domains new/ scaffold command) writes the generated plugin into the installed package's own site-packages directory — it cannot target an out-of-tree, standalone package today. New scaffold target needed for phase 4.docs/guide/plugin-authoring-guide.mdonly documents in-tree authoring and is independently stale (references aStructuredMergePluginthat doesn't exist — the real name isAddressedMergePlugin— and anop_transformmodule that's actuallyop_merge). Needs a rewrite regardless; this is the natural place to land the new instructions.Design
Use Python's standard entry_points mechanism — the same pattern pytest, flake8, and Sphinx use for third-party plugins — rather than a bespoke loader:
registry.pymerges the built-in_REGISTRYdict with whateverimportlib.metadata.entry_points(group="muse.domains")finds, lazily (resolved/imported on first use, not at module import — so a third party's broken or slow-importing plugin doesn't tax every muse invocation) and cached after the first successful build. Built-in names win on collision, loudly (a warning naming the shadowed third-party plugin), never silently.Phases (each independently shippable and testable)
Phase 0 — Two pre-existing bugs this work depends on
main()inmuse/cli/app.pywith a realexcept MuseCLIError as exc:handler that printsexccleanly to stderr (respecting--json— emit the JSON error envelope, not a traceback) and exits withexc.exit_code, not Python's default 1.muse init --domain Xmust validateXagainst the merged registry (built-in + entry-point-discovered) at creation time, the same waydomains.py's existingusesubcommand already does (domains.py:1532-1539— a real, working pattern to copy, not invent), with a clean error naming known domains.--domainvalue gets a clean one-line error, not a traceback, in both plain and--jsonmodes.Phase 1 — Core discovery mechanism
registry.py: merge_REGISTRYwithimportlib.metadata.entry_points(group="muse.domains"), lazily, cached, with loud collision handling as described above.domains.py:73(from muse.plugins.registry import _REGISTRY) to go through a proper public accessor — it can no longer assume the dict is the complete picture.registered_domains(),schema_for(),resolve_plugin(),resolve_plugin_by_domain()all reflect entry-point-discovered plugins, not just built-ins.importlib.metadatasupports this cleanly) — no real third-party package needed to test discovery.'midi'fallback claim (registry.py:12-13, 60— the real fallback is_DEFAULT_DOMAIN = "code") while touching this file.Phase 2 — Install / uninstall CLI UX
muse domains install <pip-spec-or-path>: detects the current interpreter's venv (mirror the diagnostic approach inscripts/dev/which-muse.sh— report which venv, don't assume), runspip installinto it, prints a clear "you are installing and running third-party code with full process privileges, not sandboxed" warning before proceeding.--dev/editable flag for plugin authors iterating locally (pip install -e), meant to pair withmuse-dev.muse domains uninstall <name>.muse domains list --installed(or extend the existingdomainsdashboard) to distinguish built-in vs. entry-point-discovered domains.dev_guard.py's existing editable-build detection — decide explicitly whethermuse domains install --devshould be gated by the same guard rail as other mutating commands, and document the decision (the guard's docstring already scopeshubsubcommands as out of scope for a reason — this is a new category, not obviously either bucket).Phase 3 — Durability across muse upgrades (the "load-bearing" part)
~/.local/share/muse/alongside, not inside,venv/) so it survivesuninstall.sh'srm -rf "$VENV_DIR".install.sh/uninstall.sh(generated server-side bymusehub/api/routes/musehub/install.py— this phase touches musehub, not just muse) need a hook: after recreating the venv, reinstall everything the manifest lists, or at minimum warn loudly that plugins were wiped and how to restore them.muse domains doctor(or fold into an existing health-check command): detect drift between the manifest and what's actually importable, offer to repair.Phase 4 — Out-of-tree authoring support
muse domains new --out-of-tree <path>or similar) generating a standalone package — its ownpyproject.tomlwith themuse.domainsentry point pre-filled,plugin.pyfrom the existing scaffold template, a tests directory — since_scaffold_new_domaintoday can only write into the installed package's own site-packages dir (domains.py::_scaffold_new_domain, computes its target frompathlib.Path(__file__).parents[2]) and that's fundamentally wrong for this use case.docs/guide/plugin-authoring-guide.md: fix the staleStructuredMergePlugin/op_transformreferences while rewriting, add the out-of-tree path as a first-class option alongside in-tree, coverHarmonyPlugin/StagePlugin(currently missing from the guide's protocol ladder entirely), update the version header (says "v0.1.2").Phase 5 — Close the loop: MuseHub marketplace → real installs
MusehubDomain(musehub/db/musehub_domain_models.py) — package name + index (PyPI or a private index) or a source URL. Todaycapabilitiesis pure descriptive metadata; nothing points at installable code.muse domains install @author/slugresolves the real package coordinates via the hub (extendsmuse domains info's existing lookup) and installs it — the fetch-and-load half connecting to the publish/discover half that already exists.is_verified(already a real column) factors into the install warning from Phase 2 — an unverified third-party domain should probably warn harder than a verified one.Explicitly out of scope for this issue (flag, don't solve here)
is_verifiedflag.Assigned to gabriel. Multi-phase, load-bearing — expect this to take real design review at each phase boundary, not a single sitting.