DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

shiinakaze /

shiinakaze/dsh-plugin-backup

Verified

This plugin has no description yet.

★ 0 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHub
READMESource: main@bf62da4e

dsh-plugin-backup

English | 中文

A backup plugin for DeepSeek Harness: it packages $DSH_HOME into verifiable, restorable, downloadable archives by category, and can mirror those backups to GitHub. Everything is available from three faces that share one engine and one single-run lock: the Web settings page, the model tool surface, and a CLI session.

  • Naming: dsh-backup-<YYYYMMDD>-<HHmmss>
  • Categories: settings, skills, plugins, sessions, storages
  • Per backup: download / verify / restore / delete
  • Settings: backup path, backup interval, per-category switches, retention count, verify-after-create, per-category archive ceiling, GitHub sync
  • No build step and no third-party runtime dependency: the host half is plain ESM, the browser half is a hand-writable CommonJS factory the shell loads as-is

Install

# local checkout (development)
dsh plugin --profile web add file:C:/path/to/dsh-plugin-backup

# or from GitHub
dsh plugin --profile web add github:shiinakaze/dsh-plugin-backup

The Desktop app manages its own desktop profile: install through its plugin manager (or dsh plugin --profile desktop add …) and restart the app.

dsh plugin add both records the dependency and appends the package name to dsh.profile.bundles (one composition layer). Reload or restart the Web interface afterwards, then open Settings → Data backup.

Runtime support. The plugin targets the @deepseek-ai/dsh 0.1.7 and 0.2.0 lines: peerDependencies declare ^0.1.7-rc.2 || ^0.2.0-rc.1 for dsh-tools and dsh-typert-protocol, and engines.dsh mirrors that window. This matters on 0.2.0, whose boot performs a compatibility preflight against the running runtime and refuses to install or mount a bundle whose dsh peers it cannot satisfy — an exact pin to one rc would be rejected. The two branches are not cosmetic: package managers resolve with npm's prerelease rule, under which a prerelease is admitted only when some comparator pins its exact version tuple, so one >=0.1.7-rc.2 <0.3.0 window silently drops 0.2.0-rc.1 and 0.2.0-rc.2 and fails npm install with ERESOLVE. Keep the window in package.json in step with the APIs the plugin actually uses.

A file: install is a snapshot: the profile keeps its own copy, so after changing this package's source you must dsh plugin --profile web remove <name> and add it again. The measured verification log lives in docs/verify.md.

This is a DSH plugin, not a plain dependency: the host half injects the tools service declared in its schema, and the browser half is discovered through the dsh.client declaration in package.json. Without $DSH_HOME the plugin falls back to ~/.dsh; in a non-Web profile (no webServer service) only download is unavailable — every other feature and tool still works.

The page

Settings → Data backup:

Block Contents
Backup settings Backup path (empty = C:\Users\<user>\dsh_backups on Windows, ~/dsh_backups elsewhere; ~ and ${DSH_HOME} are expanded), interval (off / 15 min / hourly / 6 h / daily / weekly), backups to keep, verify right after creating
Backup categories The selection for a run: check the categories to include, with select-all / select-none; the button names the current choice ("Back up now (3 categories)") and is disabled with a hint while nothing is selected
Existing backups Per row: id, creation time, file count, size, category tags, last verdict; actions are a download menu (one category or the manifest), verify, preview restore, restore, and delete (restore and delete take a second click to confirm; delete is the destructive-colored one)
GitHub sync Enable switch, owner/repo, branch, subdirectory, token environment variable, whether a token was detected, and push / pull

"Back up now" covers only the checked categories; saving the settings also makes that selection the default for scheduled runs.

Settings are written to the plugin's own user layer at $DSH_HOME/dsh-plugin-backup/settings.json (0600); the deployment baseline stays in the profile's cordis.patch.yml and the two are shallow-merged (user wins).

Layout on disk

<backupPath>/
  dsh-backup-20260930-141205/
    manifest.json           # schema version, host facts, per-category files/bytes/sha256/archive, last verdict
    settings.tar.gz         # one gzipped USTAR archive per category
    skills.tar.gz
    plugins.tar.gz

The manifest is written last and atomically, so "a directory with a manifest" is a complete backup. Intermediate files carry a .part suffix while a run is in flight; residue from a crash is cleaned up or ignored at the start of the next run.

Categories and exclusions

key Sources (relative to $DSH_HOME) Default
settings settings.yaml*, cordis.patch.yml*, .credentials.yaml*, .anonymous-user-id, dsh-skill-hub.json, skills-market-sync.json on
skills skills-management/** on
plugins profiles/*/package.json, pnpm-lock.yaml, pnpm-workspace.yaml, cordis.patch.yml*, plus plugins/** on
sessions sessions/** off
storages storages/** off

Always excluded: node_modules and .git* at any depth, symbolic links, and the backup root itself — so keeping the store inside $DSH_HOME never recurses.

Measured on a real harness home (settings + plugins + skills)

Category Files Raw Archive Duration
settings 5 2.0 KB 1.3 KB < 0.1 s
plugins 39 1.24 MB 1.12 MB 0.2 s
skills 29 356 342.5 MB 144.8 MB ≈ 50 s

Creation and verification are streaming (flat memory; measured RSS went 57 MB → 158 MB), so a category is never held in memory. That is why the default ceiling is 512 MiB: a realistic skills tree already passes 100 MiB compressed. Raise maxArchiveBytes in the settings if yours is larger, or leave skills out and back it up by hand. With retention 20 and skills enabled the store can reach ~3 GB.

GitHub sync

  1. Enable sync on the settings page and fill in owner/repo (a full URL also works), the branch, and the subdirectory.
  2. Token resolution, in order:
    • an environment variable — DSH_BACKUP_GITHUB_TOKEN by default (renameable in settings);
    • the state file $DSH_HOME/dsh-plugin-backup/github-token.json holding {"token": "…"}, mode 0600. The token is read from nowhere else and never written to config, logs, or tool output.
  3. Push uploads file by file through the Contents API, archives first and manifest.json last; a backup the remote already holds with the same content is skipped. Pull downloads only the backups missing locally and verifies every archive against the sha256 in its manifest — a mismatch discards that backup and reports why.

Remote layout: <subdir>/<backupId>/{manifest.json,<category>.tar.gz}.

Model tools

Tool Parameters Purpose
backup_run categories?, label? Create a backup now
backup_list limit? List backups, newest first
backup_verify id Recompute sha256 and re-read the tar structure, per category
backup_restore id, categories?, dryRun?, force? Restore; dryRun only reports the plan
backup_delete id, force? Delete; the newest backup is protected while an older one exists
backup_sync direction, ids? Push to or pull from GitHub

A typical loop: backup_run before a risky change, then backup_verify and backup_restore afterwards. backup_restore verifies first and refuses a corrupt backup unless force is set.

Safety boundaries

Situation Behaviour
Path traversal Backup ids must match ^dsh-backup-\d{8}-\d{6}$; archive entries refuse absolute paths, drive letters, and ..
Corruption verify names the category and the expected/actual sha256; restore refuses by default and, under force, skips that category and reports it
Irreversible overwrite Before a restore, every file about to be overwritten is kept beside itself as <name>.pre-restore
Concurrency One single-run lock (<backupPath>/.lock, stale after 30 minutes); a conflict returns busy instead of writing concurrently
Mis-deletion While an older backup exists, deleting the newest needs force; deleting the last one does not
Runaway size A category archive past maxArchiveBytes aborts the run and removes the partial archive
Token leakage The token lives only in the environment or the 0600 file; output only ever shows repository paths
Download links Single-use download tickets (24 random bytes, 120 s expiry, consumed on first use) served by GET /dsh-plugin-backup/download?token=…

The download route itself is not session-authenticated (the ticket is the credential), so keep the Web server bound to loopback and do not expose it to an untrusted network.

Development

Building or porting another plugin? See the DSH plugin development guide: docs/plugin-development-guide.md — Desktop 0.2.0 runtime anatomy, the peer-compatibility preflight, a six-step adaptation/verification flow with tested commands, the two-face host/Typert/browser contract cheatsheet, the 0.1.7→0.2.0 API diff table, and a cross-version gotcha list.

npm test          # 50 cases: tar / manifest / engine / plugin mount / browser half / package contract
node --check index.js
node --check client.js

The suite needs no DSH runtime running, yet it still checks against the real official packages:

  • @deepseek-ai/dsh-typert-loader's validateTypertManifest validates the host descriptors;
  • the real TypertRegistry registers all nine endpoints;
  • the real dsh-tools defineTool compiles the six tool schemas;
  • a real @deepseek-ai/cordis root mounts the plugin and drives the download route over real archives;
  • the browser half is rendered through a module-loader shim plus a minimal hooks runtime, with the client descriptor table compared field by field against the host contribution.
node test/local/measure-real-home.local.mjs settings plugins skills
node test/local/remount.local.mjs

The first script backs up the real $DSH_HOME, verifies it, dry-runs a restore, and cleans up, which is how the size and duration table above was measured. The second reproduces the repeated mount a profile recompose performs, to show the service, tools, and download route neither leak nor break.

Structure

index.js                 host plugin: Config schema, apply, download route
typert.js                host Typert descriptors + BackupService (remote method bodies)
lib/tools.js             the six backup_* tools
client.js                browser half: settings page + remote mount
lib/core/{tar,manifest,plan,store,settings,github,engine,util}.js
test/*.test.mjs          offline suite; test/local/ holds the measurement script

Troubleshooting

Symptom Action
No "Data backup" section Check the package is in dsh.profile.bundles, reload the page, and look for a FAILED fiber in the startup output
The page says the backup path is unavailable The path is not writable or a file occupies it; pick a usable directory (the default is C:\Users\<user>\dsh_backups) and save
busy Another run holds the lock (the schedule or another session); wait, or delete <backupPath>/.lock
too-large Raise maxArchiveBytes, or exclude the skills category
Restore refused as corrupt Run backup_verify to see the failing category; use a newer backup if the archive really is damaged
github-auth No token, or it lacks contents: write on the target repository
Download does nothing The ticket expires after 120 s; click download again
method "backup/remove" conflicts with its namespace service A 0.1.1-and-earlier build issue: the client carrier's namespace service owns remove. From 0.1.2 the delete endpoint is named deleteBackup; avoid that reserved set when extending this plugin
cannot get property "…" without inject A 0.1.2-and-earlier build issue: a Cordis service was read as a property without injecting it. From 0.1.3 the plugin uses ctx.get(...) and ctx.inject(...) child fibers; restart dsh web after upgrading
"Save settings" blanks the whole settings page (until the page is reloaded) A 0.1.3-and-earlier build issue: the save consumed the host's Preferences answer (no rows/total/state) as a full page snapshot, so the render threw — and the slot's error boundary retires this section for good. From 0.1.4 the save re-reads the snapshot and the section carries its own payload normalization and error boundary; reload the page after upgrading
Desktop refuses the install: "Plugin dsh-plugin-backup@… is incompatible with dsh 0.2.0-rc.2: peerDependencies …" A 0.1.4-and-earlier packaging issue: the dsh peers were pinned to 0.1.7-rc.2, and 0.2.0's boot preflight rejects a bundle whose @deepseek-ai/dsh-* peers it cannot satisfy. Remove and re-add the plugin, or grant a one-off exact-version exemption with dsh plugin allow-version as a stopgap
npm install fails with ERESOLVE … peerOptional @deepseek-ai/dsh-tools@">=0.1.7-rc.2 <0.3.0" on a 0.2.0-rc host A 0.2.0-only packaging issue: that single window reads as covering both lines, but npm admits a prerelease only when a comparator pins its exact version tuple, so 0.2.0-rc.1/0.2.0-rc.2 were excluded and the peer conflict was raised against the host's own copy. From 1.0.0 the peers read ^0.1.7-rc.2 || ^0.2.0-rc.1, which passes both npm's rule and the runtime preflight; upgrade, or fall back to pnpm/dsh plugin add, which tolerated the old range

Design notes

  • Why backups default to the user profile: C:\Users\<user>\dsh_backups on Windows (~/dsh_backups elsewhere) decouples them from the harness home, so resetting or moving $DSH_HOME cannot take the backups with it, and the folder is easy to open by hand.
  • Why settings live in the plugin's own settings.json: the backup path and interval are runtime preferences; rewriting the profile's YAML patch is riskier. A user layer over the deployment baseline is easier to diagnose.
  • Why one archive per category: a single category can be restored, downloaded, or pushed on its own, and sessions ships off by default because it is the largest.
  • Why USTAR and the Typert descriptors are hand-written: the plugin pulls in no third-party runtime dependency and needs no build after install; the descriptors match the official generated shape and are validated by the official loader and registry.
  • Why the wire method is deleteBackup, not remove: the client carrier's namespace service already owns remove, has, install, installDirect, installScoped, assertMethodAvailable, plus ctx/name/namespace/methods/empty/invokeRemote, and a remote method with one of those names is refused at mount time (method "backup/remove" conflicts with its namespace service). An assertion in test/plugin.test.mjs pins that reserved set.
  • Services are read with ctx.get(...), never as properties: a Cordis ctx is a proxy, so a property read such as ctx.webServer, ctx.tools, or ctx.remote.backup is only legal inside a fiber that injected that name; otherwise it throws cannot get property "…" without inject after activation. Therefore:
    • optional services are never declared in inject. They are awaited through a ctx.inject([...], cb) child fiber instead. webServer is the telling case: in a real boot it becomes available about 0.8 s after this plugin activates, so reading it directly in apply silently lost the download route (and with it the whole page state). The tool registry attaches the same way, and only when it is actually there.
    • every other read goes through ctx.get(name), the without-inject accessor — the tool registry and the mounted remote.backup, for instance. Both test/plugin.test.mjs and test/client.test.mjs activate against a real Cordis context, so either mistake fails the suite instead of the running app.

License

MIT

—/ 5

No ratings yet

Verified DSH bundle

Commit bf62da4ef258

Community comments

No comments yet. Be the first to write one.

DSH HUB

A community index for DSH plugins. Not an official GitHub or DeepSeek AI product.

CommunityResourcesAPIAbout