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/lib/app.js ADDED
@@ -0,0 +1,284 @@
1
+ /**
2
+ * The ChatGPT application, and the runtime it carries.
3
+ *
4
+ * The computer-use provider is the original one inside the application: this
5
+ * module locates it, checks the pieces it needs are present and not writable by
6
+ * anyone else, and builds the environment the runtime expects. Nothing here
7
+ * launches a third-party wrapper — the launcher is
8
+ * `.../@oai/cua-repl/bin/cua-repl.mjs` inside the application itself.
9
+ *
10
+ * The environment is not guesswork. Every variable below is one the original
11
+ * launcher sets, and each is named in a comment for the thing it selects.
12
+ *
13
+ * @module dsh-plugin-lcu/app
14
+ */
15
+ import { accessSync, constants, existsSync, readFileSync, statSync } from 'node:fs';
16
+ import { homedir } from 'node:os';
17
+ import { dirname, join, resolve } from 'node:path';
18
+ import { fileURLToPath } from 'node:url';
19
+ import { diag } from "./diag.js";
20
+ /** The default installation the original app uses. */
21
+ const DEFAULT_APP = '/Applications/ChatGPT.app';
22
+ /**
23
+ * The Sky service module this plugin loads instead of the application's own.
24
+ *
25
+ * It forwards to the application unchanged and adds the turn-ended hook the
26
+ * runtime has no handler for. See its own header for why that hook matters and
27
+ * what it deliberately leaves out.
28
+ */
29
+ export function skyServicePath() {
30
+ return join(dirname(fileURLToPath(import.meta.url)), '..', 'helper', 'sky-service.mjs');
31
+ }
32
+ /** A failure that names the application as its source. */
33
+ export class AppError extends Error {
34
+ constructor(message) {
35
+ super(message);
36
+ this.name = 'AppError';
37
+ }
38
+ }
39
+ function exists(path) {
40
+ try {
41
+ return statSync(path).isFile();
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ }
47
+ function isDirectory(path) {
48
+ try {
49
+ return statSync(path).isDirectory();
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ }
55
+ /** Read a plist string without depending on `plutil`. */
56
+ function plistValue(plist, key) {
57
+ try {
58
+ const text = readFileSync(plist, 'utf8');
59
+ const match = new RegExp(`<key>${key}</key>\\s*<string>([^<]*)</string>`).exec(text);
60
+ return match?.[1];
61
+ }
62
+ catch {
63
+ return undefined;
64
+ }
65
+ }
66
+ /**
67
+ * Refuse a file another account could replace.
68
+ *
69
+ * The runtime is executed from inside the application, so a world-writable file
70
+ * in that tree is a way for someone else to choose what this session runs. This
71
+ * is the cheap half of what the original installer checks; it is not a
72
+ * signature verification.
73
+ *
74
+ * @param path - the file to inspect.
75
+ * @returns whether the file is a regular file owned by this account or root and writable by neither others nor group.
76
+ */
77
+ export function isPrivateFile(path) {
78
+ try {
79
+ const info = statSync(path);
80
+ if (!info.isFile())
81
+ return false;
82
+ // Group or other write is enough for another account to choose what runs.
83
+ if ((info.mode & 0o022) !== 0)
84
+ return false;
85
+ const me = typeof process.getuid === 'function' ? process.getuid() : undefined;
86
+ return info.uid === 0 || me === undefined || info.uid === me;
87
+ }
88
+ catch {
89
+ return false;
90
+ }
91
+ }
92
+ /**
93
+ * Locate the application and the runtime inside it.
94
+ *
95
+ * @param appPath - the application to use; defaults to the standard install.
96
+ * @returns the paths and both versions.
97
+ * @throws AppError when the application or a required piece is missing.
98
+ */
99
+ export function resolveApp(appPath = DEFAULT_APP) {
100
+ const app = resolve(appPath);
101
+ if (!isDirectory(app) || !app.endsWith('.app')) {
102
+ throw new AppError(`not an application bundle: ${app}`);
103
+ }
104
+ const resources = join(app, 'Contents', 'Resources');
105
+ const runtime = join(resources, 'cua_node');
106
+ if (!isDirectory(runtime)) {
107
+ throw new AppError(`the application has no computer-use runtime: ${runtime}`);
108
+ }
109
+ for (const [label, path] of [
110
+ ['runtime node', join(runtime, 'bin/node')],
111
+ ['runtime module directory', join(runtime, 'lib/node_modules/@oai/cua-repl/bin/cua-repl.mjs')],
112
+ ['Sky service', join(runtime, 'lib/node_modules/@oai/sky/Codex Computer Use.app')],
113
+ ['Codex CLI', join(resources, 'codex-cli/bin/codex')],
114
+ ]) {
115
+ if (!exists(path) && !isDirectory(path)) {
116
+ throw new AppError(`the application is missing its ${label}: ${path}`);
117
+ }
118
+ if (exists(path) && !isPrivateFile(path)) {
119
+ throw new AppError(`the application's ${label} is writable by another account: ${path}`);
120
+ }
121
+ }
122
+ const manifest = join(runtime, 'manifest.json');
123
+ let runtimeVersion = 'unknown';
124
+ try {
125
+ const parsed = JSON.parse(readFileSync(manifest, 'utf8'));
126
+ if (typeof parsed.runtime_archive_version === 'string')
127
+ runtimeVersion = parsed.runtime_archive_version;
128
+ }
129
+ catch {
130
+ // A missing manifest is not fatal: the paths above are what actually matter.
131
+ }
132
+ return {
133
+ app,
134
+ resources,
135
+ runtime,
136
+ version: plistValue(join(app, 'Contents', 'Info.plist'), 'CFBundleShortVersionString') ?? 'unknown',
137
+ runtimeVersion,
138
+ };
139
+ }
140
+ /**
141
+ * Build the environment the runtime expects.
142
+ *
143
+ * Every entry mirrors the original launcher. The ones that matter most:
144
+ * `CUA_REPL_NODE_REPL_PATH`/`NODE_REPL_NODE_PATH` select the application's own
145
+ * node, `NODE_REPL_NODE_MODULE_DIRS` and `NODE_REPL_TRUSTED_CODE_PATHS` let it
146
+ * load its modules, `CUA_REPL_ENABLED_SURFACES` chooses the API surface,
147
+ * `SKY_CUA_SERVICE_PATH` is the signed helper the macOS native pipe launches,
148
+ * and `NODE_REPL_REQUEST_META` supplies the identity the runtime uses when a
149
+ * call carries none of its own.
150
+ *
151
+ * `NODE_REPL_TRUSTED_SERVICES` selects the `sky` service. It names this plugin's
152
+ * wrapper rather than the application's own module, because the runtime has no
153
+ * turn-ended handler of its own and a turn that never ends is a turn whose
154
+ * per-application Stop is never released.
155
+ *
156
+ * @param paths - the resolved application paths.
157
+ * @param options - surface selection and the identity fallback.
158
+ * @returns the child environment.
159
+ */
160
+ export function buildEnvironment(paths, options) {
161
+ const separator = ':';
162
+ const moduleDir = join(paths.runtime, 'lib/node_modules');
163
+ const codexHome = process.env.CODEX_HOME ?? join(homedir(), '.codex');
164
+ const existingPath = process.env.PATH ?? '/usr/bin:/bin';
165
+ const env = {
166
+ ...process.env,
167
+ // The original runtime selects and trusts CODEX_HOME verbatim.
168
+ CODEX_HOME: codexHome,
169
+ PATH: `${join(paths.runtime, 'bin')}${separator}${existingPath}`,
170
+ CUA_REPL_NODE_REPL_PATH: join(paths.runtime, 'bin/node_repl'),
171
+ NODE_REPL_NODE_PATH: join(paths.runtime, 'bin/node'),
172
+ NODE_REPL_NODE_MODULE_DIRS: [moduleDir, process.env.NODE_REPL_NODE_MODULE_DIRS].filter(Boolean).join(separator),
173
+ NODE_REPL_TRUSTED_CODE_PATHS: [
174
+ codexHome, moduleDir, join(paths.resources, 'plugins'), process.env.NODE_REPL_TRUSTED_CODE_PATHS,
175
+ ].filter(Boolean).join(separator),
176
+ // The original launcher picks both its API and its instructions from this.
177
+ CUA_REPL_ENABLED_SURFACES: options.chrome ? 'browser,computer' : 'computer',
178
+ CUA_REPL_BROWSER_ENV: 'codex-app',
179
+ CODEX_CLI_PATH: join(paths.resources, 'codex-cli/bin/codex'),
180
+ // The signed helper the macOS native pipe transport launches.
181
+ SKY_CUA_SERVICE_PATH: join(moduleDir, '@oai/sky/Codex Computer Use.app'),
182
+ NODE_REPL_NATIVE_PIPE_CONNECT_TIMEOUT_MS: '1000',
183
+ BROWSER_USE_AVAILABLE_BACKENDS: 'chrome',
184
+ BROWSER_USE_TINYSKY_ENABLED: '1',
185
+ BROWSER_USE_CODEX_APP_BUILD_FLAVOR: 'prod',
186
+ BROWSER_USE_CODEX_APP_VERSION: paths.version,
187
+ NODE_REPL_DISABLE_ANALYTICS: '1',
188
+ BROWSER_USE_DISABLE_AMBIENT_NETWORK: '1',
189
+ // The application's own service and the signed client its turn cleanup goes
190
+ // through. The wrapper reads these two; they name the application's files,
191
+ // not ours.
192
+ DSH_SKY_SERVICE_PATH: join(moduleDir, '@oai/sky/dist/project/cua/sky_js/src/service.js'),
193
+ DSH_SKY_CLIENT_PATH: join(moduleDir, '@oai/sky/dist/project/cua/sky_js/src/targets/mac/client.js'),
194
+ };
195
+ // Load our Sky service in place of the application's. The map has to keep the
196
+ // runtime's other defaults, so `browser` is restated when that surface is on.
197
+ const wrapper = skyServicePath();
198
+ if (existsSync(wrapper)) {
199
+ const services = {};
200
+ if (options.chrome)
201
+ services.browser = '@oai/browser-desktop/service';
202
+ services.sky = wrapper;
203
+ env.NODE_REPL_TRUSTED_SERVICES = JSON.stringify(services);
204
+ // A trusted service is only loaded from a trusted code path, so the wrapper's
205
+ // own directory has to be one.
206
+ env.NODE_REPL_TRUSTED_CODE_PATHS = [dirname(wrapper), env.NODE_REPL_TRUSTED_CODE_PATHS]
207
+ .filter(Boolean)
208
+ .join(separator);
209
+ diag(`environment: sky service -> ${wrapper}`);
210
+ }
211
+ else {
212
+ diag(`environment: no wrapper at ${wrapper}; the application's own Sky service will be used`);
213
+ }
214
+ if (options.audio) {
215
+ env.SKY_ENABLE_AUDIO = '1';
216
+ env.NODE_REPL_ENABLE_AUDIO = '1';
217
+ }
218
+ // Supplied metadata keeps precedence, as the original runtime intends; this is
219
+ // only the identity used when a call arrives with none of its own.
220
+ if (env.NODE_REPL_REQUEST_META === undefined) {
221
+ const identity = options.identity ?? `dsh-${String(process.pid)}-${Date.now().toString(36)}`;
222
+ env.NODE_REPL_REQUEST_META = JSON.stringify({
223
+ 'x-codex-turn-metadata': { session_id: identity, turn_id: `${identity}-connection` },
224
+ });
225
+ }
226
+ return env;
227
+ }
228
+ /**
229
+ * The full plan for one connection.
230
+ *
231
+ * @param options - the application, surfaces and override.
232
+ * @returns the command, its arguments, the environment and the paths.
233
+ */
234
+ export function planLaunch(options) {
235
+ const paths = resolveApp(options.appPath ?? DEFAULT_APP);
236
+ const env = buildEnvironment(paths, {
237
+ chrome: options.chrome,
238
+ audio: options.audio,
239
+ ...(options.identity === undefined ? {} : { identity: options.identity }),
240
+ });
241
+ if (options.command !== undefined && options.command !== '') {
242
+ diag(`launch: explicit command ${options.command}`);
243
+ return { command: options.command, args: options.chrome ? ['--chrome'] : [], env, paths };
244
+ }
245
+ const launcher = join(paths.runtime, 'lib/node_modules/@oai/cua-repl/bin/cua-repl.mjs');
246
+ diag(`launch: app=${paths.app} version=${paths.version} runtime=${paths.runtimeVersion} launcher=${launcher}`);
247
+ return { command: env.NODE_REPL_NODE_PATH ?? join(paths.runtime, 'bin/node'), args: [launcher], env, paths };
248
+ }
249
+ /**
250
+ * A fingerprint of the application generation the runtime is run from.
251
+ *
252
+ * The runtime is executed from files the application replaces when it updates, so
253
+ * a session that is already connected can end up running a mix of two
254
+ * generations. Both versions are read from the bundle and both timestamps are
255
+ * part of the answer, because an update replaces the whole bundle and a
256
+ * same-version reinstall is still a different set of files.
257
+ *
258
+ * @param paths - the resolved application paths.
259
+ * @returns a value that changes exactly when the files underneath change.
260
+ */
261
+ export function generationOf(paths) {
262
+ const stamps = [];
263
+ for (const file of [join(paths.runtime, 'manifest.json'), join(paths.app, 'Contents', 'Info.plist')]) {
264
+ try {
265
+ const info = statSync(file);
266
+ stamps.push(`${String(info.mtimeMs)}:${String(info.size)}`);
267
+ }
268
+ catch {
269
+ stamps.push('missing');
270
+ }
271
+ }
272
+ return `${paths.version}|${paths.runtimeVersion}|${stamps.join('|')}`;
273
+ }
274
+ /** Whether the application can be executed by this account. */
275
+ export function appIsRunnable(paths) {
276
+ try {
277
+ accessSync(join(paths.runtime, 'bin/node'), constants.X_OK);
278
+ return true;
279
+ }
280
+ catch {
281
+ return false;
282
+ }
283
+ }
284
+ //# sourceMappingURL=app.js.map
package/lib/app.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"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"}
package/lib/approval.js CHANGED
@@ -1,16 +1,16 @@
1
1
  /**
2
- * LCU approval shapes.
2
+ * Approval shapes.
3
3
  *
4
- * LCU's original runtime asks the host before it touches an app, and the request
4
+ * The runtime asks the host before it touches an app, and the request
5
5
  * is a specific `elicitation/create` form. This ports the recognition and
6
- * mapping rules from LCU's own shared client so a DSH host presents exactly the
6
+ * mapping rules from the runtime's shared client so a DSH host presents exactly the
7
7
  * choices the runtime offered — no more (granting an unoffered persistence scope
8
8
  * would let the agent keep desktop access the runtime meant to bound) and no
9
9
  * fewer (dropping `always` would nag the user every turn).
10
10
  *
11
11
  * @module dsh-plugin-lcu/approval
12
12
  */
13
- /** Persistence scopes the runtime can offer, in the order LCU presents them. */
13
+ /** Persistence scopes the runtime can offer, in the order it presents them. */
14
14
  const PERSISTENCE = [
15
15
  ['session', 'Allow for this session'],
16
16
  ['always', 'Always allow'],
@@ -101,6 +101,47 @@ export function approvalValueForLabel(approval, label) {
101
101
  return 'cancel';
102
102
  return approval.choices.find((choice) => choice.label === label)?.value ?? 'cancel';
103
103
  }
104
+ /**
105
+ * Normalize the pre-approved application bundle identifiers.
106
+ *
107
+ * Matching is case-insensitive because a bundle identifier's case is an
108
+ * implementation detail of the application's Info.plist, and a user reading it
109
+ * out of a log should not have to reproduce it exactly. Entries are otherwise
110
+ * kept verbatim: nothing here widens what a match admits.
111
+ *
112
+ * @param raw - configured identifiers, possibly absent or malformed.
113
+ * @returns the normalized set.
114
+ */
115
+ export function normalizeApps(raw) {
116
+ const apps = new Set();
117
+ for (const entry of raw ?? []) {
118
+ if (typeof entry !== 'string')
119
+ continue;
120
+ const value = entry.trim().toLowerCase();
121
+ // An empty entry would match nothing but reads like a grant; drop it.
122
+ if (value === '')
123
+ continue;
124
+ apps.add(value);
125
+ }
126
+ return apps;
127
+ }
128
+ /**
129
+ * Whether the user has already decided about this application.
130
+ *
131
+ * This is what makes an unattended run possible: without it the runtime asks
132
+ * before it first uses each application, and an unanswered question is a refusal.
133
+ * It is checked only after the agent-host guard, so a list can never authorize
134
+ * the application the agent itself is running in.
135
+ *
136
+ * @param approval - the application approval the runtime sent.
137
+ * @param allowedApps - normalized pre-approved identifiers.
138
+ * @returns whether the application may be admitted without asking.
139
+ */
140
+ export function isPreApprovedApp(approval, allowedApps) {
141
+ if (allowedApps.size === 0)
142
+ return false;
143
+ return allowedApps.has(approval.resource.trim().toLowerCase());
144
+ }
104
145
  /** The exact origin a browser-site approval names, when it names one. */
105
146
  export function originApprovalOrigin(request) {
106
147
  const meta = asObject(request._meta) ?? asObject(request.meta);
@@ -1 +1 @@
1
- {"version":3,"file":"approval.js","sourceRoot":"","sources":["../src/approval.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"approval.js","sourceRoot":"","sources":["../src/approval.ts"],"names":[],"mappings":"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"}
package/lib/connection.js CHANGED
@@ -1,14 +1,14 @@
1
1
  /**
2
- * The LCU MCP client.
2
+ * The runtime MCP client.
3
3
  *
4
4
  * Why not `@modelcontextprotocol/sdk` or the host's `@modelcontextprotocol/client`:
5
5
  * the harness's own MCP bridge declares `capabilities: {}` and therefore cannot
6
- * answer elicitation, which is how LCU asks for per-app approval; and pulling a
6
+ * answer elicitation, which is how the runtime asks for per-app approval; and pulling a
7
7
  * second SDK into a profile plugin would pin a version the host does not own.
8
8
  * MCP over stdio is newline-delimited JSON-RPC, so the wire is small enough to
9
9
  * own here and keeps this plugin dependency-free at runtime.
10
10
  *
11
- * Contract kept from `amontlabs/lcu`:
11
+ * Contract taken from the runtime:
12
12
  * - declare `capabilities.elicitation` and answer `elicitation/create`
13
13
  * - expose only `js` and `js_reset` to the model; `turn_ended` and
14
14
  * `js_add_node_module_dir` are host-only
@@ -22,6 +22,7 @@
22
22
  export const MODEL_TOOL_NAMES = ['js', 'js_reset'];
23
23
  /** Host-only tools, named so a mistake is loud rather than silent. */
24
24
  import { diag } from "./diag.js";
25
+ import { ControlServer } from "./control.js";
25
26
  export const HOST_ONLY_TOOL_NAMES = ['turn_ended', 'js_add_node_module_dir'];
26
27
  /**
27
28
  * Build the `_meta` the original runtime reads its session and turn from.
@@ -50,10 +51,10 @@ export function turnMetadataFor(sessionId, turnId, callId) {
50
51
  }
51
52
  /** MCP protocol revision this client asks for; the server negotiates. */
52
53
  const PROTOCOL_VERSION = '2025-06-18';
53
- /** LCU's own approval timeout: effectively "never expire a pending approval". */
54
+ /** The runtime's own approval timeout: effectively "never expire a pending approval". */
54
55
  const NO_APPROVAL_TIMEOUT_MS = 2 ** 31 - 1;
55
56
  const DEFAULT_CALL_TIMEOUT_MS = 120_000;
56
- /** The original host's control channel is bounded; 45 s matches LCU's own client. */
57
+ /** The original host's control channel is bounded; 45 s matches its own client. */
57
58
  const CONTROL_TIMEOUT_MS = 45_000;
58
59
  const CONTROL_RESPONSE_LIMIT = 1024 * 1024;
59
60
  const DEFAULT_TURN_END_TIMEOUT_MS = 120_000;
@@ -83,7 +84,7 @@ export class LcuError extends Error {
83
84
  *
84
85
  * The runtime reports its own cleanup timeout as an ordinary error result with a
85
86
  * recognizable detail. Missing it would let a stalled cleanup look like a
86
- * healthy turn, which is exactly what LCU's contract asks a host not to do.
87
+ * healthy turn, which is exactly what the runtime's contract asks a host not to do.
87
88
  *
88
89
  * @param result - the failed call's result.
89
90
  * @returns the classified error, or `undefined` when the call succeeded.
@@ -104,7 +105,7 @@ export function classifyTurnEndFailure(result) {
104
105
  /**
105
106
  * Answer one elicitation, failing closed on every unexpected shape.
106
107
  *
107
- * LCU requires this exact discipline: a host that cannot present the request,
108
+ * The runtime requires this exact discipline: a host that cannot present the request,
108
109
  * a handler that throws, or an answer that is not one of the three actions must
109
110
  * all end as `cancel`, because anything else would silently grant desktop access.
110
111
  *
@@ -136,9 +137,9 @@ export async function resolveElicitation(handler, request, activeCalls = new Set
136
137
  return { action: 'cancel' };
137
138
  }
138
139
  /**
139
- * One long-lived connection to the `lcu` MCP server.
140
+ * One long-lived connection to the runtime's MCP server.
140
141
  *
141
- * The connection is per Agent/Session: LCU carries a persistent JavaScript
142
+ * The connection is per Agent/Session: the runtime carries a persistent JavaScript
142
143
  * session on its side, and its approvals are scoped to a real host session and
143
144
  * turn, so sharing one connection across agents would interleave both.
144
145
  */
@@ -155,6 +156,10 @@ export class LcuConnection {
155
156
  #activeCalls = new Set();
156
157
  #controlDirectory;
157
158
  #controlSocketPath;
159
+ /** Serves the path the child is told about, and relays to the Sky wrapper. */
160
+ #controlServer;
161
+ /** Set once a connection attempt shows nothing is listening. */
162
+ #controlUnavailable = false;
158
163
  constructor(options) {
159
164
  this.#options = options;
160
165
  }
@@ -177,7 +182,7 @@ export class LcuConnection {
177
182
  return this.#tools.filter((tool) => MODEL_TOOL_NAMES.includes(tool.name));
178
183
  }
179
184
  /**
180
- * Spawn `lcu`, complete the MCP handshake, and discover tools.
185
+ * Spawn the runtime, complete the MCP handshake, and discover tools.
181
186
  *
182
187
  * @throws {LcuError} when the process cannot start or the handshake fails.
183
188
  */
@@ -190,10 +195,16 @@ export class LcuConnection {
190
195
  this.#controlSocketPath = undefined;
191
196
  if (process.platform === 'darwin') {
192
197
  try {
193
- const { mkdtempSync, chmodSync } = await import('node:fs');
198
+ const { mkdtempSync, chmodSync, existsSync } = await import('node:fs');
194
199
  const { tmpdir } = await import('node:os');
195
200
  const { join } = await import('node:path');
196
- const directory = mkdtempSync(join(tmpdir(), 'dsh-lcu-'));
201
+ // `/private/tmp`, not the per-user temporary directory. The wrapper that
202
+ // connects to this socket runs inside the runtime's JavaScript sandbox,
203
+ // and that sandbox refuses a Unix socket under `/var/folders/…` with
204
+ // EPERM — the same boundary that denies its file writes. `/private/tmp`
205
+ // is on the allowed side, which is why the wrapper this replaces uses it.
206
+ const base = existsSync('/private/tmp') ? '/private/tmp' : tmpdir();
207
+ const directory = mkdtempSync(join(base, 'dsh-lcu-'));
197
208
  chmodSync(directory, 0o700);
198
209
  this.#controlDirectory = directory;
199
210
  this.#controlSocketPath = join(directory, 'c.sock');
@@ -203,7 +214,19 @@ export class LcuConnection {
203
214
  this.#controlSocketPath = undefined;
204
215
  }
205
216
  }
206
- const childEnv = { ...process.env, ...this.#options.env };
217
+ // Serve the socket before the child exists: the wrapper connects to it as soon
218
+ // as the runtime asks it for anything, and a missing listener looks exactly
219
+ // like a configuration that has no control channel at all.
220
+ if (this.#controlSocketPath !== undefined) {
221
+ try {
222
+ this.#controlServer = await ControlServer.open(this.#controlSocketPath, (message) => { this.#options.onDiag?.(message); });
223
+ }
224
+ catch (error) {
225
+ this.#controlServer = undefined;
226
+ this.#options.onDiag?.(`control: could not serve ${this.#controlSocketPath}: ${error instanceof Error ? error.message : String(error)}`);
227
+ }
228
+ }
229
+ const childEnv = this.#options.env ?? { ...process.env };
207
230
  if (this.#controlSocketPath === undefined)
208
231
  delete childEnv.LCU_MAC_CONTROL_SOCKET;
209
232
  else
@@ -283,11 +306,11 @@ export class LcuConnection {
283
306
  /**
284
307
  * Close the connection and let the runtime tear itself down.
285
308
  *
286
- * Order matters: the server owns a process tree (the CUA kernel, its trusted
287
- * worker, the macOS supervisor and the Sky helper), and LCU's supervisor exits
288
- * when its lifetime socket closes. Closing stdin gives the server that
289
- * orderly path; signalling first would orphan the tree on a machine that then
290
- * keeps reporting computer use as active.
309
+ * Order matters: the server owns a process tree — the runtime kernel, its
310
+ * trusted worker and the signed Sky helper the macOS native pipe launches.
311
+ * Closing stdin gives the server its orderly shutdown path; signalling first
312
+ * would orphan that tree on a machine that then keeps reporting computer use
313
+ * as active.
291
314
  */
292
315
  /**
293
316
  * The original runtime reads its session and turn from `_meta`, and its
@@ -305,9 +328,19 @@ export class LcuConnection {
305
328
  diag(`call meta: session=${this.sessionId ?? 'none'} turn=${turnId ?? 'none'} sent=${meta === undefined ? 'no' : 'yes'}`);
306
329
  return meta === undefined ? {} : { _meta: meta };
307
330
  }
308
- /** Whether this platform and connection expose the explicit per-app Stop. */
331
+ /**
332
+ * Whether the explicit per-app Stop is actually available.
333
+ *
334
+ * The socket path alone is not the answer: a configuration that interposes no
335
+ * lifecycle service sets nothing up to serve it, and reporting a channel that
336
+ * cannot be reached would turn "this is not supported here" into a connection
337
+ * error the caller has to interpret.
338
+ */
309
339
  get hasHostControl() {
310
- return this.#controlSocketPath !== undefined && !this.#closed;
340
+ // Not "the path exists": a configuration that installs no wrapper never
341
+ // connects to it, and reporting a channel nobody serves turns "this is not
342
+ // supported here" into a connection error the caller has to interpret.
343
+ return this.#controlServer?.connected === true && !this.#closed && !this.#controlUnavailable;
311
344
  }
312
345
  /**
313
346
  * List the applications the runtime currently holds for one turn.
@@ -383,7 +416,13 @@ export class LcuConnection {
383
416
  };
384
417
  const timer = setTimeout(() => { finish(new LcuError('turn_ended', 'the Computer Use control request timed out')); }, CONTROL_TIMEOUT_MS);
385
418
  const socket = createConnection(socketPath);
386
- socket.on('error', (error) => { finish(new LcuError('turn_ended', `control connection failed: ${error.message}`)); });
419
+ socket.on('error', (error) => {
420
+ // Nothing listening means this configuration has no control channel,
421
+ // rather than a channel that refused this particular request.
422
+ if (error.code === 'ENOENT' || error.code === 'ECONNREFUSED')
423
+ this.#controlUnavailable = true;
424
+ finish(new LcuError('turn_ended', `control connection failed: ${error.message}`));
425
+ });
387
426
  socket.on('connect', () => { socket.write(`${JSON.stringify(request)}\n`); });
388
427
  socket.on('data', (chunk) => {
389
428
  buffer += chunk.toString('utf8');
@@ -427,25 +466,33 @@ export class LcuConnection {
427
466
  async close() {
428
467
  this.#closed = true;
429
468
  this.#failAll(new LcuError('spawn', 'connection closed'));
469
+ await this.#controlServer?.close();
470
+ this.#controlServer = undefined;
430
471
  const child = this.#child;
431
472
  this.#child = undefined;
432
- if (child === undefined || child.exitCode !== null) {
433
- this.#removeControlDirectory();
434
- return;
435
- }
436
473
  try {
437
- child.stdin?.end();
474
+ if (child === undefined || child.exitCode !== null)
475
+ return;
476
+ try {
477
+ child.stdin?.end();
478
+ }
479
+ catch {
480
+ // An already-broken stdin just means we fall through to signalling.
481
+ }
482
+ if (await this.#exited(child, 5_000))
483
+ return;
484
+ child.kill('SIGTERM');
485
+ if (await this.#exited(child, 3_000))
486
+ return;
487
+ child.kill('SIGKILL');
488
+ await this.#exited(child, 2_000);
438
489
  }
439
- catch {
440
- // A already-broken stdin just means we fall through to signalling.
490
+ finally {
491
+ // Always. A socket in a directory nobody removes is a leak per connection,
492
+ // and the earlier version left one behind on every path that had to signal
493
+ // the child rather than have it exit on stdin.
494
+ this.#removeControlDirectory();
441
495
  }
442
- if (await this.#exited(child, 5_000))
443
- return;
444
- child.kill('SIGTERM');
445
- if (await this.#exited(child, 3_000))
446
- return;
447
- child.kill('SIGKILL');
448
- await this.#exited(child, 2_000);
449
496
  }
450
497
  /** Resolve `true` once the child has exited, or `false` after `timeoutMs`. */
451
498
  #exited(child, timeoutMs) {