@ai-workbench/remote-agent 0.8.13 → 0.8.15
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 +10 -8
- 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 +62 -35
- 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-presentation.d.ts +6 -0
- package/dist/employee-presentation.js +16 -0
- package/dist/employee-provider.js +59 -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 +18 -4
- package/dist/index.js +183 -55
- 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/model-presentation.d.ts +13 -0
- package/dist/model-presentation.js +18 -0
- package/dist/presentation.js +2 -1
- 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/package.json +1 -1
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
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { GenerateOptions } from '@deepseek-ai/dsh-llm';
|
|
2
|
+
export declare const CLAUDE_EMPLOYEE_SCOPE_NOTICE = "\u672C\u8F6E\u516D\u7C7B\u6587\u4EF6\u4E0E\u547D\u4EE4\u5DE5\u5177\u5728\u4F60\u7684\u7535\u8111\u6267\u884C\uFF0C\u4F7F\u7528\u4F60\u9009\u62E9\u7684\u6587\u4EF6\u5939\uFF1BGit\u4E5F\u4F7F\u7528\u672C\u673A\u4ED3\u5E93\u3002\u5B50\u4EE3\u7406\u548C\u5185\u90E8\u76F4\u63A5\u8C03\u7528\u5C1A\u4E0D\u652F\u6301\u672C\u673A\uFF0C\u53EF\u80FD\u4ECD\u5728\u4E3B\u673A\uFF1B\u5DF2\u8BC6\u522B\u7684\u6B64\u7C7B\u5BA1\u6279\u4E0D\u4F1A\u6267\u884C\u3002PDF\u7B49\u4E8C\u8FDB\u5236\u8BFB\u53D6\u3001\u540E\u53F0\u547D\u4EE4\u6682\u4E0D\u652F\u6301\u3002";
|
|
3
|
+
export declare const CODEX_EMPLOYEE_SCOPE_NOTICE = "\u6587\u4EF6\u4E0E\u547D\u4EE4\u5728\u672C\u673A\u9009\u5B9A\u76EE\u5F55\u8FD0\u884C\uFF0CGit \u4F7F\u7528\u672C\u673A\u4ED3\u5E93\uFF1B\u6A21\u578B\u3001\u767B\u5F55\u548C\u5168\u5C40\u52A9\u624B\u8981\u6C42\u4ECD\u5728\u8FDC\u7A0B\u4E3B\u673A\u3002\u672C\u673A\u81EA\u5B9A\u4E49\u7684\u9644\u52A0\u5DE5\u5177\u3001\u81EA\u52A8\u52A8\u4F5C\u3001\u63D2\u4EF6\u53CA\u65AD\u5F00\u540E\u7684\u4F1A\u8BDD\u6062\u590D\u6682\u4E0D\u652F\u6301\u3002\u5B50\u4EFB\u52A1\u88AB\u7CFB\u7EDF\u56DE\u6536\u540E\u80FD\u5426\u7EE7\u7EED\uFF0C\u5C1A\u672A\u9A8C\u8BC1\u3002";
|
|
4
|
+
export declare const EMPLOYEE_EXECUTION_NOTICE = "\u6587\u4EF6\u548C\u547D\u4EE4\u5728\u4F60\u7684\u7535\u8111\u4E0A\u6267\u884C";
|
|
5
|
+
/** Presentation receipt only; it grants no process, workspace or replay authority. */
|
|
6
|
+
export declare function employeeNoticePreviouslyShown(options: Pick<GenerateOptions, 'messages' | 'sessionId'>): boolean;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AGENTS } from "./agents.js";
|
|
2
|
+
export const CLAUDE_EMPLOYEE_SCOPE_NOTICE = '本轮六类文件与命令工具在你的电脑执行,使用你选择的文件夹;Git也使用本机仓库。子代理和内部直接调用尚不支持本机,可能仍在主机;已识别的此类审批不会执行。PDF等二进制读取、后台命令暂不支持。';
|
|
3
|
+
export const CODEX_EMPLOYEE_SCOPE_NOTICE = '文件与命令在本机选定目录运行,Git 使用本机仓库;模型、登录和全局助手要求仍在远程主机。本机自定义的附加工具、自动动作、插件及断开后的会话恢复暂不支持。子任务被系统回收后能否继续,尚未验证。';
|
|
4
|
+
export const EMPLOYEE_EXECUTION_NOTICE = '文件和命令在你的电脑上执行';
|
|
5
|
+
/** Presentation receipt only; it grants no process, workspace or replay authority. */
|
|
6
|
+
export function employeeNoticePreviouslyShown(options) {
|
|
7
|
+
if (!options.sessionId)
|
|
8
|
+
return false;
|
|
9
|
+
return options.messages.some(message => {
|
|
10
|
+
if (message.role !== 'assistant' || !AGENTS.some(agent => agent.id === message.source.provider))
|
|
11
|
+
return false;
|
|
12
|
+
const pointer = message.source.replayState?.response;
|
|
13
|
+
const sameSession = !!pointer && pointer.version === 2 && pointer.agent === message.source.provider && pointer.dshSessionId === options.sessionId && [pointer.identity, pointer.cwd, pointer.sessionId].every(value => typeof value === 'string' && value.length > 0);
|
|
14
|
+
return sameSession && (pointer.presentation?.employeeExecutionNotice === 'local-v1' || message.content.some(block => block.type === 'text' && [CLAUDE_EMPLOYEE_SCOPE_NOTICE, CODEX_EMPLOYEE_SCOPE_NOTICE].some(notice => block.text.startsWith(notice + '\n\n'))));
|
|
15
|
+
});
|
|
16
|
+
}
|
|
@@ -6,6 +6,8 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
6
6
|
}
|
|
7
7
|
return path;
|
|
8
8
|
};
|
|
9
|
+
import { CODEX_EMPLOYEE_SCOPE_NOTICE } from "./employee-presentation.js";
|
|
10
|
+
import { CloseDiagnostics, closeCode, closeErrorCodes } from "./close-diagnostics.js";
|
|
9
11
|
import { createHash } from 'node:crypto';
|
|
10
12
|
import { admitEmployeeLocation } from "./employee-location.js";
|
|
11
13
|
import { EmployeeExecution } from "./employee-exec.js";
|
|
@@ -104,7 +106,7 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
104
106
|
prior = live.get(sessionId);
|
|
105
107
|
}
|
|
106
108
|
if (prior && (prior.current.api !== api || info?.workspaceLocations !== 1 || info?.localWorkspaceContext !== 1)) {
|
|
107
|
-
await prior.dispose(fail('EMPLOYEE_CONTEXT_CHANGED'));
|
|
109
|
+
await prior.dispose(fail('EMPLOYEE_CONTEXT_CHANGED'), { layer: 'endpoint', initiator: 'provider.scope-change', reason: 'EMPLOYEE_CONTEXT_CHANGED', action: 'dispose' });
|
|
108
110
|
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
109
111
|
}
|
|
110
112
|
if (info?.workspaceLocations !== 1 && info?.localWorkspaceContext !== 1)
|
|
@@ -121,12 +123,12 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
121
123
|
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
122
124
|
if (row.source !== 'local') {
|
|
123
125
|
if (prior)
|
|
124
|
-
await prior.dispose(fail('EMPLOYEE_CONTEXT_CHANGED'));
|
|
126
|
+
await prior.dispose(fail('EMPLOYEE_CONTEXT_CHANGED'), { layer: 'endpoint', initiator: 'provider.scope-change', reason: 'EMPLOYEE_CONTEXT_CHANGED', action: 'dispose' });
|
|
125
127
|
if (request.localAcceptance)
|
|
126
128
|
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
127
129
|
return;
|
|
128
130
|
}
|
|
129
|
-
let details;
|
|
131
|
+
let details, closeDiagnostics;
|
|
130
132
|
try {
|
|
131
133
|
const acceptance = request.localAcceptance;
|
|
132
134
|
if (!acceptance || acceptance.instanceId !== connection.instanceId || acceptance.epoch !== connection.epoch || acceptance.sessionId !== sessionId || acceptance.provider !== agent.loginProvider || acceptance.workspaceId !== workspace.id || acceptance.bindingId !== row.bindingId)
|
|
@@ -134,6 +136,10 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
134
136
|
details = await options.details?.(request, api, connection, target);
|
|
135
137
|
if (details)
|
|
136
138
|
request.timing?.bind(detail => details.record(detail));
|
|
139
|
+
const sink = details;
|
|
140
|
+
closeDiagnostics = prior?.runtime.closeDiagnostics ?? new CloseDiagnostics(event => sink?.record(event));
|
|
141
|
+
closeDiagnostics.bind(event => sink?.record(event));
|
|
142
|
+
request.closeDiagnostics = closeDiagnostics;
|
|
137
143
|
const unsupported = request.mode !== 'session' || agent.loginProvider !== 'codex' || !!request.recoveryCheckpointId || !!request.resumeSessionId && !prior;
|
|
138
144
|
if (unsupported)
|
|
139
145
|
throw fail('EMPLOYEE_OPERATION_UNSUPPORTED');
|
|
@@ -145,7 +151,7 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
145
151
|
if (!network || typeof network.prepareEmployeeEndpoint !== 'function' || typeof network.onState !== 'function')
|
|
146
152
|
throw fail('DEPENDENCY_REQUIRED');
|
|
147
153
|
if (prior && prior.network !== network) {
|
|
148
|
-
await prior.dispose(fail('EMPLOYEE_CONTEXT_CHANGED'));
|
|
154
|
+
await prior.dispose(fail('EMPLOYEE_CONTEXT_CHANGED'), { layer: 'endpoint', initiator: 'provider.scope-change', reason: 'EMPLOYEE_CONTEXT_CHANGED', action: 'dispose' });
|
|
149
155
|
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
150
156
|
}
|
|
151
157
|
const admitted = await admitEmployeeLocation({ api: api, network, workspace, connection, expectedLocal: { instanceId: acceptance.instanceId, epoch: acceptance.epoch, workspaceId: acceptance.workspaceId, bindingId: acceptance.bindingId, directory: { ...acceptance.directory } }, settingsPath: target.session.settingsPath, signal: request.signal });
|
|
@@ -167,7 +173,7 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
167
173
|
native: { path: native.path, sha256: native.sha256 },
|
|
168
174
|
});
|
|
169
175
|
if (prior && (prior.scope !== scope || prior.ended))
|
|
170
|
-
await prior.dispose(fail('EMPLOYEE_CONTEXT_CHANGED'));
|
|
176
|
+
await prior.dispose(fail('EMPLOYEE_CONTEXT_CHANGED'), { layer: 'endpoint', initiator: 'provider.scope-change', reason: 'EMPLOYEE_CONTEXT_CHANGED', action: 'dispose' });
|
|
171
177
|
let entry = prior && prior.scope === scope && !prior.ended ? prior : undefined;
|
|
172
178
|
let candidate = entry;
|
|
173
179
|
const base = async (signal) => {
|
|
@@ -177,7 +183,7 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
177
183
|
if (candidate?.ended)
|
|
178
184
|
throw candidate.reason ?? fail('EMPLOYEE_EXEC_CLOSED');
|
|
179
185
|
await (candidate?.current ?? input).stable(signal);
|
|
180
|
-
await admitted.validate(signal);
|
|
186
|
+
await closeDiagnostics.run('endpoint.prepare', () => admitted.validate(signal));
|
|
181
187
|
if (get() !== api || employee.getNetwork() !== network)
|
|
182
188
|
throw fail('EMPLOYEE_CONTEXT_CHANGED');
|
|
183
189
|
await (candidate?.current ?? input).stable(signal);
|
|
@@ -193,25 +199,26 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
193
199
|
}
|
|
194
200
|
const io = employee.io ?? sshIo(target, argv, request.acpMaxMessageBytes);
|
|
195
201
|
const scopedIo = { run: (command, signal) => io.run(command, signal), attach: async (command) => {
|
|
202
|
+
const observe = closeDiagnostics.capture();
|
|
196
203
|
const child = await io.attach(command);
|
|
197
|
-
child.once('close', () => { if (candidate && !candidate.ended)
|
|
198
|
-
void candidate.dispose(fail('EMPLOYEE_EXEC_UNKNOWN')).catch(() => { }); });
|
|
204
|
+
child.once('close', (code, signal) => { observe({ layer: 'relay', initiator: 'ssh.process.close', reason: 'EMPLOYEE_EXEC_UNKNOWN', action: 'exit', exitCode: code, signal }); if (candidate && !candidate.ended)
|
|
205
|
+
void candidate.dispose(fail('EMPLOYEE_EXEC_UNKNOWN'), { layer: 'runtime', initiator: 'provider.child-close', reason: 'EMPLOYEE_EXEC_UNKNOWN', action: 'dispose' }).catch(() => { }); });
|
|
199
206
|
return child;
|
|
200
207
|
} };
|
|
201
|
-
const ssh = { target, endpoint: admitted.endpoint, employeeBinary: native.path, binarySha256: native.sha256, acpMaxMessageBytes: request.acpMaxMessageBytes, argv: argv ?? (() => []), validate: base, io: scopedIo };
|
|
208
|
+
const ssh = { closeDiagnostics, target, endpoint: admitted.endpoint, employeeBinary: native.path, binarySha256: native.sha256, acpMaxMessageBytes: request.acpMaxMessageBytes, argv: argv ?? (() => []), validate: base, io: scopedIo };
|
|
202
209
|
const validate = async (signal) => { try {
|
|
203
210
|
await validateEmployeeSshTarget(ssh, signal);
|
|
204
211
|
}
|
|
205
212
|
catch (error) {
|
|
206
213
|
if (candidate && errorCode(error) !== 'ABORTED')
|
|
207
|
-
await candidate.dispose(asNeed(error));
|
|
214
|
+
await candidate.dispose(asNeed(error), { layer: 'endpoint', initiator: 'provider.validation-failure', reason: closeCode(error), action: 'invalidate' });
|
|
208
215
|
throw error;
|
|
209
216
|
} };
|
|
210
217
|
await validate(request.signal ?? new AbortController().signal);
|
|
211
218
|
if (!options.open)
|
|
212
219
|
throw fail('RUNTIME_REQUIRED');
|
|
213
220
|
// Host cwd is only the protected agent bootstrap/config directory. Native tools use admitted.cwd.
|
|
214
|
-
const host = await options.open(target, { name: 'employee', path: target.projectRoot }, { ...request, agent, employeeScope: scope, bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, bindingRevision: state.revision, detailChannel: details, validateContext: async (ignore = false) => {
|
|
221
|
+
const host = await options.open(target, { name: 'employee', path: target.projectRoot }, { ...request, agent, employeeScope: scope, bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, bindingRevision: state.revision, closeDiagnostics, detailChannel: details, validateContext: async (ignore = false) => {
|
|
215
222
|
// Disposal addresses only the already-owned host broker instance through its frozen IO/CAS.
|
|
216
223
|
// A withdrawn local source must prevent dispatch, but cannot forbid that instance's exact kill.
|
|
217
224
|
if (ignore)
|
|
@@ -226,49 +233,65 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
226
233
|
await validate(request.signal ?? new AbortController().signal);
|
|
227
234
|
}
|
|
228
235
|
catch (error) {
|
|
229
|
-
await entry.dispose(asNeed(error));
|
|
236
|
+
await entry.dispose(asNeed(error), { layer: 'endpoint', initiator: 'provider.final-validation', reason: closeCode(error), action: 'dispose' });
|
|
230
237
|
throw error;
|
|
231
238
|
}
|
|
232
239
|
entry.runtime.details = details;
|
|
233
240
|
entry.runtime.connection = { ...connection };
|
|
234
241
|
entry.runtime.binding = { bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, revision: state.revision };
|
|
242
|
+
entry.runtime.restartNotice = host.restartNotice;
|
|
243
|
+
entry.runtime.restartInputIds = host.restartInputIds;
|
|
244
|
+
entry.runtime.freshSessionConfirmed = host.freshSessionConfirmed;
|
|
235
245
|
return entry.runtime;
|
|
236
246
|
}
|
|
237
247
|
const disposers = [];
|
|
238
|
-
const execution = new EmployeeExecution({ bindingId: admitted.bindingId, acpMaxMessageBytes: request.acpMaxMessageBytes, validate, transport: signal => createEmployeeSshTransport(ssh, signal) });
|
|
239
|
-
const runtime = { ...host, cwd: admitted.cwd, details, connection: { ...connection }, binding: { bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, revision: state.revision }, saveCheckpoint: undefined, recoveredSession: undefined,
|
|
240
|
-
entry = { scope, runtime, current: input, details, execution, network, ended: false, dispose: reason => {
|
|
248
|
+
const execution = new EmployeeExecution({ closeDiagnostics, bindingId: admitted.bindingId, acpMaxMessageBytes: request.acpMaxMessageBytes, validate, transport: signal => createEmployeeSshTransport(ssh, signal) });
|
|
249
|
+
const runtime = { ...host, closeDiagnostics, cwd: admitted.cwd, details, connection: { ...connection }, binding: { bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, revision: state.revision }, saveCheckpoint: undefined, recoveredSession: undefined, employeeExecutionMeta: { version: 1, bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, cwd: admitted.cwd, acpMaxMessageBytes: request.acpMaxMessageBytes }, employeePresentation: CODEX_EMPLOYEE_SCOPE_NOTICE };
|
|
250
|
+
entry = { scope, runtime, current: input, details, execution, network, ended: false, dispose: (reason, cause = { layer: 'runtime', initiator: 'provider.dispose', reason: reason?.code ?? 'EMPLOYEE_EXEC_CLOSED', action: 'dispose' }) => {
|
|
251
|
+
closeDiagnostics.record({ ...cause, changes: cause.changes ?? [{ field: 'provider.ended', before: entry.ended, after: true }] });
|
|
241
252
|
if (entry.closing)
|
|
242
253
|
return entry.closing;
|
|
243
254
|
entry.ended = true;
|
|
244
255
|
entry.reason = reason;
|
|
245
|
-
execution.invalidate(reason ?? fail('EMPLOYEE_EXEC_CLOSED'));
|
|
256
|
+
execution.invalidate(reason ?? fail('EMPLOYEE_EXEC_CLOSED'), { layer: 'executor', initiator: 'provider.invalidate-executor', reason: reason?.code ?? 'EMPLOYEE_EXEC_CLOSED', action: 'invalidate' });
|
|
246
257
|
for (const off of disposers)
|
|
247
258
|
off();
|
|
248
259
|
host.broker?.fail(reason ?? fail('EMPLOYEE_EXEC_CLOSED'));
|
|
249
260
|
entry.closing = (async () => {
|
|
250
|
-
|
|
261
|
+
const cleanup = [], reporting = [];
|
|
251
262
|
try {
|
|
252
|
-
await execution.dispose();
|
|
263
|
+
await execution.dispose({ layer: 'executor', initiator: 'provider.cleanup-executor', reason: 'RUNTIME_CLEANUP', action: 'dispose' });
|
|
253
264
|
}
|
|
254
265
|
catch (error) {
|
|
255
|
-
cleanup
|
|
266
|
+
cleanup.push(...closeErrorCodes(error, 'EXECUTOR_CLEANUP_FAILED'));
|
|
256
267
|
}
|
|
257
268
|
try {
|
|
258
|
-
await host.dispose?.();
|
|
269
|
+
await host.dispose?.({ layer: 'broker', initiator: 'provider.cleanup-host', reason: 'RUNTIME_CLEANUP', action: 'dispose' });
|
|
259
270
|
}
|
|
260
271
|
catch (error) {
|
|
261
|
-
cleanup
|
|
272
|
+
cleanup.push(...closeErrorCodes(error, 'HOST_CLEANUP_FAILED'));
|
|
273
|
+
}
|
|
274
|
+
const firstReason = String(closeDiagnostics.first?.reason ?? reason?.code ?? 'EMPLOYEE_EXEC_CLOSED');
|
|
275
|
+
try {
|
|
276
|
+
entry.details?.record({ kind: 'need', severity: 'warning', code: cleanup.length ? 'REMOTE_CLEANUP_UNCONFIRMED' : 'EMPLOYEE_EXEC_UNKNOWN', summary: '员工执行通道已关闭,命令结果 UNKNOWN', payload: { bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, result: 'UNKNOWN', reason: reason?.code ?? 'EMPLOYEE_EXEC_CLOSED', firstReason, cleanup: cleanup.length ? 'unconfirmed' : 'closed', cleanupCodes: cleanup } });
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
reporting.push(closeCode(error, 'CLOSE_RECORD_FAILED'));
|
|
262
280
|
}
|
|
263
281
|
try {
|
|
264
|
-
entry.details?.record({ kind: 'need', severity: 'warning', code: cleanup ? 'REMOTE_CLEANUP_UNCONFIRMED' : 'EMPLOYEE_EXEC_UNKNOWN', summary: '员工执行通道已关闭,命令结果 UNKNOWN', payload: { bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, result: 'UNKNOWN', reason: reason?.code ?? 'EMPLOYEE_EXEC_CLOSED', cleanup: cleanup ? 'unconfirmed' : 'closed' } });
|
|
265
282
|
await entry.details?.flush();
|
|
266
283
|
}
|
|
267
|
-
catch {
|
|
268
|
-
|
|
284
|
+
catch (error) {
|
|
285
|
+
reporting.push(...closeErrorCodes(error, 'CLOSE_FLUSH_FAILED'));
|
|
286
|
+
}
|
|
287
|
+
try {
|
|
288
|
+
closeDiagnostics.assertRecorded();
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
reporting.push(closeDiagnostics.recordingFailureCode ?? closeCode(error, 'CLOSE_RECORD_FAILED'));
|
|
269
292
|
}
|
|
270
|
-
if (cleanup)
|
|
271
|
-
throw
|
|
293
|
+
if (cleanup.length || reporting.length)
|
|
294
|
+
throw Object.assign(new NeedError(reporting.length ? 'CONVERSATION_DETAILS_FAILED' : 'REMOTE_CLEANUP_UNCONFIRMED', '执行连接已关闭;首次原因 ' + firstReason + ',清理或诊断保存未确认。', '首次关闭 ' + firstReason + ';清理 ' + (cleanup.join(',') || 'closed') + ';保存 ' + (reporting.join(',') || 'recorded')), { cause: new AggregateError([firstReason, ...cleanup, ...reporting].map(code => new NeedError(code, '关闭收尾代码 ' + code, '技术详情中的固定代码')), 'EMPLOYEE_CLOSE_AND_DIAGNOSTIC_FAILURES') });
|
|
272
295
|
if (live.get(sessionId) === entry)
|
|
273
296
|
live.delete(sessionId);
|
|
274
297
|
})();
|
|
@@ -279,35 +302,35 @@ export function createEmployeeProvider(get, options, agent) {
|
|
|
279
302
|
try {
|
|
280
303
|
const result = await execution.handle(params, signal);
|
|
281
304
|
if (result.closed)
|
|
282
|
-
await entry.dispose();
|
|
305
|
+
await entry.dispose(undefined, { layer: 'runtime', initiator: 'provider.rpc-closed-result', reason: 'EMPLOYEE_EXEC_CLOSED', operation: typeof params === 'object' && params && 'operation' in params && typeof params.operation === 'string' ? params.operation : 'unknown', action: 'dispose' });
|
|
283
306
|
return { ...result };
|
|
284
307
|
}
|
|
285
308
|
catch (error) {
|
|
286
309
|
// EmployeeExecution reserves ABORTED for an unconsumed read wait. Lost responses are UNKNOWN.
|
|
287
310
|
if (!(params && typeof params === 'object' && 'operation' in params && params.operation === 'read' && errorCode(error) === 'ABORTED'))
|
|
288
|
-
await entry.dispose(asNeed(error));
|
|
311
|
+
await entry.dispose(asNeed(error), { layer: 'runtime', initiator: 'provider.rpc-failure', reason: closeCode(error), action: 'dispose' });
|
|
289
312
|
throw error;
|
|
290
313
|
}
|
|
291
314
|
};
|
|
292
|
-
runtime.dispose =
|
|
293
|
-
runtime.detach = runtime.
|
|
294
|
-
const revoke = (reason) => { void entry.dispose(reason).catch(() => { }); };
|
|
315
|
+
runtime.dispose = cause => entry.dispose(undefined, cause ?? { layer: 'runtime', initiator: 'runtime.dispose', reason: 'RUNTIME_DISPOSE', action: 'dispose' });
|
|
316
|
+
runtime.detach = cause => entry.dispose(undefined, cause ?? { layer: 'runtime', initiator: 'runtime.detach', reason: 'RUNTIME_DETACH', action: 'detach' });
|
|
317
|
+
const revoke = (reason, initiator, changes) => { void entry.dispose(reason, { layer: 'endpoint', initiator, reason: reason.code, action: 'invalidate', changes }).catch(() => { }); };
|
|
295
318
|
disposers.push(api.onWorkspaceLocationState(next => {
|
|
296
319
|
const matches = next.items.filter(item => item.workspaceId === admitted.workspaceId);
|
|
297
320
|
if (get() !== api || next.phase !== 'ready' || next.instanceId !== connection.instanceId || next.epoch !== connection.epoch || matches.length !== 1 || matches[0].bindingId !== admitted.bindingId || matches[0].source !== 'local' || matches[0].status !== 'selected')
|
|
298
|
-
revoke(fail('EMPLOYEE_CONTEXT_CHANGED'));
|
|
321
|
+
revoke(fail('EMPLOYEE_CONTEXT_CHANGED'), 'endpoint.workspace-change', [{ field: 'workspace.binding-id', before: admitted.bindingId, after: matches[0]?.bindingId ?? null }, { field: 'workspace.source', before: 'local', after: matches[0]?.source ?? null }, { field: 'workspace.status', before: 'selected', after: matches[0]?.status ?? null }, { field: 'workspace.match-count', before: 1, after: matches.length }]);
|
|
299
322
|
}), api.onConnectionState(next => { if (get() !== api || next.phase !== 'connected' || next.instanceId !== connection.instanceId || next.epoch !== connection.epoch)
|
|
300
|
-
revoke(fail('CONNECTION_LOST')); }), network.onState(event => { if (event.kind === 'lost' || employee.getNetwork() !== network)
|
|
301
|
-
revoke(fail('CONNECTION_LOST')); }));
|
|
323
|
+
revoke(fail('CONNECTION_LOST'), 'endpoint.connection-change', [{ field: 'connection.instance-id', before: connection.instanceId, after: next.instanceId }, { field: 'connection.epoch', before: connection.epoch, after: next.epoch }, { field: 'connection.phase', before: 'connected', after: next.phase }]); }), network.onState(event => { if (event.kind === 'lost' || employee.getNetwork() !== network)
|
|
324
|
+
revoke(fail('CONNECTION_LOST'), 'endpoint.network-loss', [{ field: 'network.phase', before: null, after: event.state.phase }]); }));
|
|
302
325
|
if (host.broker)
|
|
303
326
|
disposers.push(host.broker.onChange(() => { if (!host.broker.online)
|
|
304
|
-
revoke(fail('EMPLOYEE_EXEC_UNKNOWN')); }));
|
|
327
|
+
revoke(fail('EMPLOYEE_EXEC_UNKNOWN'), 'endpoint.broker-offline'); }));
|
|
305
328
|
live.set(sessionId, entry);
|
|
306
329
|
try {
|
|
307
330
|
await validate(request.signal ?? new AbortController().signal);
|
|
308
331
|
}
|
|
309
332
|
catch (error) {
|
|
310
|
-
await entry.dispose(asNeed(error));
|
|
333
|
+
await entry.dispose(asNeed(error), { layer: 'endpoint', initiator: 'provider.final-validation', reason: closeCode(error), action: 'dispose' });
|
|
311
334
|
throw error;
|
|
312
335
|
}
|
|
313
336
|
return runtime;
|
package/dist/employee-shell.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CloseDiagnostics } from './close-diagnostics.ts';
|
|
1
2
|
import type { ChildProcessWithoutNullStreams } from 'node:child_process';
|
|
2
3
|
import type { EmployeeEndpointTarget } from './employee-endpoint.ts';
|
|
3
4
|
export interface EmployeeShellResult {
|
|
@@ -13,6 +14,7 @@ export type EmployeeShell = (request: {
|
|
|
13
14
|
signal: AbortSignal;
|
|
14
15
|
}) => Promise<EmployeeShellResult>;
|
|
15
16
|
interface Options {
|
|
17
|
+
closeDiagnostics?: CloseDiagnostics;
|
|
16
18
|
target: EmployeeEndpointTarget;
|
|
17
19
|
maxBytes: number;
|
|
18
20
|
killGraceMs: number;
|
package/dist/employee-shell.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { closeCode } from "./close-diagnostics.js";
|
|
1
2
|
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { EMPLOYEE_TARGET_PYTHON, EMPLOYEE_PYTHON_ERRORS } from "./employee-relay.js";
|
|
3
4
|
import { NeedError } from "./remote.js";
|
|
@@ -15,11 +16,13 @@ const unknown = (code) => new NeedError(code, '本机命令结果未知;没有
|
|
|
15
16
|
export function createEmployeeShell(options) {
|
|
16
17
|
return async ({ command, timeout, signal }) => {
|
|
17
18
|
signal.throwIfAborted();
|
|
19
|
+
const diagnostics = options.closeDiagnostics?.child();
|
|
18
20
|
const executionId = randomUUID(), target = options.target;
|
|
19
21
|
const child = await options.attach({ command: '/usr/bin/python3', args: ['-c', SHELL_PYTHON, JSON.stringify({ endpoint: target.endpoint, bridge: target.bridge, directory: target.local.directory, maxBytes: options.maxBytes, executionId })] });
|
|
20
22
|
return new Promise((yes, no) => {
|
|
21
23
|
let settled = false, bytes = 0, stdout = [], stderr = [], reason, grace, timer;
|
|
22
24
|
const finish = (error, code) => {
|
|
25
|
+
diagnostics?.record({ layer: 'executor', initiator: 'shell.finish', reason: closeCode(error, 'SHELL_FINISHED'), action: 'close', exitCode: code ?? null, changes: [{ field: 'shell.settled', before: settled, after: true }] });
|
|
23
26
|
if (settled)
|
|
24
27
|
return;
|
|
25
28
|
settled = true;
|
|
@@ -62,11 +65,20 @@ export function createEmployeeShell(options) {
|
|
|
62
65
|
no(unknown('EMPLOYEE_EXIT_UNKNOWN'));
|
|
63
66
|
return;
|
|
64
67
|
}
|
|
68
|
+
try {
|
|
69
|
+
diagnostics?.assertRecorded();
|
|
70
|
+
}
|
|
71
|
+
catch (failure) {
|
|
72
|
+
no(failure);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
65
75
|
yes({ stdout: out, stderr: err.slice(header.length, start) + err.slice(start + started.length), stderrSource: 'combined-ssh-and-command', exitCode: code, signal: null });
|
|
66
76
|
};
|
|
67
|
-
const stop = (code) => { if (settled || reason)
|
|
68
|
-
return; reason = unknown(code); child.kill('SIGTERM'); grace = setTimeout(() => { if (child.exitCode === null && child.signalCode === null)
|
|
69
|
-
|
|
77
|
+
const stop = (code) => { diagnostics?.record({ layer: 'executor', initiator: 'shell.stop', reason: code, action: 'terminate', changes: [{ field: 'shell.stop-code', before: reason?.code ?? null, after: settled || reason ? reason?.code ?? null : code }] }); if (settled || reason)
|
|
78
|
+
return; reason = unknown(code); child.kill('SIGTERM'); grace = setTimeout(() => { if (child.exitCode === null && child.signalCode === null) {
|
|
79
|
+
diagnostics?.record({ layer: 'executor', initiator: 'shell.kill-grace', reason: reason?.code ?? 'SHELL_CLEANUP', action: 'terminate', signal: 'SIGKILL' });
|
|
80
|
+
child.kill('SIGKILL');
|
|
81
|
+
} finish(reason); }, options.killGraceMs); };
|
|
70
82
|
const abort = () => stop('EMPLOYEE_CANCELLED_UNKNOWN');
|
|
71
83
|
const collect = (destination, chunk) => { bytes += chunk.length; if (bytes > options.maxBytes) {
|
|
72
84
|
stop('EMPLOYEE_OUTPUT_TOO_LARGE_UNKNOWN');
|
|
@@ -75,8 +87,8 @@ export function createEmployeeShell(options) {
|
|
|
75
87
|
child.stdout.on('data', (b) => collect(stdout, b));
|
|
76
88
|
child.stderr.on('data', (b) => collect(stderr, b));
|
|
77
89
|
child.stdin.on('error', () => stop('EMPLOYEE_INPUT_UNKNOWN'));
|
|
78
|
-
child.once('error', () => { finish(unknown('EMPLOYEE_TRANSPORT_UNKNOWN')); });
|
|
79
|
-
child.once('close', code => finish(reason, code));
|
|
90
|
+
child.once('error', () => { diagnostics?.record({ layer: 'relay', initiator: 'shell.process-error', reason: 'EMPLOYEE_TRANSPORT_UNKNOWN', action: 'failure' }); finish(unknown('EMPLOYEE_TRANSPORT_UNKNOWN')); });
|
|
91
|
+
child.once('close', (code, exitSignal) => { diagnostics?.record({ layer: 'relay', initiator: 'shell.process-close', reason: reason?.code ?? 'PROCESS_EXIT', action: 'exit', exitCode: code, signal: exitSignal }); finish(reason, code); });
|
|
80
92
|
signal.addEventListener('abort', abort, { once: true });
|
|
81
93
|
if (signal.aborted)
|
|
82
94
|
abort();
|
package/dist/employee-ssh.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type CloseDiagnostics } from './close-diagnostics.ts';
|
|
1
2
|
import type { EmployeeEndpointTarget } from '@ai-workbench/network/api';
|
|
2
3
|
import { type BrokerIo, type SshArgv } from './broker.ts';
|
|
3
4
|
import type { ConnectTarget } from './provider.ts';
|
|
@@ -5,6 +6,7 @@ import { type EmployeeExecutionTransport } from './employee-exec.ts';
|
|
|
5
6
|
export interface EmployeeSshOptions {
|
|
6
7
|
target: ConnectTarget;
|
|
7
8
|
endpoint: EmployeeEndpointTarget;
|
|
9
|
+
closeDiagnostics?: CloseDiagnostics;
|
|
8
10
|
/** Automatically discovered package binary path on this same employee device. */
|
|
9
11
|
employeeBinary: string;
|
|
10
12
|
binarySha256: string;
|
package/dist/employee-ssh.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { closeCode } from "./close-diagnostics.js";
|
|
1
2
|
import { sshIo } from "./broker.js";
|
|
2
3
|
import { createEmployeeProcessTransport } from "./employee-exec.js";
|
|
3
4
|
import { NeedError, shellQuote } from "./remote.js";
|
|
@@ -92,7 +93,8 @@ async function currentTarget(options, signal) {
|
|
|
92
93
|
await gate();
|
|
93
94
|
const b = endpoint.bridge, e = endpoint.endpoint;
|
|
94
95
|
const command = [b.command, 'target', '--settings', b.settingsPath, '--device', e.deviceId, '--if-instance', b.bridgeInstanceId, '--if-generation', String(b.generation), '--if-endpoint', e.endpointInstanceId, '--if-context', e.executionContextId].map(shellQuote).join(' ');
|
|
95
|
-
const
|
|
96
|
+
const run = () => awaitSignal(io.run(command, signal), signal);
|
|
97
|
+
const raw = await (options.closeDiagnostics ? options.closeDiagnostics.run('endpoint.target', run) : run());
|
|
96
98
|
const args = readTarget(raw, endpoint, options.acpMaxMessageBytes);
|
|
97
99
|
await gate();
|
|
98
100
|
return { io, args, gate };
|
|
@@ -107,9 +109,9 @@ export async function createEmployeeSshTransport(options, signal = new AbortCont
|
|
|
107
109
|
checkSignal(signal);
|
|
108
110
|
// EmployeeExecution races the user's open wait; this factory retains any late child cleanup.
|
|
109
111
|
const child = await io.attach('exec ' + ['/usr/bin/ssh', ...args, remote].map(shellQuote).join(' '));
|
|
110
|
-
const transport = createEmployeeProcessTransport(child, options.acpMaxMessageBytes, ['AW_EMPLOYEE_BINARY_UNAVAILABLE', 'AW_EMPLOYEE_BINARY_HASH_FAILED', 'AW_EMPLOYEE_BINARY_HASH_MISMATCH', 'AW_EMPLOYEE_BINARY_VERSION_FAILED', 'AW_EMPLOYEE_BINARY_VERSION_MISMATCH']);
|
|
112
|
+
const transport = createEmployeeProcessTransport(child, options.acpMaxMessageBytes, ['AW_EMPLOYEE_BINARY_UNAVAILABLE', 'AW_EMPLOYEE_BINARY_HASH_FAILED', 'AW_EMPLOYEE_BINARY_HASH_MISMATCH', 'AW_EMPLOYEE_BINARY_VERSION_FAILED', 'AW_EMPLOYEE_BINARY_VERSION_MISMATCH'], options.closeDiagnostics?.capture());
|
|
111
113
|
if (signal.aborted) {
|
|
112
|
-
await transport.close();
|
|
114
|
+
await transport.close({ layer: 'endpoint', initiator: 'ssh.open-abort', reason: 'ABORTED', operation: 'open', action: 'abort' });
|
|
113
115
|
throw fail('ABORTED');
|
|
114
116
|
}
|
|
115
117
|
try {
|
|
@@ -117,7 +119,7 @@ export async function createEmployeeSshTransport(options, signal = new AbortCont
|
|
|
117
119
|
return transport;
|
|
118
120
|
}
|
|
119
121
|
catch (error) {
|
|
120
|
-
await transport.close();
|
|
122
|
+
await transport.close({ layer: 'endpoint', initiator: 'ssh.post-open-validation', reason: closeCode(error), operation: 'open', action: 'failure' });
|
|
121
123
|
throw error;
|
|
122
124
|
}
|
|
123
125
|
}
|
package/dist/employee-tools.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { type CloseCause, type CloseDiagnostics } from './close-diagnostics.ts';
|
|
1
2
|
import type { DetailObservation } from './conversation-details.ts';
|
|
2
3
|
import type { EmployeeShell } from './employee-shell.ts';
|
|
3
4
|
export declare const EMPLOYEE_TOOL_NAMES: readonly ["Read", "Write", "Edit", "Glob", "Grep", "Bash"];
|
|
4
5
|
export type EmployeeToolName = typeof EMPLOYEE_TOOL_NAMES[number];
|
|
5
6
|
interface Options {
|
|
7
|
+
closeDiagnostics?: CloseDiagnostics;
|
|
6
8
|
cwd: string;
|
|
7
9
|
shell: string;
|
|
8
10
|
uid: number;
|
|
@@ -112,6 +114,6 @@ export declare function createEmployeeTools(options: Options): {
|
|
|
112
114
|
} | undefined;
|
|
113
115
|
isError?: boolean | undefined;
|
|
114
116
|
}>;
|
|
115
|
-
close(): Promise<void>;
|
|
117
|
+
close(cause?: CloseCause): Promise<void>;
|
|
116
118
|
};
|
|
117
119
|
export {};
|