@ai-workbench/remote-agent 0.8.12 → 0.8.14
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/README.md +11 -7
- package/dist/acp.d.ts +12 -7
- package/dist/acp.js +65 -41
- package/dist/broker-runtime.js +46 -20
- package/dist/broker.d.ts +4 -2
- package/dist/broker.js +18 -9
- package/dist/claude-local.d.ts +2 -1
- package/dist/claude-local.js +61 -34
- package/dist/close-diagnostics.d.ts +56 -0
- package/dist/close-diagnostics.js +104 -0
- package/dist/conversation-details.js +67 -9
- package/dist/employee-endpoint.d.ts +3 -1
- package/dist/employee-endpoint.js +8 -6
- package/dist/employee-exec.d.ts +7 -4
- package/dist/employee-exec.js +39 -29
- package/dist/employee-mcp.d.ts +6 -3
- package/dist/employee-mcp.js +75 -39
- package/dist/employee-provider.js +58 -36
- package/dist/employee-shell.d.ts +2 -0
- package/dist/employee-shell.js +17 -5
- package/dist/employee-ssh.d.ts +2 -0
- package/dist/employee-ssh.js +6 -4
- package/dist/employee-tools.d.ts +3 -1
- package/dist/employee-tools.js +6 -5
- package/dist/index.d.ts +4 -0
- package/dist/index.js +133 -44
- package/dist/local-user-turn.js +2 -1
- package/dist/model-catalog.d.ts +9 -1
- package/dist/model-catalog.js +63 -5
- package/dist/model-configuration.d.ts +14 -1
- package/dist/model-configuration.js +122 -89
- package/dist/presentation.js +3 -2
- package/dist/provider.d.ts +8 -2
- package/dist/recovery.d.ts +4 -0
- package/dist/rpc-namespace.d.ts +2 -2
- package/dist/rpc-namespace.js +2 -1
- package/dist/session-header.d.ts +4 -0
- package/dist/session-header.js +12 -0
- package/package.json +3 -3
package/dist/employee-exec.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CloseCause, type CloseDiagnostics, type CloseObserver } from './close-diagnostics.ts';
|
|
1
2
|
import type { ChildProcessWithoutNullStreams } from 'node:child_process';
|
|
2
3
|
import { NeedError } from './remote.ts';
|
|
3
4
|
export type EmployeeMessage = Record<string, unknown>;
|
|
@@ -5,10 +6,11 @@ export interface EmployeeExecutionTransport {
|
|
|
5
6
|
write(message: EmployeeMessage, signal?: AbortSignal): Promise<void>;
|
|
6
7
|
read(signal?: AbortSignal): Promise<EmployeeMessage | null>;
|
|
7
8
|
/** Closing a carrier never proves that an employee command completed. */
|
|
8
|
-
close(): void | Promise<void>;
|
|
9
|
+
close(cause?: CloseCause): void | Promise<void>;
|
|
9
10
|
}
|
|
10
11
|
export interface EmployeeExecutionOptions {
|
|
11
12
|
bindingId: string;
|
|
13
|
+
closeDiagnostics?: CloseDiagnostics;
|
|
12
14
|
acpMaxMessageBytes: number;
|
|
13
15
|
/** Rechecks the current L CAS/directory/account and N/S five-tuple. No cached permit. */
|
|
14
16
|
validate(signal: AbortSignal): Promise<void>;
|
|
@@ -42,12 +44,13 @@ export declare class EmployeeExecution {
|
|
|
42
44
|
private gate;
|
|
43
45
|
private stop;
|
|
44
46
|
private closeTransport;
|
|
45
|
-
invalidate(error?: NeedError): void;
|
|
47
|
+
invalidate(error?: NeedError, cause?: CloseCause): void;
|
|
46
48
|
private cleanup;
|
|
47
|
-
dispose(): Promise<void>;
|
|
49
|
+
dispose(cause?: CloseCause): Promise<void>;
|
|
48
50
|
handle(params: unknown, signal?: AbortSignal): Promise<EmployeeExecutionResult>;
|
|
51
|
+
private handleRequest;
|
|
49
52
|
private open;
|
|
50
53
|
private read;
|
|
51
54
|
}
|
|
52
55
|
/** NDJSON carrier with byte bounds and backpressure derived solely from acpMaxMessageBytes. */
|
|
53
|
-
export declare function createEmployeeProcessTransport(child: ChildProcessWithoutNullStreams, acpMaxMessageBytes: number, stderrCodes?: readonly string[]): EmployeeExecutionTransport;
|
|
56
|
+
export declare function createEmployeeProcessTransport(child: ChildProcessWithoutNullStreams, acpMaxMessageBytes: number, stderrCodes?: readonly string[], observe?: CloseObserver): EmployeeExecutionTransport;
|
package/dist/employee-exec.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { closeCode } from "./close-diagnostics.js";
|
|
1
2
|
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { NeedError } from "./remote.js";
|
|
3
4
|
const object = (value) => !!value && typeof value === 'object' && !Array.isArray(value) && (Object.getPrototypeOf(value) === Object.prototype || Object.getPrototypeOf(value) === null);
|
|
@@ -80,7 +81,8 @@ export class EmployeeExecution {
|
|
|
80
81
|
active() { if (this.ended)
|
|
81
82
|
throw this.error ?? need('EMPLOYEE_EXEC_CLOSED'); }
|
|
82
83
|
async gate(signal) { this.active(); cancelled(signal); await waiting(this.options.validate(signal), signal); this.active(); cancelled(signal); }
|
|
83
|
-
stop(error) {
|
|
84
|
+
stop(error, cause = { layer: 'executor', initiator: 'executor.stop', reason: closeCode(error, 'EMPLOYEE_EXEC_CLOSED'), action: 'close' }, observe = this.options.closeDiagnostics?.capture()) {
|
|
85
|
+
observe?.({ ...cause, changes: cause.changes ?? [{ field: 'executor.ended', before: this.ended, after: true }] });
|
|
84
86
|
if (this.ended)
|
|
85
87
|
return;
|
|
86
88
|
this.ended = true;
|
|
@@ -89,12 +91,13 @@ export class EmployeeExecution {
|
|
|
89
91
|
const io = this.io;
|
|
90
92
|
this.io = undefined;
|
|
91
93
|
if (io)
|
|
92
|
-
this.closeTransport(io);
|
|
94
|
+
this.closeTransport(io, undefined, observe);
|
|
93
95
|
}
|
|
94
|
-
closeTransport(io) {
|
|
96
|
+
closeTransport(io, cause = { layer: 'executor', initiator: 'executor.transport-cleanup', reason: 'EXECUTOR_CLEANUP', action: 'close' }, observe = this.options.closeDiagnostics?.capture()) {
|
|
97
|
+
observe?.(cause);
|
|
95
98
|
let closing;
|
|
96
99
|
try {
|
|
97
|
-
closing = Promise.resolve(io.close());
|
|
100
|
+
closing = Promise.resolve(io.close(cause));
|
|
98
101
|
}
|
|
99
102
|
catch {
|
|
100
103
|
closing = Promise.reject(need('EMPLOYEE_EXEC_UNKNOWN'));
|
|
@@ -103,28 +106,32 @@ export class EmployeeExecution {
|
|
|
103
106
|
// Revocation starts cleanup without a caller waiting yet; retain rejection for close/dispose.
|
|
104
107
|
void this.closing.catch(() => { });
|
|
105
108
|
}
|
|
106
|
-
invalidate(error = need('EMPLOYEE_CONTEXT_CHANGED')) { this.stop(error); }
|
|
109
|
+
invalidate(error = need('EMPLOYEE_CONTEXT_CHANGED'), cause) { this.stop(error, cause ?? { layer: 'executor', initiator: 'executor.invalidate', reason: error.code, action: 'invalidate' }); }
|
|
107
110
|
async cleanup() { await this.factoryReceipt; await this.closing; }
|
|
108
|
-
async dispose() { this.stop(); await this.cleanup(); }
|
|
111
|
+
async dispose(cause = { layer: 'executor', initiator: 'executor.dispose', reason: 'EXECUTOR_DISPOSE', action: 'dispose' }) { this.stop(undefined, cause); await this.cleanup(); }
|
|
109
112
|
async handle(params, signal) {
|
|
110
|
-
const request = this.parse(params);
|
|
113
|
+
const request = this.parse(params), observe = this.options.closeDiagnostics?.capture();
|
|
114
|
+
const run = () => this.handleRequest(request, signal, observe);
|
|
115
|
+
return this.options.closeDiagnostics ? this.options.closeDiagnostics.run('employee-exec.' + request.operation, run) : run();
|
|
116
|
+
}
|
|
117
|
+
async handleRequest(request, signal, observe) {
|
|
111
118
|
cancelled(signal);
|
|
112
119
|
if (request.operation === 'close') {
|
|
113
|
-
this.stop();
|
|
120
|
+
this.stop(undefined, { layer: 'executor', initiator: 'remote.employee-exec.close', reason: 'REMOTE_REASON_UNAVAILABLE', remoteReason: 'unavailable', operation: 'close', action: 'close' }, observe);
|
|
114
121
|
await this.cleanup();
|
|
115
122
|
return { version: 1, closed: true, result: 'UNKNOWN' };
|
|
116
123
|
}
|
|
117
124
|
this.active();
|
|
118
125
|
const current = signal ? AbortSignal.any([signal, this.lifetime.signal]) : this.lifetime.signal;
|
|
119
126
|
if (request.operation === 'open')
|
|
120
|
-
return this.open(current);
|
|
127
|
+
return this.open(current, observe);
|
|
121
128
|
if (request.operation === 'read')
|
|
122
|
-
return this.read(current);
|
|
129
|
+
return this.read(current, observe);
|
|
123
130
|
const value = encoded(request.message, this.options.acpMaxMessageBytes);
|
|
124
131
|
const size = value.line.byteLength;
|
|
125
132
|
if (this.queuedWriteBytes + size > this.options.acpMaxMessageBytes) {
|
|
126
133
|
const error = need('ACP_MESSAGE_TOO_LARGE');
|
|
127
|
-
this.stop(error);
|
|
134
|
+
this.stop(error, { layer: 'executor', initiator: 'executor.write-capacity', reason: error.code, operation: 'write', action: 'failure' }, observe);
|
|
128
135
|
throw error;
|
|
129
136
|
}
|
|
130
137
|
this.queuedWriteBytes += size;
|
|
@@ -140,7 +147,7 @@ export class EmployeeExecution {
|
|
|
140
147
|
}
|
|
141
148
|
catch (error) {
|
|
142
149
|
const known = this.error ?? failure(error);
|
|
143
|
-
this.stop(known);
|
|
150
|
+
this.stop(known, { layer: 'executor', initiator: 'executor.write-failure', reason: known.code, operation: 'write', action: 'failure' }, observe);
|
|
144
151
|
throw known;
|
|
145
152
|
}
|
|
146
153
|
finally {
|
|
@@ -150,7 +157,7 @@ export class EmployeeExecution {
|
|
|
150
157
|
this.writeTail = operation.catch(() => { });
|
|
151
158
|
return operation;
|
|
152
159
|
}
|
|
153
|
-
async open(signal) {
|
|
160
|
+
async open(signal, observe) {
|
|
154
161
|
if (this.opening || this.executionId)
|
|
155
162
|
throw need('EMPLOYEE_EXEC_ALREADY_OPEN');
|
|
156
163
|
this.opening = true;
|
|
@@ -158,7 +165,7 @@ export class EmployeeExecution {
|
|
|
158
165
|
await this.gate(signal);
|
|
159
166
|
// A factory may finish after revoke/abort. Its late executor belongs to this binding and must close.
|
|
160
167
|
const created = Promise.resolve().then(() => { cancelled(signal); return this.options.transport(signal); }).then(async (io) => { if (this.ended || signal.aborted) {
|
|
161
|
-
this.closeTransport(io);
|
|
168
|
+
this.closeTransport(io, { layer: 'executor', initiator: 'executor.late-factory', reason: this.error?.code ?? 'ABORTED', operation: 'open', action: 'close' }, observe);
|
|
162
169
|
await this.closing;
|
|
163
170
|
throw this.error ?? need('ABORTED');
|
|
164
171
|
} this.io = io; return io; });
|
|
@@ -176,14 +183,14 @@ export class EmployeeExecution {
|
|
|
176
183
|
}
|
|
177
184
|
catch (error) {
|
|
178
185
|
const known = this.error ?? failure(error);
|
|
179
|
-
this.stop(known);
|
|
186
|
+
this.stop(known, { layer: 'executor', initiator: 'executor.open-failure', reason: known.code, operation: 'open', action: 'failure' }, observe);
|
|
180
187
|
throw known;
|
|
181
188
|
}
|
|
182
189
|
finally {
|
|
183
190
|
this.opening = false;
|
|
184
191
|
}
|
|
185
192
|
}
|
|
186
|
-
async read(signal) {
|
|
193
|
+
async read(signal, observe) {
|
|
187
194
|
if (this.pendingRead)
|
|
188
195
|
throw need('EMPLOYEE_EXEC_READ_BUSY');
|
|
189
196
|
this.pendingRead = true;
|
|
@@ -201,7 +208,7 @@ export class EmployeeExecution {
|
|
|
201
208
|
}
|
|
202
209
|
await this.gate(signal);
|
|
203
210
|
if (message === null) {
|
|
204
|
-
this.stop();
|
|
211
|
+
this.stop(undefined, { layer: 'executor', initiator: 'executor.read-eof', reason: 'EXECUTOR_EOF', operation: 'read', action: 'eof' }, observe);
|
|
205
212
|
return { version: 1, messages: [], closed: true };
|
|
206
213
|
}
|
|
207
214
|
const value = encoded(message, this.options.acpMaxMessageBytes);
|
|
@@ -215,7 +222,7 @@ export class EmployeeExecution {
|
|
|
215
222
|
if (signal.aborted && !this.ended) {
|
|
216
223
|
if (consumed) {
|
|
217
224
|
const unknown = need('EMPLOYEE_EXEC_UNKNOWN');
|
|
218
|
-
this.stop(unknown);
|
|
225
|
+
this.stop(unknown, { layer: 'executor', initiator: 'executor.read-consumed-cancel', reason: unknown.code, operation: 'read', action: 'abort' }, observe);
|
|
219
226
|
throw unknown;
|
|
220
227
|
}
|
|
221
228
|
throw need('ABORTED');
|
|
@@ -223,7 +230,7 @@ export class EmployeeExecution {
|
|
|
223
230
|
if (this.ended && !this.error)
|
|
224
231
|
return { version: 1, messages: [], closed: true };
|
|
225
232
|
const known = failure(error);
|
|
226
|
-
this.stop(known);
|
|
233
|
+
this.stop(known, { layer: 'executor', initiator: 'executor.read-failure', reason: known.code, operation: 'read', action: 'failure' }, observe);
|
|
227
234
|
throw known;
|
|
228
235
|
}
|
|
229
236
|
finally {
|
|
@@ -232,7 +239,7 @@ export class EmployeeExecution {
|
|
|
232
239
|
}
|
|
233
240
|
}
|
|
234
241
|
/** NDJSON carrier with byte bounds and backpressure derived solely from acpMaxMessageBytes. */
|
|
235
|
-
export function createEmployeeProcessTransport(child, acpMaxMessageBytes, stderrCodes = []) {
|
|
242
|
+
export function createEmployeeProcessTransport(child, acpMaxMessageBytes, stderrCodes = [], observe) {
|
|
236
243
|
capacity(acpMaxMessageBytes);
|
|
237
244
|
let partial = Buffer.alloc(0), queuedBytes = 0, writingBytes = 0, closed = false, reason;
|
|
238
245
|
let processClosed = false, terminationRequested = false;
|
|
@@ -247,6 +254,7 @@ export function createEmployeeProcessTransport(child, acpMaxMessageBytes, stderr
|
|
|
247
254
|
const settle = () => { if (!pending)
|
|
248
255
|
return; const reader = pending; pending = undefined; reader.clear(); reason ? reader.reject(reason) : reader.resolve(null); };
|
|
249
256
|
const terminate = () => {
|
|
257
|
+
observe?.({ layer: 'relay', initiator: 'ssh.terminate', reason: 'TRANSPORT_CLEANUP', action: 'terminate', changes: [{ field: 'transport.termination-requested', before: terminationRequested, after: processClosed || terminationRequested ? terminationRequested : true }] });
|
|
250
258
|
if (processClosed || terminationRequested)
|
|
251
259
|
return;
|
|
252
260
|
terminationRequested = true;
|
|
@@ -267,7 +275,8 @@ export function createEmployeeProcessTransport(child, acpMaxMessageBytes, stderr
|
|
|
267
275
|
receipt.reject(need('EMPLOYEE_EXEC_UNKNOWN'));
|
|
268
276
|
}
|
|
269
277
|
};
|
|
270
|
-
const finish = (error, kill = false) => {
|
|
278
|
+
const finish = (error, kill = false, cause = { layer: 'relay', initiator: 'ssh.protocol-failure', reason: closeCode(error, 'TRANSPORT_CLOSED'), action: 'failure' }) => {
|
|
279
|
+
observe?.({ ...cause, changes: cause.changes ?? [{ field: 'transport.closed', before: closed, after: true }] });
|
|
271
280
|
if (!closed) {
|
|
272
281
|
closed = true;
|
|
273
282
|
reason = error;
|
|
@@ -353,9 +362,10 @@ export function createEmployeeProcessTransport(child, acpMaxMessageBytes, stderr
|
|
|
353
362
|
}
|
|
354
363
|
});
|
|
355
364
|
child.stderr.resume();
|
|
356
|
-
child.stdin.on('error', () => finish(need(diagnosticCode ?? 'EMPLOYEE_EXEC_UNKNOWN'), true));
|
|
357
|
-
child.once('error', () => finish(need('EMPLOYEE_EXEC_UNKNOWN'), true));
|
|
358
|
-
child.
|
|
365
|
+
child.stdin.on('error', () => finish(need(diagnosticCode ?? 'EMPLOYEE_EXEC_UNKNOWN'), true, { layer: 'relay', initiator: 'ssh.stdin-error', reason: diagnosticCode ?? 'EMPLOYEE_EXEC_UNKNOWN', action: 'failure' }));
|
|
366
|
+
child.once('error', () => finish(need('EMPLOYEE_EXEC_UNKNOWN'), true, { layer: 'relay', initiator: 'ssh.process-error', reason: 'EMPLOYEE_EXEC_UNKNOWN', action: 'failure' }));
|
|
367
|
+
child.prependOnceListener('close', (code, signal) => observe?.({ layer: 'relay', initiator: 'ssh.process.close', reason: diagnosticCode ?? (code === 0 && !signal ? 'PROCESS_EXIT' : 'EMPLOYEE_EXEC_UNKNOWN'), action: 'exit', exitCode: code, signal }));
|
|
368
|
+
child.once('close', (code, signal) => { processClosed = true; finish(partial.length ? need('EMPLOYEE_EXEC_PROTOCOL_INVALID') : code === 0 && !signal ? undefined : need(diagnosticCode ?? 'EMPLOYEE_EXEC_UNKNOWN'), false, { layer: 'relay', initiator: 'ssh.process-close-settle', reason: diagnosticCode ?? (code === 0 && !signal ? 'PROCESS_EXIT' : 'EMPLOYEE_EXEC_UNKNOWN'), action: 'exit', exitCode: code, signal }); receipt.resolve(); });
|
|
359
369
|
return {
|
|
360
370
|
write: async (message, signal) => {
|
|
361
371
|
cancelled(signal);
|
|
@@ -364,7 +374,7 @@ export function createEmployeeProcessTransport(child, acpMaxMessageBytes, stderr
|
|
|
364
374
|
const value = encoded(message, acpMaxMessageBytes);
|
|
365
375
|
if (writingBytes + value.line.byteLength > acpMaxMessageBytes) {
|
|
366
376
|
const error = need('ACP_MESSAGE_TOO_LARGE');
|
|
367
|
-
finish(error, true);
|
|
377
|
+
finish(error, true, { layer: 'relay', initiator: 'ssh.write-capacity', reason: error.code, operation: 'write', action: 'failure' });
|
|
368
378
|
throw error;
|
|
369
379
|
}
|
|
370
380
|
writingBytes += value.line.byteLength;
|
|
@@ -374,19 +384,19 @@ export function createEmployeeProcessTransport(child, acpMaxMessageBytes, stderr
|
|
|
374
384
|
return false; done = true; writingBytes -= value.line.byteLength; writes.delete(fail); signal?.removeEventListener('abort', abort); return true; };
|
|
375
385
|
const fail = (error) => { if (cleanup())
|
|
376
386
|
reject(error); };
|
|
377
|
-
const abort = () => { const error = need('EMPLOYEE_EXEC_UNKNOWN'); finish(error, true); fail(error); };
|
|
387
|
+
const abort = () => { const error = need('EMPLOYEE_EXEC_UNKNOWN'); finish(error, true, { layer: 'relay', initiator: 'ssh.write-abort', reason: error.code, operation: 'write', action: 'abort' }); fail(error); };
|
|
378
388
|
writes.add(fail);
|
|
379
389
|
signal?.addEventListener('abort', abort, { once: true });
|
|
380
390
|
try {
|
|
381
391
|
child.stdin.write(value.line, error => { if (error) {
|
|
382
|
-
finish(need('EMPLOYEE_EXEC_UNKNOWN'), true);
|
|
392
|
+
finish(need('EMPLOYEE_EXEC_UNKNOWN'), true, { layer: 'relay', initiator: 'ssh.write-callback', reason: 'EMPLOYEE_EXEC_UNKNOWN', operation: 'write', action: 'failure' });
|
|
383
393
|
fail(need('EMPLOYEE_EXEC_UNKNOWN'));
|
|
384
394
|
}
|
|
385
395
|
else if (cleanup())
|
|
386
396
|
resolve(); });
|
|
387
397
|
}
|
|
388
398
|
catch {
|
|
389
|
-
finish(need('EMPLOYEE_EXEC_UNKNOWN'), true);
|
|
399
|
+
finish(need('EMPLOYEE_EXEC_UNKNOWN'), true, { layer: 'relay', initiator: 'ssh.write-call', reason: 'EMPLOYEE_EXEC_UNKNOWN', operation: 'write', action: 'failure' });
|
|
390
400
|
fail(need('EMPLOYEE_EXEC_UNKNOWN'));
|
|
391
401
|
}
|
|
392
402
|
if (signal?.aborted)
|
|
@@ -418,6 +428,6 @@ export function createEmployeeProcessTransport(child, acpMaxMessageBytes, stderr
|
|
|
418
428
|
abort();
|
|
419
429
|
});
|
|
420
430
|
},
|
|
421
|
-
close: () => { finish(undefined, true); queue.length = 0; queuedBytes = 0; return receipt.promise; }
|
|
431
|
+
close: (cause = { layer: 'relay', initiator: 'ssh.transport-close', reason: 'TRANSPORT_CLOSE', action: 'close' }) => { finish(undefined, true, cause); queue.length = 0; queuedBytes = 0; return receipt.promise; }
|
|
422
432
|
};
|
|
423
433
|
}
|
package/dist/employee-mcp.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CloseCause, type CloseDiagnostics, type CloseObserver } from './close-diagnostics.ts';
|
|
1
2
|
import { type Socket } from 'node:net';
|
|
2
3
|
import { type JSONRPCMessage, type Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
3
4
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
@@ -10,6 +11,7 @@ export declare const EMPLOYEE_MCP_TOOLS: Tool[];
|
|
|
10
11
|
export declare class EmployeeSocketTransport implements Transport {
|
|
11
12
|
private socket;
|
|
12
13
|
private maxBytes;
|
|
14
|
+
private observeClose?;
|
|
13
15
|
onclose?: () => void;
|
|
14
16
|
onerror?: (error: Error) => void;
|
|
15
17
|
onmessage?: (message: JSONRPCMessage) => void;
|
|
@@ -19,13 +21,14 @@ export declare class EmployeeSocketTransport implements Transport {
|
|
|
19
21
|
get error(): Error | undefined;
|
|
20
22
|
private readonly lifetime;
|
|
21
23
|
readonly signal: AbortSignal;
|
|
22
|
-
constructor(socket: Socket, maxBytes: number);
|
|
24
|
+
constructor(socket: Socket, maxBytes: number, observeClose?: CloseObserver | undefined);
|
|
23
25
|
private failed;
|
|
24
26
|
start(): Promise<void>;
|
|
25
27
|
send(message: JSONRPCMessage): Promise<void>;
|
|
26
|
-
close(): Promise<void>;
|
|
28
|
+
close(cause?: CloseCause): Promise<void>;
|
|
27
29
|
}
|
|
28
30
|
export interface EmployeeMcpOptions {
|
|
31
|
+
closeDiagnostics?: CloseDiagnostics;
|
|
29
32
|
lease: EmployeeLease;
|
|
30
33
|
maxBytes: number;
|
|
31
34
|
killGraceMs: number;
|
|
@@ -37,6 +40,6 @@ export interface EmployeeMcpOptions {
|
|
|
37
40
|
export interface EmployeeMcp {
|
|
38
41
|
routing: ReturnType<typeof employeeRouting>;
|
|
39
42
|
signal: AbortSignal;
|
|
40
|
-
close(): Promise<void>;
|
|
43
|
+
close(cause?: CloseCause): Promise<void>;
|
|
41
44
|
}
|
|
42
45
|
export declare function openEmployeeMcp(options: EmployeeMcpOptions): Promise<EmployeeMcp>;
|
package/dist/employee-mcp.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { closeCode } from "./close-diagnostics.js";
|
|
1
2
|
import { createServer } from 'node:net';
|
|
2
3
|
import { mkdtemp, lstat, rm, access } from 'node:fs/promises';
|
|
3
4
|
import { constants } from 'node:fs';
|
|
@@ -20,6 +21,7 @@ export const EMPLOYEE_MCP_TOOLS = [
|
|
|
20
21
|
export class EmployeeSocketTransport {
|
|
21
22
|
socket;
|
|
22
23
|
maxBytes;
|
|
24
|
+
observeClose;
|
|
23
25
|
onclose;
|
|
24
26
|
onerror;
|
|
25
27
|
onmessage;
|
|
@@ -29,9 +31,10 @@ export class EmployeeSocketTransport {
|
|
|
29
31
|
get error() { return this.failure; }
|
|
30
32
|
lifetime = new AbortController();
|
|
31
33
|
signal = this.lifetime.signal;
|
|
32
|
-
constructor(socket, maxBytes) {
|
|
34
|
+
constructor(socket, maxBytes, observeClose) {
|
|
33
35
|
this.socket = socket;
|
|
34
36
|
this.maxBytes = maxBytes;
|
|
37
|
+
this.observeClose = observeClose;
|
|
35
38
|
}
|
|
36
39
|
failed(error) { try {
|
|
37
40
|
this.onerror?.(error);
|
|
@@ -40,11 +43,11 @@ export class EmployeeSocketTransport {
|
|
|
40
43
|
this.failure = observation instanceof Error ? observation : new Error('EMPLOYEE_MCP_OBSERVATION_FAILED');
|
|
41
44
|
}
|
|
42
45
|
finally {
|
|
43
|
-
void this.close();
|
|
46
|
+
void this.close({ layer: 'relay', initiator: 'mcp.socket-failure', reason: closeCode(error, 'EMPLOYEE_MCP_PROTOCOL_OR_SOCKET_ERROR'), action: 'failure' });
|
|
44
47
|
} }
|
|
45
48
|
async start() {
|
|
46
49
|
this.socket.on('error', error => this.failed(error));
|
|
47
|
-
this.socket.on('close', () => void this.close());
|
|
50
|
+
this.socket.on('close', () => void this.close({ layer: 'relay', initiator: 'mcp.socket-peer-close', reason: 'MCP_SOCKET_CLOSED', action: 'close' }));
|
|
48
51
|
this.socket.on('data', (bytes) => {
|
|
49
52
|
try {
|
|
50
53
|
let cursor = 0;
|
|
@@ -71,12 +74,12 @@ export class EmployeeSocketTransport {
|
|
|
71
74
|
throw new Error('EMPLOYEE_MCP_CLOSED');
|
|
72
75
|
const wire = Buffer.from(JSON.stringify(message) + '\n');
|
|
73
76
|
if (wire.length - 1 > this.maxBytes) {
|
|
74
|
-
await this.close();
|
|
77
|
+
await this.close({ layer: 'relay', initiator: 'mcp.send-capacity', reason: 'EMPLOYEE_MCP_MESSAGE_TOO_LARGE', action: 'close' });
|
|
75
78
|
throw new Error('EMPLOYEE_MCP_MESSAGE_TOO_LARGE');
|
|
76
79
|
}
|
|
77
80
|
await new Promise((yes, no) => this.socket.write(wire, error => error ? no(error) : yes()));
|
|
78
81
|
}
|
|
79
|
-
async close() { if (this.ended)
|
|
82
|
+
async close(cause = { layer: 'relay', initiator: 'mcp.transport-close', reason: 'MCP_TRANSPORT_CLOSED', action: 'close' }) { this.observeClose?.({ ...cause, changes: cause.changes ?? [{ field: 'mcp.ended', before: this.ended, after: true }] }); if (this.ended)
|
|
80
83
|
return; this.ended = true; this.lifetime.abort(); this.socket.destroy(); this.onclose?.(); }
|
|
81
84
|
}
|
|
82
85
|
export async function openEmployeeMcp(options) {
|
|
@@ -85,7 +88,7 @@ export async function openEmployeeMcp(options) {
|
|
|
85
88
|
if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 1)
|
|
86
89
|
throw new NeedError('SETTINGS_REQUIRED', '本机工具消息容量无效。', 'acpMaxMessageBytes');
|
|
87
90
|
await access('/usr/bin/nc', constants.X_OK);
|
|
88
|
-
const tools = createEmployeeTools({ cwd: options.lease.target.local.directory.path, shell: options.lease.target.endpoint.endpointShell, uid: options.lease.target.endpoint.remoteUid, maxBytes: options.maxBytes, killGraceMs: options.killGraceMs, detail: observe, runShell: options.runShell, validate: signal => options.lease.validate(signal).then(() => { }) });
|
|
91
|
+
const tools = createEmployeeTools({ closeDiagnostics: options.closeDiagnostics, cwd: options.lease.target.local.directory.path, shell: options.lease.target.endpoint.endpointShell, uid: options.lease.target.endpoint.remoteUid, maxBytes: options.maxBytes, killGraceMs: options.killGraceMs, detail: observe, runShell: options.runShell, validate: signal => options.lease.validate(signal).then(() => { }) });
|
|
89
92
|
const root = await mkdtemp('/private/tmp/aw-x87-'), socketPath = root + '/mcp';
|
|
90
93
|
try {
|
|
91
94
|
const info = await lstat(root);
|
|
@@ -93,19 +96,40 @@ export async function openEmployeeMcp(options) {
|
|
|
93
96
|
throw new NeedError('EMPLOYEE_MCP_STORAGE_INVALID', '本机工具私有通道不可用。', '当前员工的700私有Unix目录');
|
|
94
97
|
}
|
|
95
98
|
catch (error) {
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
options.closeDiagnostics?.record({ layer: 'relay', initiator: 'mcp.storage-failure', reason: closeCode(error), action: 'failure' });
|
|
100
|
+
const failures = [error];
|
|
101
|
+
try {
|
|
102
|
+
await tools.close({ layer: 'executor', initiator: 'mcp.storage-cleanup', reason: 'MCP_STORAGE_FAILED', action: 'close' });
|
|
103
|
+
}
|
|
104
|
+
catch (cleanup) {
|
|
105
|
+
failures.push(cleanup);
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
await rm(root, { recursive: true });
|
|
109
|
+
}
|
|
110
|
+
catch (cleanup) {
|
|
111
|
+
failures.push(cleanup);
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
options.closeDiagnostics?.assertRecorded();
|
|
115
|
+
}
|
|
116
|
+
catch (reporting) {
|
|
117
|
+
failures.push(reporting);
|
|
118
|
+
}
|
|
119
|
+
if (failures.length > 1)
|
|
120
|
+
throw new AggregateError(failures, 'EMPLOYEE_MCP_STORAGE_AND_CLEANUP_FAILED');
|
|
98
121
|
throw error;
|
|
99
122
|
}
|
|
100
123
|
const clients = new Set(), transports = new Set();
|
|
101
124
|
let closing, diagnosticFailure;
|
|
102
125
|
const failureLifetime = new AbortController();
|
|
103
126
|
const poison = (error) => {
|
|
127
|
+
options.closeDiagnostics?.record({ layer: 'relay', initiator: 'mcp.poison', reason: closeCode(error, 'CONVERSATION_DETAILS_FAILED'), action: 'invalidate' });
|
|
104
128
|
diagnosticFailure ??= error;
|
|
105
129
|
failureLifetime.abort(error);
|
|
106
130
|
// Close is memoized synchronously: no fresh connection/call can pass the fence.
|
|
107
|
-
void options.lease.close().catch(failure => { diagnosticFailure ??= failure; });
|
|
108
|
-
void close().catch(failure => { diagnosticFailure ??= failure; });
|
|
131
|
+
void options.lease.close({ layer: 'endpoint', initiator: 'mcp.poison-lease', reason: 'CONVERSATION_DETAILS_FAILED', action: 'close' }).catch(failure => { diagnosticFailure ??= failure; });
|
|
132
|
+
void close({ layer: 'relay', initiator: 'mcp.poison-close', reason: 'CONVERSATION_DETAILS_FAILED', action: 'close' }).catch(failure => { diagnosticFailure ??= failure; });
|
|
109
133
|
};
|
|
110
134
|
function observeUsing(event, sink) { if (diagnosticFailure)
|
|
111
135
|
throw diagnosticFailure; try {
|
|
@@ -118,10 +142,12 @@ export async function openEmployeeMcp(options) {
|
|
|
118
142
|
function observe(event) { observeUsing(event, options.detail); }
|
|
119
143
|
const server = createServer(socket => {
|
|
120
144
|
if (closing || failureLifetime.signal.aborted || options.lease.signal.aborted) {
|
|
145
|
+
options.closeDiagnostics?.record({ layer: 'relay', initiator: 'mcp.reject-late-socket', reason: 'MCP_CLOSING', action: 'close' });
|
|
121
146
|
socket.destroy();
|
|
122
147
|
return;
|
|
123
148
|
}
|
|
124
|
-
const
|
|
149
|
+
const observeConnection = options.closeDiagnostics?.child().capture();
|
|
150
|
+
const mcp = new Server({ name: 'ai-workbench-employee', version: '0.8.14' }, { capabilities: { tools: {} } }), transport = new EmployeeSocketTransport(socket, options.maxBytes, observeConnection);
|
|
125
151
|
clients.add(mcp);
|
|
126
152
|
transports.add(transport);
|
|
127
153
|
mcp.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: EMPLOYEE_MCP_TOOLS }));
|
|
@@ -133,40 +159,50 @@ export async function openEmployeeMcp(options) {
|
|
|
133
159
|
const sink = options.captureDetail?.() ?? options.detail;
|
|
134
160
|
return tools.call(request.params.name, request.params.arguments ?? {}, signal, event => observeUsing(event, sink));
|
|
135
161
|
});
|
|
136
|
-
mcp.onerror = () => { try {
|
|
162
|
+
mcp.onerror = () => { observeConnection?.({ layer: 'relay', initiator: 'mcp.server-error', reason: 'EMPLOYEE_MCP_PROTOCOL_OR_TRANSPORT_ERROR', action: 'failure' }); try {
|
|
137
163
|
observe({ kind: 'session', severity: 'warning', code: 'CLAUDE_EMPLOYEE_MCP_UNKNOWN', summary: '本机工具连接出现协议或传输错误;在途结果未知,没有重放' });
|
|
138
164
|
}
|
|
139
165
|
catch (error) {
|
|
140
166
|
poison(error);
|
|
141
167
|
} };
|
|
142
|
-
mcp.onclose = () => { clients.delete(mcp); transports.delete(transport); };
|
|
143
|
-
void mcp.connect(transport).catch(
|
|
168
|
+
mcp.onclose = () => { observeConnection?.({ layer: 'relay', initiator: 'mcp.client-closed', reason: 'MCP_CLIENT_CLOSED', action: 'close' }); clients.delete(mcp); transports.delete(transport); };
|
|
169
|
+
void mcp.connect(transport).catch(error => { observeConnection?.({ layer: 'relay', initiator: 'mcp.connect-failure', reason: closeCode(error, 'MCP_CONNECT_FAILED'), action: 'close' }); socket.destroy(); });
|
|
144
170
|
});
|
|
145
|
-
const close = (
|
|
146
|
-
options.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
171
|
+
const close = (cause = { layer: 'relay', initiator: 'mcp.close', reason: 'MCP_CLOSE_REQUESTED', action: 'close' }) => {
|
|
172
|
+
options.closeDiagnostics?.record({ ...cause, changes: cause.changes ?? [{ field: 'mcp.closing', before: !!closing, after: true }] });
|
|
173
|
+
return closing ??= Promise.resolve().then(async () => {
|
|
174
|
+
options.lease.signal.removeEventListener('abort', aborted);
|
|
175
|
+
const errors = [], clean = async (operation) => { try {
|
|
176
|
+
await operation();
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
if (!errors.includes(error))
|
|
180
|
+
errors.push(error);
|
|
181
|
+
} };
|
|
182
|
+
for (const transport of transports)
|
|
183
|
+
await clean(() => transport.close({ layer: 'relay', initiator: 'mcp.cleanup-transport', reason: 'MCP_CLEANUP', action: 'close' }));
|
|
184
|
+
await Promise.all([...clients].map(client => clean(() => client.close())));
|
|
185
|
+
await clean(() => tools.close({ layer: 'executor', initiator: 'mcp.cleanup-tools', reason: 'MCP_CLEANUP', action: 'close' }));
|
|
186
|
+
await clean(() => options.lease.close({ layer: 'endpoint', initiator: 'mcp.cleanup-lease', reason: 'MCP_CLEANUP', action: 'close' }));
|
|
187
|
+
options.closeDiagnostics?.record({ layer: 'relay', initiator: 'mcp.server-close', reason: 'MCP_CLEANUP', action: 'close' });
|
|
188
|
+
if (server.listening)
|
|
189
|
+
await clean(() => new Promise((yes, no) => server.close(error => error ? no(error) : yes())));
|
|
190
|
+
await clean(() => rm(root, { recursive: true }));
|
|
191
|
+
if (diagnosticFailure && !errors.includes(diagnosticFailure))
|
|
192
|
+
errors.unshift(diagnosticFailure);
|
|
193
|
+
try {
|
|
194
|
+
options.closeDiagnostics?.assertRecorded();
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
152
197
|
errors.push(error);
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
await clean(() => rm(root, { recursive: true }));
|
|
162
|
-
if (diagnosticFailure && !errors.includes(diagnosticFailure))
|
|
163
|
-
errors.unshift(diagnosticFailure);
|
|
164
|
-
if (errors.length === 1)
|
|
165
|
-
throw errors[0];
|
|
166
|
-
if (errors.length)
|
|
167
|
-
throw new AggregateError(errors, 'EMPLOYEE_MCP_CLEANUP_FAILED');
|
|
168
|
-
});
|
|
169
|
-
const aborted = () => { void close().catch(error => { diagnosticFailure ??= error; failureLifetime.abort(error); }); };
|
|
198
|
+
}
|
|
199
|
+
if (errors.length === 1)
|
|
200
|
+
throw errors[0];
|
|
201
|
+
if (errors.length)
|
|
202
|
+
throw new AggregateError(errors, 'EMPLOYEE_MCP_CLEANUP_FAILED');
|
|
203
|
+
});
|
|
204
|
+
};
|
|
205
|
+
const aborted = () => { void close({ layer: 'endpoint', initiator: 'mcp.lease-abort', reason: closeCode(options.lease.signal.reason, 'EMPLOYEE_CONTEXT_CHANGED'), action: 'abort' }).catch(error => { diagnosticFailure ??= error; failureLifetime.abort(error); }); };
|
|
170
206
|
try {
|
|
171
207
|
await new Promise((yes, no) => { server.once('error', no); server.listen(socketPath, () => { server.removeListener('error', no); yes(); }); });
|
|
172
208
|
options.lease.signal.addEventListener('abort', aborted, { once: true });
|
|
@@ -175,7 +211,7 @@ export async function openEmployeeMcp(options) {
|
|
|
175
211
|
return { routing: employeeRouting(options.lease.target, socketPath, options.maxBytes), signal: failureLifetime.signal, close };
|
|
176
212
|
}
|
|
177
213
|
catch (error) {
|
|
178
|
-
await close();
|
|
214
|
+
await close({ layer: 'relay', initiator: 'mcp.open-failure', reason: closeCode(error, 'MCP_OPEN_FAILED'), action: 'failure' });
|
|
179
215
|
throw error;
|
|
180
216
|
}
|
|
181
217
|
}
|