DeepSeek Harness DeepSeek Harness plugin catalog

← Catalog

dsh-plugin-market

dsh-plugin-market@0.2.3
loader dsh-plugin-market

Published Updated

Name, description, and notes are shown in the submitted language and are not translated.

插件市场:浏览、安装、卸载、更新、启停插件。从独立页面 /plugin-market 管理本机与云端目录。

For agents: Markdown · JSON · tarball

Install on a machine that already has the plugin market

curl -fsSL https://addon.dshdo.com/install-plugin-market.sh | sh
# then in /plugin-market/ install dsh-plugin-market and enable it

Open /plugin-market/ → Cloud catalog, install this id, then enable. Install only registers; enable runs it.

Versions

Files

sha256
bfe43adf437d966bb0b94af3238f584b047f5f365a4945c9e3d911092a921cb8
258261 bytes · package dsh-plugin-market

README

dsh-plugin-market

Local plugin market for DeepSeek Harness. Open /plugin-market/ on the same dsh web process. On bundled dshdo devices, install into the live $DSH_HOME (often ~/.local/share/dshdo/dsh/home), not ~/.dsh.

  • Install from ~/dsh-plugins or the cloud catalog at https://addon.dshdo.com
  • Install registers the plugin with the dsh plugin manager, which owns the profile layer and the enable/disable/remove lifecycle.
  • UI chrome is Chinese/English (中文 / English). Plugin descriptions stay the published string.
  • Hash routes: #/plugin/<id> (installed / local shelf) and #/catalog/<id> (cloud detail).
  • Cloud detail also has agent URLs: HTML /plugins/<id>, Markdown /plugins/<id>.md, JSON /v1/plugins/<id>.
curl -fsSL https://addon.dshdo.com/install-plugin-market.sh | sh

Then restart dsh web and open http://127.0.0.1:3080/plugin-market/.

Security model (v0.2.2)

Every request to /plugin-market — API and static alike — must satisfy the loopback trust fence (lib/trust.mjs), a standalone copy of the harness /api fence semantics:

  • the socket peer must be loopback (IPv4 127/8, ::1, or IPv4-mapped);
  • the Host header must be a bare loopback authority (DNS-rebinding defense);
  • Sec-Fetch-Site: cross-site is refused; an attached Origin must be a

serialized origin equal to the Host authority exactly — scheme, host, and port — where the scheme is the one the request actually arrived on (req.socket.encrypted; http: on the plain-HTTP loopback server, https: behind TLS), never assumed; the literal null origin is refused;

  • no CORS headers are emitted. A same-origin page on this host may drive the

market; cross-site browser pages cannot. Remote LAN browsers are handled by the authenticated access-gateway LAN front, which terminates them and forwards to loopback (Origin/Sec-Fetch stripped, Host rewritten to the loopback authority), so such forwards pass the fence without weakening it for direct requests.

Management mutations additionally require a strict application/json body of at most 64 KiB. Tarball installs never run a system tar: the archive is parsed by the market's own reader, which refuses symlink/hardlink/device members and path traversal and enforces per-file / total / entry-count caps before extraction into a unique staging directory.

Lifecycle ownership and restart semantics

The dsh plugin manager (ctx.pluginManager) is the single lifecycle authority. enable and disable call setBundleEnabled(<package name>, true|false), uninstall calls removeBundle(<package name>), and a catalog install hands the freshly staged directory to installBundle(stagedDir, { enabled: true }). The market never writes cordis.patch.yml, the profile manifest, or profile symlinks itself, so a change's profile result is exactly the manager's ChangeResult, returned verbatim (application, stage, error, warnings, packageResult, bundle). A resolved refusal (application: 'failed', e.g. not-bundle, not-removable, management-required) is returned as HTTP 200 with ok: true and the failure visible in value; the page inspects application/error and reports the failure instead of assuming success. A manager rejection — the service missing or throwing — propagates as the handler's HTTP error (503 when unavailable).

The market keeps only the fences the manager cannot own: it refuses to disable or uninstall itself (403), and update refuses a plugin whose running copy resolves inside the harness checkout (builtin-resolved) with 409 before any catalog work. Every other protection — builtin, removable, in-use, management-required — is the manager's decision and is surfaced as its ChangeResult.

Responses carry restartRequired when a change cannot take effect until the dsh web process restarts, and the plugin's list row then keeps the flag until the process restarts (the flag is per-plugin and process-lifetime, never read from the harness updater's update-status.json). Updates to catalog-installed plugins are gated inside catalog.mjs against the installed tree's full-payload hash (treeHashVersion 2): when the tree was edited after install — or the record predates/omits a trusted hash — the update requires an explicit consent: true field (409 needs-consent otherwise). An update swaps the shelf directory in place and leaves the profile registration the manager owns alone; if replacing the tree fails and restoring the old directory also fails, the error response carries the retained pre-update backupPath (outside the shelf) and says so in the message. List rows report runtime fields (runtimeVersion / runtimePath / runtimeSource) as null unless a loader record attests a live module (cordis keeps only the executable callback per plugin, so version and path are never inferred from disk); resolved identity is reported under resolvedVersion (null when the resolved copy's manifest is unreadable — never the shelf row's own claim) / resolvedModulePath / moduleSource. enabled and installed come from the manager's listBundles() record, joined with the local and catalog facts.

A manager refusal or cancellation is returned verbatim; the market performs no rollback of its own. Child diagnostics from a local-git git pull are never returned over HTTP. Local Git updates are fast-forward only and do not provide the catalog's directory-snapshot rollback.

API contracts: see docs/API.md. Tests: node --test "tests/*.test.mjs".

Request lifecycle and plugin config

The plugin exposes five Config fields, split by when a change applies:

  • enabled is a Cordis-only mount control, not a live setting. Invalid stored market settings prevent mounting.
  • Live through the dsh-plugin-market settings namespace: catalogUrl and requestTimeoutMs (validated integer 1..2147483647,

default 30000). requestTimeoutMs is the overall deadline each request runs under.

  • Restart-applied, captured at apply (they configure the runner object, so a

live write cannot re-bind it): maxOutputBytes (1..16777216, default 1048576) caps each of a child command's stdout/stderr streams, and graceMs (1..60000, default 1000) bounds the terminate escalation. Change them in cordis.yml and restart the process; the settings namespace never exposes them, so no UI claims a live effect.

The handler gives every request one AbortSignal: body reads, catalog fetches, and catalog install/update downloads and hash checks bind to it, and a mutation that queued behind other work re-checks the signal once it acquires its lock — so a request that exceeds the deadline (408) or loses its handler during unload (503) never keeps running and never commits state it queued for.

The handler owns every request it accepts. On unload, index.js unregisters the /plugin-market route first (the server stops dispatching to it), then disposes the handler: in-flight requests are aborted with a 503 reason and awaited until they settle, and requests that race in after disposal are refused with 503. Disposal is idempotent — repeated disposer calls await the same drain, and an unload can never return while an old HTTP mutation is still touching torn-down state. The deadline/disposal is an abort *request*, not a forced completion: a write already past its commit point (started but not yet released settings update, queued settlement) is joined to its real finish — dispose waits for it and never interrupts started work (held-write probe in tests/serve-lifecycle.test.mjs).

The one command the plugin runs — a local-git update's git pull --ff-only — goes through the injected managed subprocess service (ctx.subprocess) via a per-apply runner (lib/run.mjs), never a blocking native child: the run is bounded by the request deadline, cancelled when the request aborts or the handler is disposed, and awaited by the plugin's teardown. The child environment forwards only DSH_HOME (dshChildEnv); the provider scrubs ambient DSH_* names, and the plugin directory is the run's cwd — test controls live in the fixture's fake executable, never as DSH_* env names in a production child. A failed pull is reported generically (safe exit status only); the child's raw stdout/stderr is never echoed into the HTTP error or logs.

apply must be an arrow function (export const apply = ...), never a constructible export function apply: cordis runs a constructible callback through new and discards its return value, so a plain function's disposer would never be collected and unloading would leave the route mounted. The real-fiber probe in tests/fiber-dispose.test.mjs boots the plugin through the actual vendored Loader and proves both the arrow-form drain and the constructible-form failure mode.