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/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":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AACnF,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACjC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAClD,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAExC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEhC,sDAAsD;AACtD,MAAM,WAAW,GAAG,2BAA2B,CAAA;AAE/C;;;;;;GAMG;AACH,MAAM,UAAU,cAAc;IAC5B,OAAO,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,iBAAiB,CAAC,CAAA;AACzF,CAAC;AAED,0DAA0D;AAC1D,MAAM,OAAO,QAAS,SAAQ,KAAK;IACjC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAA;QACd,IAAI,CAAC,IAAI,GAAG,UAAU,CAAA;IACxB,CAAC;CACF;AAqBD,SAAS,MAAM,CAAC,IAAY;IAC1B,IAAI,CAAC;QACH,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,CAAA;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED,SAAS,WAAW,CAAC,IAAY;IAC/B,IAAI,CAAC;QACH,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAA;IACrC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED,yDAAyD;AACzD,SAAS,UAAU,CAAC,KAAa,EAAE,GAAW;IAC5C,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,CAAA;QACxC,MAAM,KAAK,GAAG,IAAI,MAAM,CAAC,QAAQ,GAAG,oCAAoC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACpF,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,CAAA;IACnB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAA;IAClB,CAAC;AACH,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAA;QAC3B,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE;YAAE,OAAO,KAAK,CAAA;QAChC,0EAA0E;QAC1E,IAAI,CAAC,IAAI,CAAC,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC;YAAE,OAAO,KAAK,CAAA;QAC3C,MAAM,EAAE,GAAG,OAAO,OAAO,CAAC,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;QAC9E,OAAO,IAAI,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,KAAK,SAAS,IAAI,IAAI,CAAC,GAAG,KAAK,EAAE,CAAA;IAC9D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,UAAkB,WAAW;IACtD,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC5B,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC/C,MAAM,IAAI,QAAQ,CAAC,8BAA8B,GAAG,EAAE,CAAC,CAAA;IACzD,CAAC;IACD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,UAAU,EAAE,WAAW,CAAC,CAAA;IACpD,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,EAAE,UAAU,CAAC,CAAA;IAC3C,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,QAAQ,CAAC,gDAAgD,OAAO,EAAE,CAAC,CAAA;IAC/E,CAAC;IAED,KAAK,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI;QAC1B,CAAC,cAAc,EAAE,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;QAC3C,CAAC,0BAA0B,EAAE,IAAI,CAAC,OAAO,EAAE,iDAAiD,CAAC,CAAC;QAC9F,CAAC,aAAa,EAAE,IAAI,CAAC,OAAO,EAAE,kDAAkD,CAAC,CAAC;QAClF,CAAC,WAAW,EAAE,IAAI,CAAC,SAAS,EAAE,qBAAqB,CAAC,CAAC;KAC7C,EAAE,CAAC;QACX,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC;YACxC,MAAM,IAAI,QAAQ,CAAC,kCAAkC,KAAK,KAAK,IAAI,EAAE,CAAC,CAAA;QACxE,CAAC;QACD,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC;YACzC,MAAM,IAAI,QAAQ,CAAC,qBAAqB,KAAK,oCAAoC,IAAI,EAAE,CAAC,CAAA;QAC1F,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,EAAE,eAAe,CAAC,CAAA;IAC/C,IAAI,cAAc,GAAG,SAAS,CAAA;IAC9B,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAA0C,CAAA;QAClG,IAAI,OAAO,MAAM,CAAC,uBAAuB,KAAK,QAAQ;YAAE,cAAc,GAAG,MAAM,CAAC,uBAAuB,CAAA;IACzG,CAAC;IAAC,MAAM,CAAC;QACP,6EAA6E;IAC/E,CAAC;IAED,OAAO;QACL,GAAG;QACH,SAAS;QACT,OAAO;QACP,OAAO,EAAE,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,UAAU,EAAE,YAAY,CAAC,EAAE,4BAA4B,CAAC,IAAI,SAAS;QACnG,cAAc;KACf,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,gBAAgB,CAC9B,KAAe,EACf,OAA0F;IAE1F,MAAM,SAAS,GAAG,GAAG,CAAA;IACrB,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,kBAAkB,CAAC,CAAA;IACzD,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,CAAC,CAAA;IACrE,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,eAAe,CAAA;IAExD,MAAM,GAAG,GAAsB;QAC7B,GAAG,OAAO,CAAC,GAAG;QACd,+DAA+D;QAC/D,UAAU,EAAE,SAAS;QACrB,IAAI,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,GAAG,SAAS,GAAG,YAAY,EAAE;QAChE,uBAAuB,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,eAAe,CAAC;QAC7D,mBAAmB,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,UAAU,CAAC;QACpD,0BAA0B,EAAE,CAAC,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC;QAC/G,4BAA4B,EAAE;YAC5B,SAAS,EAAE,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,SAAS,CAAC,EAAE,OAAO,CAAC,GAAG,CAAC,4BAA4B;SACjG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC;QACjC,2EAA2E;QAC3E,yBAAyB,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,UAAU;QAC3E,oBAAoB,EAAE,WAAW;QACjC,cAAc,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,EAAE,qBAAqB,CAAC;QAC5D,8DAA8D;QAC9D,oBAAoB,EAAE,IAAI,CAAC,SAAS,EAAE,iCAAiC,CAAC;QACxE,wCAAwC,EAAE,MAAM;QAChD,8BAA8B,EAAE,QAAQ;QACxC,2BAA2B,EAAE,GAAG;QAChC,kCAAkC,EAAE,MAAM;QAC1C,6BAA6B,EAAE,KAAK,CAAC,OAAO;QAC5C,2BAA2B,EAAE,GAAG;QAChC,mCAAmC,EAAE,GAAG;QACxC,4EAA4E;QAC5E,2EAA2E;QAC3E,YAAY;QACZ,oBAAoB,EAAE,IAAI,CAAC,SAAS,EAAE,iDAAiD,CAAC;QACxF,mBAAmB,EAAE,IAAI,CAAC,SAAS,EAAE,4DAA4D,CAAC;KACnG,CAAA;IAED,8EAA8E;IAC9E,8EAA8E;IAC9E,MAAM,OAAO,GAAG,cAAc,EAAE,CAAA;IAChC,IAAI,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QACxB,MAAM,QAAQ,GAA2B,EAAE,CAAA;QAC3C,IAAI,OAAO,CAAC,MAAM;YAAE,QAAQ,CAAC,OAAO,GAAG,8BAA8B,CAAA;QACrE,QAAQ,CAAC,GAAG,GAAG,OAAO,CAAA;QACtB,GAAG,CAAC,0BAA0B,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAA;QACzD,8EAA8E;QAC9E,+BAA+B;QAC/B,GAAG,CAAC,4BAA4B,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,GAAG,CAAC,4BAA4B,CAAC;aACpF,MAAM,CAAC,OAAO,CAAC;aACf,IAAI,CAAC,SAAS,CAAC,CAAA;QAClB,IAAI,CAAC,+BAA+B,OAAO,EAAE,CAAC,CAAA;IAChD,CAAC;SAAM,CAAC;QACN,IAAI,CAAC,8BAA8B,OAAO,kDAAkD,CAAC,CAAA;IAC/F,CAAC;IAED,IAAI,OAAO,CAAC,KAAK,EAAE,CAAC;QAClB,GAAG,CAAC,gBAAgB,GAAG,GAAG,CAAA;QAC1B,GAAG,CAAC,sBAAsB,GAAG,GAAG,CAAA;IAClC,CAAC;IAED,+EAA+E;IAC/E,mEAAmE;IACnE,IAAI,GAAG,CAAC,sBAAsB,KAAK,SAAS,EAAE,CAAC;QAC7C,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,OAAO,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAA;QAC5F,GAAG,CAAC,sBAAsB,GAAG,IAAI,CAAC,SAAS,CAAC;YAC1C,uBAAuB,EAAE,EAAE,UAAU,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,QAAQ,aAAa,EAAE;SACrF,CAAC,CAAA;IACJ,CAAC;IAED,OAAO,GAAG,CAAA;AACZ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,OAO1B;IACC,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,CAAC,OAAO,IAAI,WAAW,CAAC,CAAA;IACxD,MAAM,GAAG,GAAG,gBAAgB,CAAC,KAAK,EAAE;QAClC,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,GAAG,CAAC,OAAO,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC;KAC1E,CAAC,CAAA;IAEF,IAAI,OAAO,CAAC,OAAO,KAAK,SAAS,IAAI,OAAO,CAAC,OAAO,KAAK,EAAE,EAAE,CAAC;QAC5D,IAAI,CAAC,4BAA4B,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;QACnD,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAA;IAC3F,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,iDAAiD,CAAC,CAAA;IACvF,IAAI,CAAC,eAAe,KAAK,CAAC,GAAG,YAAY,KAAK,CAAC,OAAO,YAAY,KAAK,CAAC,cAAc,aAAa,QAAQ,EAAE,CAAC,CAAA;IAC9G,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,mBAAmB,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,IAAI,EAAE,CAAC,QAAQ,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,CAAA;AAC9G,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,YAAY,CAAC,KAAe;IAC1C,MAAM,MAAM,GAAa,EAAE,CAAA;IAC3B,KAAK,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,eAAe,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,UAAU,EAAE,YAAY,CAAC,CAAC,EAAE,CAAC;QACrG,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAA;YAC3B,MAAM,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAC7D,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QACxB,CAAC;IACH,CAAC;IACD,OAAO,GAAG,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,cAAc,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAA;AACvE,CAAC;AAED,+DAA+D;AAC/D,MAAM,UAAU,aAAa,CAAC,KAAe;IAC3C,IAAI,CAAC;QACH,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,SAAS,CAAC,IAAI,CAAC,CAAA;QAC3D,OAAO,IAAI,CAAA;IACb,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC"}
|
package/lib/approval.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Approval shapes.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
|
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
|
|
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);
|
package/lib/approval.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approval.js","sourceRoot":"","sources":["../src/approval.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAiBH
|
|
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
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
|
140
|
+
* One long-lived connection to the runtime's MCP server.
|
|
140
141
|
*
|
|
141
|
-
* The connection is per Agent/Session:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
287
|
-
* worker
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
|
|
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) => {
|
|
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.
|
|
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
|
-
|
|
440
|
-
// A
|
|
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) {
|