@ai-workbench/remote-agent 0.8.2 → 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 (194) hide show
  1. package/README.md +42 -7
  2. package/dist/acp.d.ts +46 -1
  3. package/dist/acp.js +296 -36
  4. package/dist/approval-label.d.ts +6 -0
  5. package/dist/approval-label.js +57 -0
  6. package/dist/broker-runtime.d.ts +10 -0
  7. package/dist/broker-runtime.js +334 -0
  8. package/dist/broker.d.ts +29 -6
  9. package/dist/broker.js +146 -142
  10. package/dist/claude-local.d.ts +37 -0
  11. package/dist/claude-local.js +207 -0
  12. package/dist/client.js +71 -2
  13. package/dist/conversation-details.d.ts +1 -1
  14. package/dist/conversation-details.js +88 -8
  15. package/dist/employee-endpoint.d.ts +91 -0
  16. package/dist/employee-endpoint.js +141 -0
  17. package/dist/employee-exec.d.ts +53 -0
  18. package/dist/employee-exec.js +423 -0
  19. package/dist/employee-location.d.ts +66 -0
  20. package/dist/employee-location.js +51 -0
  21. package/dist/employee-mcp.d.ts +42 -0
  22. package/dist/employee-mcp.js +181 -0
  23. package/dist/employee-native.d.ts +18 -0
  24. package/dist/employee-native.js +38 -0
  25. package/dist/employee-provider.d.ts +42 -0
  26. package/dist/employee-provider.js +326 -0
  27. package/dist/employee-relay.d.ts +18 -0
  28. package/dist/employee-relay.js +49 -0
  29. package/dist/employee-shell.d.ts +26 -0
  30. package/dist/employee-shell.js +89 -0
  31. package/dist/employee-ssh.d.ts +22 -0
  32. package/dist/employee-ssh.js +123 -0
  33. package/dist/employee-tools.d.ts +117 -0
  34. package/dist/employee-tools.js +292 -0
  35. package/dist/employee-workspace.d.ts +26 -0
  36. package/dist/employee-workspace.js +61 -0
  37. package/dist/index.d.ts +31 -14
  38. package/dist/index.js +335 -42
  39. package/dist/input.d.ts +15 -0
  40. package/dist/input.js +65 -24
  41. package/dist/local-user-turn.d.ts +24 -0
  42. package/dist/local-user-turn.js +103 -0
  43. package/dist/model-catalog.d.ts +55 -0
  44. package/dist/model-catalog.js +254 -0
  45. package/dist/model-configuration.d.ts +51 -0
  46. package/dist/model-configuration.js +212 -0
  47. package/dist/presentation.d.ts +1 -1
  48. package/dist/presentation.js +50 -2
  49. package/dist/protocol-boundary.d.ts +13 -0
  50. package/dist/protocol-boundary.js +49 -0
  51. package/dist/provider.d.ts +54 -2
  52. package/dist/provider.js +92 -29
  53. package/dist/readiness.js +1 -1
  54. package/dist/recovery-checkpoint.d.ts +6 -0
  55. package/dist/recovery-checkpoint.js +71 -0
  56. package/dist/recovery.d.ts +47 -0
  57. package/dist/recovery.js +61 -0
  58. package/dist/remote-details.d.ts +10 -0
  59. package/dist/remote-details.js +14 -4
  60. package/dist/rpc-namespace.d.ts +3 -0
  61. package/dist/rpc-namespace.js +50 -0
  62. package/dist/stage-timing.d.ts +27 -0
  63. package/dist/stage-timing.js +39 -0
  64. package/helpers/ripgrep/LICENSE-vscode +13 -0
  65. package/helpers/ripgrep/PROVENANCE.md +9 -0
  66. package/helpers/ripgrep/darwin-arm64/rg +0 -0
  67. package/helpers/ripgrep/darwin-x64/rg +0 -0
  68. package/helpers/ripgrep/manifest.json +56 -0
  69. package/helpers/ripgrep/notices/COPYING-ripgrep +3 -0
  70. package/helpers/ripgrep/notices/DEPENDENCIES.json +946 -0
  71. package/helpers/ripgrep/notices/LICENCE-PCRE2.md +103 -0
  72. package/helpers/ripgrep/notices/LICENSE-MIT-ripgrep +21 -0
  73. package/helpers/ripgrep/notices/UNLICENSE-ripgrep +24 -0
  74. package/helpers/ripgrep/notices/aho-corasick-1.1.3/COPYING +3 -0
  75. package/helpers/ripgrep/notices/aho-corasick-1.1.3/LICENSE-MIT +21 -0
  76. package/helpers/ripgrep/notices/aho-corasick-1.1.3/UNLICENSE +24 -0
  77. package/helpers/ripgrep/notices/anyhow-1.0.100/LICENSE-APACHE +176 -0
  78. package/helpers/ripgrep/notices/anyhow-1.0.100/LICENSE-MIT +23 -0
  79. package/helpers/ripgrep/notices/arbitrary-1.4.2/LICENSE-APACHE +201 -0
  80. package/helpers/ripgrep/notices/arbitrary-1.4.2/LICENSE-MIT +27 -0
  81. package/helpers/ripgrep/notices/bstr-1.12.0/COPYING +8 -0
  82. package/helpers/ripgrep/notices/bstr-1.12.0/LICENSE-APACHE +201 -0
  83. package/helpers/ripgrep/notices/bstr-1.12.0/LICENSE-MIT +21 -0
  84. package/helpers/ripgrep/notices/cc-1.2.41/LICENSE-APACHE +201 -0
  85. package/helpers/ripgrep/notices/cc-1.2.41/LICENSE-MIT +25 -0
  86. package/helpers/ripgrep/notices/cfg-if-1.0.4/LICENSE-APACHE +201 -0
  87. package/helpers/ripgrep/notices/cfg-if-1.0.4/LICENSE-MIT +25 -0
  88. package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-APACHE +201 -0
  89. package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-MIT +27 -0
  90. package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-THIRD-PARTY +593 -0
  91. package/helpers/ripgrep/notices/crossbeam-deque-0.8.6/LICENSE-APACHE +201 -0
  92. package/helpers/ripgrep/notices/crossbeam-deque-0.8.6/LICENSE-MIT +27 -0
  93. package/helpers/ripgrep/notices/crossbeam-epoch-0.9.18/LICENSE-APACHE +201 -0
  94. package/helpers/ripgrep/notices/crossbeam-epoch-0.9.18/LICENSE-MIT +27 -0
  95. package/helpers/ripgrep/notices/crossbeam-utils-0.8.21/LICENSE-APACHE +201 -0
  96. package/helpers/ripgrep/notices/crossbeam-utils-0.8.21/LICENSE-MIT +27 -0
  97. package/helpers/ripgrep/notices/derive_arbitrary-1.4.2/LICENSE-APACHE +201 -0
  98. package/helpers/ripgrep/notices/derive_arbitrary-1.4.2/LICENSE-MIT +27 -0
  99. package/helpers/ripgrep/notices/encoding_rs-0.8.35/COPYRIGHT +17 -0
  100. package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-APACHE +202 -0
  101. package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-MIT +25 -0
  102. package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-WHATWG +26 -0
  103. package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/COPYING +8 -0
  104. package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/LICENSE-APACHE +201 -0
  105. package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/LICENSE-MIT +21 -0
  106. package/helpers/ripgrep/notices/find-msvc-tools-0.1.4/LICENSE-APACHE +201 -0
  107. package/helpers/ripgrep/notices/find-msvc-tools-0.1.4/LICENSE-MIT +25 -0
  108. package/helpers/ripgrep/notices/getrandom-0.3.4/LICENSE-APACHE +201 -0
  109. package/helpers/ripgrep/notices/getrandom-0.3.4/LICENSE-MIT +26 -0
  110. package/helpers/ripgrep/notices/glob-0.3.3/LICENSE-APACHE +201 -0
  111. package/helpers/ripgrep/notices/glob-0.3.3/LICENSE-MIT +25 -0
  112. package/helpers/ripgrep/notices/itoa-1.0.15/LICENSE-APACHE +176 -0
  113. package/helpers/ripgrep/notices/itoa-1.0.15/LICENSE-MIT +23 -0
  114. package/helpers/ripgrep/notices/jobserver-0.1.34/LICENSE-APACHE +201 -0
  115. package/helpers/ripgrep/notices/jobserver-0.1.34/LICENSE-MIT +25 -0
  116. package/helpers/ripgrep/notices/lexopt-0.3.1/LICENSE +19 -0
  117. package/helpers/ripgrep/notices/libc-0.2.177/LICENSE-APACHE +176 -0
  118. package/helpers/ripgrep/notices/libc-0.2.177/LICENSE-MIT +25 -0
  119. package/helpers/ripgrep/notices/log-0.4.28/LICENSE-APACHE +201 -0
  120. package/helpers/ripgrep/notices/log-0.4.28/LICENSE-MIT +25 -0
  121. package/helpers/ripgrep/notices/memchr-2.7.6/COPYING +3 -0
  122. package/helpers/ripgrep/notices/memchr-2.7.6/LICENSE-MIT +21 -0
  123. package/helpers/ripgrep/notices/memchr-2.7.6/UNLICENSE +24 -0
  124. package/helpers/ripgrep/notices/memmap2-0.9.8/LICENSE-APACHE +201 -0
  125. package/helpers/ripgrep/notices/memmap2-0.9.8/LICENSE-MIT +26 -0
  126. package/helpers/ripgrep/notices/msvc_spectre_libs-0.1.3/LICENSE +21 -0
  127. package/helpers/ripgrep/notices/pcre2-0.2.11/COPYING +3 -0
  128. package/helpers/ripgrep/notices/pcre2-0.2.11/LICENSE-MIT +21 -0
  129. package/helpers/ripgrep/notices/pcre2-0.2.11/UNLICENSE +24 -0
  130. package/helpers/ripgrep/notices/pcre2-sys-0.2.10/COPYING +3 -0
  131. package/helpers/ripgrep/notices/pcre2-sys-0.2.10/LICENSE-MIT +21 -0
  132. package/helpers/ripgrep/notices/pcre2-sys-0.2.10/UNLICENSE +24 -0
  133. package/helpers/ripgrep/notices/pkg-config-0.3.32/LICENSE-APACHE +201 -0
  134. package/helpers/ripgrep/notices/pkg-config-0.3.32/LICENSE-MIT +25 -0
  135. package/helpers/ripgrep/notices/proc-macro2-1.0.101/LICENSE-APACHE +176 -0
  136. package/helpers/ripgrep/notices/proc-macro2-1.0.101/LICENSE-MIT +23 -0
  137. package/helpers/ripgrep/notices/quote-1.0.41/LICENSE-APACHE +176 -0
  138. package/helpers/ripgrep/notices/quote-1.0.41/LICENSE-MIT +23 -0
  139. package/helpers/ripgrep/notices/r-efi-5.3.0/AUTHORS +74 -0
  140. package/helpers/ripgrep/notices/regex-1.12.2/LICENSE-APACHE +201 -0
  141. package/helpers/ripgrep/notices/regex-1.12.2/LICENSE-MIT +25 -0
  142. package/helpers/ripgrep/notices/regex-automata-0.4.13/LICENSE-APACHE +201 -0
  143. package/helpers/ripgrep/notices/regex-automata-0.4.13/LICENSE-MIT +25 -0
  144. package/helpers/ripgrep/notices/regex-syntax-0.8.8/LICENSE-APACHE +201 -0
  145. package/helpers/ripgrep/notices/regex-syntax-0.8.8/LICENSE-MIT +25 -0
  146. package/helpers/ripgrep/notices/regex-syntax-0.8.8/src/unicode_tables/LICENSE-UNICODE +57 -0
  147. package/helpers/ripgrep/notices/ryu-1.0.20/LICENSE-APACHE +176 -0
  148. package/helpers/ripgrep/notices/ryu-1.0.20/LICENSE-BOOST +23 -0
  149. package/helpers/ripgrep/notices/same-file-1.0.6/COPYING +3 -0
  150. package/helpers/ripgrep/notices/same-file-1.0.6/LICENSE-MIT +21 -0
  151. package/helpers/ripgrep/notices/same-file-1.0.6/UNLICENSE +24 -0
  152. package/helpers/ripgrep/notices/serde-1.0.228/LICENSE-APACHE +176 -0
  153. package/helpers/ripgrep/notices/serde-1.0.228/LICENSE-MIT +23 -0
  154. package/helpers/ripgrep/notices/serde_core-1.0.228/LICENSE-APACHE +176 -0
  155. package/helpers/ripgrep/notices/serde_core-1.0.228/LICENSE-MIT +23 -0
  156. package/helpers/ripgrep/notices/serde_derive-1.0.228/LICENSE-APACHE +176 -0
  157. package/helpers/ripgrep/notices/serde_derive-1.0.228/LICENSE-MIT +23 -0
  158. package/helpers/ripgrep/notices/serde_json-1.0.145/LICENSE-APACHE +176 -0
  159. package/helpers/ripgrep/notices/serde_json-1.0.145/LICENSE-MIT +23 -0
  160. package/helpers/ripgrep/notices/shlex-1.3.0/LICENSE-APACHE +13 -0
  161. package/helpers/ripgrep/notices/shlex-1.3.0/LICENSE-MIT +21 -0
  162. package/helpers/ripgrep/notices/syn-2.0.106/LICENSE-APACHE +176 -0
  163. package/helpers/ripgrep/notices/syn-2.0.106/LICENSE-MIT +23 -0
  164. package/helpers/ripgrep/notices/termcolor-1.4.1/COPYING +3 -0
  165. package/helpers/ripgrep/notices/termcolor-1.4.1/LICENSE-MIT +21 -0
  166. package/helpers/ripgrep/notices/termcolor-1.4.1/UNLICENSE +24 -0
  167. package/helpers/ripgrep/notices/textwrap-0.16.2/LICENSE +21 -0
  168. package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/LICENSE-APACHE +201 -0
  169. package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/LICENSE-MIT +25 -0
  170. package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/jemalloc/COPYING +27 -0
  171. package/helpers/ripgrep/notices/tikv-jemallocator-0.6.0/LICENSE-APACHE +201 -0
  172. package/helpers/ripgrep/notices/tikv-jemallocator-0.6.0/LICENSE-MIT +25 -0
  173. package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-APACHE +176 -0
  174. package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-MIT +23 -0
  175. package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-UNICODE +39 -0
  176. package/helpers/ripgrep/notices/walkdir-2.5.0/COPYING +3 -0
  177. package/helpers/ripgrep/notices/walkdir-2.5.0/LICENSE-MIT +21 -0
  178. package/helpers/ripgrep/notices/walkdir-2.5.0/UNLICENSE +24 -0
  179. package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-APACHE +201 -0
  180. package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-Apache-2.0_WITH_LLVM-exception +220 -0
  181. package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-MIT +23 -0
  182. package/helpers/ripgrep/notices/winapi-util-0.1.11/COPYING +3 -0
  183. package/helpers/ripgrep/notices/winapi-util-0.1.11/LICENSE-MIT +21 -0
  184. package/helpers/ripgrep/notices/winapi-util-0.1.11/UNLICENSE +24 -0
  185. package/helpers/ripgrep/notices/windows-link-0.2.1/license-apache-2.0 +201 -0
  186. package/helpers/ripgrep/notices/windows-link-0.2.1/license-mit +21 -0
  187. package/helpers/ripgrep/notices/windows-sys-0.61.2/license-apache-2.0 +201 -0
  188. package/helpers/ripgrep/notices/windows-sys-0.61.2/license-mit +21 -0
  189. package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-APACHE +201 -0
  190. package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-Apache-2.0_WITH_LLVM-exception +220 -0
  191. package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-MIT +23 -0
  192. package/locale/en.json +1 -1
  193. package/locale/zh.json +2 -2
  194. package/package.json +30 -12
@@ -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;
@@ -0,0 +1,49 @@
1
+ /** Runs on the host. Only target discovery and SSH stdio forwarding; never user tools. */
2
+ export const EMPLOYEE_TARGET_PYTHON = String.raw `
3
+ import os,sys,json,subprocess,selectors,shlex,re
4
+ def fail(code):
5
+ sys.stderr.write(json.dumps({"code":code})+"\n");sys.exit(254)
6
+ try:
7
+ x=json.loads(sys.argv[1]);e=x["endpoint"];b=x["bridge"];maximum=x["maxBytes"]
8
+ if not isinstance(maximum,int) or isinstance(maximum,bool) or maximum<1:fail("EMPLOYEE_RELAY_SETTINGS_INVALID")
9
+ if b["command"]!="/usr/local/aw-host/bin/aw-employee-bridge":fail("EMPLOYEE_RELAY_TARGET_INVALID")
10
+ argv=[b["command"],"target","--settings",b["settingsPath"],"--device",e["deviceId"],"--if-instance",b["bridgeInstanceId"],"--if-generation",str(b["generation"]),"--if-endpoint",e["endpointInstanceId"],"--if-context",e["executionContextId"]]
11
+ p=subprocess.Popen(argv,stdin=subprocess.DEVNULL,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
12
+ selector=selectors.DefaultSelector();selector.register(p.stdout,selectors.EVENT_READ,"out");selector.register(p.stderr,selectors.EVENT_READ,"err")
13
+ data={"out":bytearray(),"err":bytearray()}
14
+ while selector.get_map():
15
+ for key,_ in selector.select():
16
+ chunk=os.read(key.fileobj.fileno(),maximum+1)
17
+ if not chunk:selector.unregister(key.fileobj);continue
18
+ data[key.data].extend(chunk)
19
+ if len(data["out"])+len(data["err"])>maximum:
20
+ p.kill();p.wait();fail("EMPLOYEE_RELAY_CONTROL_TOO_LARGE")
21
+ selector.close()
22
+ if p.wait()!=0:
23
+ reason=re.fullmatch(r"BRIDGE_ERROR ([A-Z][A-Z0-9_]{0,63})\s*",data["err"].decode("utf-8",errors="strict"))
24
+ fail(reason.group(1) if reason else "EMPLOYEE_BRIDGE_TARGET_FAILED")
25
+ t=json.loads(data["out"])
26
+ if t.get("type")!="target" or t.get("protocolVersion")!=1 or t.get("endpoint",{}).get("state")!="online":fail("EMPLOYEE_BRIDGE_PROTOCOL_INVALID")
27
+ expected=dict(e,bridgeInstanceId=b["bridgeInstanceId"],generation=b["generation"])
28
+ if any(t["endpoint"].get(k)!=v for k,v in expected.items()):fail("EMPLOYEE_CONTEXT_CHANGED")
29
+ args=t.get("sshArgs")
30
+ if not isinstance(args,list) or not args or any(not isinstance(v,str) or "\x00" in v for v in args):fail("EMPLOYEE_BRIDGE_PROTOCOL_INVALID")
31
+ `;
32
+ export const EMPLOYEE_PYTHON_ERRORS = String.raw `
33
+ except (KeyError,ValueError,TypeError,OSError):
34
+ fail("EMPLOYEE_RELAY_FAILED")
35
+ `;
36
+ export const EMPLOYEE_RELAY_PYTHON = EMPLOYEE_TARGET_PYTHON + String.raw `
37
+ socket=x["socketPath"]
38
+ if not isinstance(socket,str) or not socket.startswith("/") or "\x00" in socket:fail("EMPLOYEE_RELAY_SOCKET_INVALID")
39
+ command=shlex.join(["/usr/bin/nc","-U",socket])
40
+ os.execv("/usr/bin/ssh",["/usr/bin/ssh","-T"]+args+[command])
41
+ ` + EMPLOYEE_PYTHON_ERRORS;
42
+ export function employeeRouting(target, socketPath, maxBytes) {
43
+ const names = ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash'];
44
+ return {
45
+ // ACP0.94.0 uses absence of "type" to recognize stdio. Do not add type:"stdio".
46
+ mcpServers: [{ name: 'employee', command: '/usr/bin/python3', args: ['-c', EMPLOYEE_RELAY_PYTHON, JSON.stringify({ endpoint: target.endpoint, bridge: target.bridge, socketPath, maxBytes })], env: [] }],
47
+ toolAliases: Object.fromEntries(names.map(name => [name, 'mcp__employee__' + name])),
48
+ };
49
+ }
@@ -0,0 +1,26 @@
1
+ import type { ChildProcessWithoutNullStreams } from 'node:child_process';
2
+ import type { EmployeeEndpointTarget } from './employee-endpoint.ts';
3
+ export interface EmployeeShellResult {
4
+ stdout: string;
5
+ stderr: string;
6
+ stderrSource: 'combined-ssh-and-command';
7
+ exitCode: number;
8
+ signal: null;
9
+ }
10
+ export type EmployeeShell = (request: {
11
+ command: string;
12
+ timeout?: number;
13
+ signal: AbortSignal;
14
+ }) => Promise<EmployeeShellResult>;
15
+ interface Options {
16
+ target: EmployeeEndpointTarget;
17
+ maxBytes: number;
18
+ killGraceMs: number;
19
+ attach: (spec: {
20
+ command: '/usr/bin/python3';
21
+ args: string[];
22
+ }) => Promise<ChildProcessWithoutNullStreams>;
23
+ }
24
+ /** Every invocation re-discovers the exact S target. Cancellation cannot prove remote exit. */
25
+ export declare function createEmployeeShell(options: Options): EmployeeShell;
26
+ export {};
@@ -0,0 +1,89 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { EMPLOYEE_TARGET_PYTHON, EMPLOYEE_PYTHON_ERRORS } from "./employee-relay.js";
3
+ import { NeedError } from "./remote.js";
4
+ const SHELL_PYTHON = EMPLOYEE_TARGET_PYTHON + String.raw `
5
+ cwd=x["directory"]["path"];device=x["directory"]["device"];inode=x["directory"]["inode"]
6
+ # User source is stdin, never argv. The sentinel preserves trailing newlines.
7
+ wrapper="cd -- "+shlex.quote(cwd)+" || exit 254; "+'test "$(/usr/bin/stat -f '+shlex.quote('%d %i')+' .)" = '+shlex.quote(str(device)+' '+str(inode))+" || exit 254; "+'test "$(/usr/bin/id -u)" = '+shlex.quote(str(e['remoteUid']))+" || exit 254; "+"printf '%s\\n' "+shlex.quote('AW_EMPLOYEE_SHELL_STARTED '+x['executionId'])+" >&2; "+'aw_x_source=$(/bin/cat; printf x); aw_x_source='+chr(36)+'{aw_x_source%?}; eval "'+chr(36)+'aw_x_source"'
8
+ command=shlex.join([e["endpointShell"],"-c",wrapper])
9
+ sys.stderr.write("AW_EMPLOYEE_SHELL_READY "+x["executionId"]+"\n");sys.stderr.flush()
10
+ p=subprocess.Popen(["/usr/bin/ssh","-T"]+args+[command],stdin=sys.stdin,stdout=sys.stdout,stderr=sys.stderr)
11
+ code=p.wait();sys.exit(code if code>=0 else 255)
12
+ ` + EMPLOYEE_PYTHON_ERRORS;
13
+ const unknown = (code) => new NeedError(code, '本机命令结果未知;没有重放,也未改在主机执行。', '员工端真实退出回执');
14
+ /** Every invocation re-discovers the exact S target. Cancellation cannot prove remote exit. */
15
+ export function createEmployeeShell(options) {
16
+ return async ({ command, timeout, signal }) => {
17
+ signal.throwIfAborted();
18
+ const executionId = randomUUID(), target = options.target;
19
+ 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
+ return new Promise((yes, no) => {
21
+ let settled = false, bytes = 0, stdout = [], stderr = [], reason, grace, timer;
22
+ const finish = (error, code) => {
23
+ if (settled)
24
+ return;
25
+ settled = true;
26
+ signal.removeEventListener('abort', abort);
27
+ if (grace)
28
+ clearTimeout(grace);
29
+ if (timer)
30
+ clearTimeout(timer);
31
+ child.stdin.destroy();
32
+ child.stdout.destroy();
33
+ child.stderr.destroy();
34
+ if (error) {
35
+ no(error);
36
+ return;
37
+ }
38
+ let out, err;
39
+ try {
40
+ out = new TextDecoder('utf8', { fatal: true, ignoreBOM: true }).decode(Buffer.concat(stdout));
41
+ err = new TextDecoder('utf8', { fatal: true, ignoreBOM: true }).decode(Buffer.concat(stderr));
42
+ }
43
+ catch {
44
+ no(unknown('EMPLOYEE_OUTPUT_BINARY_UNSUPPORTED'));
45
+ return;
46
+ }
47
+ const header = 'AW_EMPLOYEE_SHELL_READY ' + executionId + '\n', started = 'AW_EMPLOYEE_SHELL_STARTED ' + executionId + '\n';
48
+ const start = err.indexOf(started, header.length);
49
+ if (!err.startsWith(header) || start < header.length || start > header.length && err[start - 1] !== '\n') {
50
+ const raw = err.startsWith(header) ? err.slice(header.length) : err;
51
+ let source;
52
+ try {
53
+ const parsed = JSON.parse(raw);
54
+ if (typeof parsed.code === 'string' && /^[A-Z][A-Z0-9_]{0,63}$/.test(parsed.code))
55
+ source = parsed.code;
56
+ }
57
+ catch { /* Never expose arbitrary transport diagnostics. */ }
58
+ no(unknown(source ?? 'EMPLOYEE_EXIT_UNKNOWN'));
59
+ return;
60
+ }
61
+ if (code === null || code === undefined || code === 255) {
62
+ no(unknown('EMPLOYEE_EXIT_UNKNOWN'));
63
+ return;
64
+ }
65
+ yes({ stdout: out, stderr: err.slice(header.length, start) + err.slice(start + started.length), stderrSource: 'combined-ssh-and-command', exitCode: code, signal: null });
66
+ };
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
+ child.kill('SIGKILL'); finish(reason); }, options.killGraceMs); };
70
+ const abort = () => stop('EMPLOYEE_CANCELLED_UNKNOWN');
71
+ const collect = (destination, chunk) => { bytes += chunk.length; if (bytes > options.maxBytes) {
72
+ stop('EMPLOYEE_OUTPUT_TOO_LARGE_UNKNOWN');
73
+ return;
74
+ } destination.push(chunk); };
75
+ child.stdout.on('data', (b) => collect(stdout, b));
76
+ child.stderr.on('data', (b) => collect(stderr, b));
77
+ 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));
80
+ signal.addEventListener('abort', abort, { once: true });
81
+ if (signal.aborted)
82
+ abort();
83
+ if (timeout !== undefined)
84
+ timer = setTimeout(() => stop('EMPLOYEE_TIMEOUT_UNKNOWN'), timeout);
85
+ if (!reason)
86
+ child.stdin.end(command);
87
+ });
88
+ };
89
+ }
@@ -0,0 +1,22 @@
1
+ import type { EmployeeEndpointTarget } from '@ai-workbench/network/api';
2
+ import { type BrokerIo, type SshArgv } from './broker.ts';
3
+ import type { ConnectTarget } from './provider.ts';
4
+ import { type EmployeeExecutionTransport } from './employee-exec.ts';
5
+ export interface EmployeeSshOptions {
6
+ target: ConnectTarget;
7
+ endpoint: EmployeeEndpointTarget;
8
+ /** Automatically discovered package binary path on this same employee device. */
9
+ employeeBinary: string;
10
+ binarySha256: string;
11
+ acpMaxMessageBytes: number;
12
+ argv: SshArgv;
13
+ validate(signal: AbortSignal): Promise<void>;
14
+ /** Fixture seam only. Production uses L's exported argv through sshIo. */
15
+ io?: BrokerIo;
16
+ }
17
+ /** Fixed bootstrap only; cwd for every native operation remains in its original RPC. */
18
+ export declare function employeeExecServerCommand(binary: string, binarySha256: string): string;
19
+ /** Invoke this in EmployeeExecution.validate before and after each native RPC write. */
20
+ export declare function validateEmployeeSshTarget(options: EmployeeSshOptions, signal?: AbortSignal): Promise<void>;
21
+ /** Outer L-authenticated host SSH, then S-authenticated employee SSH; no host execution fallback. */
22
+ export declare function createEmployeeSshTransport(options: EmployeeSshOptions, signal?: AbortSignal): Promise<EmployeeExecutionTransport>;