{"ok":true,"value":{"id":"dsh-plugin-market","name":"dsh-plugin-market","version":"0.2.3","developer":"DeepSeek Harness","description":"插件市场：浏览、安装、卸载、更新、启停插件。从独立页面 /plugin-market 管理本机与云端目录。","sha256":"bfe43adf437d966bb0b94af3238f584b047f5f365a4945c9e3d911092a921cb8","byteSize":258261,"publishedAt":"2026-08-25T06:20:22.976Z","updatedAt":"2026-09-21T17:16:40.504Z","releasedAt":"2026-09-21T17:16:40.504Z","packageName":"dsh-plugin-market","loaderId":"dsh-plugin-market","changelog":"","readme":{"md":"# dsh-plugin-market\n\nLocal 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`.\n\n- Install from `~/dsh-plugins` or the cloud catalog at https://addon.dshdo.com\n- Install registers the plugin with the dsh plugin manager, which owns the profile layer and the enable/disable/remove lifecycle.\n- UI chrome is Chinese/English (`中文` / `English`). Plugin descriptions stay the published string.\n- Hash routes: `#/plugin/<id>` (installed / local shelf) and `#/catalog/<id>` (cloud detail).\n- Cloud detail also has agent URLs: HTML `/plugins/<id>`, Markdown `/plugins/<id>.md`, JSON `/v1/plugins/<id>`.\n\n```\ncurl -fsSL https://addon.dshdo.com/install-plugin-market.sh | sh\n```\n\nThen restart `dsh web` and open `http://127.0.0.1:3080/plugin-market/`.\n\n## Security model (v0.2.2)\n\nEvery request to `/plugin-market` — API and static alike — must satisfy the\nloopback trust fence (`lib/trust.mjs`), a standalone copy of the harness\n`/api` fence semantics:\n\n- the socket peer must be loopback (IPv4 `127/8`, `::1`, or IPv4-mapped);\n- the `Host` header must be a bare loopback authority (DNS-rebinding defense);\n- `Sec-Fetch-Site: cross-site` is refused; an attached `Origin` must be a\n  serialized origin equal to the Host authority exactly — scheme, host, and\n  port — where the scheme is the one the request actually arrived on\n  (`req.socket.encrypted`; `http:` on the plain-HTTP loopback server, `https:`\n  behind TLS), never assumed; the literal `null` origin is refused;\n- no CORS headers are emitted. A same-origin page on this host may drive the\n  market; cross-site browser pages cannot. Remote LAN browsers are handled by\n  the authenticated access-gateway LAN front, which terminates them and\n  forwards to loopback (Origin/Sec-Fetch stripped, Host rewritten to the\n  loopback authority), so such forwards pass the fence without weakening it\n  for direct requests.\n\nManagement mutations additionally require a strict `application/json` body of\nat most 64 KiB. Tarball installs never run a system `tar`: the archive is\nparsed by the market's own reader, which refuses symlink/hardlink/device\nmembers and path traversal and enforces per-file / total / entry-count caps\nbefore extraction into a unique staging directory.\n\n## Lifecycle ownership and restart semantics\n\nThe dsh plugin manager (`ctx.pluginManager`) is the single lifecycle\nauthority. `enable` and `disable` call\n`setBundleEnabled(<package name>, true|false)`, `uninstall` calls\n`removeBundle(<package name>)`, and a catalog install hands the freshly staged\ndirectory to `installBundle(stagedDir, { enabled: true })`. The market never\nwrites `cordis.patch.yml`, the profile manifest, or profile symlinks itself,\nso a change's profile result is exactly the manager's `ChangeResult`, returned\nverbatim (`application`, `stage`, `error`, `warnings`, `packageResult`,\n`bundle`). A resolved refusal (`application: 'failed'`, e.g. `not-bundle`,\n`not-removable`, `management-required`) is returned as HTTP 200 with\n`ok: true` and the failure visible in `value`; the page inspects\n`application`/`error` and reports the failure instead of assuming success. A\nmanager rejection — the service missing or throwing — propagates as the\nhandler's HTTP error (503 when unavailable).\n\nThe market keeps only the fences the manager cannot own: it refuses to\ndisable or uninstall itself (403), and `update` refuses a plugin whose\nrunning copy resolves inside the harness checkout (`builtin-resolved`) with\n409 before any catalog work. Every other protection — builtin, removable,\nin-use, management-required — is the manager's decision and is surfaced as its\n`ChangeResult`.\n\nResponses carry `restartRequired` when a change cannot take effect until the\n`dsh web` process restarts, and the plugin's list row then keeps the flag\nuntil the process restarts (the flag is per-plugin and process-lifetime, never\nread from the harness updater's `update-status.json`). Updates to\ncatalog-installed plugins are gated inside `catalog.mjs` against the installed\ntree's full-payload hash (treeHashVersion 2): when the tree was edited after\ninstall — or the record predates/omits a trusted hash — the update requires an\nexplicit `consent: true` field (409 `needs-consent` otherwise). An update\nswaps the shelf directory in place and leaves the profile registration the\nmanager owns alone; if replacing the tree fails and restoring the old\ndirectory also fails, the error response carries the retained pre-update\n`backupPath` (outside the shelf) and says so in the message. List rows report\nruntime fields (`runtimeVersion` / `runtimePath` / `runtimeSource`) as `null`\nunless a loader record attests a live module (cordis keeps only the executable\ncallback per plugin, so version and path are never inferred from disk);\nresolved identity is reported under `resolvedVersion` (null when the resolved\ncopy's manifest is unreadable — never the shelf row's own claim) /\n`resolvedModulePath` / `moduleSource`. `enabled` and `installed` come from the\nmanager's `listBundles()` record, joined with the local and catalog facts.\n\nA manager refusal or cancellation is returned verbatim; the market performs no\nrollback of its own. Child diagnostics from a local-git `git pull` are never\nreturned over HTTP. Local Git updates are fast-forward only and do not provide\nthe catalog's directory-snapshot rollback.\n\nAPI contracts: see [docs/API.md](docs/API.md). Tests: `node --test \"tests/*.test.mjs\"`.\n\n## Request lifecycle and plugin config\n\nThe plugin exposes five Config fields, split by when a change applies:\n\n- `enabled` is a Cordis-only mount control, not a live setting. Invalid stored market settings prevent mounting.\n- Live through the `dsh-plugin-market` settings namespace: `catalogUrl` and `requestTimeoutMs` (validated integer 1..2147483647,\n  default 30000). `requestTimeoutMs` is the overall deadline each request\n  runs under.\n- Restart-applied, captured at apply (they configure the runner object, so a\n  live write cannot re-bind it): `maxOutputBytes` (1..16777216, default\n  1048576) caps each of a child command's stdout/stderr streams, and\n  `graceMs` (1..60000, default 1000) bounds the terminate escalation. Change\n  them in cordis.yml and restart the process; the settings namespace never\n  exposes them, so no UI claims a live effect.\n\nThe handler gives every request one\n`AbortSignal`: body reads, catalog fetches, and catalog install/update\ndownloads and hash checks bind to it, and a mutation that queued behind other\nwork re-checks the signal once it acquires its lock — so a request that\nexceeds the deadline (408) or loses its handler during unload (503) never\nkeeps running and never commits state it queued for.\n\nThe handler owns every request it accepts. On unload, index.js unregisters\nthe `/plugin-market` route first (the server stops dispatching to it), then\ndisposes the handler: in-flight requests are aborted with a 503 reason and\nawaited until they settle, and requests that race in after disposal are\nrefused with 503. Disposal is idempotent — repeated disposer calls await the\nsame drain, and an unload can never return while an old HTTP mutation is still\ntouching torn-down state. The deadline/disposal is an abort *request*, not a\nforced completion: a write already past its commit point (started but not yet\nreleased settings update, queued settlement) is joined to its real finish —\ndispose waits for it and never interrupts started work (held-write probe in\n`tests/serve-lifecycle.test.mjs`).\n\nThe one command the plugin runs — a local-git update's `git pull --ff-only` —\ngoes through the injected managed subprocess service (`ctx.subprocess`) via a\nper-apply runner (`lib/run.mjs`), never a blocking native child: the run is\nbounded by the request deadline, cancelled when the request aborts or the\nhandler is disposed, and awaited by the plugin's teardown. The child\nenvironment forwards only `DSH_HOME` (`dshChildEnv`); the provider scrubs\nambient `DSH_*` names, and the plugin directory is the run's cwd — test\ncontrols live in the fixture's fake executable, never as `DSH_*` env names in\na production child. A failed pull is reported generically (safe exit status\nonly); the child's raw stdout/stderr is never echoed into the HTTP error or\nlogs.\n\n`apply` must be an arrow function (`export const apply = ...`), never a\nconstructible `export function apply`: cordis runs a constructible callback\nthrough `new` and discards its return value, so a plain function's disposer\nwould never be collected and unloading would leave the route mounted. The\nreal-fiber probe in `tests/fiber-dispose.test.mjs` boots the plugin through\nthe actual vendored Loader and proves both the arrow-form drain and the\nconstructible-form failure mode.\n","zhMd":"# dsh-plugin-market\n\nDeepSeek Harness 本机插件市场。在同一 `dsh web` 进程打开 `/plugin-market/`。dshdo 打包设备要装进正在运行的 `$DSH_HOME`（常见是 `~/.local/share/dshdo/dsh/home`），不是 `~/.dsh`。\n\n- 从 `~/dsh-plugins` 或云端目录 https://addon.dshdo.com 安装\n- 安装把插件登记到 dsh 插件管理器；profile 层与启用/停用/卸载生命周期都由它统一管理\n- 界面中英切换；插件简介保持发布时的那一条\n- 哈希路由：`#/plugin/<id>`（已安装 / 本机货架），`#/catalog/<id>`（云端详情）\n- 云端详情同时给 Agent：HTML `/plugins/<id>`，Markdown `/plugins/<id>.md`，JSON `/v1/plugins/<id>`\n\n```\ncurl -fsSL https://addon.dshdo.com/install-plugin-market.sh | sh\n```\n\n然后重启 `dsh web`，打开 `http://127.0.0.1:3080/plugin-market/`。\n\n## 安全模型（v0.2.2）\n\n对 `/plugin-market` 的每个请求（API 与静态资源）都必须通过回环信任栅栏\n（`lib/trust.mjs`，独立复刻 harness `/api` 栅栏语义）：\n\n- 对端 socket 必须是回环（IPv4 `127/8`、`::1` 或 IPv4-mapped）；\n- `Host` 头必须是裸的回环 authority（防 DNS 重绑定）；\n- 拒绝 `Sec-Fetch-Site: cross-site`；带 `Origin` 时必须是与 Host\n  authority 完全一致的源——scheme、host、port 缺一不可，其中 scheme 取\n  请求实际到达的方式（`req.socket.encrypted`；明文回环服务器上是\n  `http:`，TLS 后是 `https:`），不做部署假设；拒绝字面 `null` origin；\n- 不输出任何 CORS 头。本机同源页面可以驱动市场；跨站浏览器页面不行。\n  远程 LAN 浏览器由 access-gateway 的 LAN 前端（带认证）终结后转发到\n  回环（Origin/Sec-Fetch 被剥离、Host 重写为回环 authority），这类转发\n  通过栅栏而不削弱对直连请求的防跨站语义。\n\n管理类变更请求体必须是严格 `application/json`，且 ≤ 64 KiB。tarball 安装\n从不调用系统 `tar`：由市场自带的解析器读取，拒绝符号链接 / 硬链接 /\n设备成员与路径穿越，并在写入唯一 staging 目录前执行单文件 / 总量 /\n条目数上限。\n\n## 生命周期归属与重启语义\n\ndsh 插件管理器（`ctx.pluginManager`）是唯一的生命周期权威：`enable` /\n`disable` 调用 `setBundleEnabled(<包名>, true|false)`，`uninstall` 调用\n`removeBundle(<包名>)`，云端安装把刚 staging 好的目录交给\n`installBundle(stagedDir, { enabled: true })`。市场自己绝不写\n`cordis.patch.yml`、profile manifest 或 profile symlink，因此变更的 profile\n结果就是管理器的 `ChangeResult`，原样返回（`application`、`stage`、\n`error`、`warnings`、`packageResult`、`bundle`）。已解析的拒绝\n（`application: 'failed'`，如 `not-bundle`、`not-removable`、\n`management-required`）以 HTTP 200 且 `ok: true` 返回，失败体现在 `value`\n里；页面检查 `application`/`error` 并如实报错，不再假定成功。管理器本身\n拒绝（服务缺失或抛错）则按 handler 的 HTTP 错误向上传递（不可用时 503）。\n\n市场只保留管理器无法承担的两道栅栏：不允许停用/卸载市场自身（403）；\n`update` 对运行副本解析到 harness checkout 内的插件（`builtin-resolved`）\n在做任何云端工作前返回 409。其余保护——内置、可移除性、占用中、\nmanagement-required——都是管理器的判定，以其 `ChangeResult` 如实上报。\n\n响应携带 `restartRequired`，表示该变更要等 `dsh web` 进程重启后才生效；\n插件行随后保留该标记直到进程重启（标记按插件记录、进程生命周期有效，\n绝不读取 harness 更新器的 `update-status.json`）。对云端目录插件的\n更新在 `catalog.mjs` 内按已装树的完整载荷哈希（treeHashVersion 2）判定\nconsent：装好后本地文件被改过、或记录缺失/不含可信哈希时，更新必须显式\n传 `consent: true`（否则 409 `needs-consent`）。更新就地替换货架目录，\n不触碰管理器负责的 profile 登记；若替换目录失败且恢复原目录也失败，错误\n响应会带货架外保留的更新前副本 `backupPath` 并在文案中告知。列表行的\n运行时字段（`runtimeVersion` / `runtimePath` / `runtimeSource`）除非有\nloader 记录证言正在运行的模块，否则一律为 `null`（cordis 对每个插件只\n保存可执行回调，版本与路径绝不由磁盘推断）；解析身份在 `resolvedVersion`\n（解析副本的 manifest 缺失/不可读时为 `null`，绝不回退到货架行自身的\n版本）`resolvedModulePath` `moduleSource` 中如实上报。`enabled` /\n`installed` 取自管理器 `listBundles()` 的记录，与本机、云端事实合并。\n\n管理器的拒绝或取消原样返回，市场不做自己的回退。本地 git `git pull` 的\n原始诊断流不回传 HTTP。本地 Git 更新只允许快进，不提供云端更新的目录\n快照回退。\n\nAPI 契约见 [docs/API.md](docs/API.md)。测试：`node --test \"tests/*.test.mjs\"`。\n\n## 请求生命周期与插件配置\n\n插件暴露五个 Config 字段：\n\n- `enabled` 仅在 Cordis 配置中控制挂载，不是实时设置。无效的已保存市场设置会阻止挂载。\n- 经 `dsh-plugin-market` settings 命名空间实时同步：`catalogUrl` 与\n  `requestTimeoutMs`（校验为 `1..2147483647` 的整数，默认 `30000`，每个请求\n  的整体时限）。\n- **重启后生效**、在 apply 时捕获（它们配置 runner 对象本身，实时写入无法\n  重新绑定）：`maxOutputBytes`（`1..16777216`，默认 `1048576`，分别限制\n  子命令 stdout/stderr 每个流的上限）与 `graceMs`（`1..60000`，默认 `1000`，\n  终止升级的宽限）。改它们请在 cordis.yml 中修改并重启进程；settings\n  命名空间从不暴露它们，因此不会有任何 UI 谎称其实时生效。\n\nhandler 为每个\n请求持有一个 `AbortSignal`：读取变更请求体、代理云端目录的 fetch、云端\n安装/更新的下载与哈希校验都绑定它；变更操作在锁内排队、拿到锁后会再次\n检查 signal——超过时限（408）或卸载中失去 handler（503）的请求不会继续\n运行，也不会提交它排队等到的任何状态。\n\nhandler 拥有它接受的每个请求。卸载时 index.js 先注销 `/plugin-market`\n路由（服务器停止向其派发请求），再 dispose handler：在途请求以 503 中止\n并等待其全部落定，卸载后竞态到达的请求一律 503 拒绝。dispose 幂等——\n重复调用等待同一个落定 promise，卸载绝不在旧 HTTP 变更仍在触碰已拆除\n状态时返回。超时/卸载是**中止请求**而非强制完成：已越过提交点、不可\n取消的写入与已入队的结算会被 join 到其真实落定——dispose 等待它，绝不\n打断已经开始的工作（held-write 探针见 tests/serve-lifecycle.test.mjs）。\n\n插件运行的唯一命令——本地 git 更新的 `git pull --ff-only`——经注入的受管\n子进程服务（`ctx.subprocess`）+ 本 apply 的 runner（lib/run.mjs）执行，\n绝不使用阻塞式原生子进程：该次运行受请求时限约束、请求中止或 handler 卸载\n时被取消，并被插件 teardown 等待排空。子进程环境只显式转发 `DSH_HOME`\n（dshChildEnv）：provider 会擦除环境里的 `DSH_*` 名，插件目录就是运行的\ncwd——测试开关应放进 fixture 的假可执行文件，绝不作为 `DSH_*` 环境变量\n进入生产子进程。`git pull` 失败只返回通用错误（安全退出码）——子进程原始\nstdout/stderr 绝不回显进 HTTP 错误或日志。\n\n`apply` 必须是箭头函数（`export const apply = …`），不能写成可构造的\n`export function apply`：cordis 对带 `.prototype` 的回调走 `new` 并丢弃\n返回值，普通函数返回的卸载器永远不会被收集，卸载后路由仍会挂着。真实\nfiber 探针在 `tests/fiber-dispose.test.mjs`：用真实的 vendored Loader 启动\n插件，既验证箭头形态能排空在途请求，也验证可构造形态的失败模式。\n"},"versions":[{"version":"0.2.3","changelog":"","sha256":"bfe43adf437d966bb0b94af3238f584b047f5f365a4945c9e3d911092a921cb8","byteSize":258261,"releasedAt":"2026-09-21T17:16:40.504Z","reviewStatus":"approved"},{"version":"0.2.2","changelog":"0.2.2：完整注册回滚（含 registration rollback 恢复 patch/dep/link）；Config 仅 Cordis 装配且 settings 读取失败大声报错；git 通用流式输出；非 ENOENT unlink 错误不再吞掉。内置冻结资产 market-6t1ytd2v.js / market-rfzjgc40.css。","sha256":"45cc6b329b10b60f31e3ce28482bc390531dabc227d63d0aff5324e621269d04","byteSize":270314,"releasedAt":"2026-09-08T12:03:10.501Z","reviewStatus":"approved"},{"version":"0.2.1","changelog":"bundled dshdo 设备侧栏改走 tapIndex；安装脚本写入正在运行的 DSH_HOME。","sha256":"6e476156e91ebc7a40ef54be77551d836046a3fd264a33690666138becbde1d2","byteSize":73468,"releasedAt":"2026-08-25T06:20:22.976Z","reviewStatus":"approved"}],"urls":{"html":"https://addon.dshdo.com/plugins/dsh-plugin-market","markdown":"https://addon.dshdo.com/plugins/dsh-plugin-market.md","json":"https://addon.dshdo.com/v1/plugins/dsh-plugin-market","tarball":"https://addon.dshdo.com/v1/plugins/dsh-plugin-market/versions/0.2.3/tarball"}}}