dsh-plugin-lcu 0.2.9 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/README.md +174 -104
- package/cordis.patch.yml +11 -6
- package/docs/README.zh.md +154 -168
- package/helper/sky-service.mjs +484 -0
- package/lib/app.js +284 -0
- package/lib/app.js.map +1 -0
- package/lib/approval.js +45 -4
- package/lib/approval.js.map +1 -1
- package/lib/connection.js +82 -35
- package/lib/connection.js.map +1 -1
- package/lib/control.js +214 -0
- package/lib/control.js.map +1 -0
- package/lib/diag.js +1 -1
- package/lib/host-guard.js +2 -2
- package/lib/index.js +100 -30
- package/lib/index.js.map +1 -1
- package/lib/session.js +137 -0
- package/lib/session.js.map +1 -0
- package/lib/tool.js +7 -4
- package/lib/tool.js.map +1 -1
- package/lib/types/app.d.ts +118 -0
- package/lib/types/app.d.ts.map +1 -0
- package/lib/types/approval.d.ts +28 -3
- package/lib/types/approval.d.ts.map +1 -1
- package/lib/types/connection.d.ts +27 -13
- package/lib/types/connection.d.ts.map +1 -1
- package/lib/types/control.d.ts +54 -0
- package/lib/types/control.d.ts.map +1 -0
- package/lib/types/diag.d.ts +1 -1
- package/lib/types/host-guard.d.ts +2 -2
- package/lib/types/index.d.ts +26 -6
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/session.d.ts +89 -0
- package/lib/types/session.d.ts.map +1 -0
- package/lib/types/tool.d.ts +6 -5
- package/lib/types/tool.d.ts.map +1 -1
- package/package.json +8 -6
- package/scripts/probe-lcu.mjs +40 -18
- package/scripts/gen-presets.mjs +0 -266
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.4
|
|
4
|
+
|
|
5
|
+
**The plugin no longer installs, invokes or depends on LCU.** It locates and validates the ChatGPT application
|
|
6
|
+
itself, builds the environment the computer-use runtime expects, and starts the application's own entry point.
|
|
7
|
+
|
|
8
|
+
- **Direct launch.** `src/app.ts` resolves the application, refuses files another account could replace, and
|
|
9
|
+
computes the runtime environment — its own `node` and `node_repl`, its module roots, trusted code paths, the
|
|
10
|
+
API surface, and the signed helper the macOS native pipe launches. No second interpreter: the Python 3.12+
|
|
11
|
+
requirement is gone, and attaching is roughly an order of magnitude faster.
|
|
12
|
+
- **Turn cleanup through the application's own IPC.** `helper/sky-service.mjs` is loaded as the runtime's `sky`
|
|
13
|
+
trusted service. It forwards every request to the application's service unchanged and adds the one thing the
|
|
14
|
+
runtime has no handler for: the turn-ended hook that releases a per-application Stop. It deliberately omits
|
|
15
|
+
the step that signalled a supervisor process and spawned the signed client with a `turn-ended` argument —
|
|
16
|
+
that needs Apple Events, which macOS refuses a hardened-runtime harness, so it always timed out. The
|
|
17
|
+
application's own IPC needs no Apple Events and settles in tens of milliseconds.
|
|
18
|
+
- **A Stop is released by the turn that asked for it.** `computer_use_stop` now works: the plugin serves the
|
|
19
|
+
runtime's control channel and relays `status` and `stop` to the wrapper, which answers them from inside the
|
|
20
|
+
runtime. Verified end to end — a Stop refuses the application for the turn that asked for it, and the next
|
|
21
|
+
turn can use it again.
|
|
22
|
+
- **A long session stays on one generation.** The runtime is executed from files the application replaces when
|
|
23
|
+
it updates, so a facade compares the application's fingerprint before every call and reconnects when it
|
|
24
|
+
changes, carrying the session identity across the replacement.
|
|
25
|
+
- **The preset generator moved out.** It generates presets that serve more than one plugin, and the region it
|
|
26
|
+
writes is also written by DSH's settings UI, so it is now
|
|
27
|
+
[its own project](https://github.com/ckanner/dsh-preset-generator). `scripts/gen-presets.mjs` and the
|
|
28
|
+
`presets` script are gone from this package.
|
|
29
|
+
- Fixed: a per-connection temporary directory leaked on every path that had to signal the child rather than
|
|
30
|
+
have it exit on stdin.
|
|
31
|
+
- Fixed: an unreadable application fingerprint was treated as a change, which started a second runtime for a
|
|
32
|
+
bundle that was merely unreadable for a moment mid-update.
|
|
33
|
+
|
|
34
|
+
### A note on the version numbers
|
|
35
|
+
|
|
36
|
+
`v0.3.0` through `v0.3.3` are **git tags**, each with its own content, and npm's `0.3.0` is not any of them:
|
|
37
|
+
it was published from the tree *after* `v0.3.3`, and so contains everything below plus the work above. The
|
|
38
|
+
next number that is unambiguous is `0.3.4`, which is this release.
|
|
39
|
+
|
|
40
|
+
## 0.3.3
|
|
41
|
+
|
|
42
|
+
Reverted the Codex model-selection change from 0.3.2. `modelSelectionSettings: true` cannot be enabled on the
|
|
43
|
+
Codex row: the provider declares `NO_START_CAPABILITIES`, which has no `agentOptions`, and `tool-subagent`
|
|
44
|
+
asserts that pairing at load — the row vanished with no tool and no error. The "never drop a preset" guardrail
|
|
45
|
+
from 0.3.2 is kept.
|
|
46
|
+
|
|
47
|
+
## 0.3.2
|
|
48
|
+
|
|
49
|
+
Added the guardrail that a run never drops a preset the profile already defines, and `--daily-only` refuses when
|
|
50
|
+
the file already has `heavy`. Also attempted the Codex model surface, reverted in 0.3.3.
|
|
51
|
+
|
|
52
|
+
## 0.3.1
|
|
53
|
+
|
|
54
|
+
The `apply` log line records the two lists that decide whether an unattended run can proceed, so an empty
|
|
55
|
+
allowlist can be told from a populated one without opening the patch layers.
|
|
56
|
+
|
|
57
|
+
## 0.3.0
|
|
58
|
+
|
|
59
|
+
An application can be pre-approved with `allowedApps`, so an unattended run — where an unanswered question is a
|
|
60
|
+
refusal — can proceed without a person at the keyboard.
|
package/README.md
CHANGED
|
@@ -2,14 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
English | [中文](docs/README.zh.md)
|
|
4
4
|
|
|
5
|
-
Drive the desktop and Chrome from [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)
|
|
6
|
-
|
|
5
|
+
Drive the desktop and Chrome from [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) using the
|
|
6
|
+
computer-use runtime that ships **inside the ChatGPT desktop application**.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
This plugin launches that runtime directly and speaks to it over MCP. It is the whole integration: locating and
|
|
9
|
+
validating the application, building the environment the runtime expects, the approval bridge, the turn
|
|
10
|
+
lifecycle, and the macOS service wrapper that performs per-turn cleanup. **Nothing else has to be installed** —
|
|
11
|
+
no wrapper project, no second interpreter, no Python.
|
|
12
|
+
|
|
13
|
+
**No Codex or ChatGPT authentication is involved.** The runtime comes from your local installation and is never
|
|
14
|
+
downloaded, rewritten or authenticated.
|
|
13
15
|
|
|
14
16
|
## Table of Contents
|
|
15
17
|
|
|
@@ -21,6 +23,7 @@ downloads, installs, authenticates, or rewrites it.
|
|
|
21
23
|
- [Approvals and the security model](#approvals-and-the-security-model)
|
|
22
24
|
- [Understand the implementation](#understand-the-implementation)
|
|
23
25
|
- [Troubleshooting](#troubleshooting)
|
|
26
|
+
- [Relationship to LCU](#relationship-to-lcu)
|
|
24
27
|
- [Companion tools](#companion-tools)
|
|
25
28
|
- [Known limitations and deferred work](#known-limitations-and-deferred-work)
|
|
26
29
|
- [Development](#development)
|
|
@@ -28,8 +31,6 @@ downloads, installs, authenticates, or rewrites it.
|
|
|
28
31
|
|
|
29
32
|
## What you get
|
|
30
33
|
|
|
31
|
-
Two model-facing tools, exactly as LCU defines them — this plugin does not invent a schema:
|
|
32
|
-
|
|
33
34
|
| Tool | What it does |
|
|
34
35
|
|---|---|
|
|
35
36
|
| `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. |
|
|
@@ -42,7 +43,7 @@ The plugin adds exactly one tool of its own:
|
|
|
42
43
|
|
|
43
44
|
| Tool | What it does |
|
|
44
45
|
|---|---|
|
|
45
|
-
| `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.
|
|
46
|
+
| `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. |
|
|
46
47
|
|
|
47
48
|
Screenshots arrive as durable images through DSH's attachment store, so a model route that declares
|
|
48
49
|
image input can actually look at the screen.
|
|
@@ -51,45 +52,18 @@ image input can actually look at the screen.
|
|
|
51
52
|
|
|
52
53
|
| | |
|
|
53
54
|
|---|---|
|
|
54
|
-
| OS | macOS on Apple Silicon
|
|
55
|
-
| ChatGPT desktop app | Installed
|
|
56
|
-
| Python | 3.12 or newer, on `PATH` or in `/opt/homebrew/bin`, `/usr/local/bin`, `/usr/bin`. |
|
|
57
|
-
| LCU | Installed separately — see below. |
|
|
55
|
+
| 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. |
|
|
56
|
+
| ChatGPT desktop app | Installed at `/Applications/ChatGPT.app` (or configured with `app`). It supplies the runtime, its instructions and the signed helper. |
|
|
58
57
|
| DSH | A profile you can install a bundle into. |
|
|
59
58
|
|
|
60
|
-
|
|
61
|
-
|
|
59
|
+
There is **no** Python requirement and **no** separate runtime to install. The plugin starts the application's
|
|
60
|
+
own `node` and its own entry point.
|
|
62
61
|
|
|
63
62
|
## Install
|
|
64
63
|
|
|
65
|
-
### 1. Install
|
|
66
|
-
|
|
67
|
-
Download the release archive for your platform, verify its checksum, and run its installer. Do **not**
|
|
68
|
-
register another harness: this plugin is your harness.
|
|
69
|
-
|
|
70
|
-
```sh
|
|
71
|
-
TAG=v0.9.6
|
|
72
|
-
TARGET=darwin-arm64
|
|
73
|
-
curl -fLO "https://github.com/amontlabs/lcu/releases/download/$TAG/lcu-${TAG#v}-$TARGET.tar.gz"
|
|
74
|
-
curl -fLO "https://github.com/amontlabs/lcu/releases/download/$TAG/lcu-${TAG#v}-$TARGET.tar.gz.sha256"
|
|
75
|
-
shasum -a 256 -c "lcu-${TAG#v}-$TARGET.tar.gz.sha256" # must print OK
|
|
76
|
-
tar -xzf "lcu-${TAG#v}-$TARGET.tar.gz" && cd "lcu-${TAG#v}-$TARGET"
|
|
77
|
-
./scripts/install.sh --runtime-only --yes
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
Then confirm the runtime loads:
|
|
81
|
-
|
|
82
|
-
```sh
|
|
83
|
-
~/.local/share/lcu/current/bin/lcu doctor --non-interactive
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
You want `Original Mac provider loaded; app listing and app-state methods are available`. Privacy
|
|
87
|
-
permissions are granted on first use, not here.
|
|
88
|
-
|
|
89
|
-
### 2. Install the plugin into a profile
|
|
64
|
+
### 1. Install the plugin into a profile
|
|
90
65
|
|
|
91
|
-
Installing makes the plugin's one row
|
|
92
|
-
time.
|
|
66
|
+
Installing makes the plugin's one row active in that profile. It opens nothing at load time.
|
|
93
67
|
|
|
94
68
|
```sh
|
|
95
69
|
dsh plugin --profile <profile> add dsh-plugin-lcu
|
|
@@ -98,34 +72,40 @@ dsh plugin --profile <profile> add dsh-plugin-lcu
|
|
|
98
72
|
For the **desktop** app's managed profile, the CLI refuses; install it through the app's plugin
|
|
99
73
|
manager (Settings ▸ Plugins) instead, which runs the same pnpm operation.
|
|
100
74
|
|
|
101
|
-
###
|
|
75
|
+
### 2. Generate the presets
|
|
102
76
|
|
|
103
77
|
DSH agent presets have **no inheritance**: a preset's `config.plugins` is its complete plugin list, and
|
|
104
|
-
a patch replaces a whole entry rather than merging into it. So a custom preset must restate its base
|
|
105
|
-
|
|
78
|
+
a patch replaces a whole entry rather than merging into it. So a custom preset must restate its base —
|
|
79
|
+
and the base ships inside the application and changes when DSH is upgraded.
|
|
80
|
+
|
|
81
|
+
That restatement is **not part of this package**. It serves more than one plugin, and the region it writes
|
|
82
|
+
is also written by DSH's own settings UI, so it lives on its own:
|
|
106
83
|
|
|
107
84
|
```sh
|
|
108
|
-
|
|
85
|
+
git clone https://github.com/ckanner/dsh-preset-generator
|
|
86
|
+
node dsh-preset-generator/src/gen-presets.mjs --profile ~/.dsh/profiles/<profile>
|
|
109
87
|
```
|
|
110
88
|
|
|
111
89
|
This writes a marked block into that profile's `cordis.patch.yml` containing two presets:
|
|
112
90
|
|
|
113
91
|
| Preset | Base | Adds |
|
|
114
92
|
|---|---|---|
|
|
115
|
-
| `daily` | the shipped `ptc` preset |
|
|
93
|
+
| `daily` | the shipped `ptc` preset | a Codex delegation provider of its own |
|
|
116
94
|
| `heavy` | the same, with `tool-presentation: both` | everything above, and this plugin attaches |
|
|
117
95
|
|
|
118
|
-
Re-run it after a DSH upgrade so the copies keep up. `--
|
|
119
|
-
without writing, and `--out FILE` writes somewhere else.
|
|
96
|
+
Re-run it after a DSH upgrade so the copies keep up. `--daily-only` emits just `daily`, `--dry-run`
|
|
97
|
+
prints without writing, and `--out FILE` writes somewhere else. The official `tool-subagent-codex` row
|
|
98
|
+
stays disabled, as the base ships it: a separate provider registers its own `subagent_codex`, and two
|
|
99
|
+
rows registering that name in one scope collide.
|
|
120
100
|
|
|
121
101
|
> `heavy` sets the tool presentation to `both` on purpose. In pure `ptc` presentation the model only
|
|
122
102
|
> sees `run_code`, so `js` would have to be nested as a JavaScript string inside another JavaScript
|
|
123
103
|
> program. `both` keeps `js` directly callable.
|
|
124
104
|
|
|
125
|
-
###
|
|
105
|
+
### 3. Configure which modes get the capability
|
|
126
106
|
|
|
127
|
-
The plugin is one root row with a `presets` allowlist. Edit the installed
|
|
128
|
-
|
|
107
|
+
The plugin is one root row with a `presets` allowlist. Edit the installed `cordis.patch.yml`
|
|
108
|
+
(or the profile patch) to match the preset ids you generated:
|
|
129
109
|
|
|
130
110
|
```yaml
|
|
131
111
|
- id: lcu
|
|
@@ -135,7 +115,7 @@ The plugin is one root row with a `presets` allowlist. Edit the installed
|
|
|
135
115
|
- heavy
|
|
136
116
|
```
|
|
137
117
|
|
|
138
|
-
###
|
|
118
|
+
### 4. Restart, then make one call
|
|
139
119
|
|
|
140
120
|
Restart the harness — plugin **code and configuration changes are not hot-reloaded**. Then start a
|
|
141
121
|
task in the `heavy` mode (displayed as **重活**) and ask it to do something harmless:
|
|
@@ -144,6 +124,16 @@ task in the `heavy` mode (displayed as **重活**) and ask it to do something ha
|
|
|
144
124
|
|
|
145
125
|
The first time an app is touched, the runtime asks for approval. See below.
|
|
146
126
|
|
|
127
|
+
On the first attach the plugin resolves the application and writes what it found to the diagnostic log:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
app: /Applications/ChatGPT.app version=26.1002.52244 runtime=0.0.29/20261003001300-807782c586fc
|
|
131
|
+
attach: launching /Applications/ChatGPT.app/Contents/Resources/cua_node/bin/node [...]
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
If the application is missing or one of its files is writable by another account, attach is refused with the
|
|
135
|
+
reason instead, and the session simply runs without the capability.
|
|
136
|
+
|
|
147
137
|
### Optional: enable Chrome
|
|
148
138
|
|
|
149
139
|
```yaml
|
|
@@ -151,16 +141,12 @@ config:
|
|
|
151
141
|
chrome: true
|
|
152
142
|
```
|
|
153
143
|
|
|
154
|
-
|
|
144
|
+
This turns on the runtime's browser surface. The browser half of the runtime is OpenAI's, and driving a page
|
|
145
|
+
happens through the **official ChatGPT Chrome extension**, which talks to a native messaging host. On a machine
|
|
146
|
+
where the ChatGPT desktop application is installed that host is already registered, so nothing further is
|
|
147
|
+
needed: enable the extension in the profile you want to drive, and confirm it appears in `cua.getState()`.
|
|
155
148
|
|
|
156
|
-
|
|
157
|
-
~/.local/share/lcu/current/bin/lcu browser install
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
Enable the official ChatGPT extension in the Chrome profile you want to drive, and restart Chrome (or
|
|
161
|
-
toggle the extension at `chrome://extensions`) so it reconnects through LCU's relay rather than Codex's.
|
|
162
|
-
`lcu browser status` reports whether the connector points at this installation. Sites stay
|
|
163
|
-
exact-origin approvals.
|
|
149
|
+
Sites stay exact-origin approvals.
|
|
164
150
|
|
|
165
151
|
### Optional: pre-approve sites
|
|
166
152
|
|
|
@@ -192,12 +178,14 @@ Open Safari, go to example.com and read the page title.
|
|
|
192
178
|
|
|
193
179
|
| Field | Default | Meaning |
|
|
194
180
|
|---|---|---|
|
|
195
|
-
| `
|
|
196
|
-
| `
|
|
197
|
-
| `
|
|
181
|
+
| `app` | `/Applications/ChatGPT.app` | The application whose runtime provides computer use. |
|
|
182
|
+
| `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. |
|
|
183
|
+
| `chrome` | `false` | Enable the browser surface. |
|
|
184
|
+
| `audio` | `false` | Enable the runtime's computer-audio API. |
|
|
198
185
|
| `presets` | `["heavy"]` | Agent preset ids whose sessions get the tools. |
|
|
199
186
|
| `allowedOrigins` | `[]` | Exact HTTP(S) origins answered without asking. Invalid entries are dropped, never widened. |
|
|
200
|
-
| `
|
|
187
|
+
| `allowedApps` | `[]` | Bundle identifiers computer use may use without asking. The application hosting the agent is refused even when it is listed here. |
|
|
188
|
+
| `sectionOrder` | `0` | Prompt section order for the injected runtime instructions. |
|
|
201
189
|
|
|
202
190
|
## Approvals and the security model
|
|
203
191
|
|
|
@@ -215,35 +203,95 @@ The model cannot approve anything. Every decision is a person's:
|
|
|
215
203
|
- **Fail closed.** No question surface, a dismissed prompt, an unrecognized request shape, or an
|
|
216
204
|
aborted call all end as *cancel*, which the runtime treats as a refusal.
|
|
217
205
|
|
|
218
|
-
|
|
206
|
+
The plugin keeps no permission cache of its own; `Always allow` is remembered by the runtime, per app.
|
|
207
|
+
|
|
208
|
+
### Unattended operation
|
|
209
|
+
|
|
210
|
+
Every approval belongs to a person, and an unanswered question is a **refusal** —
|
|
211
|
+
the runtime does not default to granting. A run with nobody at the keyboard
|
|
212
|
+
therefore has to have both halves already decided:
|
|
213
|
+
|
|
214
|
+
```yaml
|
|
215
|
+
config:
|
|
216
|
+
allowedApps:
|
|
217
|
+
- com.google.Chrome # use this application without asking
|
|
218
|
+
allowedOrigins:
|
|
219
|
+
- https://example.com # and reach this exact origin without asking
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
- `allowedApps` is matched against the bundle identifier, case-insensitively. The
|
|
223
|
+
application hosting the agent is refused **before** the list is consulted, so no
|
|
224
|
+
entry can authorize it.
|
|
225
|
+
- `allowedOrigins` matches an exact origin only; a path, a trailing slash or
|
|
226
|
+
different case is a different origin and is still asked.
|
|
227
|
+
- Anything not listed is asked, and with nobody there it is declined.
|
|
228
|
+
|
|
229
|
+
The diagnostic log names every application and origin that was asked
|
|
230
|
+
(`approval: site https://example.com asking (add it to allowedOrigins to skip this)`),
|
|
231
|
+
which is how to discover the exact values a run needs.
|
|
219
232
|
|
|
220
233
|
## Understand the implementation
|
|
221
234
|
|
|
222
235
|
```
|
|
236
|
+
src/app.ts locate and validate the application; build the runtime environment; plan the launch
|
|
223
237
|
src/connection.ts the MCP client: handshake, tool discovery, calls, elicitation, lifecycle
|
|
224
238
|
src/approval.ts approval-shape recognition and label→value mapping
|
|
225
239
|
src/host-guard.ts the anti-self-approval guard
|
|
226
240
|
src/tool.ts tool definitions, text projection, durable screenshots
|
|
227
241
|
src/index.ts the plugin: per-Agent attach, instructions, turn_ended, approvals
|
|
242
|
+
src/control.ts the relay that carries the runtime's control API to and from the wrapper
|
|
243
|
+
src/session.ts the connection, following the application across an update
|
|
228
244
|
src/diag.ts the attach/approval diagnostic log
|
|
245
|
+
helper/sky-service.mjs the runtime's Sky service: the turn-ended hook and the control channel
|
|
229
246
|
```
|
|
230
247
|
|
|
231
|
-
**
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
248
|
+
**The runtime is launched directly.** `src/app.ts` finds `ChatGPT.app`, checks that the pieces it needs are
|
|
249
|
+
present and not writable by another account, and computes the environment the runtime expects — its own `node`
|
|
250
|
+
and `node_repl`, its module roots, its trusted code paths, the API surface, the signed helper it launches for
|
|
251
|
+
the macOS native pipe. It does not set `NODE_REPL_TRUSTED_SERVICES` to the application's default: see below.
|
|
252
|
+
|
|
253
|
+
**The turn-ended hook is the one thing the runtime lacks.** A turn that never ends is a turn whose
|
|
254
|
+
per-application Stop is never released, and the application then refuses every later turn. The runtime exposes
|
|
255
|
+
`addTurnEndedHandler` to its trusted services but installs no handler of its own, so
|
|
256
|
+
`helper/sky-service.mjs` is loaded **as** the `sky` trusted service: it forwards every request to the
|
|
257
|
+
application's own service unchanged and adds the hook, which asks the application to clean up a finished turn
|
|
258
|
+
through the application's own signed client.
|
|
259
|
+
|
|
260
|
+
That is deliberately less than the obvious implementation. Signalling a supervisor process, which then spawns
|
|
261
|
+
the signed client with a `turn-ended` argument, needs **Apple Events** — which macOS refuses to grant, and
|
|
262
|
+
refuses to even prompt for, when a hardened-runtime harness is the responsible process. That step always times
|
|
263
|
+
out. The application's own IPC is the step that performs the cleanup, it needs no Apple Events, and it settles
|
|
264
|
+
in tens of milliseconds. There is no supervisor process, and no lifetime socket.
|
|
265
|
+
|
|
266
|
+
**The control channel is a relay, and the plugin serves it.** Releasing one application early needs the
|
|
267
|
+
runtime's control API, which lives inside the runtime's process. The wrapper connects to a socket the plugin
|
|
268
|
+
serves, announces itself as the *service*, reports the turn contexts it has seen, and answers `status` and
|
|
269
|
+
`stop` requests there. The relay decides nothing: whether a session and turn are real, and whether an
|
|
270
|
+
application is actually held, are questions only the wrapper can answer, because only it has the turn
|
|
271
|
+
metadata the runtime keys that state by.
|
|
272
|
+
|
|
273
|
+
Two details of that channel are not guessable. The wrapper runs inside the runtime's JavaScript sandbox, which
|
|
274
|
+
refuses an ordinary socket connection with `EPERM` — for the per-user temporary directory and `/private/tmp`
|
|
275
|
+
alike — so it connects through the runtime's own `nativePipe` API instead. And that pipe **silently drops a
|
|
276
|
+
string write**; messages have to be written as buffers, which is easy to mistake for a peer that never
|
|
277
|
+
answered.
|
|
278
|
+
|
|
279
|
+
**No MCP SDK dependency.** The harness's own MCP bridge declares `capabilities: {}` and therefore cannot answer
|
|
280
|
+
elicitation — which is exactly how the runtime asks for approval — and pulling a second SDK into a profile
|
|
281
|
+
plugin would pin a version the host does not own. MCP over stdio is newline-delimited JSON-RPC, so the wire is
|
|
282
|
+
owned here. Together with type-only imports of the DSH packages, the plugin has **no runtime dependencies at
|
|
283
|
+
all**.
|
|
236
284
|
|
|
237
285
|
**Tools are registered per Agent, not at mount.** The server owns the tool schemas, so they can only be
|
|
238
|
-
fetched after the handshake. An Agent's connection is opened when the Agent is created or when it
|
|
239
|
-
|
|
240
|
-
|
|
286
|
+
fetched after the handshake. An Agent's connection is opened when the Agent is created or when it commits a
|
|
287
|
+
preset choice, and everything the plugin contributes is registered into that Agent's own context, so it unwinds
|
|
288
|
+
on disposal.
|
|
241
289
|
|
|
242
290
|
**Both preset timings are handled.** A new task is created with the deployment default and the picker's
|
|
243
291
|
choice is applied afterwards, so `agent/created` alone would see the wrong composition; the registry
|
|
244
292
|
re-emits `agent-preset/selected`, and the plugin reacts to that too.
|
|
245
293
|
|
|
246
|
-
**Lazy by construction.** Nothing starts at load time. No enabled session, no
|
|
294
|
+
**Lazy by construction.** Nothing starts at load time. No enabled session, no runtime process.
|
|
247
295
|
|
|
248
296
|
**Instructions are injected.** The server's `initialize.instructions` becomes a prompt section on the
|
|
249
297
|
Agent. It is short by design — the API manual lives in the `js` tool description and in the first tool
|
|
@@ -251,7 +299,7 @@ result.
|
|
|
251
299
|
|
|
252
300
|
## Troubleshooting
|
|
253
301
|
|
|
254
|
-
Everything the plugin decides about attaching and approving is appended to:
|
|
302
|
+
Everything the plugin decides about attaching, launching and approving is appended to:
|
|
255
303
|
|
|
256
304
|
```
|
|
257
305
|
~/.dsh/lcu-diag.log
|
|
@@ -264,17 +312,35 @@ otherwise swallowed silently.
|
|
|
264
312
|
| Symptom | Cause and fix |
|
|
265
313
|
|---|---|
|
|
266
314
|
| Tools never appear in an enabled mode | Check the log for `decide … composed=`. If the composed preset is not in `presets`, fix the allowlist. If there is no `agent-preset/selected` line, the mode was never committed. |
|
|
315
|
+
| `computer use is unavailable (…)` at load, or no tools | The application could not be resolved. The log names the reason and the path; check `app`. |
|
|
267
316
|
| `no userQuestions service -> cancel (fail closed)` | The approval surface is not mounted in this profile. |
|
|
268
317
|
| `refusing to approve the app hosting this agent` | Working as designed; ask for a different app. |
|
|
269
|
-
|
|
|
270
|
-
|
|
|
271
|
-
|
|
|
272
|
-
|
|
273
|
-
|
|
318
|
+
| The ChatGPT app still shows computer use on an app | Ask for `computer_use_stop`, or close the session — the connection owns the runtime process tree and releases it on the way out. |
|
|
319
|
+
| Chrome tabs never appear with `chrome: true` | The official extension is not enabled in that browser profile. Open `chrome://extensions`, enable it, and confirm it shows up in `cua.getState()`. |
|
|
320
|
+
| An application refuses every turn with "explicitly stopped by the user" | The host application's turn cleanup has not run. Check the log for a `turn cleanup` failure, and quit and relaunch the ChatGPT application to clear the state. |
|
|
321
|
+
|
|
322
|
+
`node scripts/probe-lcu.mjs` starts the runtime with no harness involved and prints the protocol version,
|
|
323
|
+
server identity, instructions length and the tool list — useful to separate a plugin problem from a runtime
|
|
324
|
+
problem.
|
|
325
|
+
|
|
326
|
+
`helper/sky-service.mjs` has three diagnostic switches, all default off, because it runs where the obvious
|
|
327
|
+
channels do not work: the runtime's JavaScript sandbox denies file writes, and it captures console output.
|
|
328
|
+
`DSH_SKY_DEBUG=1` traces the hook, `DSH_SKY_REPORT=1` makes the next call fail with the last cleanup outcome,
|
|
329
|
+
and `DSH_SKY_FORCE_ERROR=1` proves the module loaded at all.
|
|
330
|
+
|
|
331
|
+
## Relationship to LCU
|
|
332
|
+
|
|
333
|
+
[LCU](https://github.com/amontlabs/lcu) (MIT, Amont Labs) is the project that showed this was possible:
|
|
334
|
+
*Codex computer use, decoupled from the app*. It locates the same runtime and exposes it over MCP.
|
|
335
|
+
|
|
336
|
+
This plugin does that job itself, and no longer installs or invokes LCU. The differences that matter:
|
|
274
337
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
338
|
+
- **No second interpreter.** LCU's launcher is Python and requires 3.12+; this is TypeScript, and its only
|
|
339
|
+
requirement is the application itself. Attach is roughly an order of magnitude faster for it.
|
|
340
|
+
- **No supervisor process, and no Apple Events.** The turn cleanup goes through the application's own IPC
|
|
341
|
+
instead of a supervisor that shells out to the signed client.
|
|
342
|
+
- **Diagnostics at the moment of failure.** The application is resolved and reported at load, and every
|
|
343
|
+
attach, launch and approval decision is one line in the log.
|
|
278
344
|
|
|
279
345
|
## Companion tools
|
|
280
346
|
|
|
@@ -291,16 +357,15 @@ usually wants both:
|
|
|
291
357
|
## Known limitations and deferred work
|
|
292
358
|
|
|
293
359
|
- **A delegated child cannot be asked for approval.** DSH only accepts a human answer for a live
|
|
294
|
-
runtime root, so a subagent's
|
|
360
|
+
runtime root, so a subagent's approval fails closed. Subagents can perform read-only work that
|
|
295
361
|
needs no approval; anything that needs one must be driven from the top-level session.
|
|
296
|
-
- **A per-application Stop
|
|
297
|
-
host application's "using your computer" banner — ask the runtime to stop using one application
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
description says so, so a model does not stop an application on its own initiative.
|
|
362
|
+
- **A per-application Stop is released by the turn that asked for it.** `computer_use_stop` — and pressing
|
|
363
|
+
Esc on the host application's "using your computer" banner — ask the runtime to stop using one application
|
|
364
|
+
for the current turn. The plugin performs the host application's turn-ended cleanup through its own IPC, so
|
|
365
|
+
the next turn can use that application again, and `computer_use_stop` with no argument reports what is held.
|
|
366
|
+
If an application is still refused every turn, that cleanup failed and the log says so. **Ordinary use —
|
|
367
|
+
screenshots, clicks, typing, browser tabs — is unaffected**; the plugin's tool description says so, so a
|
|
368
|
+
model does not stop an application on its own initiative.
|
|
304
369
|
- **The `js` sandbox cannot write files.** Every write fails with `EPERM`, including in the temporary
|
|
305
370
|
directory, so a screenshot cannot be saved from inside `js`. Images are delivered to the harness as
|
|
306
371
|
attachments instead, and each stored image reports its host filesystem path in the tool result;
|
|
@@ -312,11 +377,15 @@ usually wants both:
|
|
|
312
377
|
Screen Recording and Accessibility must therefore be enabled for **DeepSeek Harness** in System
|
|
313
378
|
Settings; granting them only to ChatGPT or to "Codex Computer Use" is not enough, and macOS will not
|
|
314
379
|
prompt for the missing ones on its own.
|
|
315
|
-
- **
|
|
380
|
+
- **A browser surface without a Codex account is not wired up.** Driving a page works through the native
|
|
381
|
+
messaging host the desktop application registers. The relay that makes it work on a machine with no Codex
|
|
382
|
+
app present — forcing the extension's agent-request header — is not implemented.
|
|
383
|
+
- **The application updating under a running session is only warned about.** The runtime is executed from
|
|
384
|
+
files the application replaces when it updates, so a long session can run a mix of two generations.
|
|
385
|
+
Restart the harness after the application updates.
|
|
386
|
+
- **One connection per Agent.** The runtime's JavaScript session is per connection and its approvals are
|
|
316
387
|
bound to a real session and turn, so sharing one connection across Agents would interleave both.
|
|
317
|
-
-
|
|
318
|
-
`lcu browser install`, yields no browser surface.
|
|
319
|
-
- **Not verified on Linux.** See [Requirements](#requirements).
|
|
388
|
+
- **Not verified on Linux or Windows.** See [Requirements](#requirements).
|
|
320
389
|
|
|
321
390
|
## Development
|
|
322
391
|
|
|
@@ -327,14 +396,15 @@ npm run build # emits lib/
|
|
|
327
396
|
npm test # node --test, no build step
|
|
328
397
|
```
|
|
329
398
|
|
|
330
|
-
The connection
|
|
331
|
-
`npm test` is meaningful locally and still passes on CI. The
|
|
332
|
-
pure and always run.
|
|
399
|
+
The connection and launcher suites talk to the **real** installed computer-use runtime and skip themselves
|
|
400
|
+
when the application is not present, so `npm test` is meaningful locally and still passes on CI. The
|
|
401
|
+
approval, projection, guard and wrapper suites are pure and always run.
|
|
333
402
|
|
|
334
403
|
Plugin code and configuration are **not hot-reloaded** by the harness: a running process keeps the
|
|
335
404
|
module it loaded. Rebuild and restart to see a change.
|
|
336
405
|
|
|
337
406
|
## License
|
|
338
407
|
|
|
339
|
-
MIT.
|
|
340
|
-
terms and are used from
|
|
408
|
+
MIT. Nothing is vendored: the native half is the application's own, and this package has no runtime
|
|
409
|
+
dependencies. The ChatGPT application and its instructions remain under their own terms and are used from
|
|
410
|
+
your local installation.
|
package/cordis.patch.yml
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
# The dsh-plugin-lcu bundle patch.
|
|
2
2
|
#
|
|
3
|
-
# One row. The plugin owns every
|
|
4
|
-
# computer-use tools per Agent; it starts nothing at load time, so a
|
|
5
|
-
# for the
|
|
3
|
+
# One row. The plugin owns every runtime connection and registers the
|
|
4
|
+
# model-facing computer-use tools per Agent; it starts nothing at load time, so a
|
|
5
|
+
# session pays for the runtime only when its Agent preset is listed here.
|
|
6
6
|
#
|
|
7
7
|
# `presets` is an explicit allowlist rather than a capability the preset
|
|
8
8
|
# declares, because the server owns the tool schemas: they can only be fetched
|
|
9
9
|
# after the MCP handshake, which happens once a matching Agent is created.
|
|
10
10
|
#
|
|
11
|
-
# `chrome: true` enables
|
|
12
|
-
#
|
|
13
|
-
#
|
|
11
|
+
# `chrome: true` enables the runtime's browser surface. It also needs the official
|
|
12
|
+
# ChatGPT extension enabled in the browser profile you want to drive; sites stay
|
|
13
|
+
# exact-origin approvals.
|
|
14
14
|
|
|
15
15
|
- insert:
|
|
16
16
|
- id: lcu
|
|
@@ -19,5 +19,10 @@
|
|
|
19
19
|
presets:
|
|
20
20
|
- heavy
|
|
21
21
|
chrome: true
|
|
22
|
+
# Approvals are a person's, and an unanswered question is a refusal, so an
|
|
23
|
+
# unattended run needs both halves decided here. The diagnostic log names
|
|
24
|
+
# every application and origin that was asked, which is how to find them.
|
|
25
|
+
# allowedApps:
|
|
26
|
+
# - com.google.Chrome
|
|
22
27
|
# allowedOrigins:
|
|
23
28
|
# - http://localhost:3000
|