@ai-workbench/remote-agent 0.8.5 → 0.8.10

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.
Files changed (180) hide show
  1. package/README.md +33 -8
  2. package/dist/acp.d.ts +22 -2
  3. package/dist/acp.js +143 -25
  4. package/dist/approval-label.d.ts +1 -1
  5. package/dist/approval-label.js +15 -3
  6. package/dist/broker-runtime.js +40 -8
  7. package/dist/broker.js +5 -1
  8. package/dist/claude-local.d.ts +37 -0
  9. package/dist/claude-local.js +207 -0
  10. package/dist/client.js +71 -2
  11. package/dist/conversation-details.d.ts +0 -1
  12. package/dist/conversation-details.js +37 -7
  13. package/dist/employee-endpoint.d.ts +91 -0
  14. package/dist/employee-endpoint.js +141 -0
  15. package/dist/employee-exec.d.ts +53 -0
  16. package/dist/employee-exec.js +423 -0
  17. package/dist/employee-location.d.ts +66 -0
  18. package/dist/employee-location.js +51 -0
  19. package/dist/employee-mcp.d.ts +42 -0
  20. package/dist/employee-mcp.js +181 -0
  21. package/dist/employee-native.d.ts +18 -0
  22. package/dist/employee-native.js +38 -0
  23. package/dist/employee-provider.d.ts +42 -0
  24. package/dist/employee-provider.js +326 -0
  25. package/dist/employee-relay.d.ts +18 -0
  26. package/dist/employee-relay.js +49 -0
  27. package/dist/employee-shell.d.ts +26 -0
  28. package/dist/employee-shell.js +89 -0
  29. package/dist/employee-ssh.d.ts +22 -0
  30. package/dist/employee-ssh.js +123 -0
  31. package/dist/employee-tools.d.ts +117 -0
  32. package/dist/employee-tools.js +292 -0
  33. package/dist/employee-workspace.d.ts +26 -0
  34. package/dist/employee-workspace.js +61 -0
  35. package/dist/index.d.ts +6 -0
  36. package/dist/index.js +148 -28
  37. package/dist/input.d.ts +11 -0
  38. package/dist/input.js +61 -23
  39. package/dist/local-user-turn.d.ts +24 -0
  40. package/dist/local-user-turn.js +103 -0
  41. package/dist/presentation.d.ts +1 -1
  42. package/dist/presentation.js +46 -2
  43. package/dist/provider.d.ts +36 -2
  44. package/dist/provider.js +33 -0
  45. package/dist/readiness.js +1 -1
  46. package/dist/recovery.d.ts +14 -1
  47. package/dist/recovery.js +11 -2
  48. package/dist/remote-details.d.ts +7 -0
  49. package/dist/remote-details.js +11 -1
  50. package/helpers/ripgrep/LICENSE-vscode +13 -0
  51. package/helpers/ripgrep/PROVENANCE.md +9 -0
  52. package/helpers/ripgrep/darwin-arm64/rg +0 -0
  53. package/helpers/ripgrep/darwin-x64/rg +0 -0
  54. package/helpers/ripgrep/manifest.json +56 -0
  55. package/helpers/ripgrep/notices/COPYING-ripgrep +3 -0
  56. package/helpers/ripgrep/notices/DEPENDENCIES.json +946 -0
  57. package/helpers/ripgrep/notices/LICENCE-PCRE2.md +103 -0
  58. package/helpers/ripgrep/notices/LICENSE-MIT-ripgrep +21 -0
  59. package/helpers/ripgrep/notices/UNLICENSE-ripgrep +24 -0
  60. package/helpers/ripgrep/notices/aho-corasick-1.1.3/COPYING +3 -0
  61. package/helpers/ripgrep/notices/aho-corasick-1.1.3/LICENSE-MIT +21 -0
  62. package/helpers/ripgrep/notices/aho-corasick-1.1.3/UNLICENSE +24 -0
  63. package/helpers/ripgrep/notices/anyhow-1.0.100/LICENSE-APACHE +176 -0
  64. package/helpers/ripgrep/notices/anyhow-1.0.100/LICENSE-MIT +23 -0
  65. package/helpers/ripgrep/notices/arbitrary-1.4.2/LICENSE-APACHE +201 -0
  66. package/helpers/ripgrep/notices/arbitrary-1.4.2/LICENSE-MIT +27 -0
  67. package/helpers/ripgrep/notices/bstr-1.12.0/COPYING +8 -0
  68. package/helpers/ripgrep/notices/bstr-1.12.0/LICENSE-APACHE +201 -0
  69. package/helpers/ripgrep/notices/bstr-1.12.0/LICENSE-MIT +21 -0
  70. package/helpers/ripgrep/notices/cc-1.2.41/LICENSE-APACHE +201 -0
  71. package/helpers/ripgrep/notices/cc-1.2.41/LICENSE-MIT +25 -0
  72. package/helpers/ripgrep/notices/cfg-if-1.0.4/LICENSE-APACHE +201 -0
  73. package/helpers/ripgrep/notices/cfg-if-1.0.4/LICENSE-MIT +25 -0
  74. package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-APACHE +201 -0
  75. package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-MIT +27 -0
  76. package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-THIRD-PARTY +593 -0
  77. package/helpers/ripgrep/notices/crossbeam-deque-0.8.6/LICENSE-APACHE +201 -0
  78. package/helpers/ripgrep/notices/crossbeam-deque-0.8.6/LICENSE-MIT +27 -0
  79. package/helpers/ripgrep/notices/crossbeam-epoch-0.9.18/LICENSE-APACHE +201 -0
  80. package/helpers/ripgrep/notices/crossbeam-epoch-0.9.18/LICENSE-MIT +27 -0
  81. package/helpers/ripgrep/notices/crossbeam-utils-0.8.21/LICENSE-APACHE +201 -0
  82. package/helpers/ripgrep/notices/crossbeam-utils-0.8.21/LICENSE-MIT +27 -0
  83. package/helpers/ripgrep/notices/derive_arbitrary-1.4.2/LICENSE-APACHE +201 -0
  84. package/helpers/ripgrep/notices/derive_arbitrary-1.4.2/LICENSE-MIT +27 -0
  85. package/helpers/ripgrep/notices/encoding_rs-0.8.35/COPYRIGHT +17 -0
  86. package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-APACHE +202 -0
  87. package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-MIT +25 -0
  88. package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-WHATWG +26 -0
  89. package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/COPYING +8 -0
  90. package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/LICENSE-APACHE +201 -0
  91. package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/LICENSE-MIT +21 -0
  92. package/helpers/ripgrep/notices/find-msvc-tools-0.1.4/LICENSE-APACHE +201 -0
  93. package/helpers/ripgrep/notices/find-msvc-tools-0.1.4/LICENSE-MIT +25 -0
  94. package/helpers/ripgrep/notices/getrandom-0.3.4/LICENSE-APACHE +201 -0
  95. package/helpers/ripgrep/notices/getrandom-0.3.4/LICENSE-MIT +26 -0
  96. package/helpers/ripgrep/notices/glob-0.3.3/LICENSE-APACHE +201 -0
  97. package/helpers/ripgrep/notices/glob-0.3.3/LICENSE-MIT +25 -0
  98. package/helpers/ripgrep/notices/itoa-1.0.15/LICENSE-APACHE +176 -0
  99. package/helpers/ripgrep/notices/itoa-1.0.15/LICENSE-MIT +23 -0
  100. package/helpers/ripgrep/notices/jobserver-0.1.34/LICENSE-APACHE +201 -0
  101. package/helpers/ripgrep/notices/jobserver-0.1.34/LICENSE-MIT +25 -0
  102. package/helpers/ripgrep/notices/lexopt-0.3.1/LICENSE +19 -0
  103. package/helpers/ripgrep/notices/libc-0.2.177/LICENSE-APACHE +176 -0
  104. package/helpers/ripgrep/notices/libc-0.2.177/LICENSE-MIT +25 -0
  105. package/helpers/ripgrep/notices/log-0.4.28/LICENSE-APACHE +201 -0
  106. package/helpers/ripgrep/notices/log-0.4.28/LICENSE-MIT +25 -0
  107. package/helpers/ripgrep/notices/memchr-2.7.6/COPYING +3 -0
  108. package/helpers/ripgrep/notices/memchr-2.7.6/LICENSE-MIT +21 -0
  109. package/helpers/ripgrep/notices/memchr-2.7.6/UNLICENSE +24 -0
  110. package/helpers/ripgrep/notices/memmap2-0.9.8/LICENSE-APACHE +201 -0
  111. package/helpers/ripgrep/notices/memmap2-0.9.8/LICENSE-MIT +26 -0
  112. package/helpers/ripgrep/notices/msvc_spectre_libs-0.1.3/LICENSE +21 -0
  113. package/helpers/ripgrep/notices/pcre2-0.2.11/COPYING +3 -0
  114. package/helpers/ripgrep/notices/pcre2-0.2.11/LICENSE-MIT +21 -0
  115. package/helpers/ripgrep/notices/pcre2-0.2.11/UNLICENSE +24 -0
  116. package/helpers/ripgrep/notices/pcre2-sys-0.2.10/COPYING +3 -0
  117. package/helpers/ripgrep/notices/pcre2-sys-0.2.10/LICENSE-MIT +21 -0
  118. package/helpers/ripgrep/notices/pcre2-sys-0.2.10/UNLICENSE +24 -0
  119. package/helpers/ripgrep/notices/pkg-config-0.3.32/LICENSE-APACHE +201 -0
  120. package/helpers/ripgrep/notices/pkg-config-0.3.32/LICENSE-MIT +25 -0
  121. package/helpers/ripgrep/notices/proc-macro2-1.0.101/LICENSE-APACHE +176 -0
  122. package/helpers/ripgrep/notices/proc-macro2-1.0.101/LICENSE-MIT +23 -0
  123. package/helpers/ripgrep/notices/quote-1.0.41/LICENSE-APACHE +176 -0
  124. package/helpers/ripgrep/notices/quote-1.0.41/LICENSE-MIT +23 -0
  125. package/helpers/ripgrep/notices/r-efi-5.3.0/AUTHORS +74 -0
  126. package/helpers/ripgrep/notices/regex-1.12.2/LICENSE-APACHE +201 -0
  127. package/helpers/ripgrep/notices/regex-1.12.2/LICENSE-MIT +25 -0
  128. package/helpers/ripgrep/notices/regex-automata-0.4.13/LICENSE-APACHE +201 -0
  129. package/helpers/ripgrep/notices/regex-automata-0.4.13/LICENSE-MIT +25 -0
  130. package/helpers/ripgrep/notices/regex-syntax-0.8.8/LICENSE-APACHE +201 -0
  131. package/helpers/ripgrep/notices/regex-syntax-0.8.8/LICENSE-MIT +25 -0
  132. package/helpers/ripgrep/notices/regex-syntax-0.8.8/src/unicode_tables/LICENSE-UNICODE +57 -0
  133. package/helpers/ripgrep/notices/ryu-1.0.20/LICENSE-APACHE +176 -0
  134. package/helpers/ripgrep/notices/ryu-1.0.20/LICENSE-BOOST +23 -0
  135. package/helpers/ripgrep/notices/same-file-1.0.6/COPYING +3 -0
  136. package/helpers/ripgrep/notices/same-file-1.0.6/LICENSE-MIT +21 -0
  137. package/helpers/ripgrep/notices/same-file-1.0.6/UNLICENSE +24 -0
  138. package/helpers/ripgrep/notices/serde-1.0.228/LICENSE-APACHE +176 -0
  139. package/helpers/ripgrep/notices/serde-1.0.228/LICENSE-MIT +23 -0
  140. package/helpers/ripgrep/notices/serde_core-1.0.228/LICENSE-APACHE +176 -0
  141. package/helpers/ripgrep/notices/serde_core-1.0.228/LICENSE-MIT +23 -0
  142. package/helpers/ripgrep/notices/serde_derive-1.0.228/LICENSE-APACHE +176 -0
  143. package/helpers/ripgrep/notices/serde_derive-1.0.228/LICENSE-MIT +23 -0
  144. package/helpers/ripgrep/notices/serde_json-1.0.145/LICENSE-APACHE +176 -0
  145. package/helpers/ripgrep/notices/serde_json-1.0.145/LICENSE-MIT +23 -0
  146. package/helpers/ripgrep/notices/shlex-1.3.0/LICENSE-APACHE +13 -0
  147. package/helpers/ripgrep/notices/shlex-1.3.0/LICENSE-MIT +21 -0
  148. package/helpers/ripgrep/notices/syn-2.0.106/LICENSE-APACHE +176 -0
  149. package/helpers/ripgrep/notices/syn-2.0.106/LICENSE-MIT +23 -0
  150. package/helpers/ripgrep/notices/termcolor-1.4.1/COPYING +3 -0
  151. package/helpers/ripgrep/notices/termcolor-1.4.1/LICENSE-MIT +21 -0
  152. package/helpers/ripgrep/notices/termcolor-1.4.1/UNLICENSE +24 -0
  153. package/helpers/ripgrep/notices/textwrap-0.16.2/LICENSE +21 -0
  154. package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/LICENSE-APACHE +201 -0
  155. package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/LICENSE-MIT +25 -0
  156. package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/jemalloc/COPYING +27 -0
  157. package/helpers/ripgrep/notices/tikv-jemallocator-0.6.0/LICENSE-APACHE +201 -0
  158. package/helpers/ripgrep/notices/tikv-jemallocator-0.6.0/LICENSE-MIT +25 -0
  159. package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-APACHE +176 -0
  160. package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-MIT +23 -0
  161. package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-UNICODE +39 -0
  162. package/helpers/ripgrep/notices/walkdir-2.5.0/COPYING +3 -0
  163. package/helpers/ripgrep/notices/walkdir-2.5.0/LICENSE-MIT +21 -0
  164. package/helpers/ripgrep/notices/walkdir-2.5.0/UNLICENSE +24 -0
  165. package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-APACHE +201 -0
  166. package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-Apache-2.0_WITH_LLVM-exception +220 -0
  167. package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-MIT +23 -0
  168. package/helpers/ripgrep/notices/winapi-util-0.1.11/COPYING +3 -0
  169. package/helpers/ripgrep/notices/winapi-util-0.1.11/LICENSE-MIT +21 -0
  170. package/helpers/ripgrep/notices/winapi-util-0.1.11/UNLICENSE +24 -0
  171. package/helpers/ripgrep/notices/windows-link-0.2.1/license-apache-2.0 +201 -0
  172. package/helpers/ripgrep/notices/windows-link-0.2.1/license-mit +21 -0
  173. package/helpers/ripgrep/notices/windows-sys-0.61.2/license-apache-2.0 +201 -0
  174. package/helpers/ripgrep/notices/windows-sys-0.61.2/license-mit +21 -0
  175. package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-APACHE +201 -0
  176. package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-Apache-2.0_WITH_LLVM-exception +220 -0
  177. package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-MIT +23 -0
  178. package/locale/en.json +1 -1
  179. package/locale/zh.json +2 -2
  180. package/package.json +29 -11
@@ -0,0 +1,181 @@
1
+ import { createServer } from 'node:net';
2
+ import { mkdtemp, lstat, rm, access } from 'node:fs/promises';
3
+ import { constants } from 'node:fs';
4
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
5
+ import { CallToolRequestSchema, ListToolsRequestSchema, JSONRPCMessageSchema } from '@modelcontextprotocol/sdk/types.js';
6
+ import { createEmployeeTools } from "./employee-tools.js";
7
+ import { employeeRouting } from "./employee-relay.js";
8
+ import { NeedError } from "./remote.js";
9
+ const string = { type: 'string' }, integer = { type: 'integer' }, boolean = { type: 'boolean' };
10
+ function schema(name, properties, required) { return { name, description: '在员工电脑所选工作区执行' + name + ';不在主机回退。', inputSchema: { type: 'object', properties, required, additionalProperties: false } }; }
11
+ export const EMPLOYEE_MCP_TOOLS = [
12
+ schema('Read', { file_path: string, offset: integer, limit: integer, pages: string }, ['file_path']),
13
+ schema('Write', { file_path: string, content: string }, ['file_path', 'content']),
14
+ schema('Edit', { file_path: string, old_string: string, new_string: string, replace_all: boolean }, ['file_path', 'old_string', 'new_string']),
15
+ schema('Glob', { pattern: string, path: string }, ['pattern']),
16
+ schema('Grep', { pattern: string, path: string, glob: string, output_mode: string, '-B': integer, '-A': integer, '-C': integer, '-n': boolean, '-i': boolean, type: string, head_limit: integer, offset: integer, multiline: boolean }, ['pattern']),
17
+ schema('Bash', { command: string, timeout: integer, description: string, run_in_background: boolean, dangerouslyDisableSandbox: boolean }, ['command']),
18
+ ];
19
+ /** Same newline-delimited MCP messages as stdio, over an employee-only Unix stream. */
20
+ export class EmployeeSocketTransport {
21
+ socket;
22
+ maxBytes;
23
+ onclose;
24
+ onerror;
25
+ onmessage;
26
+ pending = Buffer.alloc(0);
27
+ ended = false;
28
+ failure;
29
+ get error() { return this.failure; }
30
+ lifetime = new AbortController();
31
+ signal = this.lifetime.signal;
32
+ constructor(socket, maxBytes) {
33
+ this.socket = socket;
34
+ this.maxBytes = maxBytes;
35
+ }
36
+ failed(error) { try {
37
+ this.onerror?.(error);
38
+ }
39
+ catch (observation) {
40
+ this.failure = observation instanceof Error ? observation : new Error('EMPLOYEE_MCP_OBSERVATION_FAILED');
41
+ }
42
+ finally {
43
+ void this.close();
44
+ } }
45
+ async start() {
46
+ this.socket.on('error', error => this.failed(error));
47
+ this.socket.on('close', () => void this.close());
48
+ this.socket.on('data', (bytes) => {
49
+ try {
50
+ let cursor = 0;
51
+ while (cursor < bytes.length) {
52
+ const newline = bytes.indexOf(10, cursor), end = newline < 0 ? bytes.length : newline;
53
+ if (this.pending.length + end - cursor > this.maxBytes)
54
+ throw new NeedError('EMPLOYEE_MCP_MESSAGE_TOO_LARGE', '本机工具消息超过acpMaxMessageBytes,未截断。', '调整具名会话消息容量');
55
+ this.pending = Buffer.concat([this.pending, bytes.subarray(cursor, end)]);
56
+ cursor = newline < 0 ? bytes.length : newline + 1;
57
+ if (newline >= 0) {
58
+ const line = new TextDecoder('utf8', { fatal: true }).decode(this.pending);
59
+ this.pending = Buffer.alloc(0);
60
+ this.onmessage?.(JSONRPCMessageSchema.parse(JSON.parse(line)));
61
+ }
62
+ }
63
+ }
64
+ catch (error) {
65
+ this.failed(error);
66
+ }
67
+ });
68
+ }
69
+ async send(message) {
70
+ if (this.ended)
71
+ throw new Error('EMPLOYEE_MCP_CLOSED');
72
+ const wire = Buffer.from(JSON.stringify(message) + '\n');
73
+ if (wire.length - 1 > this.maxBytes) {
74
+ await this.close();
75
+ throw new Error('EMPLOYEE_MCP_MESSAGE_TOO_LARGE');
76
+ }
77
+ await new Promise((yes, no) => this.socket.write(wire, error => error ? no(error) : yes()));
78
+ }
79
+ async close() { if (this.ended)
80
+ return; this.ended = true; this.lifetime.abort(); this.socket.destroy(); this.onclose?.(); }
81
+ }
82
+ export async function openEmployeeMcp(options) {
83
+ if (process.platform !== 'darwin')
84
+ throw new NeedError('EMPLOYEE_PLATFORM_UNSUPPORTED', '本机执行目前只支持Mac。', '已交Mac员工端合同');
85
+ if (!Number.isSafeInteger(options.maxBytes) || options.maxBytes < 1)
86
+ throw new NeedError('SETTINGS_REQUIRED', '本机工具消息容量无效。', 'acpMaxMessageBytes');
87
+ 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(() => { }) });
89
+ const root = await mkdtemp('/private/tmp/aw-x87-'), socketPath = root + '/mcp';
90
+ try {
91
+ const info = await lstat(root);
92
+ if ((info.mode & 0o077) !== 0 || info.uid !== process.getuid?.())
93
+ throw new NeedError('EMPLOYEE_MCP_STORAGE_INVALID', '本机工具私有通道不可用。', '当前员工的700私有Unix目录');
94
+ }
95
+ catch (error) {
96
+ await tools.close();
97
+ await rm(root, { recursive: true });
98
+ throw error;
99
+ }
100
+ const clients = new Set(), transports = new Set();
101
+ let closing, diagnosticFailure;
102
+ const failureLifetime = new AbortController();
103
+ const poison = (error) => {
104
+ diagnosticFailure ??= error;
105
+ failureLifetime.abort(error);
106
+ // 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; });
109
+ };
110
+ function observeUsing(event, sink) { if (diagnosticFailure)
111
+ throw diagnosticFailure; try {
112
+ sink(event);
113
+ }
114
+ catch (error) {
115
+ poison(error);
116
+ throw error;
117
+ } }
118
+ function observe(event) { observeUsing(event, options.detail); }
119
+ const server = createServer(socket => {
120
+ if (closing || failureLifetime.signal.aborted || options.lease.signal.aborted) {
121
+ socket.destroy();
122
+ return;
123
+ }
124
+ const mcp = new Server({ name: 'ai-workbench-employee', version: '0.8.10' }, { capabilities: { tools: {} } }), transport = new EmployeeSocketTransport(socket, options.maxBytes);
125
+ clients.add(mcp);
126
+ transports.add(transport);
127
+ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: EMPLOYEE_MCP_TOOLS }));
128
+ mcp.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
129
+ const active = options.activeSignal();
130
+ if (!active || active.aborted)
131
+ return { isError: true, content: [{ type: 'text', text: 'EMPLOYEE_NO_ACTIVE_TURN:当前没有已接纳的用户轮次,本机工具未执行。' }] };
132
+ const signal = AbortSignal.any([active, extra.signal, transport.signal, options.lease.signal, failureLifetime.signal]);
133
+ const sink = options.captureDetail?.() ?? options.detail;
134
+ return tools.call(request.params.name, request.params.arguments ?? {}, signal, event => observeUsing(event, sink));
135
+ });
136
+ mcp.onerror = () => { try {
137
+ observe({ kind: 'session', severity: 'warning', code: 'CLAUDE_EMPLOYEE_MCP_UNKNOWN', summary: '本机工具连接出现协议或传输错误;在途结果未知,没有重放' });
138
+ }
139
+ catch (error) {
140
+ poison(error);
141
+ } };
142
+ mcp.onclose = () => { clients.delete(mcp); transports.delete(transport); };
143
+ void mcp.connect(transport).catch(() => { socket.destroy(); });
144
+ });
145
+ const close = () => closing ??= Promise.resolve().then(async () => {
146
+ options.lease.signal.removeEventListener('abort', aborted);
147
+ const errors = [], clean = async (operation) => { try {
148
+ await operation();
149
+ }
150
+ catch (error) {
151
+ if (!errors.includes(error))
152
+ errors.push(error);
153
+ } };
154
+ for (const transport of transports)
155
+ await clean(() => transport.close());
156
+ await Promise.all([...clients].map(client => clean(() => client.close())));
157
+ await clean(() => tools.close());
158
+ await clean(() => options.lease.close());
159
+ if (server.listening)
160
+ await clean(() => new Promise((yes, no) => server.close(error => error ? no(error) : yes())));
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); }); };
170
+ try {
171
+ await new Promise((yes, no) => { server.once('error', no); server.listen(socketPath, () => { server.removeListener('error', no); yes(); }); });
172
+ options.lease.signal.addEventListener('abort', aborted, { once: true });
173
+ if (options.lease.signal.aborted)
174
+ throw options.lease.signal.reason;
175
+ return { routing: employeeRouting(options.lease.target, socketPath, options.maxBytes), signal: failureLifetime.signal, close };
176
+ }
177
+ catch (error) {
178
+ await close();
179
+ throw error;
180
+ }
181
+ }
@@ -0,0 +1,18 @@
1
+ /** Fixed native input already proven by D18/D20; never resolve a PATH executable. */
2
+ export declare const EMPLOYEE_NATIVE: Readonly<{
3
+ version: "0.160.1";
4
+ platform: "darwin";
5
+ arch: "arm64";
6
+ package: "@openai/codex-darwin-arm64";
7
+ packageVersion: "0.160.1-darwin-arm64";
8
+ triple: "aarch64-apple-darwin";
9
+ sha256: "09fa44fdc37a5fc70dc1ace31235f90468a2e193d0e85f7552eab068ea2582be";
10
+ }>;
11
+ export declare function resolveEmployeeNative(ports?: {
12
+ platform?: string;
13
+ arch?: string;
14
+ resolve?: (name: string) => string;
15
+ }): Promise<{
16
+ path: string;
17
+ sha256: string;
18
+ }>;
@@ -0,0 +1,38 @@
1
+ import { createRequire } from 'node:module';
2
+ import { readFile, lstat, realpath, access } from 'node:fs/promises';
3
+ import { constants } from 'node:fs';
4
+ import { dirname, join } from 'node:path';
5
+ import { createHash } from 'node:crypto';
6
+ import { NeedError } from "./remote.js";
7
+ const require = createRequire(import.meta.url);
8
+ /** Fixed native input already proven by D18/D20; never resolve a PATH executable. */
9
+ export const EMPLOYEE_NATIVE = Object.freeze({ version: '0.160.1', platform: 'darwin', arch: 'arm64', package: '@openai/codex-darwin-arm64', packageVersion: '0.160.1-darwin-arm64', triple: 'aarch64-apple-darwin', sha256: '09fa44fdc37a5fc70dc1ace31235f90468a2e193d0e85f7552eab068ea2582be' });
10
+ export async function resolveEmployeeNative(ports = {}) {
11
+ if ((ports.platform ?? process.platform) !== EMPLOYEE_NATIVE.platform || (ports.arch ?? process.arch) !== EMPLOYEE_NATIVE.arch)
12
+ throw new NeedError('EMPLOYEE_PLATFORM_UNSUPPORTED', '当前版本的Codex本机执行仅验证了Apple芯片Mac,本轮没有执行。', '对应平台固定执行器及验证');
13
+ let manifest;
14
+ try {
15
+ manifest = (ports.resolve ?? require.resolve)(EMPLOYEE_NATIVE.package + '/package.json');
16
+ }
17
+ catch {
18
+ throw new NeedError('AW_EMPLOYEE_BINARY_MISSING', 'Codex本机执行组件没有装齐,本轮没有执行,请联系管理员。', '随插件安装的固定Codex0.160.1执行组件');
19
+ }
20
+ try {
21
+ const metadata = JSON.parse(await readFile(manifest, 'utf8'));
22
+ if (metadata.version !== EMPLOYEE_NATIVE.packageVersion)
23
+ throw new NeedError('AW_EMPLOYEE_BINARY_VERSION', 'Codex本机执行组件版本不匹配,本轮没有执行。', '固定Codex0.160.1组件');
24
+ const path = join(dirname(manifest), 'vendor', EMPLOYEE_NATIVE.triple, 'bin', 'codex');
25
+ const info = await lstat(path);
26
+ if (!info.isFile() || info.isSymbolicLink() || await realpath(path) !== path)
27
+ throw new NeedError('AW_EMPLOYEE_BINARY_INVALID', 'Codex本机执行组件位置无效,本轮没有执行。', '安装包内的固定原生执行器');
28
+ await access(path, constants.X_OK);
29
+ if (createHash('sha256').update(await readFile(path)).digest('hex') !== EMPLOYEE_NATIVE.sha256)
30
+ throw new NeedError('AW_EMPLOYEE_BINARY_HASH', 'Codex本机执行组件校验不一致,本轮没有执行。', '固定原生执行器SHA256');
31
+ return { path, sha256: EMPLOYEE_NATIVE.sha256 };
32
+ }
33
+ catch (error) {
34
+ if (error instanceof NeedError)
35
+ throw error;
36
+ throw new NeedError('AW_EMPLOYEE_BINARY_INVALID', 'Codex本机执行组件无法核验,本轮没有执行。', '完整安装包与固定执行器');
37
+ }
38
+ }
@@ -0,0 +1,42 @@
1
+ import type { NetworkApi } from '@ai-workbench/network/api';
2
+ import type { AgentDefinition } from './agents.ts';
3
+ import type { ConnectApi, ConnectTarget, ConnectionState, Project, Runtime, RuntimeRequest } from './provider.ts';
4
+ import { type BrokerIo, type SshArgv } from './broker.ts';
5
+ import type { DetailChannel } from './remote-details.ts';
6
+ import type { LocalUserTurnAcceptance } from '@ai-workbench/connect/api';
7
+ export interface EmployeeProviderOptions {
8
+ currentUserTurn(request: RuntimeRequest): {
9
+ turn: number;
10
+ userMessageId: string;
11
+ };
12
+ getNetwork(): NetworkApi | undefined;
13
+ workspace(request: RuntimeRequest): Promise<{
14
+ id: string;
15
+ path: string;
16
+ }>;
17
+ native(): Promise<{
18
+ path: string;
19
+ sha256: string;
20
+ }>;
21
+ argv?: SshArgv;
22
+ io?: BrokerIo;
23
+ }
24
+ interface Options {
25
+ employee?: EmployeeProviderOptions;
26
+ open?: (target: ConnectTarget, project: Project, request: RuntimeRequest) => Promise<Runtime>;
27
+ details?: (request: RuntimeRequest, api: ConnectApi, connection: ConnectionState, target: ConnectTarget) => Promise<DetailChannel>;
28
+ }
29
+ interface Admission {
30
+ api: ConnectApi;
31
+ target: ConnectTarget;
32
+ connection: ConnectionState;
33
+ request: RuntimeRequest;
34
+ stable: (signal?: AbortSignal) => Promise<unknown>;
35
+ }
36
+ /** Read-only local classification and explicit acceptance precede even ensureReady's SSH status. */
37
+ export declare function acceptEmployeeUserTurn(get: () => ConnectApi | undefined, employee: EmployeeProviderOptions, agent: AgentDefinition, api: ConnectApi, request: RuntimeRequest): Promise<Readonly<LocalUserTurnAcceptance> | undefined>;
38
+ /** Recheck the admitted read-only facts after readiness awaits, before the catalog command. */
39
+ export declare function validateEmployeeUserTurn(get: () => ConnectApi | undefined, employee: EmployeeProviderOptions, api: ConnectApi, request: RuntimeRequest, connection: ConnectionState): Promise<void>;
40
+ /** Source admission is separate from L's legacy host project resolver. */
41
+ export declare function createEmployeeProvider(get: () => ConnectApi | undefined, options: Options, agent: AgentDefinition): (input: Admission) => Promise<Runtime | undefined>;
42
+ export {};
@@ -0,0 +1,326 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { createHash } from 'node:crypto';
10
+ import { admitEmployeeLocation } from "./employee-location.js";
11
+ import { EmployeeExecution } from "./employee-exec.js";
12
+ import { createEmployeeSshTransport, validateEmployeeSshTarget } from "./employee-ssh.js";
13
+ import { sshIo } from "./broker.js";
14
+ import { NeedError } from "./remote.js";
15
+ const fail = (code) => new NeedError(code, code === 'EMPLOYEE_OPERATION_UNSUPPORTED' ? '当前本机工作区只支持正在连接的会话;单次运行和断线后的冷恢复暂不支持,请新建会话。' : '本机工作区的执行连接无法继续(' + code + '),本轮没有改用主机执行。', '当前 L 工作区绑定、N/S 员工执行端点与支持本机执行的会话适配器');
16
+ const errorCode = (error) => error instanceof NeedError ? error.code : 'EMPLOYEE_EXEC_UNKNOWN';
17
+ const asNeed = (error) => error instanceof NeedError ? error : fail('EMPLOYEE_EXEC_UNKNOWN');
18
+ const digest = (value) => createHash('sha256').update(JSON.stringify(value)).digest('hex');
19
+ const absolute = (value) => typeof value === 'string' && value.startsWith('/') && !value.includes('\0');
20
+ const localApiCurrent = (api) => { const c = api.getContractInfo?.(); return c?.workspaceLocations === 1 && c.localWorkspaceContext === 1 && c.localUserTurnAcceptance === 1 && typeof api.getWorkspaceLocationState === 'function' && typeof api.getLocalWorkspaceContext === 'function' && typeof api.acceptLocalWorkspaceUserTurn === 'function'; };
21
+ /** Read-only local classification and explicit acceptance precede even ensureReady's SSH status. */
22
+ export async function acceptEmployeeUserTurn(get, employee, agent, api, request) {
23
+ const info = api.getContractInfo?.();
24
+ if (info?.workspaceLocations !== 1)
25
+ return;
26
+ if (typeof api.getWorkspaceLocationState !== 'function')
27
+ throw fail('DEPENDENCY_REQUIRED');
28
+ const state = api.getWorkspaceLocationState();
29
+ if (!state.items.length) {
30
+ request.localAdmissionCheck = () => { if (get() !== api || typeof api.getWorkspaceLocationState !== 'function' || api.getContractInfo?.().workspaceLocations !== 1 || api.getWorkspaceLocationState().items.length)
31
+ throw fail('EMPLOYEE_CONTEXT_CHANGED'); };
32
+ return;
33
+ }
34
+ const workspace = { ...await employee.workspace(request) };
35
+ request.signal?.throwIfAborted();
36
+ const rows = state.items.filter(row => row.workspaceId === workspace.id), row = rows[0];
37
+ const stamp = (s) => JSON.stringify([s.instanceId, s.epoch, s.phase, s.items.filter(x => x.workspaceId === workspace.id).map(x => [x.bindingId, x.source, x.status])]);
38
+ const captured = stamp(state), sourceStable = () => { if (get() !== api || typeof api.getWorkspaceLocationState !== 'function' || api.getContractInfo?.().workspaceLocations !== 1 || row?.source === 'local' && !localApiCurrent(api) || stamp(api.getWorkspaceLocationState()) !== captured)
39
+ throw fail('EMPLOYEE_CONTEXT_CHANGED'); request.signal?.throwIfAborted(); };
40
+ request.localAdmissionCheck = sourceStable;
41
+ sourceStable();
42
+ if (rows.length !== 1)
43
+ throw fail('EMPLOYEE_CONTEXT_CHANGED');
44
+ if (row.source !== 'local')
45
+ return;
46
+ if (agent.id !== 'codex-remote' || agent.loginProvider !== 'codex' || state.phase !== 'ready' || row.status !== 'selected' || !row.bindingId || info.localWorkspaceContext !== 1 || info.localUserTurnAcceptance !== 1 || typeof api.getLocalWorkspaceContext !== 'function' || typeof api.acceptLocalWorkspaceUserTurn !== 'function' || typeof employee.currentUserTurn !== 'function')
47
+ throw fail('DEPENDENCY_REQUIRED');
48
+ const connection = api.getConnectionState();
49
+ const stable = () => { sourceStable(); const now = api.getConnectionState(), location = api.getWorkspaceLocationState(), matches = location.items.filter(x => x.workspaceId === workspace.id); if (get() !== api || now.phase !== 'connected' || now.instanceId !== connection.instanceId || now.epoch !== connection.epoch || location.phase !== 'ready' || location.instanceId !== connection.instanceId || location.epoch !== connection.epoch || matches.length !== 1 || matches[0].source !== 'local' || matches[0].status !== 'selected' || matches[0].bindingId !== row.bindingId)
50
+ throw fail('EMPLOYEE_CONTEXT_CHANGED'); request.signal?.throwIfAborted(); };
51
+ stable();
52
+ const context = await api.getLocalWorkspaceContext({ instanceId: connection.instanceId, epoch: connection.epoch, workspaceId: workspace.id, expectedBindingId: row.bindingId });
53
+ if (!context.ok)
54
+ throw new NeedError(context.error.code, '无法核验本机工作区,本轮已停止。', '当前本机工作区');
55
+ stable();
56
+ const c = context.value, d = { ...c.directory };
57
+ if (c.version !== 1 || c.source !== 'local' || c.instanceId !== connection.instanceId || c.epoch !== connection.epoch || c.workspaceId !== workspace.id || c.bindingId !== row.bindingId || !absolute(d?.path) || d.path !== workspace.path || ![d.device, d.inode].every(n => Number.isSafeInteger(n) && n >= 0))
58
+ throw fail('EMPLOYEE_CONTEXT_CHANGED');
59
+ const expected = { ...employee.currentUserTurn(request) };
60
+ if (!Number.isSafeInteger(expected.turn) || expected.turn < 0 || typeof expected.userMessageId !== 'string' || !expected.userMessageId.trim())
61
+ throw fail('LOCAL_USER_TURN_UNCONFIRMED');
62
+ const response = await api.acceptLocalWorkspaceUserTurn({ instanceId: connection.instanceId, epoch: connection.epoch, sessionId: request.sessionId, provider: agent.loginProvider, purpose: 'user-turn', workspaceId: workspace.id, expectedBindingId: row.bindingId, expectedDirectory: { device: d.device, inode: d.inode }, expectedUserTurn: { ...expected } });
63
+ if (!response.ok)
64
+ throw new NeedError(response.error.code, '本次员工输入或工作区无法核验,本轮已停止。', 'L 本地真实输入接纳');
65
+ stable();
66
+ const a = response.value;
67
+ if (!a || a.version !== 1 || typeof a.acceptanceId !== 'string' || !a.acceptanceId.trim() || a.instanceId !== connection.instanceId || a.epoch !== connection.epoch || a.sessionId !== request.sessionId || a.provider !== agent.loginProvider || a.workspaceId !== workspace.id || a.bindingId !== row.bindingId || a.source !== 'local' || a.directory?.path !== d.path || a.directory?.device !== d.device || a.directory?.inode !== d.inode || a.userTurn?.turn !== expected.turn || a.userTurn?.userMessageId !== expected.userMessageId || ![a.userTurn?.startSequence, a.userTurn?.userMessageSequence].every(n => Number.isSafeInteger(n) && n >= 0) || a.userTurn.userMessageSequence <= a.userTurn.startSequence)
68
+ throw fail('LOCAL_USER_TURN_ACCEPTANCE_INVALID');
69
+ const accepted = Object.freeze({ ...a, directory: Object.freeze({ ...a.directory }), userTurn: Object.freeze({ ...a.userTurn }) });
70
+ request.localAdmissionCheck = () => { sourceStable(); const c = api.getConnectionState(); if (c.phase !== 'connected' || c.instanceId !== accepted.instanceId || c.epoch !== accepted.epoch)
71
+ throw fail('EMPLOYEE_CONTEXT_CHANGED'); const turn = employee.currentUserTurn(request); if (turn.turn !== accepted.userTurn.turn || turn.userMessageId !== accepted.userTurn.userMessageId)
72
+ throw new NeedError('LOCAL_USER_TURN_CHANGED', '本次输入已变化,本轮已停止。', '当前官方真实员工输入'); };
73
+ return accepted;
74
+ }
75
+ /** Recheck the admitted read-only facts after readiness awaits, before the catalog command. */
76
+ export async function validateEmployeeUserTurn(get, employee, api, request, connection) {
77
+ const a = request.localAcceptance;
78
+ if (!a)
79
+ return;
80
+ const stable = () => { if (get() !== api || !localApiCurrent(api))
81
+ throw fail('EMPLOYEE_CONTEXT_CHANGED'); const c = api.getConnectionState(), state = api.getWorkspaceLocationState(), rows = state.items.filter(x => x.workspaceId === a.workspaceId); if (c.phase !== 'connected' || c.instanceId !== a.instanceId || c.epoch !== a.epoch || connection.instanceId !== a.instanceId || connection.epoch !== a.epoch || state.phase !== 'ready' || state.instanceId !== a.instanceId || state.epoch !== a.epoch || rows.length !== 1 || rows[0].source !== 'local' || rows[0].status !== 'selected' || rows[0].bindingId !== a.bindingId)
82
+ throw fail('EMPLOYEE_CONTEXT_CHANGED'); request.signal?.throwIfAborted(); };
83
+ stable();
84
+ const c = await api.getLocalWorkspaceContext({ instanceId: a.instanceId, epoch: a.epoch, workspaceId: a.workspaceId, expectedBindingId: a.bindingId });
85
+ stable();
86
+ if (!c.ok)
87
+ throw new NeedError(c.error.code, '无法核验本机工作区,本轮已停止。', '当前本机工作区');
88
+ if (c.value.version !== 1 || c.value.source !== 'local' || c.value.instanceId !== a.instanceId || c.value.epoch !== a.epoch || c.value.workspaceId !== a.workspaceId || c.value.bindingId !== a.bindingId || c.value.directory.path !== a.directory.path || c.value.directory.device !== a.directory.device || c.value.directory.inode !== a.directory.inode)
89
+ throw fail('EMPLOYEE_CONTEXT_CHANGED');
90
+ const turn = employee.currentUserTurn(request);
91
+ if (turn.turn !== a.userTurn.turn || turn.userMessageId !== a.userTurn.userMessageId)
92
+ throw new NeedError('LOCAL_USER_TURN_CHANGED', '本次输入已变化,本轮已停止。', '当前官方真实员工输入');
93
+ }
94
+ /** Source admission is separate from L's legacy host project resolver. */
95
+ export function createEmployeeProvider(get, options, agent) {
96
+ const live = new Map();
97
+ return async (input) => {
98
+ const { api, target, connection, request } = input, info = api.getContractInfo?.(), sessionId = request.sessionId;
99
+ let prior = live.get(sessionId);
100
+ // A failed close remains a session tombstone. Source/endpoint changes cannot authorize a new host.
101
+ // Wait for an in-progress close before re-admission; its rejected result is intentionally sticky.
102
+ if (prior?.ended) {
103
+ await prior.closing;
104
+ prior = live.get(sessionId);
105
+ }
106
+ if (prior && (prior.current.api !== api || info?.workspaceLocations !== 1 || info?.localWorkspaceContext !== 1)) {
107
+ await prior.dispose(fail('EMPLOYEE_CONTEXT_CHANGED'));
108
+ throw fail('EMPLOYEE_CONTEXT_CHANGED');
109
+ }
110
+ if (info?.workspaceLocations !== 1 && info?.localWorkspaceContext !== 1)
111
+ return;
112
+ if (info.workspaceLocations !== 1 || info.localWorkspaceContext !== 1 || typeof api.getWorkspaceLocationState !== 'function' || typeof api.getLocalWorkspaceContext !== 'function' || !options.employee)
113
+ throw fail('DEPENDENCY_REQUIRED');
114
+ const employee = options.employee;
115
+ const workspace = await employee.workspace(request);
116
+ await input.stable(request.signal);
117
+ if (!workspace.id || !absolute(workspace.path))
118
+ throw fail('EMPLOYEE_CONTEXT_CHANGED');
119
+ const state = api.getWorkspaceLocationState(), rows = state.items.filter(row => row.workspaceId === workspace.id), row = rows[0];
120
+ if (state.phase !== 'ready' || state.instanceId !== connection.instanceId || state.epoch !== connection.epoch || rows.length !== 1 || row?.status !== 'selected' || !row.bindingId || !['local', 'host', 'legacy-host'].includes(row.source ?? ''))
121
+ throw fail('EMPLOYEE_CONTEXT_CHANGED');
122
+ if (row.source !== 'local') {
123
+ if (prior)
124
+ await prior.dispose(fail('EMPLOYEE_CONTEXT_CHANGED'));
125
+ if (request.localAcceptance)
126
+ throw fail('EMPLOYEE_CONTEXT_CHANGED');
127
+ return;
128
+ }
129
+ let details;
130
+ try {
131
+ const acceptance = request.localAcceptance;
132
+ 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)
133
+ throw fail('LOCAL_USER_TURN_UNCONFIRMED');
134
+ details = await options.details?.(request, api, connection, target);
135
+ if (details)
136
+ request.timing?.bind(detail => details.record(detail));
137
+ const unsupported = request.mode !== 'session' || agent.loginProvider !== 'codex' || !!request.recoveryCheckpointId || !!request.resumeSessionId && !prior;
138
+ if (unsupported)
139
+ throw fail('EMPLOYEE_OPERATION_UNSUPPORTED');
140
+ if (!Number.isSafeInteger(request.acpMaxMessageBytes) || request.acpMaxMessageBytes < 1)
141
+ throw fail('SETTINGS_REQUIRED');
142
+ if (typeof api.onWorkspaceLocationState !== 'function' || typeof api.onConnectionState !== 'function')
143
+ throw fail('DEPENDENCY_REQUIRED');
144
+ const network = employee.getNetwork();
145
+ if (!network || typeof network.prepareEmployeeEndpoint !== 'function' || typeof network.onState !== 'function')
146
+ throw fail('DEPENDENCY_REQUIRED');
147
+ if (prior && prior.network !== network) {
148
+ await prior.dispose(fail('EMPLOYEE_CONTEXT_CHANGED'));
149
+ throw fail('EMPLOYEE_CONTEXT_CHANGED');
150
+ }
151
+ 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 });
152
+ if (request.cwd && request.cwd !== admitted.cwd || request.bindingId && request.bindingId !== admitted.bindingId || request.workspaceId && request.workspaceId !== admitted.workspaceId)
153
+ throw fail('SESSION_CONTEXT_MISMATCH');
154
+ const native = await employee.native();
155
+ if (!absolute(native.path) || !/^[a-f0-9]{64}$/.test(native.sha256))
156
+ throw fail('EMPLOYEE_BINARY_REQUIRED');
157
+ // Only executable identity belongs in this scope. Diagnostic metadata and object key
158
+ // insertion order cannot revoke a live binding; every public identity field stays bound.
159
+ const endpoint = admitted.endpoint, local = endpoint.local, account = endpoint.endpoint, bridge = endpoint.bridge;
160
+ const scope = digest({
161
+ target: { host: target.host, username: target.username, port: target.port, proxyCommand: target.proxyCommand, knownHostsFile: target.knownHostsFile, projectRoot: target.projectRoot,
162
+ session: { command: target.session.command, root: target.session.root, settingsPath: target.session.settingsPath, protocolVersion: target.session.protocolVersion } },
163
+ endpoint: { version: endpoint.version,
164
+ local: { instanceId: local.instanceId, epoch: local.epoch, workspaceId: local.workspaceId, bindingId: local.bindingId, directory: { path: local.directory.path, device: local.directory.device, inode: local.directory.inode } },
165
+ endpoint: { deviceId: account.deviceId, endpointInstanceId: account.endpointInstanceId, executionContextId: account.executionContextId, remoteUser: account.remoteUser, remoteUid: account.remoteUid, remoteHome: account.remoteHome, endpointCwd: account.endpointCwd, endpointShell: account.endpointShell },
166
+ bridge: { protocolVersion: bridge.protocolVersion, command: bridge.command, settingsPath: bridge.settingsPath, bridgeInstanceId: bridge.bridgeInstanceId, generation: bridge.generation } },
167
+ native: { path: native.path, sha256: native.sha256 },
168
+ });
169
+ if (prior && (prior.scope !== scope || prior.ended))
170
+ await prior.dispose(fail('EMPLOYEE_CONTEXT_CHANGED'));
171
+ let entry = prior && prior.scope === scope && !prior.ended ? prior : undefined;
172
+ let candidate = entry;
173
+ const base = async (signal) => {
174
+ signal.throwIfAborted();
175
+ if (get() !== api || employee.getNetwork() !== network)
176
+ throw fail('EMPLOYEE_CONTEXT_CHANGED');
177
+ if (candidate?.ended)
178
+ throw candidate.reason ?? fail('EMPLOYEE_EXEC_CLOSED');
179
+ await (candidate?.current ?? input).stable(signal);
180
+ await admitted.validate(signal);
181
+ if (get() !== api || employee.getNetwork() !== network)
182
+ throw fail('EMPLOYEE_CONTEXT_CHANGED');
183
+ await (candidate?.current ?? input).stable(signal);
184
+ signal.throwIfAborted();
185
+ };
186
+ let argv = employee.argv;
187
+ if (!argv && !employee.io) {
188
+ const name = '@ai-workbench/connect';
189
+ const supplied = await import(__rewriteRelativeImportExtension(name));
190
+ if (typeof supplied.sshArgs !== 'function')
191
+ throw fail('DEPENDENCY_REQUIRED');
192
+ argv = supplied.sshArgs;
193
+ }
194
+ const io = employee.io ?? sshIo(target, argv, request.acpMaxMessageBytes);
195
+ const scopedIo = { run: (command, signal) => io.run(command, signal), attach: async (command) => {
196
+ const child = await io.attach(command);
197
+ child.once('close', () => { if (candidate && !candidate.ended)
198
+ void candidate.dispose(fail('EMPLOYEE_EXEC_UNKNOWN')).catch(() => { }); });
199
+ return child;
200
+ } };
201
+ const ssh = { target, endpoint: admitted.endpoint, employeeBinary: native.path, binarySha256: native.sha256, acpMaxMessageBytes: request.acpMaxMessageBytes, argv: argv ?? (() => []), validate: base, io: scopedIo };
202
+ const validate = async (signal) => { try {
203
+ await validateEmployeeSshTarget(ssh, signal);
204
+ }
205
+ catch (error) {
206
+ if (candidate && errorCode(error) !== 'ABORTED')
207
+ await candidate.dispose(asNeed(error));
208
+ throw error;
209
+ } };
210
+ await validate(request.signal ?? new AbortController().signal);
211
+ if (!options.open)
212
+ throw fail('RUNTIME_REQUIRED');
213
+ // 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) => {
215
+ // Disposal addresses only the already-owned host broker instance through its frozen IO/CAS.
216
+ // A withdrawn local source must prevent dispatch, but cannot forbid that instance's exact kill.
217
+ if (ignore)
218
+ await (candidate?.current ?? input).stable();
219
+ else
220
+ await validate(request.signal ?? new AbortController().signal);
221
+ }, resumeSessionId: undefined, recoveryCheckpointId: undefined, verifyRecovery: undefined });
222
+ if (entry) {
223
+ entry.current = input;
224
+ entry.details = details;
225
+ try {
226
+ await validate(request.signal ?? new AbortController().signal);
227
+ }
228
+ catch (error) {
229
+ await entry.dispose(asNeed(error));
230
+ throw error;
231
+ }
232
+ entry.runtime.details = details;
233
+ entry.runtime.connection = { ...connection };
234
+ entry.runtime.binding = { bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, revision: state.revision };
235
+ return entry.runtime;
236
+ }
237
+ 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, restartNotice: undefined, restartInputIds: undefined, employeeExecutionMeta: { version: 1, bindingId: admitted.bindingId, workspaceId: admitted.workspaceId, cwd: admitted.cwd, acpMaxMessageBytes: request.acpMaxMessageBytes }, employeePresentation: '文件与命令在本机选定目录运行,Git 使用本机仓库;模型、登录和全局助手要求仍在远程主机。本机自定义的附加工具、自动动作、插件及断开后的会话恢复暂不支持。子任务被系统回收后能否继续,尚未验证。' };
240
+ entry = { scope, runtime, current: input, details, execution, network, ended: false, dispose: reason => {
241
+ if (entry.closing)
242
+ return entry.closing;
243
+ entry.ended = true;
244
+ entry.reason = reason;
245
+ execution.invalidate(reason ?? fail('EMPLOYEE_EXEC_CLOSED'));
246
+ for (const off of disposers)
247
+ off();
248
+ host.broker?.fail(reason ?? fail('EMPLOYEE_EXEC_CLOSED'));
249
+ entry.closing = (async () => {
250
+ let cleanup;
251
+ try {
252
+ await execution.dispose();
253
+ }
254
+ catch (error) {
255
+ cleanup = error;
256
+ }
257
+ try {
258
+ await host.dispose?.();
259
+ }
260
+ catch (error) {
261
+ cleanup = error;
262
+ }
263
+ 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
+ await entry.details?.flush();
266
+ }
267
+ catch {
268
+ throw fail('CONVERSATION_DETAILS_FAILED');
269
+ }
270
+ if (cleanup)
271
+ throw fail('REMOTE_CLEANUP_UNCONFIRMED');
272
+ if (live.get(sessionId) === entry)
273
+ live.delete(sessionId);
274
+ })();
275
+ return entry.closing;
276
+ } };
277
+ candidate = entry;
278
+ runtime.employeeExecution = async (params, signal) => {
279
+ try {
280
+ const result = await execution.handle(params, signal);
281
+ if (result.closed)
282
+ await entry.dispose();
283
+ return { ...result };
284
+ }
285
+ catch (error) {
286
+ // EmployeeExecution reserves ABORTED for an unconsumed read wait. Lost responses are UNKNOWN.
287
+ if (!(params && typeof params === 'object' && 'operation' in params && params.operation === 'read' && errorCode(error) === 'ABORTED'))
288
+ await entry.dispose(asNeed(error));
289
+ throw error;
290
+ }
291
+ };
292
+ runtime.dispose = () => entry.dispose();
293
+ runtime.detach = runtime.dispose;
294
+ const revoke = (reason) => { void entry.dispose(reason).catch(() => { }); };
295
+ disposers.push(api.onWorkspaceLocationState(next => {
296
+ const matches = next.items.filter(item => item.workspaceId === admitted.workspaceId);
297
+ 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'));
299
+ }), 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')); }));
302
+ if (host.broker)
303
+ disposers.push(host.broker.onChange(() => { if (!host.broker.online)
304
+ revoke(fail('EMPLOYEE_EXEC_UNKNOWN')); }));
305
+ live.set(sessionId, entry);
306
+ try {
307
+ await validate(request.signal ?? new AbortController().signal);
308
+ }
309
+ catch (error) {
310
+ await entry.dispose(asNeed(error));
311
+ throw error;
312
+ }
313
+ return runtime;
314
+ }
315
+ catch (error) {
316
+ details?.record({ kind: 'need', severity: 'action-required', code: errorCode(error), summary: asNeed(error).message, payload: { result: 'UNKNOWN' } });
317
+ try {
318
+ await details?.flush();
319
+ }
320
+ catch {
321
+ throw fail('CONVERSATION_DETAILS_FAILED');
322
+ }
323
+ throw error;
324
+ }
325
+ };
326
+ }
@@ -0,0 +1,18 @@
1
+ import type { EmployeeEndpointTarget } from './employee-endpoint.ts';
2
+ /** Runs on the host. Only target discovery and SSH stdio forwarding; never user tools. */
3
+ export declare const EMPLOYEE_TARGET_PYTHON: string;
4
+ export declare const EMPLOYEE_PYTHON_ERRORS: string;
5
+ export declare const EMPLOYEE_RELAY_PYTHON: string;
6
+ export interface EmployeeRouting {
7
+ mcpServers: {
8
+ name: string;
9
+ command: string;
10
+ args: string[];
11
+ env: {
12
+ name: string;
13
+ value: string;
14
+ }[];
15
+ }[];
16
+ toolAliases: Record<string, string>;
17
+ }
18
+ export declare function employeeRouting(target: EmployeeEndpointTarget, socketPath: string, maxBytes: number): EmployeeRouting;