ckanner/dsh-plugin-lcu

dsh-plugin-lcu

DeepSeek Harness plugin: drive the desktop and Chrome through LCU (Codex computer use, decoupled from the app)

未分類computer-usedeepseek-harnessmcp

dsh-plugin-lcu

English | 中文

Drive the desktop and Chrome from DeepSeek Harness using the computer-use runtime that ships inside the ChatGPT desktop application.

This plugin launches that runtime directly and speaks to it over MCP. It is the whole integration: locating and validating the application, building the environment the runtime expects, the approval bridge, the turn lifecycle, and the macOS service wrapper that performs per-turn cleanup. Nothing else has to be installed — no wrapper project, no second interpreter, no Python.

No Codex or ChatGPT authentication is involved. The runtime comes from your local installation and is never downloaded, rewritten or authenticated.

Table of Contents

  • What you get
  • Requirements
  • Install
  • Use
  • Configuration
  • Approvals and the security model
  • Understand the implementation
  • Troubleshooting
  • Relationship to LCU
  • Companion tools
  • Known limitations and deferred work
  • Development
  • License

What you get

Tool · What it does js · Run one JavaScript program against the cua desktop/browser API. The first call returns the API documentation, and app or tab selection returns the initial UI state. js_reset · Discard the persistent JavaScript session and start a fresh runtime.

Two host-only tools stay reachable by the plugin and are never shown to the model: turn_ended (per-turn cleanup) and js_add_node_module_dir.

The plugin adds exactly one tool of its own:

Tool · What it does computer_use_stop · With no argument, list the applications the runtime currently holds for this session. With app set to one of their bundle identifiers, release it. It clears the host application's "computer use is active" state for one application without ending the session.

Screenshots arrive as durable images through DSH's attachment store, so a model route that declares image input can actually look at the screen.

Requirements

· OS · macOS on Apple Silicon. The launcher resolves a macOS application bundle and the lifecycle wrapper is a macOS service; other platforms would need both revisited. ChatGPT desktop app · Installed at /Applications/ChatGPT.app (or configured with app). It supplies the runtime, its instructions and the signed helper. DSH · A profile you can install a bundle into.

There is no Python requirement and no separate runtime to install. The plugin starts the application's own node and its own entry point.

Install

1. Install the plugin into a profile

Installing makes the plugin's one row active in that profile. It opens nothing at load time.

dsh plugin --profile <profile> add dsh-plugin-lcu

For the desktop app's managed profile, the CLI refuses; install it through the app's plugin manager (Settings ▸ Plugins) instead, which runs the same pnpm operation.

2. Generate the presets

DSH agent presets have no inheritance: a preset's config.plugins is its complete plugin list, and a patch replaces a whole entry rather than merging into it. So a custom preset must restate its base — and the base ships inside the application and changes when DSH is upgraded.

That restatement is not part of this package. It serves more than one plugin, and the region it writes is also written by DSH's own settings UI, so it lives on its own:

git clone https://github.com/ckanner/dsh-preset-generator
node dsh-preset-generator/src/gen-presets.mjs --profile ~/.dsh/profiles/<profile>

This writes a marked block into that profile's cordis.patch.yml containing two presets:

Preset · Base · Adds daily · the shipped ptc preset · a Codex delegation provider of its own heavy · the same, with tool-presentation: native · everything above, and this plugin attaches

Re-run it after a DSH upgrade so the copies keep up. --daily-only emits just daily, --dry-run prints without writing, and --out FILE writes somewhere else. The official Codex tool is no longer a per-preset row: since DSH 0.2.1 the official bundle contributes a global subagent_codex, and a separate provider registers its own same-named tool per Agent for the generated presets, shadowing the global one. Two registrations in one scope still collide, so nothing may enable a same-name tool-subagent row beside that provider.

heavy sets the tool presentation to native on purpose. DSH 0.2.1 removed the old both mode —
a preset declares one presentation, native or ptc — and in pure ptc presentation the model
only sees run_code, so js would have to be nested as a JavaScript string inside another
JavaScript program. native keeps js directly callable and gives up PTC for that preset.

3. Configure which modes get the capability

The plugin is one root row with a presets allowlist. Edit the installed cordis.patch.yml (or the profile patch) to match the preset ids you generated:

- id: lcu
  name: 'dsh-plugin-lcu'
  config:
    presets:
      - heavy

4. Restart, then make one call

Restart the harness — plugin code and configuration changes are not hot-reloaded. Then start a task in the heavy mode (displayed as 重活) and ask it to do something harmless:

Use the js tool to run await cua.getState(); and tell me which apps are running.

The first time an app is touched, the runtime asks for approval. See below.

On the first attach the plugin resolves the application and writes what it found to the diagnostic log:

app: /Applications/ChatGPT.app version=26.1002.52244 runtime=0.0.29/20261003001300-807782c586fc
attach: launching /Applications/ChatGPT.app/Contents/Resources/cua_node/bin/node [...]

If the application is missing or one of its files is writable by another account, attach is refused with the reason instead, and the session simply runs without the capability.

Optional: enable Chrome

config:
  chrome: true

This turns on the runtime's browser surface. The browser half of the runtime is OpenAI's, and driving a page happens through the official ChatGPT Chrome extension, which talks to a native messaging host. On a machine where the ChatGPT desktop application is installed that host is already registered, so nothing further is needed: enable the extension in the profile you want to drive, and confirm it appears in cua.getState().

Sites stay exact-origin approvals.

Optional: pre-approve sites

Every site the runtime wants to use asks once. To skip the prompt for origins you trust, list exact origins — the message must round-trip through new URL(...).origin unchanged:

config:
  allowedOrigins:
    - http://localhost:3000

The diagnostic log names every origin that was asked, which is the easiest way to discover them.

Use

Pick an enabled mode when you start a task. The tools are attached per Agent: a session in any other mode never sees them, and never spawns the runtime.

Typical asks:

Take a screenshot of the Finder window and tell me its resolution.
List my current Chrome tabs.
Open Safari, go to example.com and read the page title.

Configuration

Field · Default · Meaning app · /Applications/ChatGPT.app · The application whose runtime provides computer use. command · unset · An explicit executable that replaces the computed launch, for an application the built-in resolution cannot reach. It bypasses the runtime's own environment setup. chrome · false · Enable the browser surface. audio · false · Enable the runtime's computer-audio API. presets · ["heavy"] · Agent preset ids whose sessions get the tools. allowedOrigins · [] · Exact HTTP(S) origins answered without asking. Invalid entries are dropped, never widened. allowedApps · [] · Bundle identifiers computer use may use without asking. The application hosting the agent is refused even when it is listed here. sectionOrder · 0 · Prompt section order for the injected runtime instructions.

Approvals and the security model

The model cannot approve anything. Every decision is a person's:

  • Per-app approval. The runtime asks before it uses an app. The plugin renders its own choices —

*Allow once*, *Allow for this session* and *Always allow* when the runtime offers them, and *Decline* — through DSH's question surface. An answer is mapped back to exactly what was offered; a scope the runtime did not offer cannot be granted.

  • Site approval. Browser access asks per exact origin. allowedOrigins only ever matches an exact

origin; a trailing slash, a path, or different case is a different origin and is asked, not granted.

  • The agent's own host is never approvable. Computer use can click anything an approved app shows,

including the approval prompt itself. The guard refuses the application hosting the agent — its process ancestry and a list of agent hosts and terminals — before any question is asked.

  • Fail closed. No question surface, a dismissed prompt, an unrecognized request shape, or an

aborted call all end as *cancel*, which the runtime treats as a refusal.

The plugin keeps no permission cache of its own; Always allow is remembered by the runtime, per app.

Unattended operation

Every approval belongs to a person, and an unanswered question is a refusal — the runtime does not default to granting. A run with nobody at the keyboard therefore has to have both halves already decided:

config:
  allowedApps:
    - com.google.Chrome        # use this application without asking
  allowedOrigins:
    - https://example.com      # and reach this exact origin without asking
  • allowedApps is matched against the bundle identifier, case-insensitively. The

application hosting the agent is refused before the list is consulted, so no entry can authorize it.

  • allowedOrigins matches an exact origin only; a path, a trailing slash or

different case is a different origin and is still asked.

  • Anything not listed is asked, and with nobody there it is declined.

The diagnostic log names every application and origin that was asked (approval: site https://example.com asking (add it to allowedOrigins to skip this)), which is how to discover the exact values a run needs.

Understand the implementation

src/app.ts          locate and validate the application; build the runtime environment; plan the launch
src/connection.ts   the MCP client: handshake, tool discovery, calls, elicitation, lifecycle
src/approval.ts     approval-shape recognition and label→value mapping
src/host-guard.ts   the anti-self-approval guard
src/tool.ts         tool definitions, text projection, durable screenshots
src/index.ts        the plugin: per-Agent attach, instructions, turn_ended, approvals
src/control.ts      the relay that carries the runtime's control API to and from the wrapper
src/session.ts      the connection, following the application across an update
src/diag.ts         the attach/approval diagnostic log
helper/sky-service.mjs  the runtime's Sky service: the turn-ended hook and the control channel

The runtime is launched directly. src/app.ts finds ChatGPT.app, checks that the pieces it needs are present and not writable by another account, and computes the environment the runtime expects — its own node and node_repl, its module roots, its trusted code paths, the API surface, the signed helper it launches for the macOS native pipe. It does not set NODE_REPL_TRUSTED_SERVICES to the application's default: see below.

The turn-ended hook is the one thing the runtime lacks. A turn that never ends is a turn whose per-application Stop is never released, and the application then refuses every later turn. The runtime exposes addTurnEndedHandler to its trusted services but installs no handler of its own, so helper/sky-service.mjs is loaded as the sky trusted service: it forwards every request to the application's own service unchanged and adds the hook, which asks the application to clean up a finished turn through the application's own signed client.