DeepSeek Harness Plugin Hub

发布与管理完整 Harness Profiles,发现适合你的插件。

探索

插件目录环境预设文档中心动态

社区

发布插件联系我们报告问题

相关链接

Plugin Hub GitHubDeepSeek Harness 官方项目系统状态隐私说明
© 2026 DeepSeek Harness Plugin HubPowered byPaxTech

独立、非官方社区项目,与 DeepSeek 官方无隶属、授权或背书关系。

Power Switch — DeepSeek Harness 插件(DSH Plugin)
DeepSeek Harness Plugin Hub
ProfilesPlugins分类动态文档登录管理 Profiles
ProfilesPlugins分类动态文档登录
← Plugins
P

dsh-power-switch

Power Switch

仅限 Windows 的 DSH 插件:可从侧边栏一键关机,并提供应用窗口/普通标签页启动切换,同时修复桌面快捷方式。

插件会安装到这里;不确定时保持 web。

npx -y @deepseek-ai/dsh plugin --profile web add github:SanYe-SanJiu/dsh-power-switch#c1728e471fb3f0c7e47cb8f1f089816306767113
README兼容性版本

兼容性与来源证明

Power Switch 以 dsh-power-switch 发布,当前版本为 1.1.4。Plugin Hub 会校验它的 manifest,并保存精确安装来源,便于复现安装结果。

DSH 兼容范围
*
运行环境
web
发布来源
github
Registry 更新时间
2026/9/24

版本

1.1.4stable
2026/9/24

相关插件

正在加载相关插件…

最新版
1.1.4
DSH
*
HMR
重启进程
Tree shaking
未声明可安全裁剪
解包体积
未提供
文件数
未提供
Surface
web
许可证
MIT
发布源
github
GitHub
★ 0
周下载
0
最近提交
2026/9/24
查看源码 ↗
README Badge

点击下方 Badge 复制 Markdown,粘贴到 README 即可。

这是你的 Plugin?认领权益 · 优先安全扫描

验证 package.json 声明的 GitHub 仓库,即可管理这个公开页面。认领后,Hub 会优先安排当前版本的安全扫描,并在通过后公开展示结果。

认领这个 Plugin →
报告问题

相关插件

继续浏览 productivity-workflow 分类下经过校验的插件。

Acp App@deepseek-ai/dsh-acp-appdsh ACP 配置文件包:基于 dsh-base 的仅限自动化的 JSON-RPC stdio 和进程生命周期管理Client Ui Task Board@linxin666/dsh-client-ui-task-board面向 DSH Web GUI 的主机权威任务面板,支持实际会话执行、主机 cron 调度以及可选的跨平台空闲睡眠保护;以挂载方式提供,无需修改 DSH 源代码。Web All@linxin666/dsh-web-allDSH Web UI 全家桶聚合插件:一键安装全部功能插件(task-board / git-graph / pet / remote-web-ui / web-ui-settings / skin-center / community-plugins / compat shim)。compat 桥接层已并入本包(src/client),无需独立 compat npm 包。Codex Ui@michengai/dsh-codex-uiDSH Codex UI — 为 DeepSeek Harness Web 提供 Codex 风格侧栏、工作区会话树、全局搜索和轮次导航

README

dsh-power-switch

English | 中文

A DeepSeek Harness (DSH) plugin providing two features:

  • Process shutdown: a ⏻ button in the sidebar foot (with confirmation) that shuts down the local dsh web process through DSH's own graceful exit path, so sessions and settings are flushed first.
  • Launch mode switch: a switch on the Settings -> Plugins card that selects how the next launch opens — an app window or a normal tab. A single press saves the setting, updates the desktop shortcut and restarts DSH.

Platform support

Windows only. package.json declares "os": ["win32"], so other platforms cannot install it.

App-window mode depends on a desktop shortcut (Windows Script Host and .lnk), for which there is no equivalent yet. The shutdown feature itself is platform-independent (it goes through ctx.appExit and never calls a shell), but shipping a plugin whose primary feature would silently degrade on macOS/Linux is worse than declaring no support. Adding it later requires open/xdg-open, macOS browser paths and a .desktop equivalent.

Requirements

ItemRequirement
Operating systemWindows 10 or later
DSH>=0.1.6-alpha.2 <0.1.7-0 || >=0.1.7-0 <0.2.0-0 (see engines.dsh in package.json)
Node.js>=20
Windows Script HostRequired by the desktop-shortcut layer (cscript.exe / wscript.exe). It can be removed, or blocked by antivirus, Attack Surface Reduction rules or group policy — a locked-down machine is a real case, not a hypothetical one. When that happens the card says so in its own words, and the shutdown feature, the settings section and the mode switch are unaffected.

The DSH range is spelled per minor line, and that is not decoration: DSH has only ever published prereleases, and semver admits a prerelease into a range only when a comparator on the same major.minor.patch carries a prerelease of its own — so *, >=0.1.0 and a bare >=0.1.6-alpha.2 all admit no DSH build that exists. The floor is the first DSH release shipping @deepseek-ai/dsh-plugin-manager: before it there is no dsh plugin install and no dsh.bundle layer, so this package cannot be installed at all. Tested against 0.1.6-alpha.2 (the full suite) and the 0.1.7 line; a new DSH minor line needs another branch here.

Install

You should cd to the deepseek harness folder first

The three commands below are exactly equivalent — they differ only in how DSH is invoked:

# ① the `dsh` command is installed (npm global install, or the desktop app)
dsh plugin --profile web add github:SanYe-SanJiu/dsh-power-switch#v1.1.4

# ② running from a source checkout that has been built (apps/cli/lib/bin.js exists)
#    run this from the checkout root
node apps\cli\lib\bin.js plugin --profile web add github:SanYe-SanJiu/dsh-power-switch#v1.1.4

# ③ running from a source checkout that is not built, or you prefer the TypeScript
#    sources — this is the form the upstream development documentation
#    (docs/user/develop/basic/publish.md) gives for a source checkout
#    run this from the checkout root
pnpm dsh plugin --profile web add github:SanYe-SanJiu/dsh-power-switch#v1.1.4

All three entries are exactly equivalent, and every other command in this document can be substituted the same way (replace dsh with node apps\cli\lib\bin.js or pnpm dsh).

Other specs:

# track the branch: resolves whatever `main` is at that moment. pnpm caches git
# installs by ref, so a repeated add is not guaranteed to refresh either
<entry> plugin --profile web add github:SanYe-SanJiu/dsh-power-switch

# from a local checkout (for development; link: keeps pointing at the checkout
# instead of copying it)
<entry> plugin --profile web add link:<absolute path to this checkout>

Notes:

  • --profile is required. The Web UI profile is named web (dsh web is equivalent to dsh --profile web).
  • apps/cli/lib/bin.js is pnpm build output and not a file in the repository, so a fresh clone uses form ③.
  • The arguments after add are forwarded to pnpm verbatim, so an npm package name, a github:owner/repo[#ref] spec, a link:path and a tarball URL are all accepted. A relative path (./x, ../x, link:../x) is resolved against the working directory the command is run from, not against the profile directory.
  • Installation registers the bundle automatically: DSH appends the package to the profile's dsh.profile.bundles because the package declares dsh.bundle.patch, so no JSON editing is needed. A package without a bundle patch is reported as installed only as a plain dependency.
  • The built host artifact lib/ is committed with the repository, so a GitHub install needs no build step and never triggers pnpm's allowBuilds prompt.
  • Releases are append-only: a fix ships as a new version (1.1.1, 1.1.2, …) and a released tag is never moved, deleted and re-pointed, so a pinned install cannot change under the user's feet.
  • The package carries its own display metadata, in the form DSH 0.1.7 reads it: package.json.icon (a manifest-relative SVG) and locale/<language>.json files whose meta block holds the title and description shown in the Plugins list. Both are exported (./locale/*.json) so the reader can resolve them; on earlier DSH versions the fields are simply ignored.

When the install fails: two cases

① ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION

When the profile contains a plugin version younger than pnpm's release-age policy, pnpm verifies the entire lockfile before any change, which rejects every plugin operation, including uninstalling an unrelated plugin. Allow it for this one command:

dsh plugin --profile web add github:SanYe-SanJiu/dsh-power-switch --config.minimum-release-age=0

The spelling must be kebab-case: the camel-case form --config.minimumReleaseAge=0 is silently ignored by pnpm >= 12.3, so the flag appears to be accepted while the error is unchanged. Running pnpm clean --lockfile as the message suggests is not recommended either: it re-resolves the whole profile and changes the other installed plugins with it.

② A slow network

A github: install fetches the entire repository, which can exceed pnpm's default 60-second fetch timeout. Add a second one-shot flag:

dsh plugin --profile web add github:SanYe-SanJiu/dsh-power-switch --config.minimum-release-age=0 --config.fetchTimeout=600000

Restart DSH once after installing, then open Settings -> Plugins: a card labelled "DSH power button" appears in the list, and the ⏻ button appears in the sidebar foot.

Verifying and uninstalling

  • Verify: the profile's dsh.profile.bundles contains this plugin, and node_modules\dsh-power-switch is an ordinary directory (a local link: install is a symbolic link or junction).
  • Uninstall: dsh plugin --profile web remove dsh-power-switch. The desktop shortcut does not depend on the package, so nothing has to be undone first — but if the mode is app window, switch back to the normal tab in the card before removing it if you want your original launch method back immediately. Otherwise the next double-click of the icon does it: the copy the shortcut points at notices the package is gone, puts the original shortcut back, and says so. restore-shortcut.vbs in the state directory does the same thing by hand.
  • Return to a local checkout: uninstall first, then run dsh plugin --profile web add link:<absolute path to this checkout>; both steps should carry the one-shot flag above.
  • Do not install the GitHub build and a local link: build into the same profile: the package name is identical, so the later install replaces the earlier one.

dsh plugin writes into $DSH_HOME/profiles/web/. If dsh runs under a sandbox, run it from an ordinary terminal.

Shutdown

The sidebar ⏻, the card's "Shut down DSH" button and the POST /api/dsh-power-switch/shutdown route behave identically. Both controls first show a confirmation dialog, and that dialog also offers Restart — see Restart for what it does:

  1. The response is sent before the exit begins, so the page can report that shutdown was requested; no response within 10 seconds is reported as a failure.
  2. The exit is graceful: sessions and settings are flushed, and the plugin tree is disposed.
  3. If that disposal stalls, a watchdog forces the process to end when its deadline expires.

The appExit service becomes available progressively while the host boots. A shutdown triggered in the first moments, before that service is registered, falls back to SIGTERM (measured log line: no appExit service; sending SIGTERM to self). Where a graceful exit must be guaranteed, wait a few seconds after startup before shutting down.

In app-window mode the page closes itself once the process is gone. In normal-tab mode a browser refuses to let a page close a tab the user opened, so the card states to press Ctrl+W. That is a browser restriction, not a gap in the plugin.

"The process is gone" is the moment the window closes, and that is what makes delayMs visible: with a 10 s wait the window stays up for about 10 s and then closes with the process it belongs to. Both power controls work this way. The sidebar button used to close the window as soon as the host accepted the request — a different moment, and one that looked broken as soon as the wait became real.

Restart

Restarting is the third option in the confirmation dialog both power controls open (Cancel / Restart DSH / Shut down), it is the last step of the mode switch, and it stands on its own when the launch about to be replaced is the only thing you want to change:

# double-click scripts\restart-dsh-web.vbs, or run the script directly
node scripts\restart-dsh-web.mjs --delay-seconds 3

The dialog option and the script use POST /api/dsh-power-switch/restart; the dialog sends no mode, so it restarts into whatever mode is already configured — it replaces the process and nothing else, and it never touches the desktop shortcut.

The manual entry stops the host on the port it is actually serving on, starts the recorded launch command again, waits for the new run's token URL, and then proves this plugin's browser half made it into the boot graph that host serves. --port N overrides the port it works on, --cli <entry> names the CLI on a machine with no recorded command, and --open / --app open a window afterwards.

Both entries share one rule: the replacement is started by a helper that must confirm it is up before the host is allowed to exit. When that confirmation does not arrive the host keeps serving and the answer says why — a refusal is always better than a service that never comes back. What "the recorded launch command" means, and the refusals that apply, are described under Launch mode; the same rules govern this entry.

Launch mode: app window / normal tab

App windowNormal tab
Opened asChromium --app= (no address bar or tab strip)a tab in the default browser
Sizefills the work area of the screen it opens onwherever the browser puts a tab
After a shutdownthe page closes itselfpress Ctrl+W

The window's size is set by the page, not by the launching command line, and that is measured rather than preferred. Once a Chromium browser already has an instance running — which it does whenever DSH is on screen — it hands the URL to that instance, which creates the window with its own bounds and never reads the switches of the command line that handed the URL over: a bare --app=<url> measured 1010x1084 on a 2048x1152 screen, and --start-maximized, --window-size=1920,1080 --window-position=0,0, --kiosk and --start-fullscreen all measured the same half-width window. What does work is the page moving and resizing its own app window (resizeTo took it from 1010x1085 to 1202x702 in the same measurement series).

That is why the launcher opens the window on the plugin's own page rather than on DSH: /api/dsh-power-switch/app-window?next=<the authenticated URL> fills the work area as its first action and then replaces itself with that URL. Opening DSH directly left the window at the browser's default size for as long as DSH's own boot took — the half-width window that then jumped. With the hand-over page the window is already filled when it appears: a watcher polling every 40 ms from before the launch saw exactly one size, 2048x1104 at 0,0, with the hand-over already done. The page accepts only a loopback http URL carrying a token, because a browser follows its redirect, and a window the person opened or installed themselves is never involved. client.js retries the resize once DSH is up if the browser refused it.

Filling the work area is as far as an automatic action goes: true fullscreen, taskbar included, needs one gesture, which the browser will not let a page supply for itself (measured: a load-time requestFullscreen() is refused, the same call after a real click reaches 2048x1152, and F11 does it from the browser). Press F11 in the app window when you want that; the plugin does not press keys for you.

One press of the card's switch does three things: it saves the setting, updates the desktop shortcut, and restarts DSH. The setting is written to the dsh-power-switch section of $DSH_HOME/settings.yaml and to the composition entry as well, so the choice survives a restart even when the host serves no settings provider.

It works on both settings models

DSH 0.1.7 replaced the plugin settings model: ctx.settings became a generated-forms service (describe / update) with no register / installSection, and settings.yaml was retired — it is imported into the active profile once and renamed. Neither change breaks this plugin:

  • the persistence path is detected, not version-sniffed: a host that still has the registered-namespace API is written through its write scope, and a host that has the newer service is written through update for this plugin's own entry, found by its live value rather than by a guessed id;
  • the choice is also recorded by the plugin itself, in launch-mode.txt, and every process that runs outside the host reads that record — the desktop launcher, the restart helper and the supervisor. So the mode survives a cold start even on a host whose settings document is gone, which is exactly the case where "I switched to the app window and the next start was a tab" would otherwise come back;
  • the one difference is the settings section: on 0.1.7 a form is generated from a Loader entry's declared schema, and this plugin declares none, so there is no DSH-native settings page for it there. Everything the plugin can configure lives on the card — the launch-mode switch and the advanced settings.

It refuses to restart when that cannot be done safely

The restart helper must first confirm the takeover to the host, and only then does the host exit; when that confirmation does not arrive, the route answers 500 and keeps serving. Three refusal reasons each have a dedicated line on the card:

  • this DSH was not started as a dsh web command line (the desktop application, for example), so there is no launch command to replay;
  • the restart helper could not be started;
  • the restart helper started but never confirmed it was up.

In any of them the shutdown feature is unaffected and the setting has already been saved.

Desktop shortcut

Switching to the app window adopts the existing DSH shortcut, or creates DSH 启动器 when none exists; switching back to a normal tab restores the original launch method. The original target, arguments, icon and description are recorded in shortcut-backup.txt in the state directory, so the change can be undone at any time.

What the shortcut points at is a copy of the launcher in the state directory, not the packaged one: shortcut-launch.vbs, written there on every boot beside that record and beside restore-shortcut.vbs. An icon that stops working the moment its plugin is uninstalled is not worth placing, so the file the icon names is one the uninstaller cannot delete. What it does with a double-click:

  1. The plugin is installed — a profile under the harness home still names it in its manifest (dsh.profile.bundles in <profile>\package.json, the list the host loads plugins from) and carries <profile>\node_modules\dsh-power-switch\scripts\launch-dsh.vbs. The copy hands that launcher the arguments it arrived with, waits, and forwards its exit code. The launch path is still launch-dsh.mjs through launch-dsh.vbs, so a normal start is unchanged; a boot re-points an icon adopted by an earlier version at this copy.
  2. The plugin is gone — no profile names it any more, so it replays the record through restore-shortcut.vbs: the shortcut is the one you had before this plugin ever touched it (or is removed, if this plugin created it), and one dialog says so. The next double-click starts DSH exactly as it used to.

The manifest is the authority rather than a recorded path or a surviving directory, and both halves of that were measured rather than assumed. Uninstalling a link: install rewrites the manifest but leaves the junction in node_modules; a launcher file that still resolves is therefore not evidence that the plugin is installed, and treating it as evidence is exactly how an icon keeps launching a plugin that is gone while the restore never happens. The recorded path that an earlier build wrote has the same failure mode, which is why a boot deletes it.

Which harness home the launch means is answered by looking, not by picking one: the environment's DSH_HOME, the --home the shortcut carries and the default ~/.dsh are each searched for a profile whose manifest names the plugin, and the home that has it wins — and is the one exported to the launcher, so the wrapper reads the state directory belonging to the launcher it was handed. A DSH_HOME that changed after the shortcut was created therefore cannot make an installed plugin look uninstalled, and an icon cannot decide to put itself back while the plugin is still there.

Nothing in this layer is a fixed path. shortcut-launch.vbs uses its own folder, the harness home above, DSH's profiles directory (the same constant the CLI resolves --profile against) and <profile>\node_modules\<package> — where pnpm puts a direct dependency, and where DSH says pnpm-managed entries stay authoritative. restore-shortcut.vbs uses its own folder, then %DSH_HOME%, then %USERPROFILE%\.dsh, and the .lnk it rewrites is the one named in the record. make-shortcut.vbs asks the shell for Desktop rather than composing it, which is why a redirected or non-English desktop works, and names %SystemRoot%\System32\wscript.exe with a bare-name fallback. Every one of those is derived at run time, so an install under any user name, drive or directory behaves the same.

Those dialogs are shown in the language of the Windows the person is using, because this is a Windows dialog: the scripts read HKCU\Control Panel\International\LocaleName, falling back to English, and DSH_POWER_SWITCH_LANG=zh|en overrides both. The translations live in shortcut-messages.txt in the state directory rather than in the scripts, which cannot carry them: wscript reads a .vbs as ANSI, so Chinese written into one arrives as mojibake — and reading the file as UTF-16 instead would make it a binary blob in the repository with no reviewable diff. English is not duplicated into the table; it is the text compiled into each call site and the fallback whenever a key or the whole file is missing.

Why this layer is necessary: dsh web hands its URL to the default browser, so it always opens a tab. It has no app-window option, and that hand-off runs a platform opener with a scrubbed environment, so no plugin can intercept it. The window shape for the next launch can therefore only be chosen by whatever launches dsh — which is this package's launcher, scripts/launch-dsh.mjs (wrapped by launch-dsh.vbs). It reads the stored mode, starts the host with the recorded launch command when no host is running, waits for the token the host prints, and opens the window in that mode.

"Recorded launch command" means the plugin writes its own launch facts (process.execPath, argv, cwd) to boot.json in the state directory while it runs, and the launcher replays them verbatim. The earlier implementation rebuilt <checkout>/apps/cli/lib/bin.js, a path that exists only in a DSH source checkout and therefore failed under every other installation.

Two facts a cold start cannot guess are derived or recorded, never assumed. The port is read back from what the host wrote about itself: the authenticated URL of its last run, then a --port in the recorded command line, then the documented default; and the wait for the host's readiness line accepts any loopback port, so a DSH that serves on a non-default port is started and opened rather than waited on for two minutes at a port it never used. The harness home is written into the shortcut as --home <dir> and handed down as DSH_HOME (a variable that is already set wins), so a shortcut double-clicked from Explorer reads the same state directory as the host even when the person's own shell is the only place DSH_HOME was ever defined.

A launch that fails says so. The wrapper waits for the launcher, and a non-zero exit opens a message box with the exit code, the last lines of the log and the log path — a shortcut is double-clicked, and a wrapper that exits silently is indistinguishable from a shortcut that was never wired up.

The launcher can also be run directly:

node scripts/launch-dsh.mjs                            # the mode from the setting
node scripts/launch-dsh.mjs --app                      # force an app window for this run
node scripts/launch-dsh.mjs --tab                      # force a normal tab for this run
node scripts/launch-dsh.mjs --cli <path to the dsh CLI entry>   # name the CLI entry

The launcher never starts a duplicate instance: when a host is already serving it only opens the window, and when the port answers but no live token can be obtained it refuses to start and reports why.

State and logs

Runtime state lives in $DSH_HOME/storages/dsh-power-switch/, never inside the package:

FileContents
restart-dsh.logDiagnostic log shared by the plugin, the restart helper and the supervisor
boot.jsonThe most recent host launch command (used by the launcher and the supervisor)
token-url.txtThe authenticated URL the host recorded for the current run (how the helper and the launcher identify the serving process)
launch-mode.txtThe mode the next launch should use, recorded by the plugin itself. It is what a cold start reads on a DSH whose settings document no longer exists (0.1.7), and an explicit settings document always wins over it
settings.jsonThe advanced settings changed from the card (delayMs, exitCode, hard); they override the loader row's config:
node-path.txtThe node.exe path the host is running on; both .vbs wrappers read it, so a Node installed through nvm/fnm/volta or an app store can still launch DSH from the shortcut
dsh-web.<stamp>.logThe stdout of each host this plugin started, including that run's token URL
shortcut-backup.txtThe original shortcut recorded before it was adopted, for restoring it
shortcut-launch.vbsThe launcher the adopted shortcut points at, refreshed at every boot. While a profile carries the plugin it hands the launch to that profile's packaged launcher; once none does it restores the original shortcut from the record. Being outside the package is the point
shortcut-messages.txtThe dialogs the two .vbs helpers show, in the languages beyond the English compiled into them, as key.language=text lines. UTF-16, because a .vbs is read as ANSI and Chinese written into one would land on screen as mojibake
restore-shortcut.vbsA copy of the repair script, made at every boot. Double-clicking it restores the shortcut from shortcut-backup.txt and then deletes that record; shortcut-launch.vbs runs it with /quiet and reports the result itself
shortcut-result.txtThe raw result of the most recent shortcut operation

State is kept outside the package for two reasons: the package may sit in a read-only store, and the log contains authenticated token URLs and local paths, so a log inside the package would be a log inside the repository.

Configuration

The card's configuration page, or the loader row's config::

KeyDefaultMeaning
launchModetabWindow for the next launch: tab or app
delayMs1000Milliseconds to wait after answering before the exit begins. The answer itself needs 1200 ms to reach the browser, so that is the floor: 0 means "as soon as the answer is out", and anything below 1200 waits 1200
exitCode0The status the DSH process ends with, for whatever started it — a shell ($LASTEXITCODE), a service manager, a script. 0 is success, so a non-zero value is for a wrapper that wants to tell "the person closed DSH" apart from a crash
hardfalseSkip the graceful path and end the process at once

launchMode is switched from the card. The other three are edited from the card's own Advanced settings block, which writes them to settings.json in the state directory. That block exists because it is the only UI for those three on a DSH that generates settings forms (0.1.7) — a plugin without a declared schema gets no generated form, so without it they would be editable only by hand-editing the profile patch. On a host that still has the registered-namespace settings API, the same save is written to the settings document as well, so the two surfaces cannot disagree; the recorded value is the one that governs, and deleting settings.json hands the decision back to the loader row's config: and the settings document.

The card sends no delay of its own: delayMs governs every exit, the card's buttons included. (It used to send 700 ms "so the interface reacts sooner" — a number that could not matter, being below the 1200 ms the answer needs, and that silently outranked the setting.) A caller that wants a one-off delay can still put delayMs in the shutdown request body, which outranks the setting for that request only. With hard, the process ends after that same wait instead of after the watchdog.

Environment variables:

VariableMeaning
DSH_POWER_SWITCH_NO_WINDOW=1Do not open a window for this launch
DSH_POWER_SWITCH_WINDOW_HANDLED=1Another process is opening this launch's window; the launcher and the supervisor set it, and the plugin then opens no second window
DSH_POWER_SWITCH_CLIThe dsh CLI entry, for a machine with no recorded launch command
DSH_POWER_SWITCH_PORTPort to probe (default 3080)
DSH_POWER_SWITCH_LAUNCH_MODE, _DELAY, _HOST_LOG, _HANDSHAKEInternal interface between the helper and the supervisor; no configuration needed

Security boundary

All six routes (GET /config, GET /app-window, POST /shutdown, POST /restart, POST /shortcut, POST /settings) share one floor: loopback requests only. The peer must be 127.0.0.1/::1, and any forwarding header (forwarded, x-forwarded-for, x-real-ip, x-forwarded-host) is refused.

The write routes (shutdown/restart/shortcut/settings) additionally require Origin to match Host exactly. The read routes (GET /config, GET /app-window) accept a missing Origin, because some hosts issue their own requests without it, while still requiring loopback and still refusing forwarding headers; that difference is documented in the code.

POST /shortcut is the only route that produces a file outside the package, so its body accepts exactly one fixed action (scan / install / restore); the directory, file name, target and arguments are all derived by the host from its own installation location, and the client cannot choose any of them.

The plugin reads no credentials, makes no network requests and never touches session content.

Two boundaries are worth stating plainly rather than leaving to be discovered:

  • boot.json is a trust boundary. The restart replays the command line the running host recorded for itself, verbatim — that is the whole design, and it is why no installation path is ever reconstructed. Anyone who can write the state directory can therefore have that command executed with your privileges. That is not a privilege escalation (the same is true of settings.yaml, the profile's plugin list, and every other file the harness reads), and it is exactly why the state directory lives inside your user profile and never inside the package. Treat write access to $DSH_HOME as equivalent to running code as yourself. Two more files sit on the same boundary. shortcut-launch.vbs and restore-shortcut.vbs are refreshed from the package at every boot and are meant to be run — the desktop shortcut itself starts the first one — so a state-directory writer could put something else under those names; the launch copy only ever starts the launcher a profile resolves to, and the repair copy only ever edits a .lnk from the recorded fields, but treat both like any other executable in your profile.
  • The authenticated ?token=… URL is a local access credential. It is written in exactly two places, both under $DSH_HOME/storages/dsh-power-switch/: the host's own stdout log (dsh-web.<stamp>.log), which is how a replacement host is identified, and token-url.txt. Every diagnostic line that would repeat it is redacted to ?token=*** — including the wrapper log in %TEMP%, which a desktop shortcut writes. Do not paste those two files anywhere public.

Development

npm run build                            # copy src/ to lib/ (the host-half artifact)
node scripts/run-tests.mjs               # every suite in tests/ (node:test), including the one that runs the repair script on the real Windows Script Host
node scripts/verify-client-artifact.mjs  # fetch client.js over real HTTP and render both views
node scripts/verify-live.mjs             # health-check a running DSH

client.js is a hand-written lazy-CJS factory artifact (window.__ModuleLoader__.load) and needs no build step. tests/ is not published with the npm package, so npm test runs from a checkout. The suites are tests/host.test.mjs (request fence, argument parsing, scheduler, routes, relaunch command), tests/host-wiring.test.mjs (plugin assembly and ctx.appExit), tests/client.test.mjs (card registration and interaction) and tests/package.test.mjs (artifact layout, src/lib agreement, compiling the generated supervisor source).

License

MIT