dsh-plugin-lcu 0.1.0

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.
@@ -0,0 +1,384 @@
1
+ /**
2
+ * The LCU MCP client.
3
+ *
4
+ * Why not `@modelcontextprotocol/sdk` or the host's `@modelcontextprotocol/client`:
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
7
+ * second SDK into a profile plugin would pin a version the host does not own.
8
+ * MCP over stdio is newline-delimited JSON-RPC, so the wire is small enough to
9
+ * own here and keeps this plugin dependency-free at runtime.
10
+ *
11
+ * Contract kept from `amontlabs/lcu`:
12
+ * - declare `capabilities.elicitation` and answer `elicitation/create`
13
+ * - expose only `js` and `js_reset` to the model; `turn_ended` and
14
+ * `js_add_node_module_dir` are host-only
15
+ * - inject the server's `initialize.instructions` into model context
16
+ * - fail closed: an elicitation with no handler, or any unknown shape, cancels
17
+ * - approvals never time out; a pending approval freezes the call deadline
18
+ *
19
+ * @module dsh-plugin-lcu/connection
20
+ */
21
+ /** Tools the model may see. Everything else is host-only. */
22
+ export const MODEL_TOOL_NAMES = ['js', 'js_reset'];
23
+ /** Host-only tools, named so a mistake is loud rather than silent. */
24
+ export const HOST_ONLY_TOOL_NAMES = ['turn_ended', 'js_add_node_module_dir'];
25
+ /** MCP protocol revision this client asks for; the server negotiates. */
26
+ const PROTOCOL_VERSION = '2025-06-18';
27
+ /** LCU's own approval timeout: effectively "never expire a pending approval". */
28
+ const NO_APPROVAL_TIMEOUT_MS = 2 ** 31 - 1;
29
+ const DEFAULT_CALL_TIMEOUT_MS = 120_000;
30
+ const DEFAULT_TURN_END_TIMEOUT_MS = 120_000;
31
+ const CONNECT_TIMEOUT_MS = 120_000;
32
+ /**
33
+ * Code for the turn-cleanup timeout: the original host stopped waiting for its
34
+ * per-turn cleanup. The cleanup keeps running in the worker and the runtime
35
+ * retries it before the next action, so this is a warning the caller should
36
+ * surface rather than a reason to abort the session.
37
+ */
38
+ export const TURN_CLEANUP_TIMEOUT_CODE = 'LCU_TURN_CLEANUP_TIMEOUT';
39
+ /** A failure that names the protocol stage it happened at. */
40
+ export class LcuError extends Error {
41
+ stage;
42
+ /** Stable machine-readable classification, when one applies. */
43
+ code;
44
+ constructor(stage, message, cause, code) {
45
+ super(message, cause === undefined ? undefined : { cause });
46
+ this.name = 'LcuError';
47
+ this.stage = stage;
48
+ if (code !== undefined)
49
+ this.code = code;
50
+ }
51
+ }
52
+ /**
53
+ * Classify a failed `turn_ended` call.
54
+ *
55
+ * The runtime reports its own cleanup timeout as an ordinary error result with a
56
+ * recognizable detail. Missing it would let a stalled cleanup look like a
57
+ * healthy turn, which is exactly what LCU's contract asks a host not to do.
58
+ *
59
+ * @param result - the failed call's result.
60
+ * @returns the classified error, or `undefined` when the call succeeded.
61
+ */
62
+ export function classifyTurnEndFailure(result) {
63
+ if (!result.isError)
64
+ return undefined;
65
+ const detail = result.content
66
+ .filter((block) => block.type === 'text' && typeof block.text === 'string')
67
+ .map((block) => block.text)
68
+ .join('\n');
69
+ if (/turn-ended handlers timed out/i.test(detail)) {
70
+ return new LcuError('turn_ended', "the CUA turn cleanup did not finish within the host's wait; it may still be finishing in the background "
71
+ + 'and the runtime retries it before the next action', undefined, TURN_CLEANUP_TIMEOUT_CODE);
72
+ }
73
+ return new LcuError('turn_ended', `turn cleanup failed: ${detail === '' ? 'unknown error' : detail}`);
74
+ }
75
+ /**
76
+ * Answer one elicitation, failing closed on every unexpected shape.
77
+ *
78
+ * LCU requires this exact discipline: a host that cannot present the request,
79
+ * a handler that throws, or an answer that is not one of the three actions must
80
+ * all end as `cancel`, because anything else would silently grant desktop access.
81
+ *
82
+ * @param handler - the host's presenter, or `undefined` when there is none.
83
+ * @param request - the server's elicitation params.
84
+ * @param activeCalls - signals of in-flight tool calls; aborting one cancels.
85
+ * @returns an answer the server accepts; never rejects.
86
+ */
87
+ export async function resolveElicitation(handler, request, activeCalls = new Set()) {
88
+ if (handler === undefined)
89
+ return { action: 'cancel' };
90
+ const abort = new AbortController();
91
+ const signal = AbortSignal.any([abort.signal, ...activeCalls]);
92
+ const dismissed = new Promise((resolve) => {
93
+ if (signal.aborted)
94
+ resolve({ action: 'cancel' });
95
+ else
96
+ signal.addEventListener('abort', () => resolve({ action: 'cancel' }), { once: true });
97
+ });
98
+ let answer;
99
+ try {
100
+ answer = await Promise.race([handler(request, signal), dismissed]);
101
+ }
102
+ catch {
103
+ return { action: 'cancel' };
104
+ }
105
+ if (answer?.action === 'accept' || answer?.action === 'decline' || answer?.action === 'cancel')
106
+ return answer;
107
+ return { action: 'cancel' };
108
+ }
109
+ /**
110
+ * One long-lived connection to the `lcu` MCP server.
111
+ *
112
+ * The connection is per Agent/Session: LCU carries a persistent JavaScript
113
+ * session on its side, and its approvals are scoped to a real host session and
114
+ * turn, so sharing one connection across agents would interleave both.
115
+ */
116
+ export class LcuConnection {
117
+ #options;
118
+ #child;
119
+ #nextId = 1;
120
+ #pending = new Map();
121
+ #tools = [];
122
+ #instructions = '';
123
+ #serverInfo = {};
124
+ #closed = false;
125
+ #stderrBuffer = '';
126
+ #activeCalls = new Set();
127
+ constructor(options) {
128
+ this.#options = options;
129
+ }
130
+ /** The server's `initialize.instructions`, injected into model context. */
131
+ get instructions() {
132
+ return this.#instructions;
133
+ }
134
+ /** Server identity, for diagnostics. */
135
+ get serverInfo() {
136
+ return this.#serverInfo;
137
+ }
138
+ /** Everything the server advertised, including host-only tools. */
139
+ get allTools() {
140
+ return this.#tools;
141
+ }
142
+ /** The tools the model may see, in server order. */
143
+ modelTools() {
144
+ return this.#tools.filter((tool) => MODEL_TOOL_NAMES.includes(tool.name));
145
+ }
146
+ /**
147
+ * Spawn `lcu`, complete the MCP handshake, and discover tools.
148
+ *
149
+ * @throws {LcuError} when the process cannot start or the handshake fails.
150
+ */
151
+ async connect(signal) {
152
+ const { spawn } = await import('node:child_process');
153
+ let child;
154
+ try {
155
+ child = spawn(this.#options.command, [...this.#options.args ?? []], {
156
+ cwd: this.#options.cwd,
157
+ env: { ...process.env, ...this.#options.env },
158
+ stdio: ['pipe', 'pipe', 'pipe'],
159
+ });
160
+ }
161
+ catch (error) {
162
+ throw new LcuError('spawn', `could not start ${this.#options.command}`, error);
163
+ }
164
+ this.#child = child;
165
+ child.stderr?.on('data', (chunk) => this.#consumeStderr(chunk.toString()));
166
+ child.on('exit', (code, signalName) => {
167
+ const detail = signalName === null ? `exit ${String(code)}` : `signal ${signalName}`;
168
+ this.#failAll(new LcuError('spawn', `lcu ${detail}`));
169
+ this.#closed = true;
170
+ });
171
+ const { createInterface } = await import('node:readline');
172
+ createInterface({ input: child.stdout }).on('line', (line) => {
173
+ this.#onLine(line);
174
+ });
175
+ const result = await this.#request('initialize', {
176
+ protocolVersion: PROTOCOL_VERSION,
177
+ capabilities: { elicitation: {} },
178
+ clientInfo: { name: 'dsh-plugin-lcu', version: '0.0.1' },
179
+ }, CONNECT_TIMEOUT_MS, 'initialize', signal);
180
+ this.#instructions = typeof result.instructions === 'string' ? result.instructions : '';
181
+ const info = result.serverInfo;
182
+ this.#serverInfo = info !== null && typeof info === 'object' && !Array.isArray(info)
183
+ ? info
184
+ : {};
185
+ this.#notify('notifications/initialized');
186
+ await this.#refreshTools(signal);
187
+ }
188
+ /**
189
+ * Call one tool. `js_reset` and `js` are the model-facing ones; the host-only
190
+ * tools are reachable here too because the lifecycle needs `turn_ended`.
191
+ *
192
+ * Requests carry no client-side deadline by default: a pause for a human
193
+ * approval must not look like a hung call.
194
+ */
195
+ async callTool(name, args, options = {}) {
196
+ const result = await this.#request('tools/call', { name, arguments: args }, options.timeoutMs ?? DEFAULT_CALL_TIMEOUT_MS, 'tools/call', options.signal);
197
+ const content = Array.isArray(result.content) ? result.content : [];
198
+ return {
199
+ content,
200
+ isError: result.isError === true,
201
+ ...(result.structuredContent === undefined ? {} : { structuredContent: result.structuredContent }),
202
+ };
203
+ }
204
+ /**
205
+ * Tell the server a host turn ended, so its per-turn cleanup runs.
206
+ *
207
+ * @param sessionId - the real host session id, and nothing synthetic.
208
+ * @param turnId - the real host turn id.
209
+ * @param event - `Stop`, `Interrupt`, or `SubagentStop`.
210
+ */
211
+ async turnEnded(sessionId, turnId, event = 'Stop') {
212
+ if (sessionId === '' || turnId === '') {
213
+ throw new LcuError('turn_ended', 'turn_ended requires a real session id and turn id');
214
+ }
215
+ const result = await this.callTool('turn_ended', {
216
+ hook_event_name: event,
217
+ session_id: sessionId,
218
+ turn_id: turnId,
219
+ }, { timeoutMs: DEFAULT_TURN_END_TIMEOUT_MS });
220
+ const failure = classifyTurnEndFailure(result);
221
+ if (failure !== undefined)
222
+ throw failure;
223
+ }
224
+ /** Close the connection, terminating the child process. */
225
+ async close() {
226
+ this.#closed = true;
227
+ this.#failAll(new LcuError('spawn', 'connection closed'));
228
+ const child = this.#child;
229
+ this.#child = undefined;
230
+ if (child === undefined || child.exitCode !== null)
231
+ return;
232
+ child.kill('SIGTERM');
233
+ await new Promise((resolve) => {
234
+ const timer = setTimeout(() => {
235
+ child.kill('SIGKILL');
236
+ resolve();
237
+ }, 3_000);
238
+ child.once('exit', () => {
239
+ clearTimeout(timer);
240
+ resolve();
241
+ });
242
+ });
243
+ }
244
+ // -------------------------------------------------------------------------
245
+ // Wire
246
+ // -------------------------------------------------------------------------
247
+ #consumeStderr(text) {
248
+ this.#stderrBuffer += text;
249
+ const lines = this.#stderrBuffer.split('\n');
250
+ this.#stderrBuffer = lines.pop() ?? '';
251
+ for (const line of lines)
252
+ if (line.trim() !== '')
253
+ this.#options.onStderr?.(line);
254
+ }
255
+ #onLine(line) {
256
+ if (line.trim() === '')
257
+ return;
258
+ let message;
259
+ try {
260
+ message = JSON.parse(line);
261
+ }
262
+ catch {
263
+ return; // non-protocol chatter on stdout is not ours to interpret
264
+ }
265
+ if (message.method !== undefined) {
266
+ void this.#onServerMessage(message);
267
+ return;
268
+ }
269
+ if (typeof message.id !== 'number')
270
+ return;
271
+ const entry = this.#pending.get(message.id);
272
+ if (entry === undefined)
273
+ return;
274
+ this.#pending.delete(message.id);
275
+ entry.cleanup();
276
+ if (message.error !== undefined) {
277
+ entry.reject(new LcuError('tools/call', message.error.message ?? 'server error'));
278
+ return;
279
+ }
280
+ entry.resolve(message.result ?? {});
281
+ }
282
+ async #onServerMessage(message) {
283
+ const id = message.id;
284
+ if (message.method === 'notifications/tools/list_changed') {
285
+ try {
286
+ await this.#refreshTools();
287
+ }
288
+ catch {
289
+ // A failed refresh keeps the previous tool set, matching the host bridge.
290
+ }
291
+ return;
292
+ }
293
+ // Requests from the server are the elicitation surface; answer or cancel.
294
+ if (id === undefined)
295
+ return;
296
+ if (message.method === 'elicitation/create') {
297
+ const request = (message.params ?? {});
298
+ const response = await this.#answerElicitation(request);
299
+ this.#respond(id, response);
300
+ return;
301
+ }
302
+ // Unknown server requests fail closed rather than hanging the child.
303
+ this.#respondError(id, -32601, `unsupported server request: ${message.method}`);
304
+ }
305
+ async #answerElicitation(request) {
306
+ return await resolveElicitation(this.#options.onElicitation, request, this.#activeCalls);
307
+ }
308
+ async #refreshTools(signal) {
309
+ const result = await this.#request('tools/list', {}, CONNECT_TIMEOUT_MS, 'tools/list', signal);
310
+ const listed = Array.isArray(result.tools) ? result.tools : [];
311
+ const missing = MODEL_TOOL_NAMES.filter((name) => !listed.some((tool) => tool.name === name));
312
+ if (missing.length > 0) {
313
+ throw new LcuError('tools/list', `server does not expose ${missing.join(', ')}`);
314
+ }
315
+ this.#tools = listed;
316
+ this.#options.onToolsChanged?.(listed);
317
+ }
318
+ #write(message) {
319
+ const child = this.#child;
320
+ if (child === undefined || child.stdin === null || this.#closed) {
321
+ throw new LcuError('spawn', 'lcu is not connected');
322
+ }
323
+ child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', ...message })}\n`);
324
+ }
325
+ #notify(method, params) {
326
+ this.#write(params === undefined ? { method } : { method, params });
327
+ }
328
+ #respond(id, result) {
329
+ this.#write({ id, result });
330
+ }
331
+ #respondError(id, code, message) {
332
+ this.#write({ id, error: { code, message } });
333
+ }
334
+ #request(method, params, timeoutMs, stage, signal) {
335
+ if (signal !== undefined)
336
+ this.#activeCalls.add(signal);
337
+ const id = this.#nextId++;
338
+ return new Promise((resolve, reject) => {
339
+ const onAbort = () => {
340
+ this.#pending.delete(id);
341
+ reject(new LcuError(stage, `${method} was aborted`));
342
+ };
343
+ const timer = timeoutMs >= NO_APPROVAL_TIMEOUT_MS
344
+ ? undefined
345
+ : setTimeout(() => {
346
+ this.#pending.delete(id);
347
+ signal?.removeEventListener('abort', onAbort);
348
+ reject(new LcuError(stage, `${method} timed out after ${String(timeoutMs)}ms`));
349
+ }, timeoutMs);
350
+ const cleanup = () => {
351
+ if (timer !== undefined)
352
+ clearTimeout(timer);
353
+ signal?.removeEventListener('abort', onAbort);
354
+ if (signal !== undefined)
355
+ this.#activeCalls.delete(signal);
356
+ };
357
+ if (signal?.aborted === true) {
358
+ cleanup();
359
+ reject(new LcuError(stage, `${method} was aborted`));
360
+ return;
361
+ }
362
+ signal?.addEventListener('abort', onAbort, { once: true });
363
+ this.#pending.set(id, {
364
+ resolve: (value) => { cleanup(); resolve(value); },
365
+ reject: (error) => { cleanup(); reject(error); },
366
+ cleanup,
367
+ });
368
+ try {
369
+ this.#write({ id, method, params });
370
+ }
371
+ catch (error) {
372
+ cleanup();
373
+ this.#pending.delete(id);
374
+ reject(error instanceof Error ? error : new LcuError(stage, String(error)));
375
+ }
376
+ });
377
+ }
378
+ #failAll(error) {
379
+ for (const entry of this.#pending.values())
380
+ entry.reject(error);
381
+ this.#pending.clear();
382
+ }
383
+ }
384
+ //# sourceMappingURL=connection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection.js","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAqCH,6DAA6D;AAC7D,MAAM,CAAC,MAAM,gBAAgB,GAAsB,CAAC,IAAI,EAAE,UAAU,CAAC,CAAA;AAErE,sEAAsE;AACtE,MAAM,CAAC,MAAM,oBAAoB,GAAsB,CAAC,YAAY,EAAE,wBAAwB,CAAC,CAAA;AAK/F,yEAAyE;AACzE,MAAM,gBAAgB,GAAG,YAAY,CAAA;AAErC,iFAAiF;AACjF,MAAM,sBAAsB,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;AAE1C,MAAM,uBAAuB,GAAG,OAAO,CAAA;AACvC,MAAM,2BAA2B,GAAG,OAAO,CAAA;AAC3C,MAAM,kBAAkB,GAAG,OAAO,CAAA;AAoClC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAG,0BAA0B,CAAA;AAEnE,8DAA8D;AAC9D,MAAM,OAAO,QAAS,SAAQ,KAAK;IACxB,KAAK,CAAU;IACxB,gEAAgE;IACvD,IAAI,CAAS;IAEtB,YAAY,KAAe,EAAE,OAAe,EAAE,KAAe,EAAE,IAAa;QAC1E,KAAK,CAAC,OAAO,EAAE,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAA;QAC3D,IAAI,CAAC,IAAI,GAAG,UAAU,CAAA;QACtB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;QAClB,IAAI,IAAI,KAAK,SAAS;YAAE,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;IAC1C,CAAC;CACF;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAAqB;IAC1D,IAAI,CAAC,MAAM,CAAC,OAAO;QAAE,OAAO,SAAS,CAAA;IACrC,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO;SAC1B,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,KAAK,MAAM,IAAI,OAAQ,KAA4B,CAAC,IAAI,KAAK,QAAQ,CAAC;SAClG,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAE,KAA0B,CAAC,IAAI,CAAC;SAChD,IAAI,CAAC,IAAI,CAAC,CAAA;IACb,IAAI,gCAAgC,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,OAAO,IAAI,QAAQ,CACjB,YAAY,EACZ,0GAA0G;cACxG,mDAAmD,EACrD,SAAS,EACT,yBAAyB,CAC1B,CAAA;IACH,CAAC;IACD,OAAO,IAAI,QAAQ,CAAC,YAAY,EAAE,wBAAwB,MAAM,KAAK,EAAE,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAA;AACvG,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,OAA8C,EAC9C,OAA8B,EAC9B,cAAwC,IAAI,GAAG,EAAE;IAEjD,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAA;IACtD,MAAM,KAAK,GAAG,IAAI,eAAe,EAAE,CAAA;IACnC,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,WAAW,CAAC,CAAC,CAAA;IAC9D,MAAM,SAAS,GAAG,IAAI,OAAO,CAAyB,CAAC,OAAO,EAAE,EAAE;QAChE,IAAI,MAAM,CAAC,OAAO;YAAE,OAAO,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAA;;YAC5C,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;IAC5F,CAAC,CAAC,CAAA;IACF,IAAI,MAA8B,CAAA;IAClC,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,SAAS,CAAC,CAAC,CAAA;IACpE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAA;IAC7B,CAAC;IACD,IAAI,MAAM,EAAE,MAAM,KAAK,QAAQ,IAAI,MAAM,EAAE,MAAM,KAAK,SAAS,IAAI,MAAM,EAAE,MAAM,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAA;IAC7G,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAA;AAC7B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,OAAO,aAAa;IACf,QAAQ,CAAsB;IACvC,MAAM,CAAuD;IAC7D,OAAO,GAAG,CAAC,CAAA;IACX,QAAQ,GAAG,IAAI,GAAG,EAAsH,CAAA;IACxI,MAAM,GAAc,EAAE,CAAA;IACtB,aAAa,GAAG,EAAE,CAAA;IAClB,WAAW,GAAwC,EAAE,CAAA;IACrD,OAAO,GAAG,KAAK,CAAA;IACf,aAAa,GAAG,EAAE,CAAA;IAClB,YAAY,GAAG,IAAI,GAAG,EAAe,CAAA;IAErC,YAAY,OAA6B;QACvC,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAA;IACzB,CAAC;IAED,2EAA2E;IAC3E,IAAI,YAAY;QACd,OAAO,IAAI,CAAC,aAAa,CAAA;IAC3B,CAAC;IAED,wCAAwC;IACxC,IAAI,UAAU;QACZ,OAAO,IAAI,CAAC,WAAW,CAAA;IACzB,CAAC;IAED,mEAAmE;IACnE,IAAI,QAAQ;QACV,OAAO,IAAI,CAAC,MAAM,CAAA;IACpB,CAAC;IAED,oDAAoD;IACpD,UAAU;QACR,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,gBAAgB,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAA;IAC3E,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,OAAO,CAAC,MAAoB;QAChC,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,oBAAoB,CAAC,CAAA;QACpD,IAAI,KAAgD,CAAA;QACpD,IAAI,CAAC;YACH,KAAK,GAAG,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE;gBAClE,GAAG,EAAE,IAAI,CAAC,QAAQ,CAAC,GAAG;gBACtB,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE;gBAC7C,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;aAChC,CAAC,CAAA;QACJ,CAAC;QAAC,OAAO,KAAc,EAAE,CAAC;YACxB,MAAM,IAAI,QAAQ,CAAC,OAAO,EAAE,mBAAmB,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,EAAE,KAAK,CAAC,CAAA;QAChF,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,KAAK,CAAA;QACnB,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAA;QAClF,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,EAAE;YACpC,MAAM,MAAM,GAAG,UAAU,KAAK,IAAI,CAAC,CAAC,CAAC,QAAQ,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,UAAU,EAAE,CAAA;YACpF,IAAI,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,OAAO,EAAE,OAAO,MAAM,EAAE,CAAC,CAAC,CAAA;YACrD,IAAI,CAAC,OAAO,GAAG,IAAI,CAAA;QACrB,CAAC,CAAC,CAAA;QACF,MAAM,EAAE,eAAe,EAAE,GAAG,MAAM,MAAM,CAAC,eAAe,CAAC,CAAA;QACzD,eAAe,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,MAA+B,EAAE,CAAC,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAY,EAAE,EAAE;YAC5F,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;QACpB,CAAC,CAAC,CAAA;QAEF,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,YAAY,EAAE;YAC/C,eAAe,EAAE,gBAAgB;YACjC,YAAY,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE;YACjC,UAAU,EAAE,EAAE,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,OAAO,EAAE;SACzD,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,CAAC,CAAA;QAE5C,IAAI,CAAC,aAAa,GAAG,OAAO,MAAM,CAAC,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CAAA;QACvF,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,CAAA;QAC9B,IAAI,CAAC,WAAW,GAAG,IAAI,KAAK,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;YAClF,CAAC,CAAC,IAA2C;YAC7C,CAAC,CAAC,EAAE,CAAA;QACN,IAAI,CAAC,OAAO,CAAC,2BAA2B,CAAC,CAAA;QACzC,MAAM,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,CAAA;IAClC,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,QAAQ,CACZ,IAAY,EACZ,IAAuC,EACvC,UAA0E,EAAE;QAE5E,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,YAAY,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,OAAO,CAAC,SAAS,IAAI,uBAAuB,EAAE,YAAY,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;QACvJ,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAA4B,CAAC,CAAC,CAAC,EAAE,CAAA;QACxF,OAAO;YACL,OAAO;YACP,OAAO,EAAE,MAAM,CAAC,OAAO,KAAK,IAAI;YAChC,GAAG,CAAC,MAAM,CAAC,iBAAiB,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,MAAM,CAAC,iBAAiB,EAAE,CAAC;SACnG,CAAA;IACH,CAAC;IAED;;;;;;OAMG;IACH,KAAK,CAAC,SAAS,CACb,SAAiB,EACjB,MAAc,EACd,QAAyB,MAAM;QAE/B,IAAI,SAAS,KAAK,EAAE,IAAI,MAAM,KAAK,EAAE,EAAE,CAAC;YACtC,MAAM,IAAI,QAAQ,CAAC,YAAY,EAAE,mDAAmD,CAAC,CAAA;QACvF,CAAC;QACD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,YAAY,EAAE;YAC/C,eAAe,EAAE,KAAK;YACtB,UAAU,EAAE,SAAS;YACrB,OAAO,EAAE,MAAM;SAChB,EAAE,EAAE,SAAS,EAAE,2BAA2B,EAAE,CAAC,CAAA;QAC9C,MAAM,OAAO,GAAG,sBAAsB,CAAC,MAAM,CAAC,CAAA;QAC9C,IAAI,OAAO,KAAK,SAAS;YAAE,MAAM,OAAO,CAAA;IAC1C,CAAC;IAED,2DAA2D;IAC3D,KAAK,CAAC,KAAK;QACT,IAAI,CAAC,OAAO,GAAG,IAAI,CAAA;QACnB,IAAI,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAA;QACzD,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAA;QACzB,IAAI,CAAC,MAAM,GAAG,SAAS,CAAA;QACvB,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI;YAAE,OAAM;QAC1D,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;QACrB,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;YAClC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;gBAC5B,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;gBACrB,OAAO,EAAE,CAAA;YACX,CAAC,EAAE,KAAK,CAAC,CAAA;YACT,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE;gBACtB,YAAY,CAAC,KAAK,CAAC,CAAA;gBACnB,OAAO,EAAE,CAAA;YACX,CAAC,CAAC,CAAA;QACJ,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,4EAA4E;IAC5E,OAAO;IACP,4EAA4E;IAE5E,cAAc,CAAC,IAAY;QACzB,IAAI,CAAC,aAAa,IAAI,IAAI,CAAA;QAC1B,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAC5C,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,CAAA;QACtC,KAAK,MAAM,IAAI,IAAI,KAAK;YAAE,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE;gBAAE,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,CAAC,IAAI,CAAC,CAAA;IAClF,CAAC;IAED,OAAO,CAAC,IAAY;QAClB,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE;YAAE,OAAM;QAC9B,IAAI,OAAoB,CAAA;QACxB,IAAI,CAAC;YACH,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAgB,CAAA;QAC3C,CAAC;QAAC,MAAM,CAAC;YACP,OAAM,CAAC,0DAA0D;QACnE,CAAC;QACD,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YACjC,KAAK,IAAI,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAA;YACnC,OAAM;QACR,CAAC;QACD,IAAI,OAAO,OAAO,CAAC,EAAE,KAAK,QAAQ;YAAE,OAAM;QAC1C,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;QAC3C,IAAI,KAAK,KAAK,SAAS;YAAE,OAAM;QAC/B,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;QAChC,KAAK,CAAC,OAAO,EAAE,CAAA;QACf,IAAI,OAAO,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAChC,KAAK,CAAC,MAAM,CAAC,IAAI,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC,KAAK,CAAC,OAAO,IAAI,cAAc,CAAC,CAAC,CAAA;YACjF,OAAM;QACR,CAAC;QACD,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAA;IACrC,CAAC;IAED,KAAK,CAAC,gBAAgB,CAAC,OAAoB;QACzC,MAAM,EAAE,GAAG,OAAO,CAAC,EAAE,CAAA;QACrB,IAAI,OAAO,CAAC,MAAM,KAAK,kCAAkC,EAAE,CAAC;YAC1D,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,aAAa,EAAE,CAAA;YAC5B,CAAC;YAAC,MAAM,CAAC;gBACP,0EAA0E;YAC5E,CAAC;YACD,OAAM;QACR,CAAC;QACD,0EAA0E;QAC1E,IAAI,EAAE,KAAK,SAAS;YAAE,OAAM;QAC5B,IAAI,OAAO,CAAC,MAAM,KAAK,oBAAoB,EAAE,CAAC;YAC5C,MAAM,OAAO,GAAG,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAA0B,CAAA;YAC/D,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAA;YACvD,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,QAA8C,CAAC,CAAA;YACjE,OAAM;QACR,CAAC;QACD,qEAAqE;QACrE,IAAI,CAAC,aAAa,CAAC,EAAE,EAAE,CAAC,KAAK,EAAE,+BAA+B,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;IACjF,CAAC;IAED,KAAK,CAAC,kBAAkB,CAAC,OAA8B;QACrD,OAAO,MAAM,kBAAkB,CAAC,IAAI,CAAC,QAAQ,CAAC,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC,YAAY,CAAC,CAAA;IAC1F,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,MAAoB;QACtC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,YAAY,EAAE,EAAE,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,CAAC,CAAA;QAC9F,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAkB,CAAC,CAAC,CAAC,EAAE,CAAA;QAC3E,MAAM,OAAO,GAAG,gBAAgB,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAA;QAC7F,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,MAAM,IAAI,QAAQ,CAAC,YAAY,EAAE,0BAA0B,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QAClF,CAAC;QACD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,IAAI,CAAC,QAAQ,CAAC,cAAc,EAAE,CAAC,MAAM,CAAC,CAAA;IACxC,CAAC;IAED,MAAM,CAAC,OAAoB;QACzB,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAA;QACzB,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,KAAK,KAAK,IAAI,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAChE,MAAM,IAAI,QAAQ,CAAC,OAAO,EAAE,sBAAsB,CAAC,CAAA;QACrD,CAAC;QACD,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,OAAO,EAAE,CAAC,IAAI,CAAC,CAAA;IAC1E,CAAC;IAED,OAAO,CAAC,MAAc,EAAE,MAAgC;QACtD,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;IACrE,CAAC;IAED,QAAQ,CAAC,EAAmB,EAAE,MAA+B;QAC3D,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAA;IAC7B,CAAC;IAED,aAAa,CAAC,EAAmB,EAAE,IAAY,EAAE,OAAe;QAC9D,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,CAAC,CAAA;IAC/C,CAAC;IAED,QAAQ,CACN,MAAc,EACd,MAA+B,EAC/B,SAAiB,EACjB,KAAe,EACf,MAAoB;QAEpB,IAAI,MAAM,KAAK,SAAS;YAAE,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;QACvD,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,EAAE,CAAA;QACzB,OAAO,IAAI,OAAO,CAA0B,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YAC9D,MAAM,OAAO,GAAG,GAAS,EAAE;gBACzB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;gBACxB,MAAM,CAAC,IAAI,QAAQ,CAAC,KAAK,EAAE,GAAG,MAAM,cAAc,CAAC,CAAC,CAAA;YACtD,CAAC,CAAA;YACD,MAAM,KAAK,GAAG,SAAS,IAAI,sBAAsB;gBAC/C,CAAC,CAAC,SAAS;gBACX,CAAC,CAAC,UAAU,CAAC,GAAG,EAAE;oBAChB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;oBACxB,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;oBAC7C,MAAM,CAAC,IAAI,QAAQ,CAAC,KAAK,EAAE,GAAG,MAAM,oBAAoB,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAA;gBACjF,CAAC,EAAE,SAAS,CAAC,CAAA;YACf,MAAM,OAAO,GAAG,GAAS,EAAE;gBACzB,IAAI,KAAK,KAAK,SAAS;oBAAE,YAAY,CAAC,KAAK,CAAC,CAAA;gBAC5C,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;gBAC7C,IAAI,MAAM,KAAK,SAAS;oBAAE,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;YAC5D,CAAC,CAAA;YACD,IAAI,MAAM,EAAE,OAAO,KAAK,IAAI,EAAE,CAAC;gBAC7B,OAAO,EAAE,CAAA;gBACT,MAAM,CAAC,IAAI,QAAQ,CAAC,KAAK,EAAE,GAAG,MAAM,cAAc,CAAC,CAAC,CAAA;gBACpD,OAAM;YACR,CAAC;YACD,MAAM,EAAE,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;YAC1D,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,EAAE;gBACpB,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAA,CAAC,CAAC;gBACjD,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA,CAAC,CAAC;gBAC/C,OAAO;aACR,CAAC,CAAA;YACF,IAAI,CAAC;gBACH,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;YACrC,CAAC;YAAC,OAAO,KAAc,EAAE,CAAC;gBACxB,OAAO,EAAE,CAAA;gBACT,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;gBACxB,MAAM,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;YAC7E,CAAC;QACH,CAAC,CAAC,CAAA;IACJ,CAAC;IAED,QAAQ,CAAC,KAAY;QACnB,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE;YAAE,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QAC/D,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAA;IACvB,CAAC;CACF"}
package/lib/diag.js ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Best-effort diagnostic log for the LCU attach path.
3
+ *
4
+ * The harness exposes plugin logs only through surfaces a running session
5
+ * cannot read, and a failing `agent/created` listener is swallowed silently. So
6
+ * the attach decision writes here: one line per branch, including the errors
7
+ * that would otherwise vanish. It never throws and never changes behavior.
8
+ *
9
+ * Set `LCU_DIAG=0` to disable. The file is `~/.dsh/lcu-diag.log`.
10
+ *
11
+ * @module dsh-plugin-lcu/diag
12
+ */
13
+ import { appendFileSync, rmSync, statSync } from 'node:fs';
14
+ import { homedir } from 'node:os';
15
+ import { join } from 'node:path';
16
+ const LOG_PATH = join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'lcu-diag.log');
17
+ /** Rotate rather than grow without bound: the log records one line per decision. */
18
+ const MAX_BYTES = 1_048_576;
19
+ /** Append one diagnostic line; silently does nothing if the file cannot be written. */
20
+ export function diag(message) {
21
+ if (process.env.LCU_DIAG === '0')
22
+ return;
23
+ try {
24
+ // Start over once the file is full; this is a rolling support aid, not a log
25
+ // of record, and truncating keeps it bounded without a rotation scheme.
26
+ if ((statSync(LOG_PATH, { throwIfNoEntry: false })?.size ?? 0) > MAX_BYTES)
27
+ rmSync(LOG_PATH, { force: true });
28
+ appendFileSync(LOG_PATH, `${new Date().toISOString()} ${message}\n`);
29
+ }
30
+ catch {
31
+ // Diagnostics must never affect the capability they describe.
32
+ }
33
+ }
34
+ /** Describe a value for the log without dumping unbounded data. */
35
+ export function describe(value) {
36
+ if (value === undefined)
37
+ return 'undefined';
38
+ if (value === null)
39
+ return 'null';
40
+ if (typeof value === 'string')
41
+ return JSON.stringify(value);
42
+ if (typeof value === 'number' || typeof value === 'boolean')
43
+ return String(value);
44
+ if (typeof value === 'function')
45
+ return 'function';
46
+ if (Array.isArray(value))
47
+ return `array(${String(value.length)})`;
48
+ if (typeof value === 'object')
49
+ return `object{${Object.keys(value).slice(0, 8).join(',')}}`;
50
+ return typeof value;
51
+ }
52
+ //# sourceMappingURL=diag.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diag.js","sourceRoot":"","sources":["../src/diag.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAC1D,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEhC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,MAAM,CAAC,EAAE,cAAc,CAAC,CAAA;AAEtF,oFAAoF;AACpF,MAAM,SAAS,GAAG,SAAS,CAAA;AAE3B,uFAAuF;AACvF,MAAM,UAAU,IAAI,CAAC,OAAe;IAClC,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,GAAG;QAAE,OAAM;IACxC,IAAI,CAAC;QACH,6EAA6E;QAC7E,wEAAwE;QACxE,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,IAAI,CAAC,CAAC,GAAG,SAAS;YAAE,MAAM,CAAC,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;QAC7G,cAAc,CAAC,QAAQ,EAAE,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,IAAI,OAAO,IAAI,CAAC,CAAA;IACtE,CAAC;IAAC,MAAM,CAAC;QACP,8DAA8D;IAChE,CAAC;AACH,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,QAAQ,CAAC,KAAc;IACrC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,WAAW,CAAA;IAC3C,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,MAAM,CAAA;IACjC,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAA;IAC3D,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,OAAO,KAAK,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAA;IACjF,IAAI,OAAO,KAAK,KAAK,UAAU;QAAE,OAAO,UAAU,CAAA;IAClD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAA;IACjE,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,UAAU,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAA;IAC3F,OAAO,OAAO,KAAK,CAAA;AACrB,CAAC"}
@@ -0,0 +1,137 @@
1
+ /**
2
+ * The agent-host guard.
3
+ *
4
+ * LCU refuses to approve the application that is hosting the agent, because
5
+ * computer use can click anything an approved app shows — including the very
6
+ * approval prompt — so approving the host would let the agent approve itself.
7
+ *
8
+ * LCU's own shared client does this; a host that replaces that client owns the
9
+ * rule. This is the DSH implementation: the agent's own process ancestry plus a
10
+ * static list of dedicated agent hosts and terminals.
11
+ *
12
+ * A failed or unavailable process lookup falls back to the static list, so the
13
+ * guard degrades to "known hosts are refused" rather than "nothing is refused".
14
+ *
15
+ * @module dsh-plugin-lcu/host-guard
16
+ */
17
+ import { execFile } from 'node:child_process';
18
+ import { existsSync } from 'node:fs';
19
+ import { dirname, join, resolve } from 'node:path';
20
+ /**
21
+ * Dedicated agent hosts, terminals, and this harness.
22
+ *
23
+ * Editors and IDEs are deliberately absent: they are common computer-use
24
+ * targets, and when an editor hosts the agent its bundle is an ancestor of this
25
+ * process, which the dynamic check already refuses.
26
+ */
27
+ export const STATIC_HOST_BUNDLE_IDS = [
28
+ 'com.deepseek.dsh',
29
+ 'com.anthropic.claudefordesktop',
30
+ 'com.anthropic.claude-code',
31
+ 'com.openai.codex',
32
+ 'com.apple.Terminal',
33
+ 'com.googlecode.iterm2',
34
+ 'com.mitchellh.ghostty',
35
+ 'dev.warp.Warp-Stable',
36
+ 'com.github.wez.wezterm',
37
+ 'io.alacritty',
38
+ 'net.kovidgoyal.kitty',
39
+ 'co.zeit.hyper',
40
+ 'com.raphaelamorim.rio',
41
+ 'dev.commandline.wave',
42
+ ];
43
+ /** Set `LCU_ALLOW_AGENT_HOST_APPROVAL=1` to disable the guard for testing only. */
44
+ const OVERRIDE_ENV = 'LCU_ALLOW_AGENT_HOST_APPROVAL';
45
+ let cachedAncestorBundles;
46
+ /** Run a command, resolving to its trimmed stdout or `undefined` on any failure. */
47
+ function tryExec(command, args) {
48
+ return new Promise((resolvePromise) => {
49
+ execFile(command, [...args], { timeout: 5_000 }, (error, stdout) => {
50
+ resolvePromise(error === null ? stdout.trim() : undefined);
51
+ });
52
+ });
53
+ }
54
+ /** The enclosing `.app` bundle of an executable path, if any. */
55
+ export function enclosingAppBundle(executablePath) {
56
+ let current = resolve(executablePath);
57
+ for (;;) {
58
+ if (current.endsWith('.app'))
59
+ return current;
60
+ const parent = dirname(current);
61
+ if (parent === current)
62
+ return undefined;
63
+ current = parent;
64
+ }
65
+ }
66
+ /** Read one bundle identifier, preferring the modern plist helper. */
67
+ async function bundleIdentifier(appPath) {
68
+ const plist = join(appPath, 'Contents', 'Info.plist');
69
+ if (!existsSync(plist))
70
+ return undefined;
71
+ const value = await tryExec('plutil', ['-extract', 'CFBundleIdentifier', 'raw', '-o', '-', plist]);
72
+ return value !== undefined && value !== '' ? value : undefined;
73
+ }
74
+ /**
75
+ * The bundle identifiers of every `.app` on this process's ancestry.
76
+ *
77
+ * This is the dynamic half of the guard: whatever application actually launched
78
+ * DSH — a terminal, an editor, a launcher — is refused by construction, even
79
+ * when it is not on the static list.
80
+ *
81
+ * @returns the ancestor bundle identifiers; empty when the lookup fails.
82
+ */
83
+ export async function ancestorBundleIds() {
84
+ cachedAncestorBundles ??= (async () => {
85
+ const found = new Set();
86
+ let pid = process.pid;
87
+ for (let depth = 0; depth < 24; depth += 1) {
88
+ const parentText = await tryExec('ps', ['-o', 'ppid=', '-p', String(pid)]);
89
+ const executable = await tryExec('ps', ['-o', 'comm=', '-p', String(pid)]);
90
+ if (executable !== undefined) {
91
+ const bundle = enclosingAppBundle(executable);
92
+ if (bundle !== undefined) {
93
+ const id = await bundleIdentifier(bundle);
94
+ if (id !== undefined)
95
+ found.add(id);
96
+ }
97
+ }
98
+ const parent = Number(parentText);
99
+ if (!Number.isInteger(parent) || parent <= 1)
100
+ break;
101
+ pid = parent;
102
+ }
103
+ return found;
104
+ })();
105
+ return await cachedAncestorBundles;
106
+ }
107
+ /**
108
+ * Whether an approval would grant the agent control of its own host.
109
+ *
110
+ * @param app - the app the runtime is asking about: a display name, bundle id, or path.
111
+ * @returns `true` when the request must be declined without asking the user.
112
+ */
113
+ export async function isAgentHostApp(app) {
114
+ if (process.env[OVERRIDE_ENV] === '1')
115
+ return false;
116
+ const candidate = app.trim();
117
+ if (candidate === '')
118
+ return false;
119
+ // A path is decided by its own bundle, not by the ancestor set.
120
+ if (candidate.includes('/')) {
121
+ const bundle = enclosingAppBundle(candidate);
122
+ const id = bundle === undefined ? undefined : await bundleIdentifier(bundle);
123
+ if (id !== undefined && STATIC_HOST_BUNDLE_IDS.includes(id))
124
+ return true;
125
+ const ancestors = await ancestorBundleIds();
126
+ return id !== undefined && ancestors.has(id);
127
+ }
128
+ if (STATIC_HOST_BUNDLE_IDS.includes(candidate))
129
+ return true;
130
+ const ancestors = await ancestorBundleIds();
131
+ return ancestors.has(candidate);
132
+ }
133
+ /** Reset the memoized ancestry; for tests only. */
134
+ export function resetAncestorCache() {
135
+ cachedAncestorBundles = undefined;
136
+ }
137
+ //# sourceMappingURL=host-guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host-guard.js","sourceRoot":"","sources":["../src/host-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACpC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAElD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAsB;IACvD,kBAAkB;IAClB,gCAAgC;IAChC,2BAA2B;IAC3B,kBAAkB;IAClB,oBAAoB;IACpB,uBAAuB;IACvB,uBAAuB;IACvB,sBAAsB;IACtB,wBAAwB;IACxB,cAAc;IACd,sBAAsB;IACtB,eAAe;IACf,uBAAuB;IACvB,sBAAsB;CACvB,CAAA;AAED,mFAAmF;AACnF,MAAM,YAAY,GAAG,+BAA+B,CAAA;AAEpD,IAAI,qBAA+D,CAAA;AAEnE,oFAAoF;AACpF,SAAS,OAAO,CAAC,OAAe,EAAE,IAAuB;IACvD,OAAO,IAAI,OAAO,CAAC,CAAC,cAAc,EAAE,EAAE;QACpC,QAAQ,CAAC,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE;YACjE,cAAc,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;QAC5D,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,kBAAkB,CAAC,cAAsB;IACvD,IAAI,OAAO,GAAG,OAAO,CAAC,cAAc,CAAC,CAAA;IACrC,SAAS,CAAC;QACR,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC;YAAE,OAAO,OAAO,CAAA;QAC5C,MAAM,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;QAC/B,IAAI,MAAM,KAAK,OAAO;YAAE,OAAO,SAAS,CAAA;QACxC,OAAO,GAAG,MAAM,CAAA;IAClB,CAAC;AACH,CAAC;AAED,sEAAsE;AACtE,KAAK,UAAU,gBAAgB,CAAC,OAAe;IAC7C,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,YAAY,CAAC,CAAA;IACrD,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAA;IACxC,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,QAAQ,EAAE,CAAC,UAAU,EAAE,oBAAoB,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC,CAAA;IAClG,OAAO,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAA;AAChE,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB;IACrC,qBAAqB,KAAK,CAAC,KAAK,IAAI,EAAE;QACpC,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAA;QAC/B,IAAI,GAAG,GAAG,OAAO,CAAC,GAAG,CAAA;QACrB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,EAAE,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;YAC3C,MAAM,UAAU,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;YAC1E,MAAM,UAAU,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;YAC1E,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;gBAC7B,MAAM,MAAM,GAAG,kBAAkB,CAAC,UAAU,CAAC,CAAA;gBAC7C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;oBACzB,MAAM,EAAE,GAAG,MAAM,gBAAgB,CAAC,MAAM,CAAC,CAAA;oBACzC,IAAI,EAAE,KAAK,SAAS;wBAAE,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;gBACrC,CAAC;YACH,CAAC;YACD,MAAM,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,CAAA;YACjC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,MAAM,IAAI,CAAC;gBAAE,MAAK;YACnD,GAAG,GAAG,MAAM,CAAA;QACd,CAAC;QACD,OAAO,KAAK,CAAA;IACd,CAAC,CAAC,EAAE,CAAA;IACJ,OAAO,MAAM,qBAAqB,CAAA;AACpC,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,GAAW;IAC9C,IAAI,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,KAAK,GAAG;QAAE,OAAO,KAAK,CAAA;IACnD,MAAM,SAAS,GAAG,GAAG,CAAC,IAAI,EAAE,CAAA;IAC5B,IAAI,SAAS,KAAK,EAAE;QAAE,OAAO,KAAK,CAAA;IAElC,gEAAgE;IAChE,IAAI,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5B,MAAM,MAAM,GAAG,kBAAkB,CAAC,SAAS,CAAC,CAAA;QAC5C,MAAM,EAAE,GAAG,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,gBAAgB,CAAC,MAAM,CAAC,CAAA;QAC5E,IAAI,EAAE,KAAK,SAAS,IAAI,sBAAsB,CAAC,QAAQ,CAAC,EAAE,CAAC;YAAE,OAAO,IAAI,CAAA;QACxE,MAAM,SAAS,GAAG,MAAM,iBAAiB,EAAE,CAAA;QAC3C,OAAO,EAAE,KAAK,SAAS,IAAI,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IAC9C,CAAC;IAED,IAAI,sBAAsB,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,OAAO,IAAI,CAAA;IAC3D,MAAM,SAAS,GAAG,MAAM,iBAAiB,EAAE,CAAA;IAC3C,OAAO,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,CAAA;AACjC,CAAC;AAED,mDAAmD;AACnD,MAAM,UAAU,kBAAkB;IAChC,qBAAqB,GAAG,SAAS,CAAA;AACnC,CAAC"}