Desktop Hand
Real desktop mouse/keyboard control for DeepSeek Harness, plus a policy-guarded file
toolkit with quarantined deletion, session authorization, an emergency-stop hotkey and an
append-only audit log.
⚠️ HIGH RISK — READ THIS FIRST
Desktop Hand lets the model operate your physical desktop: move the real mouse pointer,
click, drag, type, press shortcuts, take screenshots — and write, move, delete and restore
real files on your machine.
- Real input and file mutations are OFF by default. They require an explicit,
session-scoped, time- and action-budgeted grant that a human approves (default
approval.authorize: "ask"; if your DSH approval policy auto-approves prompts, that
safeguard is weakened — see docs/CONFIG.md).
- This is not a sandbox. It is not a VM, not a separate user account, and not an OS
permission boundary. Desktop Hand runs with the same privileges as DeepSeek Harness and
can touch anything that process can touch, except for the paths protected by policy.
- The filesystem policy governs Desktop Hand tools only (unless you opt in to
policy.enforceOnDshFsTools). Shell tools (bash, pwsh, …) are not governed and can
bypass it.
- Deletion defaults to trash/quarantine mode (recoverable), but
direct mode is
permanently destructive when a user unlocks it.
- Start with the fake provider or a throwaway test account, walk through
docs/ACCEPTANCE.md, and keep a hand on the emergency-stop hotkey.
The default hotkey is Pause (deliberately not
F9: Blender uses F9 for Adjust Last
Operation, so F9 would fire while modelling).
- Real GUI automation of applications such as Blender was not exercised end-to-end during
development; the automated suite uses fake providers and unit tests only. Treat real-app
acceptance as a manual step.
Contents
What it is
Desktop Hand is a DeepSeek Harness (DSH) plugin that gives the model two carefully separated
capabilities:
- Real desktop input — observe the screen (
get_pointer, foreground_window,
screenshot) and act on the physical desktop (move_mouse, click, drag, scroll,
press_key, type_text). The model works in a see → act → see loop: screenshot, view it
with DSH's read_image, perform one action, screenshot again.
- Policy-guarded file operations —
read_file, list_dir, write_file, move_path,
delete_path, restore_path, list_trash. Reads are unprivileged but still subject to
protected zones; writes/moves/deletes need a separate files grant. Deletion defaults to a
recoverable quarantine ("trash") with metadata, an append-only index and one-call restore.
Both capabilities are governed by: independent input / files session grants, per-call
approval modes, action and coordinate limits, filesystem zones (read-only /
write-protected / protected roots / sensitive screen areas), a system-wide emergency stop
and a JSONL audit trail.
What it is not: a remote-desktop protocol, a sandbox, a background daemon, or a replacement
for DSH's own read/write/edit tools (use those for ordinary workspace editing).
Architecture
DSH host process
│
│ profile patch row ── cordis.patch.yml / install.ps1 (bundled patch)
▼
index.js ── plugin entry
│ • Config schema (deployment overrides: enabled/configPath/provider/trashDir/…
│ • registers 21 tools, the /desktop-hand command, the system-prompt guidance
│ • tools/pre-execute: authorization + approval + emergency-stop gate
│ • starts the emergency hotkey monitor
▼
lib/runtime.js ── per-activation wiring (created once, before tools)
│ ├── lib/config.js defaults · discovery · deep merge · validation · path expansion
│ │
│ ├── lib/tools/
│ │ input-tools.js observe + mouse/keyboard/text/screenshot (input grant)
│ │ fs-tools.js read/list/write/move/delete/restore/trash (files grant for writes)
│ │ control-tools.js authorize · revoke · status · policy_override · emergency_stop
│ │
│ ├── lib/security/
│ │ authorization.js per-session grants, budgets, unlocks, tightening
│ │ limits.js coordinate/duration/count caps, sensitive zones, title patterns
│ │ emergency.js halt/resume, AbortSignal, listeners
│ │
│ ├── lib/fs/
│ │ policy.js zones + protected roots → allow/deny decisions, delete modes
│ │ trash.js <trash>/items/<id>/payload + meta.json + index.jsonl
│ │ audit.js audit-<session>.jsonl, rotation, typed-text redaction
│ │
│ └── lib/input/
│ index.js provider factory (auto | windows | linux | macos | fake | disabled)
│ windows.js persistent PowerShell JSON-lines agent (scripts/input-agent.ps1)
│ linux.js xdotool one-shot commands
│ macos.js cliclick + osascript + screencapture
│ fake.js deterministic in-memory provider (tests / dry runs)
│ hotkey.js hidden scripts/hotkey-monitor.ps1 poller (Windows only)
│
└── audit files, trash store, screenshots (workspace .desktop-hand/screenshots)
Requirements and platform support
| Platform | Real input | Screenshots | Emergency hotkey | Notes |
|---|
| Windows 10/11 | ✅ full | ✅ full | ✅ default Pause | Primary target. Uses Windows PowerShell 5.1 (powershell.exe) with pwsh.exe as fallback, a persistent agent and a hidden hotkey monitor. No extra installs required. |
| Linux | ⚠️ best effort | ⚠️ best effort | ❌ none | Requires xdotool on PATH; screenshots need ImageMagick import, scrot or gnome-screenshot. Multi-monitor geometry is reported as one display. Emergency stop still works via /desktop-hand stop / desktop_hand_emergency_stop; the provider also checks the stop-flag file. |
| macOS | ⚠️ best effort | ⚠️ best effort | ❌ none | Requires cliclick; uses osascript, pbcopy+Cmd+V for typing (this changes the clipboard) and screencapture. Scrolling and middle-click are unsupported; drag is left-button only; Accessibility/Automation permissions must be granted manually. No hotkey monitor. |
| fake | — (simulated) | ✅ writes a real PNG | ❌ none | Deterministic in-memory provider for tests/dry runs (input.provider: "fake"). Never touches the real desktop. |
| disabled | ❌ | ❌ | ❌ | input.provider: "disabled"; input tools fail closed. |
Runtime requirements: Node.js ≥ 22 (the plugin is ESM; package.json engines), DSH with a
profile, and for Windows PowerShell 5.1 or PowerShell 7. The test suite in this repo was run on
Node v26.13.0 on Windows.
Installation
The plugin package directory is desktop-hand/ (this repository/folder). It contains
index.js, lib/, scripts/, locale/, cordis.patch.yml, desktop-hand.config.json,
install.ps1, uninstall.ps1 and these docs.
Recommended: install.ps1
# DSH runs with DSH_PROFILE_DIR already set; otherwise pass it explicitly.
pwsh -File .\install.ps1 -ProfileDir "$env:DSH_PROFILE_DIR"
Parameters:
| Parameter | Default | Meaning |
|---|
-ProfileDir | $env:DSH_PROFILE_DIR | Target DSH profile (must contain node_modules/ and cordis.patch.yml). |
-SourceDir | the script's directory | Plugin package to install. |
-DshHome | $env:DSH_HOME or $HOME\.dsh | Where desktop-hand.config.json is seeded. |
-SkipConfig | off | Do not seed the config file. |
-Force | off | Replace an existing installed copy (e.g. the app may be running). |
What it does:
- Validates the source package (
package.json, index.js, name desktop-hand).
- Backs up
cordis.patch.yml to cordis.patch.yml.bak-<timestamp>.
- Refuses to continue if a hand-written
desktop-hand row already exists outside its
managed markers — remove/move it first.
- Copies the package to
<profile>/node_modules/desktop-hand (skips node_modules, .git,
.desktop-hand, test, coverage).
- Seeds
<DSH_HOME>/desktop-hand.config.json only if it does not exist (so your policy
survives upgrades).
- Writes an idempotent managed block into the profile's
cordis.patch.yml:
# >>> desktop-hand managed block (install.ps1) >>>
- insert:
- id: desktop-hand
name: desktop-hand
config: {}
# <<< desktop-hand managed block <<<
Manual profile patch
If you prefer not to run the script:
- Copy the package folder to
<profile>/node_modules/desktop-hand.
- Add the
- insert: row shown above (without the markers is fine) to the end of the
profile's cordis.patch.yml — the user patch layer applied after bundle layers.
- Restart DSH (or reload the profile).
Development overlay (--patch)
DSH's launcher accepts extra patch layers via --patch <file> (launcher flags must come before
app arguments). A patch file is a YAML list with the same format as cordis.patch.yml; layers
compose in order: bundle layers → profile patch → --patch overlays.
For development you can keep the plugin package installed in the profile (run install.ps1
once, then -Force after each change) and enable/disable or reconfigure it per run through an
overlay instead of editing the profile patch:
# dev.patch.yml
- insert:
- id: desktop-hand
name: desktop-hand
config:
enabled: true
provider: windows
# Example: start a separate DSH instance with the overlay. Use the launcher command/flags of
# your DSH build; the launcher flag must come before app arguments.
dsh --patch .\dev.patch.yml
The overlay is a launcher feature, not a package manager: it copies nothing. An overlay row that
points at a local index.js path (instead of the package name) can work for the row itself in
some setups, but the plugin's bare @deepseek-ai/* imports still need a profile-installed
copy to resolve. So install the package first, then use --patch only for the row/config.
Restart / HMR
install.ps1 finishes with: "Restart DeepSeek Harness (or wait for profile HMR to pick up the
new package)." For a reliable deployment restart DSH. The configuration file is read once
per plugin activation (there is no watcher), so config edits also need a reload/restart.
Uninstall
pwsh -File .\uninstall.ps1 -ProfileDir "$env:DSH_PROFILE_DIR"
Removes the managed block (backing up the patch first) and the installed package. User data is
intentionally preserved: desktop-hand.config.json, the audit directory and the trash
directory are kept so old deletions can still be audited or restored.
Package layout
desktop-hand/
├── index.js plugin entry: Config, tools, approval gate, system prompt
├── lib/ runtime, config, tools, security, fs, input
├── scripts/
│ ├── input-agent.ps1 persistent Windows JSON-lines input agent
│ ├── hotkey-monitor.ps1 hidden GetAsyncKeyState poller (emergency hotkey)
│ └── live-smoke.mjs safe read-only live backend check (no input injection)
├── locale/{en,zh}.json plugin market metadata
├── cordis.patch.yml bundled bundle-layer patch
├── desktop-hand.config.json example/user-editable config seeded by install.ps1
├── install.ps1 / uninstall.ps1
├── docs/{CONFIG.md,ACCEPTANCE.md}
└── test/ node:test unit tests (fake provider)
Quick start
- Install and restart DSH, then open a session in a throwaway workspace.
- Check status (user-only, always allowed):
/desktop-hand status
Verify provider, config, hotkey: Pause, delete mode: trash, and that both grants show
none.
- Grant input for a bounded time and action budget:
/desktop-hand authorize input 10 200
(Add sensitive to unlock taskbar/system corners: /desktop-hand authorize input 10 200 sensitive.)
Ask for a short budget; grants expire and are action-limited.
- See the screen, then act one step at a time:
- the model calls
desktop_hand_get_pointer and desktop_hand_foreground_window to learn
coordinates and confirm the target window;
desktop_hand_screenshot saves a PNG under <session cwd>/.desktop-hand/screenshots/;
- the model must call DSH's
read_image with that path to actually see it;
- one
desktop_hand_click / press_key / type_text per tool call, then screenshot again.
- File mutations need a separate grant:
/desktop-hand authorize files 5 50
With the default approval.fileMutations: "ask", each write/move/delete/restore also needs a
live approval prompt. If DSH approvals are disabled (never), these calls fail closed;
see docs/CONFIG.md for "grant" mode. Drags and key chords
(approval.drag/approval.combo) also ask by default — for the Blender shift+a step you
need a working approval prompt, or those categories switched to "grant".
- Stop instantly with
Pause, or type /desktop-hand stop. Only a human can
/desktop-hand resume.
Dry run without touching the real desktop: set "input": { "provider": "fake" } in the config,
reload, and walk the same flow against the deterministic fake screen.
Tools
21 model-visible tools (input grant = session input authorization; files grant = session
file-mutation authorization; read-only tools need no grant but obey filesystem zones and are
still blocked while emergency-stopped, except status, revoke, emergency_stop).
| # | Tool | What it does | Grant / approval | Limits and notes |
|---|
| 1 | desktop_hand_get_pointer | Current pointer position, every monitor's bounds, virtual-desktop bounds, foreground title/hwnd. | read-only — no grant | Also a useful coordinate probe before a click. |
| 2 | desktop_hand_foreground_window | Title, native handle, pid of the foreground window; reports whether its title matches a configured sensitive pattern. | read-only — no grant | Use before typing/shortcuts to confirm the target app. |
| 3 | desktop_hand_move_mouse | Move the real pointer to absolute virtual-desktop coordinates. | input grant | x,y required; duration_ms default 0 (jump), capped by limits.maxMoveDurationMs (5000). Consumes 1 input action. |
| 4 | desktop_hand_click | Click a real mouse button at coordinates, or at the pointer when x/y are omitted. | input grant | button left/right/middle (default left), count default 1 capped by limits.maxClicksPerCall (3), interval_ms default 80. Clicking a sensitive zone is refused unless the grant unlocked sensitive zones. Consumes 1 action. |
| 5 | desktop_hand_drag | Press, move along a duration, release. | input grant; approval drag (default ask) | from_*, to_* required; duration_ms default 500 capped at limits.maxDragDurationMs (10000); sensitive-zone check on the start point. Consumes 1 action. |
| 6 | desktop_hand_scroll | Wheel scroll up/down at the pointer or at x/y. | input grant | direction required; amount default 3 capped at limits.maxScrollAmount (50). Consumes 1 action. |
| 7 | desktop_hand_press_key |
Notes:
- One primitive action per call. The system prompt tells the model to perform exactly one
click/key-chord/text/drag per call and verify with a screenshot before the next. Grants are
consumed after a successful primitive action (screenshot and read-only tools consume none).
- Grant groups are independent. An input grant never permits file mutations and vice versa.
- Tool actions and policy denials are audited. Most calls append an audit entry, and denials
(
input-denied, fs-denied) are recorded with the matching rule/reason. tools/pre-execute
denies before the tool body runs when authorization/approval/emergency policy says no.
Slash commands
/desktop-hand is a user-only slash command. It never enters the model conversation, which is
why it is the authoritative consent path.
| Command | Effect |
|---|
/desktop-hand or /desktop-hand status | Show halt state, input/files grants (expiry, actions used/max), provider, config path, audit/trash dirs, delete mode, hotkey, policy summary. |
/desktop-hand authorize input [minutes] [actions] [sensitive] | Grant real mouse/keyboard/text/screenshot for this session. Defaults: session.inputDefaultMinutes (10) and inputDefaultActions (200); both clamped to the configured maxima. sensitive unlocks sensitive screen zones for this grant. |
/desktop-hand authorize files [minutes] [actions] [direct-delete] | Grant file write/move/delete/restore. Defaults 5 min / 50 actions. direct-delete unlocks permanent deletion (each call still needs confirm_direct=true, and per-call approval when configured). |
/desktop-hand revoke [input|files|all] | Revoke immediately (default all). |
/desktop-hand tighten readonly <pattern> | Add a session-only read-only zone (same syntax as the config: path, glob, or /regex/). |
/desktop-hand tighten write <pattern> | Add a session-only write-protected zone. |
/desktop-hand tighten delete=forbid | Force delete mode forbid for this session. |
/desktop-hand resume (alias reset) | Clear an emergency stop. Only the human can do this. |
/desktop-hand stop (alias halt) | Emergency stop now. |
/desktop-hand policy | Print the effective filesystem policy (configured zones, delete overrides). |
/desktop-hand help | Show the grammar. |
Session tightening is additive only: it can add zones and force forbid, never remove zones
or relax the delete mode. It lives in RAM and disappears with the session/restart.
Configuration
The full annotated reference is docs/CONFIG.md. Summary:
- The JSON config is discovered in this order: plugin
Config.configPath (must exist) →
<DSH_HOME>/desktop-hand.config.json (recommended; seeded by the installer) →
<plugin-dir>/desktop-hand.config.json → <DSH_HOME>/desktop-hand/config.json → built-in
defaults (with a warning). DSH_HOME defaults to ~/.dsh.
- The file is deep-merged over defaults; arrays replace.
~, $VAR, ${VAR}, ${env:VAR} and
%VAR% are expanded in path fields. Invalid configuration fails plugin activation loudly
rather than silently dropping protection.
- A profile patch row can override a small set of deployment knobs (
enabled, configPath,
provider, trashDir, auditDir, screenshotsDir, hotkey, emergencyStop); policy,
limits and approval modes live in the JSON file.
- The config is read once per activation; restart/reload after editing.
Key defaults you should know:
| Setting | Default | Why it matters |
|---|
policy.delete.mode | trash | Deletions are recoverable quarantine moves, not unlinks. The shipped config uses D:\DSH_Trash as the trash root. |
approval.fileMutations | ask | Every write/move/delete/restore needs a live approval prompt. |
approval.drag / approval.combo | ask | A drag and a key chord (e.g. Blender's shift+a) are high-risk primitives and each asks for approval by default. |
approval.text | grant | Typing relies on the input session grant, except when the foreground title matches limits.sensitiveZones.foregroundTitlePatterns — then it asks anyway. |
approval.directDelete | ask | Permanent deletes need an explicit per-call decision. |
emergency.hotkey | Pause | System-wide stop. F9 is deliberately avoided because Blender uses it for Adjust Last Operation. |
session.* | 10 min/200 input, 5 min/50 files; max 30 min/1000 | Grants are time- and action-budgeted. |
input.provider | auto | Windows → windows, Linux → linux, macOS → macos. Use fake for dry runs. |
"ask" is not unconditional success. If the DSH session approval policy is never (or
approval prompts are otherwise unavailable), every "ask" category fails closed — the call
is rejected before the tool body runs and nothing is touched. For the Blender acceptance flow
(shift+a is a combo), either switch the DSH approval policy to ask (recommended) or set
the relevant categories (combo, drag, and fileMutations/directDelete for file work) to
"grant" and rely on the time/action-bounded grant from /desktop-hand authorize.
Filesystem policy, delete modes and trash
Zones and precedence
Every Desktop Hand file tool normalizes the caller's path (relative paths against the session
working directory; ~/env expansion; Win32 namespace prefixes stripped; symlinks/junctions
resolved) and then checks zones against both the lexical and the real path, so a link cannot
be used to escape a zone.
Precedence is deliberate, security first:
- Read-only zone hit → deny read, list, write, move (source and destination), and delete.
This happens before any delete-mode logic, so a
direct pin can never bypass it.
- Write-protected zone hit → deny create/overwrite/append, move and delete; reads and
listings are allowed.
- OS system roots (
protectSystemRoots, default true) → deny writes and deletes; reads are
allowed.
- Plugin data (
protectPluginData, default true): the plugin directory, the audit directory
and the trash directory cannot be mutated by the tools.
- Protected delete roots (
delete.protectRoots, default true): drive roots, the user home,
the trash dir, the plugin dir, and the session workspace root and its ancestors cannot be
deleted (or moved away). Equality only — children are fine.
- Delete-mode resolution: session-forced
forbid > per-path delete.pathOverrides >
global delete.mode. In direct mode, a files grant with allowDirectDelete is required
unless directDeleteRequiresUnlock is false; a non-empty directory also requires
recursive=true. In trash mode a configured trashDir is required.
- Additionally,
desktop_hand_delete_path refuses to delete the session workspace root or
one of its ancestors.
Delete modes
| Mode | Behaviour | Evidence / recovery |
|---|
forbid | Every delete is denied (delete-forbidden). | Audit fs-denied. |
trash (default) | The file/directory is moved into <trashDir>/items/<id>/payload; a meta.json sidecar and an index.jsonl line are written first/after; the tool returns a trash id and restore hint. | desktop_hand_list_trash, desktop_hand_restore_path. Cross-volume moves fall back to copy-then-remove. |
direct | Permanent rm. Requires an unlocked files grant (allowDirectDelete), confirm_direct=true, recursive=true for non-empty directories, and (by default) a per-call directDelete approval; approval.fileMutations is also consulted. | Audit fs/delete_direct; no recovery. |
Trash layout and restore
D:\DSH_Trash\ # policy.delete.trashDir (shipped config)
├── index.jsonl # append-only journal: quarantine + restore records
└── items\
└── 20261031T162738-1a2b3c4d\ # id = UTC timestamp + random hex
├── meta.json # id, originalPath, displayPath, payloadPath, size, type,
│ # mtime, deletedAt, sessionId, callId, tool, reason,
│ # policyRule, mode, moveKind, restoredAt, restoredTo
└── payload # the moved file or directory (link stays a link)
desktop_hand_delete_path returns mode: "trash", trashId, trashPath and a
restoreHint such as desktop_hand_restore_path({ trash_id: "..." }).
desktop_hand_restore_path moves the payload back to its recorded original path (or an
explicit destination), re-checks the destination against the zones, and refuses to overwrite
unless overwrite=true. On restore meta.json gains restoredAt, restoredTo,
restoreCount, lastRestoreKind, and the index gets a restore record.
meta.json is written before the payload is moved, so a crash cannot leave recoverable
data without metadata.
- The trash payload is never unlinked automatically; retention is your responsibility.
Session tightening
/desktop-hand tighten readonly|write <pattern> and /desktop-hand tighten delete=forbid
(and desktop_hand_policy_override) can only make the session more restrictive. There is no
tool or command that relaxes config zones or re-enables deletions.
Security model and threat model
Controls
- Two independent session grants.
input and files are separate, in-memory, expire after
the requested minutes and after an action budget. Revocation is immediate
(/desktop-hand revoke, desktop_hand_revoke). Restarting DSH clears every grant.
- Human consent path.
/desktop-hand authorize … is a user-only slash command; the
model-facing desktop_hand_authorize is additionally gated by approval.authorize (default
ask), so under the default approval mode the model cannot silently grant itself rights. A
deployment that auto-approves prompts weakens this safeguard — keep approval.authorize on
ask/deny for unattended-risk environments.
- Approval gate (
tools/pre-execute). High-risk calls return ask/deny before the tool
body runs: authorization tools, policy overrides, file mutations, direct deletes, drags, key
chords and text. Defaults: fileMutations, directDelete, drag and combo are ask;
text is grant (the input session grant is consent) but a sensitive foreground-title match
asks anyway. If the DSH approval policy is never, every ask category fails closed —
configure the categories as grant or enable approvals deliberately.
- Emergency stop. The default Pause hotkey is polled by a hidden PowerShell monitor that
writes
<DSH_HOME>/desktop-hand/emergency-stop.json; the runtime then halts, aborts in-flight
input via AbortSignal, kills the input backend and notifies the model. While halted, every
Desktop Hand tool except status, revoke and emergency_stop is denied. Only a human can
/desktop-hand resume. F9 is not used because Blender binds it to Adjust Last Operation.
- Hard limits. Coordinate validation (integer, inside a real monitor, not in a virtual-desktop
gap), action caps (
maxClicksPerCall, maxComboKeys, durations, scroll, text length), and
sensitive-screen rectangles (taskbar strip, screen corners; clicks/drag starts denied unless
the input grant unlocked them). Sensitive foreground-title patterns can force an approval
before typing.
- Filesystem policy. Zones, delete modes, protected roots and symlink-realpath checks as
described above; a denial is an explicit, coded error with the matching rule and reason, never
a silent skip.
Threat model and honest limitations
Desktop Hand assumes the human user is the principal and the model may be wrong, manipulated
by prompt injection, or simply confused:
- A compromise is contained by the grant's time/action budget, the emergency hotkey, the trash's
recoverability and the audit trail — not by an OS sandbox.
- Policy scope:
policy.* governs Desktop Hand's own tools only. Unless you set
policy.enforceOnDshFsTools: true (which also guards DSH's read/read_image/list_dir/
write/edit/write_file), a shell command (bash, pwsh, …) can read or modify the same
files, including paths inside read-only zones, and bypasses the delete modes entirely.
A forbid mode is not a filesystem permission.
- Not an OS sandbox. The plugin runs with the DSH process's user privileges. It cannot
protect you from a malicious process running as the same user, nor from the model being
socially engineered into obtaining a broad grant.
- Screen privacy: screenshots capture everything visible on all monitors (or the requested
region).
type_text can type secrets. Text is redacted in the audit by default
(audit.logTypedText: false) but is still sent as real input.
- Destructive paths remain: a human who unlocks
direct-delete, supplies confirm_direct
and approves can permanently delete files; trashDir contents are not auto-purged but can be
deleted manually outside the plugin, and the plugin cannot protect them if you configure the
trash on an untrusted/unprotected volume.
- Approval dependency:
approval.fileMutations, approval.directDelete, approval.drag and
approval.combo default to ask; deployments without a live approval channel (e.g. DSH
approval policy never, headless runs) will reject those operations. This is deliberate
fail-closed behavior, not a bug.
- Platform gaps: the emergency hotkey monitor is Windows-only; on Linux/macOS the
/desktop-hand stop command and the stop-flag checks are the emergency path. The Linux/macOS
providers are best-effort, and macOS typing changes the clipboard.
- Real-app verification: the automated suite uses fake providers. Real GUI automation
(Blender, Office, browsers, …) has not been exercised end-to-end during development; use
docs/ACCEPTANCE.md and your own careful pilot before trusting it with
valuable work.
Troubleshooting
| Symptom | Likely cause / fix |
|---|
input provider ... unavailable / INPUT_MISSING_BINARY | Windows: ensure PowerShell works (powershell.exe or pwsh.exe). Linux: install xdotool (or set input.linux.xdotoolPath). macOS: install cliclick and grant Accessibility/Automation permissions. Use input.provider: "fake" for a dry run. |
| Input tools denied with "authorization is not active" | Run /desktop-hand authorize input <minutes> <actions> (or unlock sensitive zones). Grants expire and are action-budgeted — check /desktop-hand status. |
| File writes/deletes denied by approval | Default approval.fileMutations: "ask" needs a live approval prompt; with DSH approval policy never it fails closed. Set "grant" in desktop-hand.config.json and restart if a session grant should suffice. |
A drag or key chord (e.g. shift+a) is denied by approval | approval.drag and approval.combo default to ask; under DSH approval policy never they fail closed. Switch the approval policy to ask (get prompts) or set those categories to "grant" and rely on the input session grant. |
| Safe backend check without injecting input | Run node scripts/live-smoke.mjs from the package directory. It starts the Windows backend, reads screen/pointer/foreground, saves a PNG under %TEMP%\dh-live-home\live-shot.png and verifies the stop-flag guard; it never moves the mouse, clicks or types. |
file operation denied by policy: ... read-only zone ... | The path matches policy.readonly (or a session tightening). This is intentional; remove the zone in the config or use a different path. Do not try to bypass with shell commands — that defeats the point of the policy. |
delete-forbidden | Delete mode is forbid (globally, by override, or session tighten delete=forbid). Change the config/reload, or note the session tightening is permanent for that session. |
direct-delete-locked / confirm_direct required | Authorize files with direct-delete, pass , and use for non-empty directories; per-call approval may still be required. |
Development and tests
# from the desktop-hand package directory
npm test # node --test test/
npm run test:watch # node --test --watch test/
node --test test/
- Node.js ≥ 22 (ESM,
node:test). The suite currently contains 167 passing tests and
requires no DSH runtime: defineTool is injected, providers are faked, and the config/policy/
auth/limits/emergency/audit/trash modules are unit-tested directly.
- Test layout (
test/): config.test.js (defaults, discovery, merge, validation),
policy.test.js (zones, precedence, delete modes, protected roots, symlinks, tightening),
auth.test.js (grants, expiry, budgets, revoke, sensitive/direct unlocks, tightening),
limits.test.js (geometry, sensitive zones, caps), emergency.test.js (halt/reset/abort),
audit.test.js (JSONL, rotation, redaction), trash.test.js (quarantine/restore/EXDEV
fallback), input.test.js + keys.test.js (provider contract/backends and key parsing),
tools-fs.test.js and tools-input.test.js (tool handlers against fake runtimes), and
integration.test.js (runtime wiring, end-to-end flows and /desktop-hand command handling).
scripts/live-smoke.mjs is a safe, read-only live check of the Windows backend
(node scripts/live-smoke.mjs): it reads screen/pointer/foreground, saves a PNG to
%TEMP%\dh-live-home\live-shot.png, verifies the emergency-stop guard refuses an action, and
never moves the mouse, clicks or types.
- Real-desktop GUI automation is a manual acceptance procedure:
docs/ACCEPTANCE.md. The development configuration for dry runs is
input.provider: "fake".
Verified during development (cite as evidence, not as a blanket guarantee):
- The full automated suite passes: 167/167.
- A scratch DSH profile (headless template) installed the package into its
node_modules and
booted the plugin without activation warnings.
- Safe live smoke on the development machine succeeded: 2560×1600 screen reported,
pointer/foreground read, a 2560×1600 PNG screenshot written, the stop-flag guard refused an
action, and the Pause hotkey monitor started/stopped cleanly.
- Real Blender/GUI input was NOT exercised — see docs/ACCEPTANCE.md.
Contributions: keep the code style, add tests for new tools/policy behavior, and update
docs/CONFIG.md when you add a config field.
License
MIT — see the license field in package.json. (This package does not currently ship a
standalone LICENSE file; the package metadata declares MIT.)
Related docs: Configuration reference (docs/CONFIG.md) ·
Manual acceptance tests (docs/ACCEPTANCE.md) ·
中文 README