DSH HUB
HomePlugin StorePlugin PacksCommunityRankingsResourcesPublish Guide
Plugin source
Back to catalog

Zero20230619 /

Zero20230619/desktop-hand

Verified

Desktop Hand - real desktop mouse/keyboard input plus policy-guarded file tools for DeepSeek Harness

★ 1 Stars0 Forks0 IssuesN/A Community rating0 Confirmed installs
View on GitHubProject homepage
READMESource: main@03aeeb78

Desktop Hand

GitHub release license: MIT

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
  • Architecture
  • Requirements and platform support
  • Installation
  • Quick start
  • Tools
  • Slash commands
  • Configuration
  • Filesystem policy, delete modes and trash
  • Security model and threat model
  • Troubleshooting
  • Development and tests
  • License

What it is

Desktop Hand is a DeepSeek Harness (DSH) plugin that gives the model two carefully separated capabilities:

  1. 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.
  2. 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:

  1. Validates the source package (package.json, index.js, name desktop-hand).
  2. Backs up cordis.patch.yml to cordis.patch.yml.bak-<timestamp>.
  3. Refuses to continue if a hand-written desktop-hand row already exists outside its managed markers — remove/move it first.
  4. Copies the package to <profile>/node_modules/desktop-hand (skips node_modules, .git, .desktop-hand, test, coverage).
  5. Seeds <DSH_HOME>/desktop-hand.config.json only if it does not exist (so your policy survives upgrades).
  6. 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:

  1. Copy the package folder to <profile>/node_modules/desktop-hand.
  2. 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.
  3. 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

  1. Install and restart DSH, then open a session in a throwaway workspace.
  2. Check status (user-only, always allowed):
    /desktop-hand status
    
    Verify provider, config, hotkey: Pause, delete mode: trash, and that both grants show none.
  3. 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.
  4. 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.
  5. 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".
  6. 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 Press a key or chord on the real keyboard, e.g. shift+a, ctrl+z, f12, win+r. input grant; approval combo for chords with + (default ask) keys required; hold_ms default 40; chord parts capped by limits.maxComboKeys (4). Consumes 1 action.
8 desktop_hand_type_text Type a Unicode string as real keyboard input into the foreground app. input grant; approval text (default grant); a sensitive foreground title adds an approval ask text required (non-empty), capped by limits.maxTextChars (2000 UTF-16 code units); interval_ms default input.textIntervalMs (8). Typed text is redacted in the audit unless audit.logTypedText is true. Consumes 1 action.
9 desktop_hand_screenshot Capture all monitors or a region to a PNG in the session workspace. input grant (screen content is private) Optional region_x/y/width/height; PNG ≤ screenshots.maxBytes (8 MiB); consumes no action budget. Call DSH's read_image with the returned path to see it.
10 desktop_hand_read_file Read a UTF-8 text file with size/mtime metadata. read-only — no grant file_path; max_bytes default/cap 1 MiB; offset bytes; files > 64 MiB and binary files are refused; read-only zones deny with an explicit reason. No action consumed.
11 desktop_hand_list_dir List immediate entries (type/size/mtime, hidden included). read-only — no grant dir_path; max_entries default/cap 500; read-only zones deny.
12 desktop_hand_write_file Create or modify a UTF-8 text file. files grant; approval fileMutations (default ask) file_path,content; mode overwrite (default)/append; create_dirs default false; zones and system/plugin-data protections apply. Consumes 1 files action.
13 desktop_hand_move_path Move or rename a file/directory. files grant; approval fileMutations (default ask) source_path,dest_path; overwrite default false; checks both source (removal) and destination (creation) zones. Consumes 1 action.
14 desktop_hand_delete_path Delete a file/directory under the configured delete mode. files grant; approval directDelete then fileMutations for direct; fileMutations for trash path; recursive; confirm_direct (required for permanent delete); reason (audited + stored). Trash mode moves the target into quarantine and returns trashId/trashPath; forbid denies; direct needs unlock + confirm_direct=true (+ recursive=true for dirs). Consumes 1 action.
15 desktop_hand_restore_path Restore one quarantined item by id. files grant; approval fileMutations (default ask) trash_id; destination (default original path); overwrite default false. Destination is re-checked against zones. Consumes 1 action.
16 desktop_hand_list_trash List recent quarantine entries (id, original path, deleted time, size, reason). read-only — no grant limit default 50, cap 200; does not read payload contents.
17 desktop_hand_authorize Request a session grant from the model side. approval authorize (default ask) mode input/files; minutes/max_actions clamped to session maxima; allow_sensitive_zones; allow_direct_delete; note. Prefer the human /desktop-hand authorize path.
18 desktop_hand_revoke Revoke this session's input and/or files grant immediately. no grant — always allowed mode input/files/all (default all). Works even while emergency-stopped.
19 desktop_hand_status Report grants, emergency state, provider, policy summary, dirs, approval modes. read-only — always allowed Includes a warning that it is high risk; it only reads state.
20 desktop_hand_policy_override Tighten this session's filesystem policy only. approval policyOverride (default ask) add_readonly[], add_write_protected[], force_delete_mode: "forbid", reason. It is impossible to remove zones or relax the mode.
21 desktop_hand_emergency_stop Immediately halt all Desktop Hand operations and kill the input backend. no grant — always allowed reason (audited). Everything except status/revoke/emergency_stop is blocked until a human runs /desktop-hand resume.

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:

  1. 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.
  2. Write-protected zone hit → deny create/overwrite/append, move and delete; reads and listings are allowed.
  3. OS system roots (protectSystemRoots, default true) → deny writes and deletes; reads are allowed.
  4. Plugin data (protectPluginData, default true): the plugin directory, the audit directory and the trash directory cannot be mutated by the tools.
  5. 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.
  6. 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.
  7. 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.
  • Audit. Append-only JSONL per session with rotation, plus typed-text redaction by default. Tool-level denials (input-denied, fs-denied) are recorded with the matching rule/reason, as are authorization changes and emergency stops. Authorization/approval denials decided in tools/pre-execute happen before the tool body and therefore are not in the Desktop Hand audit log — use the rejection reason, /desktop-hand status and DSH logs there.

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.
  • Filesystem guard rails are not a backup: always keep independent backups of important data.

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 confirm_direct=true, and use recursive=true for non-empty directories; per-call approval may still be required.
grant expired / action budget exhausted Re-run /desktop-hand authorize …; a re-grant replaces the grant with a fresh budget (it does not stack). desktop_hand_screenshot does not consume actions.
Screenshot saved but "I can't see it" The model must call DSH's read_image with the returned path; screenshots are files under <session cwd>/.desktop-hand/screenshots/.
Emergency hotkey does nothing Only implemented on Windows. Check the DSH log for emergency-stop hotkey warnings (invalid key, missing PowerShell/script, or platform unsupported). Use /desktop-hand stop instead. After a stop, only a human /desktop-hand resume clears it.
Every tool is blocked after a stop That is the design; status, revoke and emergency_stop are the only allowed calls. Run /desktop-hand resume.
Plugin fails to activate after editing config The config is validated strictly; read the error (invalid mode/limit/regex/version, unreadable file). A broken policy never falls back silently to defaults.
Restore refuses desktop_hand_restore_path will not overwrite by default; pass overwrite=true (destination is re-checked against zones) or restore to a different destination.

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

—/ 5

No ratings yet

Verified DSH bundle

Commit 03aeeb788ba4

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