dsh-desktop-hos
Discovered★ 4Electron 37 on HarmonyOS / OpenHarmony — run DeepSeek Harness (DSH) as a native 2in1 / tablet app. The AI Agent Workstation on HarmonyOS
中文 | English
DSH Desktop · Electron on HarmonyOS Edition
Electron on HarmonyOS Edition — the official DeepSeek Harness (DSH) Web UI, unmodified, running on HarmonyOS 2in1 / tablet through Electron 37: a real Electron app on HarmonyOS, not an ArkUI rewrite of the interface. The other HarmonyOS edition, HarmonyOS Edition, drops Electron entirely — system ArkWeb plus an in-process Node runtime.
Device capture, not a mockup — HarmonyOS 6.1.0.135 (API 24), 2in1. The UI is the official dsh Web UI, loaded same-origin from the in-process dsh Host.
Status 0.2.1 (versionCode 1009) — built and signed. Core flows (chat, agent loop, tool calls, session persistence, plugin market, window state) were verified on a HarmonyOS 6.1.0.135 (API 24) device for 0.2.0; the 0.2.1 build has not been re-verified on device yet (build + signature verified only). Distribution reality up front: a debug-signed HAP is bound to the signing account/device, and a release-signed package can only ship through AppGallery — see How to get it.
Why this port is different
There are other ways to get DSH onto HarmonyOS. The trade-off is the whole story of this repo:
| This project (Electron route) | A native-host route (Node runtime + ArkUI host) | |
|---|---|---|
| Interface | The official Web UI, byte-for-byte — nothing re-implemented | A native host around the same Web content; platform chrome is rewritten |
| Fidelity to desktop | Identical semantics: same renderer, same Node APIs, same data plane | Good, but the host layer is a re-implementation that must track upstream |
| Runtime permission | Requires the ACL restricted permission ohos.permission.kernel.ALLOW_WRITABLE_CODE_MEMORY (JIT) — 2in1 / tablet only | No JIT, so no such permission |
| Device reach | HarmonyOS 2in1 / tablet | Can extend to phones |
Neither is "better"; they optimize different things. This repo exists to prove the Electron route end-to-end — including every sandbox conflict it hits. The other editions share the same shell architecture and data plane: dsh-desktop for Windows / Linux / macOS, and dsh-desktop-ark for the HarmonyOS Edition (system ArkWeb + an in-process Node runtime, no Electron).
What works today
| Capability | State |
|---|---|
| dsh Web UI in a window (100% reuse of the dsh frontend) | ✅ |
Session persistence + full-text search (better-sqlite3, aarch64 ABI 138) | ✅ |
Plugin marketplace (dshmarket built in) | ✅ |
| Window state persistence (maximized / bounds) + F11 fullscreen | ✅ |
Command execution — non-PTY resident /system/bin/sh driven by a sentinel-line protocol, with a tool-layer path fence | ✅ ⚠️ the fence is best-effort and bypassable, not a sandbox |
Image attachments — sharp replaced by a pure-JS container parser; conversions it cannot do are routed through the platform image framework by an ArkTS bridge (decode / EXIF / scale / re-encode) | ⚠️ clean 8-bit sRGB files pass through byte-identically; with no bridge reachable every conversion is refused with an explicit error; no thumbnails |
| Bundling our own Node / pnpm / Python runtime | ⛔ not possible — shipping an ELF requires a binary certificate (AGC certType: 4), which Huawei does not grant to individual developers. The host installing pnpm from the system terminal is an optional dependency, never a product capability |
Process sandbox (koffi / landlock) | ❌ out of scope |
Limits, in detail
- Command execution. Upstream's
tool-bashrow stays disabled:node-ptyhas no aarch64 artifact, and PTY is denied by SELinux. The replacement spawns a resident/system/bin/shand reads replies through a sentinel line; the path fence lives in the tool layer and is bypassable by design. - Bundled runtime. The app already sees
/data/service/hnp/binonPATHand a structurally validnodelives there — onlypnpmis missing. Once the user installspnpmfrom the system terminal (the symlink ban is app-domain-only, so it must not be installed from inside the app), the marketplace'sprobePnpm()succeeds and plugin install/uninstall works. Nothing else depends on a bundled runtime: command execution spawns the system's ownsh/toybox, and the file / search / skill / subagent / workflow / image features are pure JS. Analysis:docs/鸿蒙环境能力清单-v0.1.5.md§C.8. - Images. The stub parses PNG/JPEG/GIF/WebP container headers; anything it cannot convert is handed to the ArkTS bridge (decode, EXIF orientation, scale, re-encode to JPEG/WebP/PNG). Clean 8-bit sRGB input inside the limits passes through byte-identically at zero conversion cost. With no bridge reachable (plain Node), every conversion is refused with an explicit, attributed error. No thumbnails.
Deferred by decision, not by oversight: the frameless window (moving window controls into the renderer costs more than it buys) and launch-at-login (autoStartupManager exposes read-only queries only). The system tray ships today — status-bar icon plus right-click menu.
Screens
| Light | Dark | |
|---|---|---|
| Agent conversation — tool calls, session trace, model and cache telemetry | ![]() | ![]() |
| Settings — models, plugins, agent presets, plugin market, appearance | ![]() | ![]() |
How to get it
Three routes, in order of convenience:
- AppGallery (planned). Release-signed App Packs can only be installed from AppGallery. Two prerequisites gate the listing: the ACL restricted permission (≈3 business days review, mainland-China developer accounts only) and the AI compliance track (algorithm filing / model registration / security assessment). See AppGallery submission.
- Build it yourself — the supported route today. Four stages, then a local HAP:
collect-runtime → build-dsh → collect-dsh → tar, thenscripts/build-debug.ps1. See Development. Requirements: DevEco Studio 4.0+, HarmonyOS SDK API 17+ (targetSdk 6.1.1(24)), Node 18+, pnpm@11, HDC. - Re-sign a debug build with your own account. A debug profile is device-bound, so someone else's debug HAP will not install on your machine; regenerate the profile in DevEco (
File → Project Structure → Signing Configs) and rebuild. See Signing & restricted permissions.
⚠️
hdc app installon a release-signed package fails withcode:9568322 ... not trusted app source. For on-device regression use-BuildMode release -SignMode debug.
Compatibility
| Shell | dsh (upstream tag) | Runtime | Device / API | Verified |
|---|---|---|---|---|
0.2.1 · versionCode 1009 | dsh-v0.2.1-rc.2 | Electron 37 / Node 22.17.0 (harmonypc-electron) | HarmonyOS 6.1.0.135 · API 24 · 2in1 / tablet | built & signed; on-device re-verification pending (0.2.0 was verified) |
versionCode must be a number and must strictly increase on every AppGallery submission. Upstream dsh is a developer preview: each shell pins one tag, and patches/<dsh-tag>/ moves with it.
Architecture
┌─ HarmonyOS HAP ─────────────────────────────────────────────────┐
│ electron module (entry): EntryAbility boots Electron-on-HarmonyOS│
│ web_engine module (HAR): ArkTS bridge layer + resfile carries dsh│
│ ┌─ Electron main process (Node.js, also hosts dsh Host)─┐│
│ │ main.js: extract dsh-dist.tar.gz → runProfile('desktop')│
│ │ ├─ webserver ← 0.0.0.0:<free port>, serves dist+/api│
│ │ ├─ apiProxy ← RPC gateway ││
│ │ └─ connection ← /api + WebSocket registration ││
│ │ once ready: loadURL(http://<LAN IP>:<port>/) ││
│ └────────────────────▲──────────────────────────────────┘│
│ │ same-origin + Host header rewrite │
│ ┌────────────────────┴──────────────────────────────────┐│
│ │ Renderer: the standard dsh Web UI (fetch /api + WS) ││
│ └───────────────────────────────────────────────────────┘│
└───────────────────────────────────────────────────────────────────┘
Three HarmonyOS adaptations make that diagram work; each one is a patch with a root cause behind it:
- Loopback network isolation. HarmonyOS NEXT blocks renderer access to
127.0.0.1, so the webserver binds0.0.0.0and the renderer connects over the LAN IP — while the embedded renderer rewritesHost/Originback to127.0.0.1:<port>before requests leave the session, so dsh's loopback-only privileged-method fence still passes. Security note: because the listener binds a wildcard address,/apiaccepts the bind literal as a validHost, so a non-browser LAN peer that sendsHost: 0.0.0.0:<port>without anOriginis not turned away by the Host fence. The loopback fence therefore does not isolate this shell from the device's Wi-Fi; treat it as a single-user local app and keep it off untrusted networks. - No symlinks, no hardlinks. The sandbox rejects both (
EACCES/EPERM), so profile publishing falls back tocpSync, and atomic writes publish by same-directoryrenamewith a guarded create. os.homedir()points outside the sandbox.HOMEis redirected to the sandbox-writableuserDatadirectory before startup.
dsh's webserver already serves the SPA dist and /api on one port, so the data plane needs no CORS, no auth, no custom protocol, no IPC carrier — and the upstream tree stays unmodified apart from the patch set.
Tech stack
- Electron-on-HarmonyOS (harmonypc-electron, Electron 37 / Node 22.17.0) — native SO + ArkTS bridge layer (aki / adapter / addon + libshim.a)
- ArkTS / ArkUI (Stage model,
web_engineHAR bridge: ~46 Adapters + ~44 AdapterBinds) - deepseek-harness (
dsh, sibling directory../deepseek-harness, not a submodule, source reference) — current build is based ondsh-v0.2.1-rc.2; its patches live inpatches/dsh-v0.2.1-rc.2/ - dsh-market (sibling directory
../dsh-market, npm packagedshmarket, built-in plugin marketplace) - hvigor / DevEco Studio (HAP build + signing)
Development
Integration approach
- Runtime copy:
harmonypc-electronis a sibling HarmonyOS project (not an npm package);collect-runtime.mjsphysically copies itselectron+web_enginemodules + 3 SOs into this project at build time (sibling layout, artifact embedding), and injectslibc++_shared.so. - Source reference: dsh and dsh-market are sibling-directory source references (not submodules), consumed via patch + build + artifact collection.
- Host integration:
src-main/main.jsdynamically imports dsh'srunProfile(apps/cli build artifact), hosting the dsh Host in-process (webserver bound to0.0.0.0), returning a{ ctx, shutdown, port, url }handle. - Same-origin data plane: the renderer does
loadURL(http://<LAN IP>:<port>/)to load the dsh Web UI same-origin, reusingWebApiClient— zero CORS, zero auth, zero new carrier. - desktop profile:
profiles/desktop/(dsh.profile.bundles = [dsh-base, dsh-web-app, dshmarket], cordis.patch.yml overridingweb-runtime.printUrl: false,webserver.host: 0.0.0.0), copied to$DSH_HOME/profiles/desktopat runtime.
Build process (four stages + 13 patches)
dsh depends on Node internal APIs (HMR, native directory dialog) and conflicts with the HarmonyOS sandbox (symlink, loopback isolation), so 13 patches must be applied first (idempotent — --reverse --check detects already-applied and skips):
The pipeline is four stages (⓪–③), followed by the local build + signing step (④). Stage ⓪ is the runtime sync: it force-copies the upstream runtime and re-applies this app's own customizations (prune + overlay), so it must always be the first thing that runs.
# ⓪ sync runtime + re-apply app customizations: copy ../harmonypc-electron's electron + web_engine
# modules + 3 SOs + libc++_shared.so, prune unwanted upstream files, then overlay runtime-overlays/
node scripts/collect-runtime.mjs
# ① build dsh: clean workspace residue → apply 13 patches → pnpm build host/client/web → build ../dsh-market
node scripts/build-dsh.mjs
# ② collect dsh artifacts: pnpm deploy materialize → fill packages → sharp stub → better-sqlite3 injection → web dist + profile + dshmarket + plugins
node scripts/collect-dsh.mjs
# ③ compress dsh-dist into dsh-dist.tar.gz (--format=ustar, ~143MB, streaming decompression at runtime)
tar -czf web_engine/src/main/resources/resfile/resources/app/dsh-dist.tar.gz --format=ustar -C . dsh-dist
# ④ build + sign (dedicated entry points; see "Signing" and "Run")
# powershell -ExecutionPolicy Bypass -File scripts\build-debug.ps1 # debug (default: -Task Hap)
# powershell -ExecutionPolicy Bypass -File scripts\build-release.ps1 # release (default: -Task App)
Runtime overlay + prune (app customizations)
Stage ⓪ (scripts/collect-runtime.mjs) does a whole-directory cpSync(..., { recursive: true, force: true }) of the upstream runtime (../harmonypc-electron/ohos_hap → electron/ + web_engine/) over this project, so any app customization that is not re-applied afterwards is silently reverted. Two mechanisms preserve this project's own changes:
- Prune (stage 1b) — deletes the 4 upstream Bluetooth files this app does not use:
BluetoothAdapter.ets,BluetoothLowEnergyAdapter.etsand their two*Bind.etsjsbindings. Each file's existence is asserted first: a missing file is a hard failure, signalling upstream structural drift that must be re-evaluated (never silently skipped). - Overlay (stage 3) — re-copies each file listed in
OVERLAY_FILESfromruntime-overlays/<same relative path>over the project. Currently 8 entries: bothmodule.json5files (electron+web_engine),electron/src/main/resources/base/profile/shortcuts_config.json,web_engine'sMediaAdapter.etsandJsBindingMethod.ets, plus the 3 localestring.jsonfiles (base/en_US/zh_CN). OVERLAY_PRE_REWRITE_FILES(1 entry:web_engine/src/main/ets/adapter/PermissionManagerAdapter.ets) is overlaid in stage 3 but is also rewritten by stage 4 (bundleName literal replacement). Its overlay copy therefore intentionally keeps the generic literalcom.huawei.ohos_electron, which stage 4 rewrites to the app bundle name. Because source and destination legitimately differ after stage 4, it is excluded from the stage-7.6 md5 check and guarded by stage 7.4 plus the new guard 7.9 instead.- Guards — 7.8 asserts every
PRUNE_FILESentry is absent; 7.9 asserts that for eachOVERLAY_PRE_REWRITE_FILESentry the overlay source still contains the generic literal, and that the destination contains the app bundle name and no longer the generic literal.
To protect a new app customization: place the modified file under runtime-overlays/<same relative path>, add its relative path to the correct list (OVERLAY_FILES / OVERLAY_PRE_REWRITE_FILES / PRUNE_FILES) in collect-runtime.mjs, then run node scripts/collect-runtime.mjs to prove it. collect-runtime.mjs requires DEVECO_SDK_HOME; --verify-only runs the guards only.
Plugins: two directories, two roles
This wrapper's plugins are split by who can consume them:
| Directory | Role | Naming |
|---|---|---|
../dsh-plugins/ (parent workspace) | Generic, pluggable plugins — no dependency on any wrapper-specific patch, so any shell can consume them | dsh-plugin-XXX |
plugins/ (this project) | This project's own plugins — they depend on this wrapper's patch set / profile / runtime adaptations; baked into the HAP at build time and all loaded by default at runtime | harmony-plugin-XXX |
Either way the directory name equals the npm package name (a bare / unscoped name). The test for which side a plugin belongs on: "would it still work if installed into another dsh shell (e.g. dsh-desktop)?" Yes → generic, parent dsh-plugins/; no → this project's plugins/. See plugins/README.md and ../dsh-plugins/README.md.
- Materialization is automatic (stage ②).
scripts/collect-dsh.mjs→collectPlugins()globsplugins/harmony-plugin-*/, reads each plugin'spackage.jsonname, and copies the directory todsh-dist/node_modules/<name>/— the same non-scoped layoutdshmarketuses. The destination name comes frompackage.json, so a new plugin needs no build-script change. - Failure is loud. If
plugins/exists but a matched plugin has no readablepackage.json, or its name does not matchharmony-plugin-*, the collect stage exits non-zero. Ifplugins/is absent or holds noharmony-plugin-*directory, the stage is a no-op with one log line. - No keep-list entry. The plugins land inside
dsh-dist.tar.gz, andcollectPlugins()is the last materialization step (after the.pnpmremoval and the prebuild prune).APP_KEEP(incollect-runtime.mjs) only guardsresfile/resources/app— which is why the looseskills/directory needs it, whiledshmarket, shipped in the same node_modules layout as plugins, does not. - Mounting. Agent presets are composed per session, so the host
cordis.patch.ymlcannot reach them: a plugin is mounted by a row inHARMONY_ENSURED_PRESET_ROWS.src-main/main.jsapplies those rows at runtime andcollect-dsh.mjsbakes the same rows into the artifact — the two lists are kept identical.requireRowconfines a row to presets that already mount it (minimalstays untouched). - Runtime mirroring. A bare package name in a preset row is resolved by
dsh-agent-presets' discovery walkingnode_modulesupward from the profile directory ($DSH_HOME/profiles/desktop), so living indsh-dist/node_modulesis not enough:ensureDshPluginsProfileLink()(insrc-main/main.js) copies everyharmony-plugin-*into$DSH_HOME/profiles/node_modules/on each start (copy, not symlink — the HarmonyOS sandbox rejects symlinks withEACCES), and prunes same-family stale directories left behind by a rename.
To add a plugin: create plugins/harmony-plugin-XXX/package.json (name harmony-plugin-XXX) plus its lib/, add one row to HARMONY_ENSURED_PRESET_ROWS in both src-main/main.js and scripts/collect-dsh.mjs, then re-run stage ②.
⚠️ A preset row's
nameis a runtime import specifier, not a comment: miss one of the two lists and nothing fails at compile time — session creation simply breaks (agent-preset/invalid,row "<id>" names a plugin that cannot be resolved).
Each plugin owns its own documentation. See
plugins/harmony-plugin-fs-mutate/README.mdfor its configuration and its Known Limitations — most notably thatmovecopies text files only, becausectx.fs.writeTextrejects binary content.
Skills: two directories, two roles
Tool skills follow the same two-tier split, with one deliberate difference: skill names carry no prefix. A skill's name is a model-visible identifier and the string a human types after /name, and dsh's own skills are unprefixed too — so ownership is expressed by the directory, not the name.
| Directory | Role |
|---|---|
../skills/ (parent workspace) | Generic — applies to any dsh shell |
skills/ (this project) | This project's own — describes this wrapper's runtime (HarmonyOS HAP sandbox, hmdfs, capability gaps); shipped inside the HAP and provided at runtime as the dsh bundled skill root |
- The wiring is already complete — no build-script change is needed. Stage ⓪
collect-runtime.mjsrestoresskills/intoresfile/resources/app/skills(restoreTree),APP_KEEPkeeps that directory across re-runs, andsrc-main/main.jssetsDSH_BUNDLED_SKILL_DIRto it at startup. The directory deliberately sits outsidedsh-dist.tar.gz, so updating a skill needs only a new HAP — no ② collect-dsh, no re-tar, and no clearing the device's extracteddsh-dist(it lives underuserDataas a sibling of$DSH_HOME, not inside.dsh). - Bundled is the lowest-precedence skill root (rank 600). A same-named skill in the project (
.dsh/skills, rank 100) or the user's$DSH_HOME/skills(rank 400) overrides the bundled one. That follows directly from the rank ordering, and it is the behaviour that lets a user replace built-in behaviour — do not "fix" it, since that would remove the override capability. - Loading is lazy, not startup. At process start only
DSH_BUNDLED_SKILL_DIRis configured. The model automatically sees just the skill catalog (name + a description truncated to 500 chars), injected as a durable user-role message before a session's first request; a skill body loads on demand via theskilltool or a human/name, and is never cached. (The catalog is emitted only when theskilltool is visible and at least one model-invocable skill exists; an incomplete discovery emits nothing.) - The skill format is dsh's, not ours: top level only (
<name>/SKILL.mdor<name>.md— a nested**/SKILL.mdis not discovered); frontmatter requiresname+description, optionallywhenToUse/metadata/disable-model-invocation/user-invocable(the camelCase legacy keys are rejected). Full rules inskills/README.md, generic side in../skills/README.md.
⚠️ A skill describes this build's capability boundary, so a stale skill is worse than none:
harmony-runtime-capabilitiesonce still claimeddelete/movewere absent after they shipped, and the model then refused to use the delivered tools. Update the skills that reference a capability whenever that capability changes.
Signing (externalized — secrets never committed)
Signing material is externalized so build-profile.json5 stays secret-free and safe to commit. There are two gitignored config files, one per signing mode:
| Mode | Config (gitignored) | Committed template |
|---|---|---|
debug | signing.debug.local.json | signing.debug.local.json.sample |
release | signing.release.local.json | signing.release.local.json.sample |
Both files share the same schema (relative paths resolve against the project root):
{
"certpath": ".ohos/release/release.cer",
"storeFile": ".ohos/release/release.p12",
"profile": ".ohos/release/release.p7b",
"keyAlias": "debugKey", // optional, defaults to "debugKey"
"keyPassword": "<hvigor DecipherUtil AES-GCM ciphertext hex>",
"storePassword": "<hvigor DecipherUtil AES-GCM ciphertext hex>",
"signAlg": "SHA256withECDSA" // optional, defaults to SHA256withECDSA
}
Mode selection is driven by the SIGN_MODE environment variable (debug | release); when unset it defaults to debug. Any other value is a hard failure — there is no silent fallback to debug.
hvigorfile.ts— atafterNodeEvaluate, resolvesapp.signingConfigsin priority order:- CI env vars: all of
CERTPATH/STORE_FILE/STORE_PASSWORD/KEY_PASSWORD(plus optionalPROFILE/KEY_ALIAS/SIGN_ALG) signing.<SIGN_MODE>.local.json- nothing found →
signingConfigsis left untouched and a loud warning names the missing path (the build then yields an unsigned / IDE-auto-signed package)
- CI env vars: all of
- Injection is in-memory only (
setBuildProfileOpt):build-profile.json5is committed with"signingConfigs": []and is never written to. scripts/build-hap.ps1— CLI build+sign using DevEco's bundled JBR (avoids the Temurin/sdkman JDK 21Invalid CEN headerzip64 failure inhap-sign-tool.jar). It setsSIGN_MODEfor the hvigor child process.
keyPassword/storePasswordin either config must be hvigor DecipherUtil AES-GCM ciphertext (≥32 hex chars, not plaintext), and the.p12directory must contain thematerial/{fd,ac,ce}keychain — otherwise signing fails.
Post-build signature assertion. After a successful build,
build-hap.ps1runs the SDK'shap-sign-tool verify-appon the produced artifact, extracts the embedded provisioning profile'stype, and compares it with the requested-SignMode. A mismatch is a prominent error with a non-zero exit. (The.p7bis binary, so the profile JSON is located via a Latin-1 byte mapping plus brace-matching.) This guard exists because the project previously shipped a release build that was silently signed with debug material.
dsh version pin. This project builds against deepseek-harness tag
dsh-v0.2.1-rc.2. Patches are organized per dsh version (patches/<dsh-tag>/) andscripts/build-dsh.mjspinspatches/dsh-v0.2.1-rc.2/— when bumping to a new dsh tag, add a matchingpatches/<new-tag>/directory and update that pin.
| Patch | Purpose |
|---|---|
patches/dsh-v0.2.1-rc.2/dsh-allow-all-interfaces.patch | Remove both wildcard rejections (loopback isolation requires binding all interfaces + LAN IP): the --host CLI guard in web-app/startup.ts (and its now-unused isWildcardHost import) and the wildcard throw inside WebServer.Config's host transform, which 0.2.1 added and which the profile-level webserver.host: 0.0.0.0 override would otherwise hit |
patches/dsh-v0.2.1-rc.2/dsh-disable-native-picker.patch | Force directory-picker to use browse (native dialog worker fails to spawn under Electron) |
patches/dsh-v0.2.1-rc.2/dsh-disable-welcome-notice.patch | Drop the client's two settings.onboarding steps (versioned internal-testing notice + official-DeepSeek API-key prompt) so a first launch enters the app directly, and update apply.client.spec.ts to the shipped registration set |
patches/dsh-v0.2.1-rc.2/dsh-flock-openharmony.patch | Grant the POSIX flock write lock in-process on openharmony (no native addon; single-process host, same rationale as dsh's browser-worker stub) |
patches/dsh-v0.2.1-rc.2/dsh-hardlink-to-rename.patch | HarmonyOS sandbox refuses hard links (EACCES) → publish exclusively by same-directory rename, keeping the link-then-EEXIST preference everywhere else (session-log materialization + generation publication) |
patches/dsh-v0.2.1-rc.2/dsh-fs-hardlink-fallback.patch | Also refuses hard links on hmdfs user-directory mounts (EPERM, no .link handler) → writeFileAtomic's guarded-create path falls back to a same-directory rename when the target is verified absent, instead of failing the create with FS_IO_ERROR. Scoped to fs-local; the link preference is unchanged wherever links work |
patches/dsh-v0.2.1-rc.2/dsh-fs-remove-primitive.patch | Adds the ctx.fs seam's missing remove mutation primitive so a file tool can delete through the same sandbox fence as write/edit: a non-abstract FileSystem.remove whose default body throws (an abstract member would fail compilation for the six concrete extends FileSystem classes), a fs-local implementation in its own remove.ts, and a fs-sandbox override that runs checkedTarget first |
patches/dsh-v0.2.1-rc.2/dsh-disable-lefthook-postinstall.patch | Drops dsh's root postinstall (the lefthook git-hook installer). It refuses any checkout whose common git config carries core.worktree — true of every submodule checkout — so collect-dsh's pnpm deploy aborted with ELIFECYCLE. Git hooks are irrelevant to a shipped HAP, and the installer never succeeded here (dsh-hooks/ is absent) |
patches/dsh-v0.2.1-rc.2/dsh-fs-write-bytes.patch | Adds the ctx.fs seam's missing writeBytes mutation so a file tool can publish raw bytes behind the same sandbox fence as writeText: a non-abstract FileSystem.writeBytes whose default body throws, a fs-local implementation reusing the atomic-write path undecoded, and a fs-sandbox override that runs checkedTarget first |
patches/dsh-v0.2.1-rc.2/dsh-fs-chmod-primitive.patch | Adds the ctx.fs seam's missing chmod mutation: a non-abstract FileSystem.chmod whose default body throws, a fs-local implementation (chmod.ts) that applies the bits and reads them back, failing when the storage layer accepted the call without applying the mode (the HarmonyOS hmdfs user mounts do), and a fs-sandbox override that runs checkedTarget first |
patches/dsh-v0.2.1-rc.2/dsh-extra-writable-roots.patch | Teaches writableRoots() to honour DSH_EXTRA_WRITABLE_ROOTS (a path.delimiter-separated list) as deployment state, so a product can grant the user's Desktop/Documents/Download directories once at startup instead of leaving every write inside them to a per-operation escalation |
patches/dsh-v0.2.1-rc.2/dsh-attachment-durable-walk-sandbox.patch | The attachment store fsyncs every ancestor directory up to the filesystem root, but the HarmonyOS sandbox denies open() on /, /data, /data/storage and /data/storage/el2. syncDirectory() now ends its participation at the first directory the platform refuses to open — matching the existing Windows early return — instead of failing the save |
patches/dsh-v0.2.1-rc.2/dsh-rebrand.patch | Replaces upstream branding strings (app title, about-page copy, tray label) with DSH Desktop (HarmonyOS) for product identity |
Prerequisite — sibling source checkouts. This project consumes 3 sibling projects (not submodules); clone them next to this project before building:
git clone --branch dsh-v0.2.1-rc.2 https://github.com/deepseek-ai/deepseek-harness.git ../deepseek-harness
git clone https://github.com/dsh-market/dsh-market.git ../dsh-market && git -C ../dsh-market checkout ba73a3988bb3eee82295a695d59be59563637350 # dshmarket 1.66.21 (asserted at build time)
# ../harmonypc-electron is the Electron-on-HarmonyOS runtime project; extract the Electron 37 build artifacts to supply the 3 SOs
collect-runtime.mjs validates the 3 SOs (libelectron.so/libadapter.so/libffmpeg.so) and errors if any is missing; collect-dsh.mjs hard-fails if ../dsh-market is missing (the packaged app bundles it as dsh-dist/node_modules/dshmarket).
Run
Build with the dedicated scripts (config/SIGN_MODE details in Signing):
# debug (default task: Hap) — debug-signed packages CAN be side-loaded
powershell -ExecutionPolicy Bypass -File scripts\build-debug.ps1
# release (default task: App) — produces build\outputs\default\*-signed.app
# + build\outputs\default\symbol\release\app-symbol.zip
powershell -ExecutionPolicy Bypass -File scripts\build-release.ps1
# release-compiled but debug-signed — needed for on-device regression
powershell -ExecutionPolicy Bypass -File scripts\build-hap.ps1 -BuildMode release -SignMode debug
scripts/build-hap.ps1 is the engine (params: -Task Hap|App, -BuildMode debug|release, -SignMode auto|debug|release, plus -JbrHome -SdkHome -NodeHome -Hvigorw -DevEcoHome); -SignMode auto (the default) resolves to -BuildMode. The two thin wrappers pin their own defaults: build-debug.ps1 always uses -BuildMode debug -SignMode debug, build-release.ps1 always uses -BuildMode release -SignMode release.
HNP (native packages) is deliberately not used: declaring hnpPackages obliges every HAP and App Pack to carry a matching .hnp payload — something hvigor cannot produce, so it would need a post-pack injection and re-sign step for both artifacts — and the capability it was meant to carry is out of scope for now. Builds therefore need no injection step at all: hvigor's own signed HAP and App Pack are the deliverables, installable and submittable as they are. See docs/鸿蒙环境能力清单-v0.1.5.md A.2 #30.
Install the debug build over HDC and launch it:
hdc tconn <device-ip>:<port> # wireless (IP) debugging first; the port is shown by Developer options → Wireless debugging
hdc uninstall org.fellow99.dsh.DshDesktop # uninstall first on fresh install / artifact change, to clear stale dsh-dist in userData
hdc app install -r electron/build/default/outputs/default/electron-default-signed.hap
hdc shell aa start -a EntryAbility -b org.fellow99.dsh.DshDesktop
⚠️ Release-signed packages cannot be side-loaded.
hdc app installon a release-signed package fails withcode:9568322 ... signature verification failed due to not trusted app source. For on-device regression use-BuildMode release -SignMode debug(release-compiled, debug-signed); release-signed packages exist only for AppGallery submission.
Requirements: DevEco Studio 4.0+, HarmonyOS SDK API 17+ (targetSdk 6.1.1(24)), Node 18+, pnpm@11, HDC.
Debugging a packaged app on device
Everything below runs against an installed debug-signed HAP — no rebuild, no code change.
Main-process inspector (the most capable entry point)
The runtime already launches Electron with --inspect, so the main process exposes a Node inspector on port 9229. That inspector context has require, which reaches electron — so it can both query the dsh Host and drive the renderer.
export MSYS_NO_PATHCONV=1 # Git Bash otherwise rewrites /data/... into a Windows path
hdc fport tcp:19229 tcp:9229 # local 19229 → device 9229 (non-default local port avoids clashes)
curl -s http://127.0.0.1:19229/json/list # note the webSocketDebuggerUrl
Evaluate over CDP with Node's global WebSocket (Node ≥ 22):
// cdp.mjs <ws-url> <expression | @file>
import { readFileSync } from 'node:fs';
const [wsUrl, arg] = process.argv.slice(2);
const expression = arg.startsWith('@') ? readFileSync(arg.slice(1), 'utf8') : arg;
const ws = new WebSocket(wsUrl);
ws.addEventListener('open', async () => {
const send = (method, params) => new Promise(resolve => {
const id = Math.floor(Math.random() * 1e9);
const onMessage = event => {
const msg = JSON.parse(event.data);
if (msg.id !== id) return;
ws.removeEventListener('message', onMessage);
resolve(msg.result);
};
ws.addEventListener('message', onMessage);
ws.send(JSON.stringify({ id, method, params }));
});
await send('Runtime.enable', {});
const result = await send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true });
console.log(result.exceptionDetails
? 'EXCEPTION: ' + result.exceptionDetails.exception?.description
: JSON.stringify(result.result?.value, null, 1));
process.exit(0);
});
Useful expressions:
// which windows exist, and what URL each is on
(async () => { const { BrowserWindow } = require('electron'); return JSON.stringify(BrowserWindow.getAllWindows().map(w => w.webContents.getURL())); })()
// read the renderer's visible text
(async () => { const { BrowserWindow } = require('electron'); return await BrowserWindow.getAllWindows()[0].webContents.executeJavaScript('document.body.innerText'); })()
Typing into the composer. It is a contenteditable rich-text editor, not a <textarea>, and it is React-controlled — assigning value or innerText does not register. Use CDP Input.insertText against the attached debugger:
(async () => {
const { BrowserWindow } = require('electron');
const wc = BrowserWindow.getAllWindows()[0].webContents;
const SEL = '[contenteditable="true"][role="textbox"]';
await wc.executeJavaScript(`document.querySelector('${SEL}').focus()`);
if (!wc.debugger.isAttached()) wc.debugger.attach('1.3');
await wc.debugger.sendCommand('Input.insertText', { text: '…' });
return await wc.executeJavaScript(`document.querySelector('${SEL}').innerText.length`);
})()
To send, click the button whose aria-label is 发送消息.
Why an external browser (Playwright and friends) cannot be used
Every Host RPC method requires one browser session: GET / accepts the launch token only as ?token=<launchToken> and writes an authority-bound signed cookie, while a missing cookie returns 401 before RPC dispatch. The launch token is never logged (web-runtime runs with printUrl: false), and the renderer redirects to a clean / after the exchange — so an external browser cannot authenticate, not even through hdc fport.
Renderer UI automation via uitest (clicks only)
hdc shell uitest dumpLayout -p /data/local/tmp/layout.json
hdc file recv /data/local/tmp/layout.json ./layout.json # every node's text and bounds
hdc shell uitest uiInput click <x> <y> # clicks DO reach the Web content
- ⚠️
uitest uiInput textanduiInput keyEventdo not reach Web content (the editor is not a native control) — use the inspector'sInput.insertTextfor text. - The dump spans the whole screen, so other windows (system Settings, for example) appear alongside the app. Run
hdc shell aa start -a EntryAbility -b org.fellow99.dsh.DshDesktop, then re-dump, to confirm the app is in the foreground before clicking.
Is the Host up, and on which port?
hdc shell "hilog -x | grep dsh-harmony | tail -20" # "host 就绪: http://<lan-ip>:<port>/"
Probing http://127.0.0.1:<port>/ through hdc fport and getting 401 confirms the webserver is reachable and its auth fence is active.
⚠️ Security.
--inspectlets anyone withhdcfully control the host process (requireis available in that context). It comes from the upstream runtime's default arguments, not from this project. That is acceptable for local debugging, but evaluate removing it from a release build before AppGallery submission.
Signing & restricted permissions (full procedure)
The app cannot be installed until its provisioning profile (.p7b) grants
ohos.permission.kernel.ALLOW_WRITABLE_CODE_MEMORY (a system_basic restricted
permission, system_grant, tablet/2in1 only). There are two ways to obtain it:
A. Release path (AppGallery) — apply for the permission as an ACL in AGC
A restricted permission is not obtained by hand-editing a profile. The app applies for an
ACL (跨级别权限) in AppGallery Connect, Huawei reviews the usage scenario, and the approved
permissions are written into the profile automatically when the release profile is created —
so the release signing path needs no manual .p7b surgery at all.
- AGC → 开发与服务 → your project → your app →
项目设置→ ACL权限 tab. - Under 未获取权限 tick 我已知晓, select
ohos.permission.kernel.ALLOW_WRITABLE_CODE_MEMORY, and submit 申请 (at most 30 permissions per application; wait for approval before a new application). - Review (≈ 3 business days) checks that the permission matches the app's usage scenario — this app enables the JIT compilation feature of a bundled engine, is tablet/2in1 only, does not use the permission for hot updates, and adapts to JShield mode (坚盾模式).
- Create the release profile in AGC after approval — the granted ACL permissions are written into it automatically. If the ACL permissions change after the profile was created, recreate the profile.
- Currently only developer accounts registered in mainland China can use ACL permissions.
See AppGallery submission for the full release-certificate / release-profile flow.
B. Debug path — regenerate the debug profile in DevEco (unchanged)
The .p7b provisioning profile is signed by Huawei — it cannot be regenerated locally
(no local profile-signing CA; editing the SDK Unsigned*ProfileTemplate.json does not
auto-regenerate an existing .p7b). A default DevEco debug profile does not grant the
restricted permission, so installing a debug build fails with:
install failed due to grant request permissions failed. PermissionName: ohos.permission.kernel.ALLOW_WRITABLE_CODE_MEMORY
Regenerate it in DevEco Studio:
- Open the project in DevEco Studio.
File → Project Structure → Signing Configs.- Tick Automatically generate signature and log in with your Huawei developer account.
- DevEco regenerates a debug keystore + provisioning profile under
~/.ohos/config/(<bundle>_…=.p12/.cer/.p7b+material/keychain). - Ensure the requested permissions include the restricted one. The module already
declares it (
web_engine/src/main/module.json5→requestPermissions+definePermissions→ohos.permission.kernel.ALLOW_WRITABLE_CODE_MEMORY). For a cross-level (ACL) grant on anormal-APL app, DevEco's signing dialog surfaces the restricted permission for approval; accept it so the regenerated.p7bcarries it inacls.allowed-acls. - Point
signing.debug.local.jsonat the regenerated material (template:signing.debug.local.json.sample; paths relative to the project root are fine).
Build + sign
powershell -ExecutionPolicy Bypass -File scripts\build-debug.ps1 # debug
powershell -ExecutionPolicy Bypass -File scripts\build-release.ps1 # release (App Pack)
build-hap.ps1 verifies after every build that the artifact's embedded profile type matches the
requested signing mode (see Signing).
Install & launch
hdc uninstall org.fellow99.dsh.DshDesktop
hdc app install -r electron/build/default/outputs/default/electron-default-signed.hap
hdc shell aa start -a EntryAbility -b org.fellow99.dsh.DshDesktop
If install still reports a permission grant failure for a debug build, the
.p7bdoes not yet carry the restricted permission — repeat the DevEco regeneration (confirm the ACL approval) before rebuilding. A release-signed package can never be installed this way; release signing is for AppGallery only.
AppGallery submission
Distribution is no longer "personal-use HAP only": shipping on AppGallery is now an objective. AppGallery only accepts packages signed with a Huawei-issued release certificate + release profile — debug-signed packages cannot be listed.
Prerequisites
- A real-name-verified (实名认证) Huawei developer account — required to create a release certificate.
- Currently the ACL permissions this app needs are only available to developer accounts registered in mainland China.
- Qualification material: release preparation on AGC asks for items such as a privacy statement, an electronic copyright certificate, and an app copyright or agency certificate, plus filing/approval — confirm the exact list on AGC's 发布准备工作 page. The APP software copyright certificate (软著) is a non-mandatory qualification for non-game apps (recommended variants: 计算机软件著作权登记证书 / APP 电子版权证书 / 软件著作权认证证书); games additionally require a license number (版号).
Size limits
| Artifact | Limit |
|---|---|
App Pack (.app) | ≤ 4GB |
| HAP — PC/2-in-1, tablet, phone | ≤ 4GB |
| HAP — smartwatch / smart display | ≤ 2GB |
| HAP — sports watch | ≤ 20MB |
This project's release HAP is ≈360MB and the App Pack ≈245MB — well within the limits. HAPs must not be
installationFree, and bundleType must be app.
1. Create a release certificate
AGC → 证书、APP ID和Profile → 证书 → 新增证书, type 发布证书, upload a .csr (generate it in
DevEco Studio via Build > Generate Key and CSR, or with keytool), then download the .cer.
- Max 3 release certificates per account; validity 3 years for real-name-verified developers.
- Updating a release certificate requires updating the release profile too.
2. Apply for the restricted permission (ACL)
AGC → 开发与服务 → your project → your app → 项目设置 → ACL权限 tab → under 未获取权限 tick
我已知晓 → select ohos.permission.kernel.ALLOW_WRITABLE_CODE_MEMORY → 申请.
- At most 30 permissions per application; wait for approval before making a new application.
- Some permissions require 申请原因 (≤256 chars), a 使用场景 selection, and optional attachments.
- Review after application is ≈ 3 business days.
- Once approved, the permission appears under 已获取权限 and is automatically written into the profile when it is created. If the ACL permissions change after the profile was created, the profile must be recreated.
- A 试用调试Profile (trial debug profile) also exists: 5-day validity, max 5 per app.
The permission itself is officially classified as level system_basic, grantMode system_grant,
type 受限开放权限, startVersion API 14; available only for tablet and PC/2-in-1 devices; permitted
only for apps that enable the JIT compilation feature of a bundled engine and not permitted for hot
updates; the app must actively adapt to JShield mode (坚盾模式) and must not crash under it.
3. Create a release profile
AGC → 证书、APP ID和Profile → Profile → 添加, type 发布, bound to the bundle name + a release
certificate. Unlike a debug profile, a release profile's device list is empty (not device-bound).
4. Build, version and upload
powershell -ExecutionPolicy Bypass -File scripts\build-release.ps1 # -Task App -BuildMode release -SignMode release
- Upload the signed App Pack:
build\outputs\default\*-signed.app. - Optionally upload
build\outputs\default\symbol\release\app-symbol.zipso crash reports can be symbolicated. versionCode(currently1009) must be a number and must strictly increase on every subsequent submission — see Compatibility.- There is no separate PC/2-in-1 review track in the official review guide. At submission, if the package supports PC/2-in-1 but the configured distribution devices do not include it, AGC prompts you to update the supported devices.
To be confirmed: the end-to-end review duration for the AppGallery listing itself (only the ACL review's ≈3 business days is documented above), and the exact release-preparation document set — both must be read off AGC's own pages before the first submission.
Directory structure
This project and the 3 consumed projects plus 1 architecture-reference project live in sibling directories (not submodules):
(sibling directories)
├── dsh-desktop-hos/ # This project (HarmonyOS HAP, HarmonyOS desktop port)
│ ├── AppScope/ # App scope (icon/name/signing)
│ ├── electron/ # Entry module (copied from harmonypc-electron, contains SOs)
│ ├── web_engine/ # Bridge HAR (ArkTS bridge layer + resfile carries dsh artifacts)
│ ├── src-main/ # Main process main.js (extract + runProfile + loadURL + HarmonyOS adaptations)
│ ├── scripts/ # Four-stage build: collect-runtime → build-dsh → collect-dsh, plus build-debug / build-release
│ ├── runtime-overlays/ # App customizations re-applied after every runtime copy (prune + overlay)
│ ├── profiles/desktop/ # Custom desktop profile (cordis.patch.yml + package.json)
│ ├── patches/ # dsh upstream patches (14)
│ ├── docs/ # Engineering plan and final implementation record
│ ├── plugins/ # This project's own plugins (harmony-plugin-XXX; baked into the HAP, all loaded by default)
│ ├── skills/ # This project's own tool skills (kebab-case, no prefix; shipped in the HAP as the bundled skill root)
│ └── specs/ # Spec documents (as-built; see specs/README.md for index)
│
├── harmonypc-electron/ # Electron-on-HarmonyOS runtime (Electron 37 / Node 22.17.0)
│ └── ohos_hap/ # electron + web_engine modules + SO source (collect-runtime copy source)
│
├── deepseek-harness/ # The wrapped host (dsh, source reference, not a submodule)
│ ├── apps/ # cli (dsh bin / profile-boot), web (frontend, build:web produces dist)
│ ├── packages/ # host / client / core / session workspace packages
│ ├── vendor/ # vendored cordis framework packages (cordis / loader / hmr / …)
│ └── native/ # landlock-run native module (Linux sandbox, cut in MVP)
│
└── dsh-market/ # Plugin marketplace (source reference, npm pkg "dshmarket")
├── src/ # host half (mounts /dsh-market/* routes)
├── client/ # browser half (settings-page UI)
├── lib/ # compiled host output (materialized into dsh-dist/node_modules/dshmarket)
└── cordis.patch.yml # loader insert declaration ({ id: dsh-market, name: dshmarket })
../dsh-desktopis an architecture-design reference (reuses its architecture decisions + patches + main-process orchestration logic) and does not participate in this project's build/packaging.
Related docs
- docs/工程规划.md — full engineering plan + final implementation record (Electron 37 landing, key adaptation changes, MVP trade-offs, cross-compilation optimization path)
- specs/README.md — spec document index (project-level + 9 module spec/plan, as-built)
References
- deepseek-harness (sibling directory
../deepseek-harness) — the wrapped host; itsdocs/directory contains full architecture docs - dsh-market (sibling directory
../dsh-market) — the built-in visual plugin marketplace (npm packagedshmarket), materialized viacollect-dsh.mjs - harmonypc-electron (sibling directory
../harmonypc-electron) — the Electron-on-HarmonyOS runtime - dsh-desktop (sibling directory
../dsh-desktop) — architecture-design reference (Electron desktop shell) - dsh-desktop-ark (sibling directory
../dsh-desktop-ark) — the HarmonyOS Edition: same dsh Host onohos-node-bridge(in-process Node v24.2.0), rendered by the system ArkWeb, no Electron - DSH Desktop website — all three editions, real device screenshots, and the architecture story in one page
License
MIT © 2026 fellow99
Comments
Loading…
Similar plugins
by ahikl
DeepSeek Harness 桌面端插件:基于 Electron 的 dsh Web UI 桌面外壳。| An Electron desktop shell for the DeepSeek Harness Web UI, with a system tray and a custom title bar.
★ 0
↓ 199/wk
MIT
TypeScript
Aug 16, 2026
dsh plugin --profile web add @ahikl/dsh-desktopby oli-bot
DeepWork — community desktop shell for DeepSeek Harness: Electron + sidecar DSH engine + stock DSH web UI, sharing $DSH_HOME (unofficial, MIT)
★ 0
MIT
TypeScript
Sep 12, 2026
dsh plugin --profile web add @deepwork/desktopby ChisaAlter
DSH桌面端,支持主题和背景图等多种个性化配置。Electron desktop shell for DeepSeek Harness web UI
★ 139
↓ 293.2k/wk
MIT
JavaScript
Aug 25, 2026
dsh plugin --profile web add @deepseek-ai/dsh-baseby whitelonng
Community desktop companion for DeepSeek Harness — one-click Electron app for macOS and Windows
★ 706
↓ 293.2k/wk
MIT
TypeScript
Sep 13, 2026
dsh plugin --profile web add @deepseek-ai/dsh-baseby EricXu20266
DeepSeek Harness (DHS) Electron GUI client — webui to gui, kernel untouched
★ 8
↓ 11/wk
MIT
TypeScript
Aug 18, 2026
dsh plugin --profile web add dsh-discoveryby Hjay1101
DeepSeek Harness 插件:手机扫码遥控电脑上的 agent —— 在 dsh-remote-link 基础上增强会话持久化(dsh 重启后已配对设备保持登录)、iOS 主屏图标等
★ 0
MIT
JavaScript
Aug 23, 2026
dsh plugin --profile web add dsh-ios-control


