@ai-workbench/remote-agent 0.8.5 → 0.8.11

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 (190) hide show
  1. package/README.md +49 -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 +235 -41
  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/model-catalog.d.ts +18 -2
  42. package/dist/model-catalog.js +101 -36
  43. package/dist/model-defaults.d.ts +29 -0
  44. package/dist/model-defaults.js +62 -0
  45. package/dist/model-input-retention.d.ts +16 -0
  46. package/dist/model-input-retention.js +129 -0
  47. package/dist/presentation.d.ts +1 -1
  48. package/dist/presentation.js +52 -2
  49. package/dist/provider.d.ts +39 -2
  50. package/dist/provider.js +69 -13
  51. package/dist/readiness.js +1 -1
  52. package/dist/recovery.d.ts +14 -1
  53. package/dist/recovery.js +11 -2
  54. package/dist/remote-details.d.ts +7 -0
  55. package/dist/remote-details.js +11 -1
  56. package/dist/workspace-source-confirmation.d.ts +4 -0
  57. package/dist/workspace-source-confirmation.js +12 -0
  58. package/dist/workspace-source.d.ts +25 -0
  59. package/dist/workspace-source.js +136 -0
  60. package/helpers/ripgrep/LICENSE-vscode +13 -0
  61. package/helpers/ripgrep/PROVENANCE.md +9 -0
  62. package/helpers/ripgrep/darwin-arm64/rg +0 -0
  63. package/helpers/ripgrep/darwin-x64/rg +0 -0
  64. package/helpers/ripgrep/manifest.json +56 -0
  65. package/helpers/ripgrep/notices/COPYING-ripgrep +3 -0
  66. package/helpers/ripgrep/notices/DEPENDENCIES.json +946 -0
  67. package/helpers/ripgrep/notices/LICENCE-PCRE2.md +103 -0
  68. package/helpers/ripgrep/notices/LICENSE-MIT-ripgrep +21 -0
  69. package/helpers/ripgrep/notices/UNLICENSE-ripgrep +24 -0
  70. package/helpers/ripgrep/notices/aho-corasick-1.1.3/COPYING +3 -0
  71. package/helpers/ripgrep/notices/aho-corasick-1.1.3/LICENSE-MIT +21 -0
  72. package/helpers/ripgrep/notices/aho-corasick-1.1.3/UNLICENSE +24 -0
  73. package/helpers/ripgrep/notices/anyhow-1.0.100/LICENSE-APACHE +176 -0
  74. package/helpers/ripgrep/notices/anyhow-1.0.100/LICENSE-MIT +23 -0
  75. package/helpers/ripgrep/notices/arbitrary-1.4.2/LICENSE-APACHE +201 -0
  76. package/helpers/ripgrep/notices/arbitrary-1.4.2/LICENSE-MIT +27 -0
  77. package/helpers/ripgrep/notices/bstr-1.12.0/COPYING +8 -0
  78. package/helpers/ripgrep/notices/bstr-1.12.0/LICENSE-APACHE +201 -0
  79. package/helpers/ripgrep/notices/bstr-1.12.0/LICENSE-MIT +21 -0
  80. package/helpers/ripgrep/notices/cc-1.2.41/LICENSE-APACHE +201 -0
  81. package/helpers/ripgrep/notices/cc-1.2.41/LICENSE-MIT +25 -0
  82. package/helpers/ripgrep/notices/cfg-if-1.0.4/LICENSE-APACHE +201 -0
  83. package/helpers/ripgrep/notices/cfg-if-1.0.4/LICENSE-MIT +25 -0
  84. package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-APACHE +201 -0
  85. package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-MIT +27 -0
  86. package/helpers/ripgrep/notices/crossbeam-channel-0.5.15/LICENSE-THIRD-PARTY +593 -0
  87. package/helpers/ripgrep/notices/crossbeam-deque-0.8.6/LICENSE-APACHE +201 -0
  88. package/helpers/ripgrep/notices/crossbeam-deque-0.8.6/LICENSE-MIT +27 -0
  89. package/helpers/ripgrep/notices/crossbeam-epoch-0.9.18/LICENSE-APACHE +201 -0
  90. package/helpers/ripgrep/notices/crossbeam-epoch-0.9.18/LICENSE-MIT +27 -0
  91. package/helpers/ripgrep/notices/crossbeam-utils-0.8.21/LICENSE-APACHE +201 -0
  92. package/helpers/ripgrep/notices/crossbeam-utils-0.8.21/LICENSE-MIT +27 -0
  93. package/helpers/ripgrep/notices/derive_arbitrary-1.4.2/LICENSE-APACHE +201 -0
  94. package/helpers/ripgrep/notices/derive_arbitrary-1.4.2/LICENSE-MIT +27 -0
  95. package/helpers/ripgrep/notices/encoding_rs-0.8.35/COPYRIGHT +17 -0
  96. package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-APACHE +202 -0
  97. package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-MIT +25 -0
  98. package/helpers/ripgrep/notices/encoding_rs-0.8.35/LICENSE-WHATWG +26 -0
  99. package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/COPYING +8 -0
  100. package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/LICENSE-APACHE +201 -0
  101. package/helpers/ripgrep/notices/encoding_rs_io-0.1.7/LICENSE-MIT +21 -0
  102. package/helpers/ripgrep/notices/find-msvc-tools-0.1.4/LICENSE-APACHE +201 -0
  103. package/helpers/ripgrep/notices/find-msvc-tools-0.1.4/LICENSE-MIT +25 -0
  104. package/helpers/ripgrep/notices/getrandom-0.3.4/LICENSE-APACHE +201 -0
  105. package/helpers/ripgrep/notices/getrandom-0.3.4/LICENSE-MIT +26 -0
  106. package/helpers/ripgrep/notices/glob-0.3.3/LICENSE-APACHE +201 -0
  107. package/helpers/ripgrep/notices/glob-0.3.3/LICENSE-MIT +25 -0
  108. package/helpers/ripgrep/notices/itoa-1.0.15/LICENSE-APACHE +176 -0
  109. package/helpers/ripgrep/notices/itoa-1.0.15/LICENSE-MIT +23 -0
  110. package/helpers/ripgrep/notices/jobserver-0.1.34/LICENSE-APACHE +201 -0
  111. package/helpers/ripgrep/notices/jobserver-0.1.34/LICENSE-MIT +25 -0
  112. package/helpers/ripgrep/notices/lexopt-0.3.1/LICENSE +19 -0
  113. package/helpers/ripgrep/notices/libc-0.2.177/LICENSE-APACHE +176 -0
  114. package/helpers/ripgrep/notices/libc-0.2.177/LICENSE-MIT +25 -0
  115. package/helpers/ripgrep/notices/log-0.4.28/LICENSE-APACHE +201 -0
  116. package/helpers/ripgrep/notices/log-0.4.28/LICENSE-MIT +25 -0
  117. package/helpers/ripgrep/notices/memchr-2.7.6/COPYING +3 -0
  118. package/helpers/ripgrep/notices/memchr-2.7.6/LICENSE-MIT +21 -0
  119. package/helpers/ripgrep/notices/memchr-2.7.6/UNLICENSE +24 -0
  120. package/helpers/ripgrep/notices/memmap2-0.9.8/LICENSE-APACHE +201 -0
  121. package/helpers/ripgrep/notices/memmap2-0.9.8/LICENSE-MIT +26 -0
  122. package/helpers/ripgrep/notices/msvc_spectre_libs-0.1.3/LICENSE +21 -0
  123. package/helpers/ripgrep/notices/pcre2-0.2.11/COPYING +3 -0
  124. package/helpers/ripgrep/notices/pcre2-0.2.11/LICENSE-MIT +21 -0
  125. package/helpers/ripgrep/notices/pcre2-0.2.11/UNLICENSE +24 -0
  126. package/helpers/ripgrep/notices/pcre2-sys-0.2.10/COPYING +3 -0
  127. package/helpers/ripgrep/notices/pcre2-sys-0.2.10/LICENSE-MIT +21 -0
  128. package/helpers/ripgrep/notices/pcre2-sys-0.2.10/UNLICENSE +24 -0
  129. package/helpers/ripgrep/notices/pkg-config-0.3.32/LICENSE-APACHE +201 -0
  130. package/helpers/ripgrep/notices/pkg-config-0.3.32/LICENSE-MIT +25 -0
  131. package/helpers/ripgrep/notices/proc-macro2-1.0.101/LICENSE-APACHE +176 -0
  132. package/helpers/ripgrep/notices/proc-macro2-1.0.101/LICENSE-MIT +23 -0
  133. package/helpers/ripgrep/notices/quote-1.0.41/LICENSE-APACHE +176 -0
  134. package/helpers/ripgrep/notices/quote-1.0.41/LICENSE-MIT +23 -0
  135. package/helpers/ripgrep/notices/r-efi-5.3.0/AUTHORS +74 -0
  136. package/helpers/ripgrep/notices/regex-1.12.2/LICENSE-APACHE +201 -0
  137. package/helpers/ripgrep/notices/regex-1.12.2/LICENSE-MIT +25 -0
  138. package/helpers/ripgrep/notices/regex-automata-0.4.13/LICENSE-APACHE +201 -0
  139. package/helpers/ripgrep/notices/regex-automata-0.4.13/LICENSE-MIT +25 -0
  140. package/helpers/ripgrep/notices/regex-syntax-0.8.8/LICENSE-APACHE +201 -0
  141. package/helpers/ripgrep/notices/regex-syntax-0.8.8/LICENSE-MIT +25 -0
  142. package/helpers/ripgrep/notices/regex-syntax-0.8.8/src/unicode_tables/LICENSE-UNICODE +57 -0
  143. package/helpers/ripgrep/notices/ryu-1.0.20/LICENSE-APACHE +176 -0
  144. package/helpers/ripgrep/notices/ryu-1.0.20/LICENSE-BOOST +23 -0
  145. package/helpers/ripgrep/notices/same-file-1.0.6/COPYING +3 -0
  146. package/helpers/ripgrep/notices/same-file-1.0.6/LICENSE-MIT +21 -0
  147. package/helpers/ripgrep/notices/same-file-1.0.6/UNLICENSE +24 -0
  148. package/helpers/ripgrep/notices/serde-1.0.228/LICENSE-APACHE +176 -0
  149. package/helpers/ripgrep/notices/serde-1.0.228/LICENSE-MIT +23 -0
  150. package/helpers/ripgrep/notices/serde_core-1.0.228/LICENSE-APACHE +176 -0
  151. package/helpers/ripgrep/notices/serde_core-1.0.228/LICENSE-MIT +23 -0
  152. package/helpers/ripgrep/notices/serde_derive-1.0.228/LICENSE-APACHE +176 -0
  153. package/helpers/ripgrep/notices/serde_derive-1.0.228/LICENSE-MIT +23 -0
  154. package/helpers/ripgrep/notices/serde_json-1.0.145/LICENSE-APACHE +176 -0
  155. package/helpers/ripgrep/notices/serde_json-1.0.145/LICENSE-MIT +23 -0
  156. package/helpers/ripgrep/notices/shlex-1.3.0/LICENSE-APACHE +13 -0
  157. package/helpers/ripgrep/notices/shlex-1.3.0/LICENSE-MIT +21 -0
  158. package/helpers/ripgrep/notices/syn-2.0.106/LICENSE-APACHE +176 -0
  159. package/helpers/ripgrep/notices/syn-2.0.106/LICENSE-MIT +23 -0
  160. package/helpers/ripgrep/notices/termcolor-1.4.1/COPYING +3 -0
  161. package/helpers/ripgrep/notices/termcolor-1.4.1/LICENSE-MIT +21 -0
  162. package/helpers/ripgrep/notices/termcolor-1.4.1/UNLICENSE +24 -0
  163. package/helpers/ripgrep/notices/textwrap-0.16.2/LICENSE +21 -0
  164. package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/LICENSE-APACHE +201 -0
  165. package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/LICENSE-MIT +25 -0
  166. package/helpers/ripgrep/notices/tikv-jemalloc-sys-0.6.0+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7/jemalloc/COPYING +27 -0
  167. package/helpers/ripgrep/notices/tikv-jemallocator-0.6.0/LICENSE-APACHE +201 -0
  168. package/helpers/ripgrep/notices/tikv-jemallocator-0.6.0/LICENSE-MIT +25 -0
  169. package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-APACHE +176 -0
  170. package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-MIT +23 -0
  171. package/helpers/ripgrep/notices/unicode-ident-1.0.19/LICENSE-UNICODE +39 -0
  172. package/helpers/ripgrep/notices/walkdir-2.5.0/COPYING +3 -0
  173. package/helpers/ripgrep/notices/walkdir-2.5.0/LICENSE-MIT +21 -0
  174. package/helpers/ripgrep/notices/walkdir-2.5.0/UNLICENSE +24 -0
  175. package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-APACHE +201 -0
  176. package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-Apache-2.0_WITH_LLVM-exception +220 -0
  177. package/helpers/ripgrep/notices/wasip2-1.0.1+wasi-0.2.4/LICENSE-MIT +23 -0
  178. package/helpers/ripgrep/notices/winapi-util-0.1.11/COPYING +3 -0
  179. package/helpers/ripgrep/notices/winapi-util-0.1.11/LICENSE-MIT +21 -0
  180. package/helpers/ripgrep/notices/winapi-util-0.1.11/UNLICENSE +24 -0
  181. package/helpers/ripgrep/notices/windows-link-0.2.1/license-apache-2.0 +201 -0
  182. package/helpers/ripgrep/notices/windows-link-0.2.1/license-mit +21 -0
  183. package/helpers/ripgrep/notices/windows-sys-0.61.2/license-apache-2.0 +201 -0
  184. package/helpers/ripgrep/notices/windows-sys-0.61.2/license-mit +21 -0
  185. package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-APACHE +201 -0
  186. package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-Apache-2.0_WITH_LLVM-exception +220 -0
  187. package/helpers/ripgrep/notices/wit-bindgen-0.46.0/LICENSE-MIT +23 -0
  188. package/locale/en.json +1 -1
  189. package/locale/zh.json +2 -2
  190. package/package.json +29 -11
@@ -39,6 +39,9 @@ export function brokerFactory(get, _ask, configuration, provided) {
39
39
  const agent = request.agent ?? CODEX_AGENT;
40
40
  if (!request.sessionId)
41
41
  throw need('SESSION_REQUIRED');
42
+ const employeeScope = request.employeeScope;
43
+ if (employeeScope && (request.mode !== 'session' || request.resumeSessionId || request.recoveryCheckpointId))
44
+ throw need('EMPLOYEE_OPERATION_UNSUPPORTED');
42
45
  if (request.mode === 'one-shot') {
43
46
  const key = 'one-shot/' + identity(target) + '/' + agent.id + '/' + request.sessionId;
44
47
  if (active.has(key))
@@ -49,8 +52,18 @@ export function brokerFactory(get, _ask, configuration, provided) {
49
52
  }
50
53
  if (![settings.brokerMaxFrameBytes, settings.brokerControlMaxBytes].every(n => Number.isSafeInteger(n) && n > 0))
51
54
  throw need('SETTINGS_REQUIRED');
52
- const key = recoveryKey([identity(target), agent.id, request.sessionId]);
53
- const scope = recoveryKey([target.host, target.username, target.port, target.proxyCommand, target.knownHostsFile, target.projectRoot, target.session, project.path, agent.id, settings, request.bindingId, request.workspaceId, request.contextSha256]);
55
+ const key = recoveryKey([identity(target), agent.id, request.sessionId, ...(employeeScope ? [employeeScope] : [])]);
56
+ // New employee scopes compare execution values, independent of JSON key insertion order.
57
+ // Legacy journals retain their historical serialization bytes.
58
+ const scopedSession = employeeScope ? { command: target.session.command, root: target.session.root, settingsPath: target.session.settingsPath, protocolVersion: target.session.protocolVersion } : target.session;
59
+ const scopedSettings = employeeScope ? { brokerMaxFrameBytes: settings.brokerMaxFrameBytes, brokerControlMaxBytes: settings.brokerControlMaxBytes } : settings;
60
+ const scopeParts = [target.host, target.username, target.port, target.proxyCommand, target.knownHostsFile, target.projectRoot, scopedSession, project.path, agent.id, scopedSettings, request.bindingId, request.workspaceId];
61
+ if (employeeScope)
62
+ scopeParts.push(employeeScope);
63
+ const contextBoundScope = recoveryKey([...scopeParts, request.contextSha256]);
64
+ // A Codex ACP process can own several immutable native sessions. D18's durable
65
+ // pointer controls their context; the broker still binds every transport identity.
66
+ const scope = agent.loginProvider === 'codex' ? recoveryKey([...scopeParts, undefined]) : contextBoundScope;
54
67
  const existing = request.retainProcess ? retained.get(key) : undefined;
55
68
  if (existing) {
56
69
  if (existing.scope !== scope)
@@ -70,8 +83,11 @@ export function brokerFactory(get, _ask, configuration, provided) {
70
83
  if (!journal)
71
84
  throw need('RECOVERY_STORAGE_REQUIRED');
72
85
  let state = await journal.read();
73
- if (state && state.scope !== scope)
74
- throw need('SESSION_CONTEXT_MISMATCH');
86
+ const legacyContextScope = !!state && state.scope !== scope && agent.loginProvider === 'codex' && state.scope === contextBoundScope;
87
+ if (state && state.scope !== scope && !legacyContextScope)
88
+ throw need(agent.loginProvider === 'codex' ? 'RECOVERY_CONTEXT_POLICY_UNSUPPORTED' : 'SESSION_CONTEXT_MISMATCH');
89
+ if (legacyContextScope && state?.phase !== 'clean')
90
+ throw need('RECOVERY_CONTEXT_POLICY_UNSUPPORTED');
75
91
  let argv = provided?.argv;
76
92
  if (!provided?.io && !argv) {
77
93
  const name = '@ai-workbench/connect';
@@ -91,7 +107,7 @@ export function brokerFactory(get, _ask, configuration, provided) {
91
107
  const list = async (cleanup = false) => timing.run('broker-list', async () => { const rows = controlJson(await io.run(command(['list']), cleanup ? undefined : request.signal)); if (!Array.isArray(rows) || rows.some(row => typeof row?.id !== 'string'))
92
108
  throw need('BROKER_PROTOCOL_INVALID'); return rows; });
93
109
  let rows = await list();
94
- const id = brokerId(request.sessionId, agent.id), oldId = legacyBrokerId(request.sessionId, agent.id);
110
+ const id = brokerId(request.sessionId, employeeScope ? agent.id + '/employee/' + employeeScope : agent.id), oldId = legacyBrokerId(request.sessionId, agent.id);
95
111
  let restarted = false;
96
112
  const attachArgs = (entry, after) => ['attach', entry.id, '--after', String(after), '--framed', ...(modern(entry) ? ['--leased', '--if-instance', entry.instanceId] : [])];
97
113
  const connectWithLease = async (transport, entry, cleanup = false) => {
@@ -170,12 +186,14 @@ export function brokerFactory(get, _ask, configuration, provided) {
170
186
  probe.close();
171
187
  }
172
188
  };
173
- const legacy = rows.find(row => row.id === oldId);
189
+ const legacy = employeeScope ? undefined : rows.find(row => row.id === oldId);
174
190
  if (legacy)
175
191
  await report('PRE_0410_TEST_SESSION_NOT_RESUMED', { brokerId: legacy.id, observedPid: legacy.pid, state: legacy.state, instanceId: legacy.instanceId ?? null, reason: 'OLD_TEST_SESSION_COORDINATOR_CLEANUP', decision: '2026-10-22-00:05', approvalReplies: 0 });
176
192
  let entry = rows.find(row => row.id === id);
177
193
  if (entry)
178
194
  validEntry(entry);
195
+ if (legacyContextScope && !entry)
196
+ throw need('RECOVERY_CONTEXT_POLICY_UNSUPPORTED');
179
197
  if (legacy && !entry)
180
198
  restarted = true;
181
199
  let transport;
@@ -189,6 +207,8 @@ export function brokerFactory(get, _ask, configuration, provided) {
189
207
  throw need('RECOVERY_MODEL_CONFIGURATION_INVALID');
190
208
  (recovery.configUpdates ??= []).push(structuredClone(params.update.configOptions));
191
209
  } request.detailChannel?.record({ kind: 'session', severity: 'info', code: 'ACP_IDLE_UPDATE', summary: '恢复期间的远端状态轨迹', method: 'session/update', payload: message.params }); } } : {}), attach: after => io.attach(command(attachArgs(entry, after))), onDisconnect: async () => {
210
+ if (employeeScope)
211
+ throw need('EMPLOYEE_EXEC_UNKNOWN');
192
212
  const api = get();
193
213
  if (!provided?.io) {
194
214
  if (!api)
@@ -205,7 +225,9 @@ export function brokerFactory(get, _ask, configuration, provided) {
205
225
  if (entry) {
206
226
  if (!modern(entry))
207
227
  throw need('BROKER_CAPABILITY_REQUIRED');
208
- const clean = state?.phase === 'clean' && state.instanceId === entry.instanceId && state.acp?.sessionId === request.resumeSessionId && state.checkpointId === request.recoveryCheckpointId && state.cursor >= entry.firstSequence - 1 && state.cursor <= entry.lastSequence && entry.state === 'running' && await request.verifyRecovery?.() !== false;
228
+ const clean = !employeeScope && state?.phase === 'clean' && state.instanceId === entry.instanceId && state.acp?.sessionId === request.resumeSessionId && state.checkpointId === request.recoveryCheckpointId && state.cursor >= entry.firstSequence - 1 && state.cursor <= entry.lastSequence && entry.state === 'running' && (legacyContextScope ? await request.verifyRecovery?.() === true : await request.verifyRecovery?.() !== false);
229
+ if (legacyContextScope && !clean)
230
+ throw need('RECOVERY_CONTEXT_POLICY_UNSUPPORTED');
209
231
  if (clean) {
210
232
  transport = build(entry, state.cursor, state.acp);
211
233
  try {
@@ -213,11 +235,17 @@ export function brokerFactory(get, _ask, configuration, provided) {
213
235
  await transport.replayThrough(entry.lastSequence, request.signal);
214
236
  await request.detailChannel?.flush();
215
237
  await validate();
238
+ if (legacyContextScope) {
239
+ const migrated = { ...state, scope };
240
+ await report('CODEX_RECOVERY_SCOPE_MIGRATED', { oldScope: state.scope, newScope: scope, sessionId: state.acp.sessionId, checkpointId: state.checkpointId, instanceId: entry.instanceId, cursor: state.cursor });
241
+ await journal.write(migrated);
242
+ state = migrated;
243
+ }
216
244
  resume = state.acp;
217
245
  }
218
246
  catch (error) {
219
247
  transport.close();
220
- if (error instanceof NeedError && ['ATTACH_BUSY', 'SESSION_IN_USE', 'SESSION_INSTANCE_MISMATCH', 'ABORTED'].includes(error.code))
248
+ if (legacyContextScope || error instanceof NeedError && ['ATTACH_BUSY', 'SESSION_IN_USE', 'SESSION_INSTANCE_MISMATCH', 'ABORTED'].includes(error.code))
221
249
  throw error;
222
250
  await report('SESSION_REPLAY_UNRECOVERABLE', { brokerId: id, instanceId: entry.instanceId, reason: error instanceof NeedError ? error.code : 'UNKNOWN_FAILURE', recordedCursor: state?.cursor ?? null, firstSequence: entry.firstSequence, lastSequence: entry.lastSequence });
223
251
  }
@@ -289,6 +317,10 @@ export function brokerFactory(get, _ask, configuration, provided) {
289
317
  active.delete(key);
290
318
  retained.delete(key);
291
319
  } })(), };
320
+ if (employeeScope) {
321
+ runtime.saveCheckpoint = undefined;
322
+ runtime.detach = runtime.dispose;
323
+ }
292
324
  if (request.retainProcess)
293
325
  retained.set(key, { runtime, scope, update: next => { request = next; timing = next.timing ?? new StageTiming(); } });
294
326
  return runtime;
package/dist/broker.js CHANGED
@@ -354,7 +354,11 @@ export function controlNeed(stderr) {
354
354
  if (typeof result?.code === 'string' && /^[A-Z][A-Z0-9_]{0,63}$/.test(result.code))
355
355
  code = result.code;
356
356
  }
357
- catch { /* Only the declared JSON error code is public, never raw stderr. */ }
357
+ catch {
358
+ const bridge = /^BRIDGE_ERROR ([A-Z][A-Z0-9_]{0,63})\n?$/.exec(stderr);
359
+ if (bridge)
360
+ code = bridge[1]; /* Exact S0411 CLI reason only, never mixed/raw stderr. */
361
+ }
358
362
  return new NeedError(code, code === 'EACCES' ? '我需要管理员让该员工可读、不可写主机会话配置;当前路径访问被拒绝' : `主机会话控制失败(${code});根因${code === 'BROKER_CONTROL_FAILED' ? '未查明' : '见错误代码'},没有重试或新建替代进程`, code === 'EACCES' ? '员工可读且员工不可写的 session.settingsPath' : '恢复原员工 broker 连接并确认会话状态');
359
363
  }
360
364
  export function sshIo(target, argv, maxControlBytes) {
@@ -0,0 +1,37 @@
1
+ import { type EmployeeLease, type EmployeeLeaseOptions, type EmployeeNetworkApi, type EmployeeWorkspace } from './employee-endpoint.ts';
2
+ import { type EmployeeMcpOptions } from './employee-mcp.ts';
3
+ import { createEmployeeShell, type EmployeeShell } from './employee-shell.ts';
4
+ import type { EmployeeRouting } from './employee-relay.ts';
5
+ import { type ConnectApi, type ConnectTarget, type Project, type Runtime, type RuntimeRequest, type LocalRuntimeProvider, type ConnectionState } from './provider.ts';
6
+ import type { DetailChannel } from './remote-details.ts';
7
+ import type { LocalTurnProof } from './local-user-turn.ts';
8
+ import type { DetailObservation } from './conversation-details.ts';
9
+ interface Configuration {
10
+ acpMaxMessageBytes?: number;
11
+ brokerControlMaxBytes?: number;
12
+ employeeShellKillGraceMs?: number;
13
+ }
14
+ interface Options {
15
+ getConnect: () => ConnectApi | undefined;
16
+ getNetwork: () => EmployeeNetworkApi | undefined;
17
+ readWorkspace: (sessionId: string) => Promise<EmployeeWorkspace>;
18
+ configuration: () => Configuration;
19
+ readUserTurn: (request: RuntimeRequest) => Promise<LocalTurnProof>;
20
+ reportAdmissionFailure: (request: RuntimeRequest, code: string) => Promise<void>;
21
+ recordUnscoped: (request: RuntimeRequest, event: DetailObservation) => Promise<void>;
22
+ details: (request: RuntimeRequest, api: ConnectApi, connection: ConnectionState, target: ConnectTarget) => Promise<DetailChannel>;
23
+ open: (target: ConnectTarget, project: Project, request: RuntimeRequest) => Promise<Runtime>;
24
+ /** Explicit component-test ports only. Production supplies none. */
25
+ ports?: {
26
+ lease: (options: EmployeeLeaseOptions, signal?: AbortSignal) => Promise<EmployeeLease>;
27
+ mcp: (options: EmployeeMcpOptions) => Promise<{
28
+ routing: EmployeeRouting;
29
+ signal?: AbortSignal;
30
+ close(): Promise<void>;
31
+ }>;
32
+ shell: (options: Parameters<typeof createEmployeeShell>[0]) => EmployeeShell;
33
+ };
34
+ }
35
+ /** Local source only. Host and legacy-host remain with the original provider. */
36
+ export declare function createClaudeLocalProvider(options: Options): LocalRuntimeProvider;
37
+ export {};
@@ -0,0 +1,207 @@
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 { isDeepStrictEqual } from 'node:util';
10
+ import { openEmployeeLease } from "./employee-endpoint.js";
11
+ import { openEmployeeMcp } from "./employee-mcp.js";
12
+ import { createEmployeeShell } from "./employee-shell.js";
13
+ import { sameTarget, unwrap } from "./provider.js";
14
+ import { sshIo } from "./broker.js";
15
+ import { NeedError, shellQuote } from "./remote.js";
16
+ import { awaitL } from "./provider.js";
17
+ const failure = (code) => new NeedError(code, '当前本机执行上下文无法确认,本轮已停止;没有改在主机执行。', 'L当前本地绑定、N员工端点与S同代桥');
18
+ const NOTICE = '本轮六类文件与命令工具在你的电脑执行,使用你选择的文件夹;Git也使用本机仓库。子代理和内部直接调用尚不支持本机,可能仍在主机;已识别的此类审批不会执行。PDF等二进制读取、后台命令暂不支持。';
19
+ /** Local source only. Host and legacy-host remain with the original provider. */
20
+ export function createClaudeLocalProvider(options) {
21
+ const entries = new Map();
22
+ const admitted = new WeakMap();
23
+ const admit = async (request, api, connection, target) => {
24
+ if (admitted.has(request))
25
+ return;
26
+ try {
27
+ if (api.getContractInfo?.().workspaceLocations !== 1)
28
+ return;
29
+ const local = api;
30
+ if (typeof local.getWorkspaceLocationState !== 'function')
31
+ throw failure('EMPLOYEE_CONTEXT_UNAVAILABLE');
32
+ const workspace = await options.readWorkspace(request.sessionId), state = local.getWorkspaceLocationState(), matches = state.items.filter(row => row.workspaceId === workspace.workspaceId);
33
+ if (matches.length !== 1 || state.phase !== 'ready' || state.instanceId !== connection.instanceId || state.epoch !== connection.epoch)
34
+ throw failure('EMPLOYEE_CONTEXT_CHANGED');
35
+ const item = matches[0];
36
+ if (item.source !== 'local')
37
+ return;
38
+ if (item.status !== 'selected' || !item.bindingId || api.getContractInfo?.().localUserTurnAcceptance !== 1 || typeof api.acceptLocalWorkspaceUserTurn !== 'function' || typeof local.getLocalWorkspaceContext !== 'function')
39
+ throw failure('EMPLOYEE_CONTEXT_UNAVAILABLE');
40
+ const context = unwrap(await awaitL(() => local.getLocalWorkspaceContext({ instanceId: connection.instanceId, epoch: connection.epoch, workspaceId: workspace.workspaceId, expectedBindingId: item.bindingId }), request.signal));
41
+ if (context.instanceId !== connection.instanceId || context.epoch !== connection.epoch || context.workspaceId !== workspace.workspaceId || context.bindingId !== item.bindingId || context.source !== 'local' || !isDeepStrictEqual(context.directory, workspace.directory))
42
+ throw failure('EMPLOYEE_CONTEXT_CHANGED');
43
+ const proof = await options.readUserTurn(request);
44
+ proof.assertCurrent();
45
+ const accepted = unwrap(await awaitL(() => api.acceptLocalWorkspaceUserTurn({ instanceId: connection.instanceId, epoch: connection.epoch, sessionId: request.sessionId, provider: 'claude', purpose: 'user-turn', workspaceId: workspace.workspaceId, expectedBindingId: item.bindingId, expectedDirectory: { device: workspace.directory.device, inode: workspace.directory.inode }, expectedUserTurn: { ...proof.expectedUserTurn } }), request.signal));
46
+ proof.assertCurrent();
47
+ request.signal?.throwIfAborted();
48
+ const now = api.getConnectionState();
49
+ if (options.getConnect() !== api || now.phase !== 'connected' || now.instanceId !== connection.instanceId || now.epoch !== connection.epoch)
50
+ throw failure('CONNECTION_LOST');
51
+ if (!accepted || accepted.version !== 1 || typeof accepted.acceptanceId !== 'string' || !accepted.acceptanceId.trim() || accepted.instanceId !== connection.instanceId || accepted.epoch !== connection.epoch || accepted.sessionId !== request.sessionId || accepted.provider !== 'claude' || accepted.workspaceId !== workspace.workspaceId || accepted.bindingId !== item.bindingId || accepted.source !== 'local' || !isDeepStrictEqual(accepted.directory, workspace.directory) || accepted.userTurn?.turn !== proof.expectedUserTurn.turn || accepted.userTurn.userMessageId !== proof.expectedUserTurn.userMessageId || accepted.userTurn.startSequence !== proof.startSequence || accepted.userTurn.userMessageSequence !== proof.userMessageSequence)
52
+ throw failure('REMOTE_DETAIL_RESPONSE_INVALID');
53
+ Object.defineProperty(request, 'localAcceptanceId', { value: accepted.acceptanceId, enumerable: true });
54
+ request.detailChannel = await options.details(request, api, connection, target);
55
+ admitted.set(request, { api, workspace: structuredClone(workspace), bindingId: item.bindingId, connection: structuredClone(connection) });
56
+ }
57
+ catch (error) {
58
+ const code = error instanceof NeedError ? error.code : 'LOCAL_ADMISSION_FAILED';
59
+ try {
60
+ await options.reportAdmissionFailure(request, code);
61
+ }
62
+ catch {
63
+ throw new NeedError(code, '本轮输入无法核对,已停止本次处理;诊断保存也未完成。', '原原因 ' + code + ';私有诊断 REMOTE_DETAIL_STORAGE_FAILED');
64
+ }
65
+ throw error;
66
+ }
67
+ };
68
+ const resolve = async (request, api, connection, target) => {
69
+ if (api.getContractInfo?.().workspaceLocations !== 1)
70
+ return;
71
+ const local = api;
72
+ if (typeof local.getWorkspaceLocationState !== 'function')
73
+ throw failure('EMPLOYEE_CONTEXT_UNAVAILABLE');
74
+ const workspace = await options.readWorkspace(request.sessionId), state = local.getWorkspaceLocationState(), matches = state.items.filter(row => row.workspaceId === workspace.workspaceId);
75
+ if (state.phase !== 'ready' || state.instanceId !== connection.instanceId || state.epoch !== connection.epoch || matches.length !== 1)
76
+ throw failure('EMPLOYEE_CONTEXT_CHANGED');
77
+ const item = matches[0];
78
+ if (item.source !== 'local')
79
+ return;
80
+ if (!admitted.has(request))
81
+ await admit(request, api, connection, target);
82
+ const acceptance = admitted.get(request);
83
+ if (!acceptance || acceptance.api !== api || acceptance.bindingId !== item.bindingId || acceptance.connection.instanceId !== connection.instanceId || acceptance.connection.epoch !== connection.epoch || !isDeepStrictEqual(acceptance.workspace, workspace))
84
+ throw failure('EMPLOYEE_CONTEXT_CHANGED');
85
+ if (item.status !== 'selected' || !item.bindingId || local.getContractInfo().localWorkspaceContext !== 1)
86
+ throw failure('EMPLOYEE_CONTEXT_UNAVAILABLE');
87
+ if (request.cwd && request.cwd !== workspace.directory.path || request.bindingId && request.bindingId !== item.bindingId || request.workspaceId && request.workspaceId !== workspace.workspaceId)
88
+ throw failure('SESSION_CONTEXT_MISMATCH');
89
+ const configuration = options.configuration(), maximum = request.acpMaxMessageBytes ?? configuration.acpMaxMessageBytes, control = request.brokerSettings?.brokerControlMaxBytes ?? configuration.brokerControlMaxBytes, grace = configuration.employeeShellKillGraceMs;
90
+ if (![maximum, control, grace].every(n => Number.isSafeInteger(n) && n >= 0) || !maximum || !control)
91
+ throw failure('SETTINGS_REQUIRED');
92
+ const scope = { target, workspace, bindingId: item.bindingId, instanceId: connection.instanceId, epoch: connection.epoch, maximum, control, grace };
93
+ let entry = entries.get(request.sessionId);
94
+ if (entry && (entry.closing || !isDeepStrictEqual(entry.scope, scope) || entry.lease.signal.aborted))
95
+ throw failure('EMPLOYEE_CONTEXT_CHANGED');
96
+ const details = request.detailChannel;
97
+ request.timing?.bind(observation => details.record(observation));
98
+ const trusted = async (ignoreCancellation = false) => {
99
+ if (!ignoreCancellation)
100
+ request.signal?.throwIfAborted();
101
+ if (options.getConnect() !== api)
102
+ throw failure('CONNECTION_LOST');
103
+ const now = api.getConnectionState();
104
+ if (now.phase !== 'connected' || now.instanceId !== connection.instanceId || now.epoch !== connection.epoch || !sameTarget(unwrap(await api.getSshTarget()), target))
105
+ throw failure('CONNECTION_LOST');
106
+ };
107
+ const openRequest = (row, cold = false) => ({ ...request, detailChannel: details, bindingId: item.bindingId, workspaceId: workspace.workspaceId, bindingRevision: state.revision, ...cold ? { recoveryCheckpointId: undefined } : {}, validateContext: async (cleanup = false) => { await trusted(cleanup); if (!cleanup)
108
+ await row.lease.validate(request.signal); } });
109
+ if (entry) {
110
+ entry.details = details;
111
+ entry.request = request;
112
+ await entry.lease.validate(request.signal);
113
+ const base = await options.open(target, { name: workspace.workspaceId, path: target.session.root }, openRequest(entry));
114
+ if (base.acpTransport !== entry.runtime.acpTransport)
115
+ throw failure('SESSION_TRANSPORT_MISMATCH');
116
+ entry.runtime.details = details;
117
+ entry.runtime.restartNotice = base.restartNotice;
118
+ entry.runtime.restartInputIds = base.restartInputIds;
119
+ return entry.runtime;
120
+ }
121
+ let io;
122
+ const transport = async () => { if (!io) {
123
+ const name = '@ai-workbench/connect';
124
+ const l = await import(__rewriteRelativeImportExtension(name));
125
+ if (typeof l.sshArgs !== 'function')
126
+ throw failure('DEPENDENCY_REQUIRED');
127
+ io = sshIo(target, l.sshArgs, control);
128
+ } return io; };
129
+ let lease, mcp, base;
130
+ let privateFailure, privateTail = Promise.resolve();
131
+ try {
132
+ const unscoped = (observation) => { if (privateFailure)
133
+ throw privateFailure; privateTail = privateTail.then(() => options.recordUnscoped(request, observation)).catch(error => { privateFailure ??= error; void lease?.close().catch(failure => { privateFailure ??= failure; }); }); };
134
+ lease = await (options.ports?.lease ?? openEmployeeLease)({ getConnect: () => options.getConnect(), getNetwork: options.getNetwork, readWorkspace: () => options.readWorkspace(request.sessionId), host: target, sessionId: request.sessionId, control: async (_target, command, signal) => (await transport()).run(command, signal), detail: unscoped }, request.signal);
135
+ const shell = (options.ports?.shell ?? createEmployeeShell)({ target: lease.target, maxBytes: maximum, killGraceMs: grace, attach: async (spec) => (await transport()).attach([spec.command, ...spec.args].map(shellQuote).join(' ')) });
136
+ mcp = await (options.ports?.mcp ?? openEmployeeMcp)({ lease, maxBytes: maximum, killGraceMs: grace, runShell: shell, activeSignal: () => entry?.active?.signal, captureDetail: () => { const sink = entry?.active?.details; if (!sink)
137
+ throw failure('EMPLOYEE_NO_ACTIVE_TURN'); return observation => sink.record(observation); }, detail: unscoped });
138
+ entry = { runtime: undefined, scope, lease, details, request };
139
+ const row = entry;
140
+ base = await options.open(target, { name: workspace.workspaceId, path: target.session.root }, openRequest(row, true));
141
+ const underlying = base, endpoint = mcp;
142
+ const close = (detach) => row.closing ??= Promise.resolve().then(async () => {
143
+ row.active = undefined;
144
+ const errors = [];
145
+ try {
146
+ await endpoint.close();
147
+ }
148
+ catch (error) {
149
+ errors.push(error);
150
+ }
151
+ await privateTail;
152
+ if (privateFailure)
153
+ errors.push(privateFailure);
154
+ try {
155
+ await (detach ? underlying.detach?.() : underlying.dispose?.());
156
+ }
157
+ catch (error) {
158
+ errors.push(error);
159
+ }
160
+ if (errors.length)
161
+ throw new AggregateError(errors, 'EMPLOYEE_CLEANUP_UNCONFIRMED');
162
+ if (entries.get(request.sessionId) === row)
163
+ entries.delete(request.sessionId);
164
+ });
165
+ const executionSignal = AbortSignal.any([lease.signal, ...mcp.signal ? [mcp.signal] : []]);
166
+ row.runtime = { ...underlying, cwd: workspace.directory.path, details, connection: { ...connection }, binding: { bindingId: item.bindingId, workspaceId: workspace.workspaceId, revision: state.revision }, execution: { hostCwd: target.session.root, routing: mcp.routing, notice: NOTICE, signal: executionSignal, scope: { source: 'local', cwd: workspace.directory.path, hostCwd: target.session.root, uid: lease.target.endpoint.remoteUid, claudeAcpVersion: '0.94.0', sdkVersion: '0.3.303', unsupported: ['subagents', 'internal-direct-calls', 'binary-read', 'background-shell'] }, validate: async (signal) => { executionSignal.throwIfAborted(); await row.lease.validate(signal); }, beginTurn: async (signal) => { executionSignal.throwIfAborted(); if (row.closing || row.active)
167
+ throw failure('TURN_BUSY'); await row.lease.validate(signal); executionSignal.throwIfAborted(); row.active = { signal, details: row.details }; }, endTurn: () => { row.active = undefined; } }, dispose: () => close(false), detach: () => close(true) };
168
+ entries.set(request.sessionId, row);
169
+ lease.signal.addEventListener('abort', () => { void close(false).catch(() => { }); }, { once: true });
170
+ if (lease.signal.aborted) {
171
+ await close(false);
172
+ throw failure('EMPLOYEE_CONTEXT_CHANGED');
173
+ }
174
+ return row.runtime;
175
+ }
176
+ catch (error) {
177
+ const cleanup = [];
178
+ try {
179
+ await mcp?.close();
180
+ await lease?.close();
181
+ }
182
+ catch (e) {
183
+ cleanup.push(e);
184
+ }
185
+ try {
186
+ await base?.dispose?.();
187
+ }
188
+ catch (e) {
189
+ cleanup.push(e);
190
+ }
191
+ await privateTail;
192
+ if (privateFailure)
193
+ cleanup.push(privateFailure);
194
+ try {
195
+ await details.flush();
196
+ }
197
+ catch (e) {
198
+ cleanup.push(e);
199
+ }
200
+ if (cleanup.length)
201
+ throw new AggregateError([error, ...cleanup], 'EMPLOYEE_OPEN_CLEANUP_UNCONFIRMED');
202
+ throw error;
203
+ }
204
+ };
205
+ resolve.admit = admit;
206
+ return resolve;
207
+ }
package/dist/client.js CHANGED
@@ -1,7 +1,76 @@
1
1
  /* Official locale and provider-registration updates only; no custom UI or refresh. */
2
- window.__ModuleLoader__.load({id:'@ai-workbench/remote-agent',factory(){
3
- const messages={"en":{"administratorNote":"Administrators: use Settings → Open configuration file to edit the existing plugin entry. See README for the six fields and approved defaults.","providerName":"Codex (Remote Host)"},"zh":{"administratorNote":"管理员:从设置使用“打开配置文件”,修改已有插件条目;六个字段和已批准默认值见 README。","providerName":"Codex(远程主机)"}};
2
+ window.__ModuleLoader__.load({id:'@ai-workbench/remote-agent',factory(require){
3
+ const messages={"en":{"administratorNote":"Settings → Built-in plugins → 远程助手参数. Seven named settings with defaults, scope and effects.","providerName":"Codex (Remote Host)"},"zh":{"administratorNote":"管理员:设置 → 内置插件 → 远程助手参数。七项设置含默认值、范围、生效和后果说明。","providerName":"Codex(远程主机)"}};
4
+ const settingsFields=[{"key":"mode","name":"Codex 运行方式","kind":"select","options":[{"value":"session","name":"会话模式"},{"value":"one-shot","name":"单次模式(当前版本不可运行)"}],"range":"session / one-shot","scope":"仅 Codex;Claude 始终使用会话模式。","when":"当前轮不变;更改后原会话下一轮会拒绝继续,需新建会话。","consequence":"会话模式保留连续对话;当前单次模式会明确拒绝运行,不会发给模型。不提供关闭会话身份核对的选项。","defaultValue":"session"},{"key":"acpMaxMessageBytes","name":"完整消息与工具输出容量(字节)","kind":"number","min":1,"max":9007199254740991,"range":"1~9007199254740991 的整数;0不能关闭上限","scope":"两位远程助手的完整会话消息;也用于模型目录、本机 MCP、员工执行 RPC 和工具输出容量。","when":"当轮快照不变;下一次独立目录查询读取新值;已有会话需新建后使用。","consequence":"调大可接收更大消息、占用更多内存;调小更早拒绝超限。超限明确停止,不截断、不自动重试;不能用0取消完整消息保护。","defaultValue":4194304},{"key":"brokerMaxFrameBytes","name":"主机会话回传单段容量(字节)","kind":"number","min":1,"max":9007199254740991,"range":"1~9007199254740991 的整数;0不能关闭上限","scope":"本插件接收主机会话单段输出的容量;不会替主机修改它自己的输出策略。","when":"当轮快照不变;更改后已有会话需新建。","consequence":"调大允许更大回传段、占用更多内存;调小可能拒绝主机合法输出。超限停止,不截断;不能关闭协议帧核验。","defaultValue":2097152},{"key":"brokerControlMaxBytes","name":"主机会话管理回复容量(字节)","kind":"number","min":1,"max":9007199254740991,"range":"1~9007199254740991 的整数;0不能关闭上限","scope":"主机会话管理命令的标准输出、标准错误各自容量;也用于本机执行桥的管理回复。","when":"当轮快照不变;更改后已有会话需新建。","consequence":"调大允许更长管理回复、占用更多内存;调小可能无法核实状态。超限明确停止,不能把不完整回复当成功;0不表示无限。","defaultValue":1048576},{"key":"oneShotSandbox","name":"Codex 单次任务权限模式","kind":"select","options":[{"value":"read-only","name":"只读"},{"value":"workspace-write","name":"工作区内可写"},{"value":"danger-full-access","name":"完全访问(高权限)"}],"range":"read-only / workspace-write / danger-full-access","scope":"仅 Codex 单次通道;不改变会话模式、本机工具或员工账号的系统权限。","when":"仅单次任务启动时适用;当前版本单次任务被拒绝,因此目前不生效。","consequence":"只读不允许单次任务写入;工作区内可写允许该目录写入;完全访问放宽单次沙箱。此项不是员工目录的权限开关,也不会解锁当前不可用的单次模式。","defaultValue":"workspace-write"},{"key":"oneShotApproval","name":"Codex 单次任务审批方式","kind":"fixed","readOnly":true,"range":"固定 never;不能切换","scope":"仅 Codex 单次通道;不改变会话模式的员工审批。","when":"仅单次启动参数;当前单次模式不可运行。","consequence":"单次通道没有中途交互审批能力,所以必须固定为无中途交互;不表示自动批准或允许所有操作。不能切换为自动回答旧审批。","defaultValue":"never"},{"key":"employeeShellKillGraceMs","name":"本机命令外层连接清理宽限(毫秒)","kind":"number","min":0,"max":2147483647,"range":"0~2147483647 的整数","scope":"仅 Claude 本机 Bash 取消后,本插件自己的外层 SSH 进程清理;不控制员工命令或 Codex 执行器。","when":"当轮快照不变;更改后已有会话需新建。","consequence":"大于0时先请求外层连接退出,再等待所设毫秒后强制清理;0表示不留宽限、立即强制清理外层连接,不是关闭取消。没有员工端退出确认时结果仍未知,不能据此自动重放或声称命令已停止。","defaultValue":1000}];
5
+ /* Public settings.plugins.tab + shared ConfigForms. No private client services. */
6
+ function mountRuntimeSettings(ctx,React,fields){
7
+ const h=React.createElement;
8
+ const invariants=[
9
+ ['员工、主机和文件夹核对','始终开启,不可关闭','每次派发前核对当前连接、员工与工作区,避免操作到别人的文件或旧位置;核对失败即停止。'],
10
+ ['本轮输入与技术详情归属','始终开启,不可关闭','真实输入须先接纳,技术详情须确认保存;迟到结果仍属于原轮次,不能借下一轮的记录继续执行。'],
11
+ ['模型选择确认','始终开启,不可关闭','主机须完整确认所选模型和推理等级,不能静默换模型继续。'],
12
+ ['完整消息与精确整数','始终开启,不可关闭','容量不能用0关闭,超限明确停止、不截断;整数必须能精确表示,防止消息缺失或容量计算失真。'],
13
+ ['未知结果与旧审批保护','始终开启,不可关闭','断线或取消不等于员工命令已结束;不自动重放未知操作,不代答旧审批,清理未确认时不擅自重开。'],
14
+ ['本机执行位置','始终开启,不可关闭','已提供的本机工具通道不悄悄回退主机;文件夹不是权限隔离根,也不新增目录白名单。Claude子代理及内部直接调用尚未完整覆盖,可能仍在主机,每轮另有范围提示。']
15
+ ];
16
+ function Panel({form}){
17
+ const snapshot=React.useSyncExternalStore(fn=>form.subscribe(fn),()=>form.getSnapshot(),()=>form.getSnapshot());
18
+ const [draft,setDraft]=React.useState({}),[busy,setBusy]=React.useState(false),[notice,setNotice]=React.useState('');
19
+ const revision=React.useRef(undefined),saving=React.useRef(false),alive=React.useRef(true),prefix=React.useId();
20
+ React.useEffect(()=>{alive.current=true;return()=>{alive.current=false;};},[]);
21
+ const ready=snapshot.status==='ready',writable=ready&&snapshot.writable===true&&snapshot.mode==='host'&&snapshot.revision!==undefined;
22
+ const changed=Object.keys(draft).length>0,stale=changed&&revision.current!==snapshot.revision;
23
+ const edit=(key,value)=>{if(saving.current||!writable)return;if(!changed)revision.current=snapshot.revision;setDraft(previous=>({...previous,[key]:value}));setNotice('修改尚未保存。');};
24
+ const discard=()=>{if(saving.current)return;setDraft({});revision.current=undefined;setNotice('已放弃未保存修改,显示当前配置。');};
25
+ const save=async()=>{
26
+ if(saving.current||!writable||!changed||stale)return;
27
+ const ops=[];
28
+ for(const field of fields){const entry=draft[field.key];if(!entry)continue;
29
+ if(field.readOnly){setNotice('固定约束不能修改。');return;}
30
+ if(entry.op==='unset'){ops.push({op:'unset',path:[field.key]});continue;}
31
+ const raw=entry.value,value=field.kind==='number'?Number(raw):raw;
32
+ if(field.kind==='number'&&(String(raw).trim()===''||!Number.isSafeInteger(value)||value<field.min||value>field.max)||field.kind==='select'&&!field.options.some(option=>option.value===value)){setNotice(field.name+'的值不在所列范围内,未保存。');return;}
33
+ ops.push({op:'set',path:[field.key],value});
34
+ }
35
+ saving.current=true;setBusy(true);setNotice('正在保存,请稍候。');
36
+ try{
37
+ const accepted=await form.mutate(ops,revision.current);
38
+ if(!alive.current)return;
39
+ if(accepted===true){setDraft({});revision.current=undefined;setNotice('已保存。运行中的这一轮不变;请按各项生效说明使用新会话。');}
40
+ else setNotice('未保存:配置已变化或官方设置服务拒绝了修改。草稿已保留;核对当前值后可放弃草稿重新修改,不会自动重试。');
41
+ }catch{if(alive.current)setNotice('保存失败,草稿已保留。请检查连接及官方配置状态;不会自动重试。');}
42
+ finally{saving.current=false;if(alive.current)setBusy(false);}
43
+ };
44
+ const stateMessage=snapshot.mode!=='host'?'当前连接不提供持久配置写入,本页不能保存。':!ready?'尚未取得官方配置,当前不能修改。':!snapshot.writable?'当前配置由更高层覆盖或不可写,本页不能修改。':stale?'其他地方已更新配置;本页草稿保留。请放弃草稿后按最新值重新修改。':'';
45
+ return h('section',{'aria-label':'远程助手参数',style:{maxWidth:850}},
46
+ h('h2',null,'远程助手参数'),
47
+ h('p',null,'这些参数用于本客户端当前配置下的 Claude(远程主机)和 Codex(远程主机),不改变其他模型或主机账号权限。没有总开关;关闭整个插件请使用官方插件管理。'),
48
+ stateMessage&&h('p',{role:'status'},stateMessage),
49
+ h('form',{onSubmit:event=>{event.preventDefault();void save();}},
50
+ ...fields.map(field=>{
51
+ const entry=draft[field.key],accepted=ready?snapshot.value?.[field.key]:undefined;
52
+ const base=snapshot.base,hasBase=base!==null&&typeof base==='object'&&Object.hasOwn(base,field.key)&&base[field.key]!==undefined;
53
+ const value=entry?.op==='set'?entry.value:entry?.op==='unset'&&hasBase?base[field.key]:accepted;
54
+ const id=prefix+'-'+field.key,description=id+'-description';
55
+ const control=field.readOnly?h('p',{'aria-label':field.name+'当前值'},String(value??'尚未读取')+'(固定,不可切换)'):field.kind==='select'?h('select',{id,value:value??'',disabled:!writable||busy,'aria-describedby':description,onChange:event=>edit(field.key,{op:'set',value:event.target.value})},value===undefined&&h('option',{value:'',disabled:true},'尚未读取'),...field.options.map(option=>h('option',{key:option.value,value:option.value},option.name))):h('input',{id,type:'number',step:1,min:field.min,max:field.max,value:value??'',disabled:!writable||busy,'aria-describedby':description,onChange:event=>edit(field.key,{op:'set',value:event.target.value})});
56
+ return h('fieldset',{key:field.key,style:{marginBottom:18,padding:14,border:'1px solid currentColor',borderRadius:6}},
57
+ h('legend',null,field.name),h('label',{htmlFor:field.readOnly?undefined:id},field.name+'('+field.key+')'),
58
+ h('div',null,control),h('div',{id:description},
59
+ h('p',null,'默认值:'+field.defaultValue+';范围:'+field.range),
60
+ h('p',null,'生效范围:'+field.scope),h('p',null,'生效时间:'+field.when),h('p',null,'调整或关闭的后果:'+field.consequence)),
61
+ entry?.op==='unset'&&h('p',null,hasBase?'将移除本项覆盖,恢复所示继承值;尚未保存。':'将移除本项覆盖;继承层未设此项,保存后由官方配置确定(默认值:'+field.defaultValue+')。输入框仍显示保存前值,尚未保存。'),
62
+ !field.readOnly&&h('button',{type:'button',disabled:!writable||busy,onClick:()=>edit(field.key,{op:'unset'})},'恢复继承值:'+field.name));
63
+ }),
64
+ h('div',null,h('button',{type:'submit',disabled:!writable||!changed||busy||stale},busy?'正在保存…':'保存修改'),h('button',{type:'button',disabled:!changed||busy,onClick:discard},'放弃未保存修改'))),
65
+ h('p',{role:'status','aria-live':'polite'},notice),h('h3',null,'必须保持的正确性约束'),
66
+ h('p',null,'以下保护默认开启且不能关闭。关闭会使输入、审批、执行位置或记录失去可信归属,并非可以放宽的偏好。'),
67
+ h('dl',null,...invariants.flatMap(([name,state,why])=>[h('dt',{key:name},name+':'+state),h('dd',{key:name+'-why'},why)])));
68
+ }
69
+ ctx.effect(()=>ctx.configForms.whileServed(['ai-workbench-codex'],()=>ctx.slots.inject('settings.plugins.tab',()=>ctx.slots.register({name:'settings.plugins.tab',id:'ai-workbench-remote-parameters',order:90,label:()=> '远程助手参数',inject:()=>({form:ctx.configForms.get('ai-workbench-codex')})},Panel))));
70
+ }
71
+
4
72
  return{inject:['locale','connection'],apply(ctx){
73
+ ctx.inject(['slots','configForms'],settings=>mountRuntimeSettings(settings,require('react'),settingsFields));
5
74
  ctx.effect(()=>ctx.locale.register('ai-workbench-codex',messages),'Codex official translations');
6
75
  ctx.effect(()=>{
7
76
  let disposed=false,connected=false,previous,generation=0,request=new AbortController(),queue=Promise.resolve();
@@ -32,7 +32,6 @@ export type DetailSink = {
32
32
  /** Only objects created by this module are trusted as redaction markers. */
33
33
  export declare function isSanitizedRedaction(value: unknown): boolean;
34
34
  export declare function sanitizeDisplayLabel(value: string): string;
35
- /** Sanitize an internal observation without attaching any L wire semantics. */
36
35
  export declare function sanitizeObservation(observation: DetailObservation, protectModelMaterial?: boolean): Readonly<SanitizedObservation>;
37
36
  /** Internal diagnostic recorder; a sink is not an assertion that an L wire API exists. */
38
37
  export declare class ConversationDetails {
@@ -303,6 +303,34 @@ function modelConfigurationStructure(value, level = 'root') {
303
303
  }));
304
304
  }
305
305
  /** Sanitize an internal observation without attaching any L wire semantics. */
306
+ function contextTransitionStructure(value, history = false) {
307
+ if (value === null || typeof value !== 'object' || Array.isArray(value) || isSanitizedRedaction(value))
308
+ return opaqueStructure(value);
309
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => {
310
+ if (!history && key === 'history')
311
+ return [key, contextTransitionStructure(child, true)];
312
+ if (!history && typeof child === 'string' && (['oldSessionId', 'newSessionId'].includes(key) || (['oldContextSha256', 'newContextSha256'].includes(key) && /^[a-fA-F0-9]{64}$/u.test(child)) || (key === 'reason' && child === 'system-context-changed')))
313
+ return [key, child];
314
+ if (history && ((key === 'scope' && child === 'visible-user-assistant-text-and-images') || (key === 'reasoningOmissionReason' && child === 'dsh-internal-reasoning') || (['textBlocks', 'imageBlocks', 'offloadedImagePlaceholders', 'omittedReasoningBlocks'].includes(key) && typeof child === 'number' && Number.isSafeInteger(child) && child >= 0)))
315
+ return [key, child];
316
+ return [key, opaqueStructure(child)];
317
+ }));
318
+ }
319
+ function employeeStructure(value) {
320
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
321
+ return opaqueStructure(value);
322
+ const strings = new Set(['executionId', 'workspaceId', 'bindingId', 'instanceId', 'deviceId', 'endpointInstanceId', 'executionContextId', 'bridgeInstanceId', 'cwd', 'hostCwd', 'remoteUser', 'operation', 'status', 'reason', 'source', 'claudeAcpVersion', 'sdkVersion']);
323
+ const numbers = new Set(['uid', 'pid', 'childPid', 'generation', 'epoch', 'device', 'inode', 'killGraceMs', 'maxBytes']);
324
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => {
325
+ if (strings.has(key) && typeof child === 'string')
326
+ return [key, child];
327
+ if (numbers.has(key) && typeof child === 'number' && Number.isSafeInteger(child) && child >= 0)
328
+ return [key, child];
329
+ if (['supported', 'unsupported'].includes(key) && Array.isArray(child))
330
+ return [key, child.map(x => typeof x === 'string' ? x : opaqueStructure(x))];
331
+ return [key, opaqueStructure(child)];
332
+ }));
333
+ }
306
334
  export function sanitizeObservation(observation, protectModelMaterial = false) {
307
335
  const payload = snapshotPayload(observation.payload, new Set(), protectModelMaterial && observation.kind !== 'commands');
308
336
  return freezeDeep({
@@ -310,13 +338,15 @@ export function sanitizeObservation(observation, protectModelMaterial = false) {
310
338
  code: sanitizeDisplayLabel(observation.code), summary: sanitizeDisplayLabel(observation.summary),
311
339
  ...(observation.method === undefined ? {} : { method: sanitizeDisplayLabel(observation.method) }),
312
340
  payload: !protectModelMaterial ? payload
313
- : observation.kind === 'session' && ['MODEL_CONFIGURATION_CONFIRMED', 'MODEL_CONFIGURATION_FAILED'].includes(observation.code) ? modelConfigurationStructure(payload)
314
- : observation.kind === 'commands' ? catalogStructure(payload)
315
- : observation.kind === 'usage' ? usageStructure(payload)
316
- : observation.kind === 'session' && observation.code === 'REMOTE_STAGE_TIMING' ? timingStructure(payload)
317
- : observation.kind === 'need' ? payload
318
- : observation.kind === 'tool' || ['SDK_ITEM', 'SDK_TOOL', 'ACP_SESSION_UPDATE', 'ACP_IDLE_UPDATE'].includes(observation.code) ? toolStructure(payload)
319
- : opaqueStructure(payload),
341
+ : observation.kind === 'session' && observation.code === 'CODEX_CONTEXT_SESSION_REPLACED' ? contextTransitionStructure(payload)
342
+ : observation.kind === 'session' && ['MODEL_CONFIGURATION_CONFIRMED', 'MODEL_CONFIGURATION_FAILED'].includes(observation.code) ? modelConfigurationStructure(payload)
343
+ : observation.kind === 'commands' ? catalogStructure(payload)
344
+ : observation.kind === 'usage' ? usageStructure(payload)
345
+ : observation.kind === 'session' && observation.code === 'REMOTE_STAGE_TIMING' ? timingStructure(payload)
346
+ : observation.kind === 'session' && ['CLAUDE_EMPLOYEE_OPERATION', 'CLAUDE_EMPLOYEE_PROCESS', 'CLAUDE_EMPLOYEE_REVOKED', 'CLAUDE_EMPLOYEE_SCOPE'].includes(observation.code) ? employeeStructure(payload)
347
+ : observation.kind === 'need' ? payload
348
+ : observation.kind === 'tool' || ['SDK_ITEM', 'SDK_TOOL', 'ACP_SESSION_UPDATE', 'ACP_IDLE_UPDATE'].includes(observation.code) ? toolStructure(payload)
349
+ : opaqueStructure(payload),
320
350
  });
321
351
  }
322
352
  function validatedScope(scope) {