Files
Pine a53769955b feat(plugin): plugin marketplace, durable install persistence, skill manager
Add an in-app plugin marketplace fed by a remote web catalog: the host
plugin-inventory gateway gains marketplaceList/Install/Uninstall remotes,
a durable per-user install table reconciled against the actual profile, and
git/npm/tarball/bundle install paths with non-interactive git and vendored
pnpm. The Web Settings surface gains a sibling 插件市场 tab that lists the
catalog with recommended badges, repository links, and a combined sort
(recommended first, then catalog priority, then id).

Add a host + client skill manager: list local skills by direct filesystem
discovery, and install/uninstall/toggle/edit their SKILL.md from git/npm/
tarball/local sources in the writable user root.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-15 17:04:36 +08:00
..

@deepseek-ai/dsh-host-plugin-inventory

English | 中文

Host projection of the current Cordis Loader tree with per-plugin enable/disable. PluginInventoryGateway registers the pluginInventory service and publishes generated direct Remotes (list, setEnabled, availableBundles, installPlugin, installedBundles, uninstall). list reads ctx.loader.entries() directly, skips structural group rows, and returns the remaining entries in Loader order with only their Loader entry id, module specifier, effective enablement, and current root Fiber phase.

The phase is pending, loading, active, failed, or unloading; it is null when the entry has no live root Fiber. The snapshot is intentionally point-in-time: Loader remains the sole lifecycle authority, while this package owns no cache, history, provenance model, or event stream. setEnabled toggles one entry live through ctx.loader.update and persists an explicit disabled override into the profile's user patch layer so the choice survives a restart (a bundle-default disable needs the disabled: false override to stick).

Every entry carries a protected flag. The guard in src/required.ts is default-open with two code-editable lists: REQUIRED_PLUGINS (the blacklist of load-bearing core that must never be disabled — the entry tree, the Remote RPC spine, the session and agent spines) and USER_TOGGLEABLE_PLUGINS (the whitelist, which overrides the blacklist for an explicitly toggleable plugin); a plugin on neither list is toggleable by default. The full dependency-derived taxonomy of the shipped base bundle is in docs/plugin-system.md. setEnabled refuses to disable a required plugin; a plugin whose apply actually fails rejects through the loader's own start error, while a plugin merely pending on a dependency is left enabled and activates once the dependency resolves. The Web plugin-list tab renders one flat list of every entry: each shows its real enabled state, a toggleable plugin carries an enable or disable button (so a bundle-default-disabled plugin can be re-enabled), and a required plugin shows only a read-only note.

The gateway also manages installation through availableBundles/installPlugin/installedBundles/uninstall. availableBundles lists the curated offline-installable optional bundles in src/bundles.ts (AVAILABLE_BUNDLES); that catalog is empty until an optional bundle ships — the profile's default bundles (dsh-base, dsh-web-app, dsh-image-recognition-bundle) are part of the deployment, not optional add-ons, and uninstall refuses to remove them. installedBundles lists the profile's user-installed dependencies (the ones the user can uninstall). uninstall fully removes a user-installed dependency with pnpm remove (dropping both the dependency and any bundle layer it declared, gated by dshAllowPluginInstall like install), un-composes an offline optional bundle that was composed, and refuses the in-box default bundles. installPlugin runs pnpm against the writable profile directory for any pnpm add specifier — via the pnpm vendored into the harness, or the pnpm on PATH when no vendored pnpm is present (a development checkout), failing loudly only when neither exists — a bare npm name, a tarball path or URL (including a GitHub archive .tar.gz), or a git/GitHub URL — so every community install source shares one path. A registry install is gated behind the dshAllowPluginInstall context flag, which only the desktop boot sets; a registry-name spec tries the ordered INSTALL_REGISTRIES list (src/install.ts) until one succeeds, with the official npm registry last as the fallback, and errors only when every registry is unreachable, while a git, tarball, or path spec runs once with no registry. A registry-name spec also passes --minimum-release-age-exclude so a freshly published plugin installs.

Because pnpm 11 blocks dependency build scripts by default, install is two-phase when one is blocked: the first call returns the blocked packages as pendingBuilds instead of failing, the settings tab shows them with a sandbox warning for the user's per-package consent, and the retry carries the approved set in consentBuilds — the host writes the per-package allowBuilds map (pnpm ≥11) and onlyBuiltDependencies array (pnpm 10) directly into the profile's pnpm-workspace.yaml for exactly those packages, then reinstalls. Writing the config directly, rather than running pnpm approve-builds, does not depend on the packages being in pnpm's pending-build state. When the boot provides a dshReloadProfile handle, the gateway recomposes the running tree after the write so the plugin activates immediately (restartRequired: false); without it, the install persists the manifest and requires a restart (restartRequired: true). Its public payload types live under ./types, and Typert generates the Host and Client Remote artifacts exposed by ./typert and ./remote.

A plugin marketplace is exposed through marketplaceList/marketplaceInstall/marketplaceUninstall. The catalog lives on the static web host (default https://deepseek.pinesound.cn/plugins/plugins.json, overridable via DSH_MARKETPLACE_URL): one index JSON lists plugins, and a per-plugin JSON beside it prescribes the install method (git, npm, tarball, or bundle) with the pnpm specifier and the dependency name. marketplaceList overlays the durable install table — a small JSON document under $DSH_HOME/plugin-marketplace/installed.json — to mark each entry installed; that table is the authoritative installed check. marketplaceInstall maps a method onto the existing install path (git/npm/tarball via the registry Remote, bundle via the offline compose), records the install in the table on success, and honors the same two-phase build-consent flow; marketplaceUninstall resolves the dependency from the table, runs the existing uninstall, and drops the row. Per-plugin spec URLs are derived from the fixed catalog base plus the plugin id (never from catalog content), so a hostile catalog cannot point the app at an arbitrary URL.

The service is Remote-only and deliberately declares no same-process Cordis Context merge. Client packages consume it through the explicit api-remotes assembly rather than importing the Host implementation.

Model Experience

None, as this Host-only inventory projection registers no prompt, tool, message, or provider request.

KV Cache effect

None; this package never assembles model input.

Known Limitations and Deferred Work

  • Point-in-time state only — the result contains no durable failure history or subscription; a missing root Fiber is reported as null, regardless of why no live root exists.
  • No provenance — the service does not identify which bundle, profile, or override introduced an entry, and a row the profile does not mount (absent from every bundle) cannot be toggled from here. Enable/disable persists to the profile's user patch layer; user-installed dependencies can be uninstalled (installedBundles/uninstall), but the in-box default bundles are part of the installation and are not removable.