@ai-workbench/remote-agent 0.8.21 → 0.8.23

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 (95) hide show
  1. package/README.md +27 -156
  2. package/dist/acp.d.ts +20 -15
  3. package/dist/acp.js +197 -224
  4. package/dist/agents.d.ts +0 -1
  5. package/dist/agents.js +2 -2
  6. package/dist/broker-runtime.d.ts +3 -5
  7. package/dist/broker-runtime.js +45 -342
  8. package/dist/broker.d.ts +6 -31
  9. package/dist/broker.js +42 -113
  10. package/dist/catalog-ssh.d.ts +2 -31
  11. package/dist/catalog-ssh.js +9 -39
  12. package/dist/claude-local.d.ts +1 -1
  13. package/dist/claude-local.js +40 -105
  14. package/dist/client.js +34 -127
  15. package/dist/close-diagnostics.d.ts +6 -20
  16. package/dist/close-diagnostics.js +13 -57
  17. package/dist/connect-contract.d.ts +4 -8
  18. package/dist/conversation-details.d.ts +0 -29
  19. package/dist/conversation-details.js +8 -99
  20. package/dist/employee-endpoint.d.ts +1 -3
  21. package/dist/employee-endpoint.js +8 -39
  22. package/dist/employee-exec.d.ts +4 -3
  23. package/dist/employee-exec.js +25 -13
  24. package/dist/employee-info.d.ts +5 -1
  25. package/dist/employee-info.js +16 -5
  26. package/dist/employee-location.js +3 -4
  27. package/dist/employee-mcp.js +44 -69
  28. package/dist/employee-native.d.ts +1 -3
  29. package/dist/employee-native.js +6 -27
  30. package/dist/employee-presentation.d.ts +0 -5
  31. package/dist/employee-presentation.js +0 -15
  32. package/dist/employee-provider.d.ts +3 -11
  33. package/dist/employee-provider.js +69 -205
  34. package/dist/employee-reasons.js +1 -5
  35. package/dist/employee-shell.d.ts +2 -1
  36. package/dist/employee-shell.js +10 -13
  37. package/dist/employee-ssh.d.ts +1 -3
  38. package/dist/employee-ssh.js +13 -16
  39. package/dist/employee-tools.d.ts +5 -5
  40. package/dist/failure-evidence.d.ts +2 -10
  41. package/dist/failure-evidence.js +12 -61
  42. package/dist/host-workspace.d.ts +4 -2
  43. package/dist/host-workspace.js +1 -1
  44. package/dist/index.d.ts +13 -43
  45. package/dist/index.js +190 -489
  46. package/dist/input.js +2 -3
  47. package/dist/local-user-turn.js +5 -6
  48. package/dist/mapping.d.ts +0 -3
  49. package/dist/mapping.js +0 -34
  50. package/dist/model-catalog.d.ts +2 -41
  51. package/dist/model-catalog.js +24 -282
  52. package/dist/model-configuration.d.ts +0 -3
  53. package/dist/model-configuration.js +8 -47
  54. package/dist/model-presentation.d.ts +1 -0
  55. package/dist/model-presentation.js +2 -2
  56. package/dist/permissions.d.ts +15 -0
  57. package/dist/permissions.js +30 -0
  58. package/dist/preflight-diagnostic.js +11 -22
  59. package/dist/preflight-payload.d.ts +1 -1
  60. package/dist/preflight-payload.js +2 -2
  61. package/dist/presentation.js +21 -41
  62. package/dist/provider.d.ts +12 -34
  63. package/dist/provider.js +17 -92
  64. package/dist/readiness.js +1 -7
  65. package/dist/remote-details.d.ts +41 -17
  66. package/dist/remote-details.js +84 -381
  67. package/dist/workspace-source.d.ts +1 -25
  68. package/dist/workspace-source.js +1 -138
  69. package/package.json +11 -12
  70. package/dist/catalog-diagnostics.d.ts +0 -16
  71. package/dist/catalog-diagnostics.js +0 -108
  72. package/dist/host-layout.d.ts +0 -8
  73. package/dist/host-layout.js +0 -9
  74. package/dist/job.d.ts +0 -17
  75. package/dist/job.js +0 -57
  76. package/dist/model-defaults.d.ts +0 -29
  77. package/dist/model-defaults.js +0 -62
  78. package/dist/oneshot.d.ts +0 -13
  79. package/dist/oneshot.js +0 -78
  80. package/dist/protocol-boundary.d.ts +0 -13
  81. package/dist/protocol-boundary.js +0 -49
  82. package/dist/provider-locale.d.ts +0 -19
  83. package/dist/provider-locale.js +0 -73
  84. package/dist/recovery-checkpoint.d.ts +0 -6
  85. package/dist/recovery-checkpoint.js +0 -71
  86. package/dist/recovery.d.ts +0 -51
  87. package/dist/recovery.js +0 -61
  88. package/dist/sdk-ssh-wrapper.d.ts +0 -2
  89. package/dist/sdk-ssh-wrapper.js +0 -34
  90. package/dist/stage-timing.d.ts +0 -31
  91. package/dist/stage-timing.js +0 -61
  92. package/dist/workspace-notice.d.ts +0 -47
  93. package/dist/workspace-notice.js +0 -273
  94. package/dist/workspace-source-confirmation.d.ts +0 -4
  95. package/dist/workspace-source-confirmation.js +0 -12
package/dist/broker.js CHANGED
@@ -1,10 +1,9 @@
1
1
  import { closeCode } from "./close-diagnostics.js";
2
2
  import { spawn } from 'node:child_process';
3
3
  import { createHash } from 'node:crypto';
4
- import { ProtocolBoundary } from "./protocol-boundary.js";
5
4
  import { NeedError, shellQuote } from "./remote.js";
6
5
  import { CODEX_AGENT } from "./agents.js";
7
- /** One byte stream / SDK app, multiple SSH attachments. Input is never replayed. */
6
+ /** One byte stream / SDK app over one SSH attachment of a new agent. Input is never replayed. */
8
7
  export class BrokerTransport {
9
8
  readable;
10
9
  writable;
@@ -19,26 +18,15 @@ export class BrokerTransport {
19
18
  lease = new AbortController();
20
19
  listeners = new Set();
21
20
  permits = new Map();
22
- approvalSent = false;
23
- unconfirmed = new Set();
21
+ promptPermit;
24
22
  failure;
25
23
  heartbeat;
26
- boundary;
27
- recovering = false;
28
- promptSent = false;
29
- preparingInputs = 0;
30
24
  inputTail = Promise.resolve();
31
25
  exclusive(operation) { const task = this.inputTail.then(operation); this.inputTail = task.catch(() => { }); return task; }
32
26
  constructor(options) {
33
27
  if (!Number.isSafeInteger(options.maxFrameBytes) || options.maxFrameBytes < 1)
34
28
  throw new NeedError('SETTINGS_REQUIRED', '我需要 brokerMaxFrameBytes', '管理员确认 brokerMaxFrameBytes');
35
29
  this.options = options;
36
- this.cursor = options.initialSequence ?? 0;
37
- if (!Number.isSafeInteger(this.cursor) || this.cursor < 0)
38
- throw new NeedError('RECOVERY_CURSOR_INVALID', '会话恢复位置无效', '已处理的安全游标');
39
- if (options.maxMessageBytes)
40
- this.boundary = new ProtocolBoundary(options.maxMessageBytes);
41
- this.recovering = !!options.recoverySessionId;
42
30
  this.lease.abort();
43
31
  this.readable = new ReadableStream({ start: c => { this.output = c; }, pull: () => { this.attachment?.stdout.resume(); }, cancel: reason => { this.close({ layer: 'broker', initiator: 'broker.reader-cancel', reason: closeCode(reason, 'BROKER_READER_CANCELLED'), action: 'close' }); } });
44
32
  this.writable = new WritableStream({ write: bytes => this.write(bytes), close: () => this.close({ layer: 'broker', initiator: 'broker.writer-close', reason: 'BROKER_WRITER_CLOSED', action: 'close' }), abort: reason => this.close({ layer: 'broker', initiator: 'broker.writer-abort', reason: closeCode(reason, 'BROKER_WRITER_ABORTED'), action: 'abort' }) });
@@ -46,50 +34,11 @@ export class BrokerTransport {
46
34
  get online() { return this.attached && !this.ended; }
47
35
  get generation() { return this.epoch; }
48
36
  get signal() { return this.lease.signal; }
49
- get lastSequence() { return this.cursor; }
50
37
  get error() { return this.failure; }
51
- get promptInFlight() { return this.promptSent; }
52
- processedMessage(message) { this.boundary?.consumed(message); }
53
- checkpointSequence() { return this.online && this.preparingInputs === 0 && !!this.boundary?.complete && !this.approvalSent && this.unconfirmed.size === 0 ? this.cursor : null; }
54
- async commitCheckpoint(commit) { return this.exclusive(async () => { const cursor = this.checkpointSequence(); if (cursor === null)
55
- return false; await commit(cursor); return true; }); }
56
- finishRecovery() { if (!this.boundary?.wholeLine)
57
- throw new NeedError('RECOVERY_INCOMPLETE_OUTPUT', '原会话输出停在不完整消息中', '安全结束并新建会话'); this.recovering = false; }
58
- async replayThrough(sequence, signal) {
59
- const inspect = () => { signal?.throwIfAborted(); if (this.failure)
60
- throw this.failure; if (this.cursor >= sequence) {
61
- if (!this.boundary?.wholeLine)
62
- throw new NeedError('RECOVERY_INCOMPLETE_OUTPUT', '原会话输出不完整', '安全结束并新建会话');
63
- return true;
64
- } return false; };
65
- if (inspect())
66
- return;
67
- await new Promise((resolve, reject) => { let off = () => { }; const clear = () => { off(); signal?.removeEventListener('abort', changed); }; const changed = () => { try {
68
- if (inspect()) {
69
- clear();
70
- resolve();
71
- }
72
- }
73
- catch (error) {
74
- clear();
75
- reject(error);
76
- } }; off = this.onChange(changed); signal?.addEventListener('abort', changed, { once: true }); changed(); });
77
- }
78
38
  stopHeartbeat() { if (this.heartbeat !== undefined)
79
39
  clearInterval(this.heartbeat); this.heartbeat = undefined; }
80
- recoveryMessage(message) {
81
- const params = message.params;
82
- if (Object.hasOwn(message, 'id') || message.method !== 'session/update' || params?.sessionId !== this.options.recoverySessionId || !['available_commands_update', 'current_mode_update', 'config_option_update', 'session_info_update', 'usage_update'].includes(params?.update?.sessionUpdate ?? ''))
83
- throw new NeedError('RECOVERY_UNSAFE_OUTPUT', '原会话有未确认输出或旧交互,不能代答', '安全结束并新建会话');
84
- this.options.onRecoveryUpdate?.(message);
85
- }
86
- authorizeResponse(id, generation) { this.permits.set(JSON.stringify(id), generation); }
87
- /** Called only by the ACP protocol layer after a matching result or current-turn output. */
88
- acknowledge(method) { this.unconfirmed.delete(method); }
89
- /** A matching terminal prompt response settles all approval replies in that turn. */
90
- completePrompt() { this.promptSent = false; this.unconfirmed.delete('session/prompt'); this.approvalSent = false; for (const key of this.unconfirmed)
91
- if (key.startsWith('response:'))
92
- this.unconfirmed.delete(key); }
40
+ authorizeResponse(id, generation, valid) { this.permits.set(JSON.stringify(id), { generation, valid }); }
41
+ authorizePrompt(valid) { this.promptPermit = valid; }
93
42
  onChange(fn) { this.listeners.add(fn); return () => this.listeners.delete(fn); }
94
43
  changed() { for (const fn of this.listeners)
95
44
  fn(); }
@@ -126,28 +75,6 @@ export class BrokerTransport {
126
75
  }
127
76
  catch { /* Reader already cancelled. */ }
128
77
  }
129
- async waitOnline(signal) {
130
- signal?.throwIfAborted();
131
- if (this.online)
132
- return;
133
- if (this.ended)
134
- throw this.failure ?? new NeedError('SESSION_ENDED', '远端会话已结束', '重新建立会话');
135
- await new Promise((resolve, reject) => {
136
- const done = () => { off(); signal?.removeEventListener('abort', abort); };
137
- const changed = () => { if (this.online) {
138
- done();
139
- resolve();
140
- }
141
- else if (this.ended) {
142
- done();
143
- reject(this.failure ?? new NeedError('SESSION_ENDED', '远端会话已结束', '重新建立会话'));
144
- } };
145
- const abort = () => { done(); reject(new NeedError('ABORTED', '本轮已取消', '确认主机状态')); };
146
- const off = this.onChange(changed);
147
- signal?.addEventListener('abort', abort, { once: true });
148
- changed();
149
- });
150
- }
151
78
  connect() {
152
79
  if (this.online)
153
80
  return Promise.resolve();
@@ -159,7 +86,7 @@ export class BrokerTransport {
159
86
  return this.joining;
160
87
  }
161
88
  async attach() {
162
- const child = await this.options.attach(this.cursor);
89
+ const child = await this.options.attach();
163
90
  if (this.ended) {
164
91
  this.options.observeClose?.({ layer: 'broker', initiator: 'broker.late-attachment', reason: 'SESSION_ENDED', action: 'terminate' });
165
92
  child.kill('SIGTERM');
@@ -233,10 +160,7 @@ export class BrokerTransport {
233
160
  if (seq !== this.cursor + 1)
234
161
  throw new NeedError('REPLAY_GAP', '主机输出存在缺口,不能安全继续审批', '显式结束或恢复原会话');
235
162
  if (f.channel === 'stdout') {
236
- const bytes = Buffer.from(f.data, 'base64');
237
- this.boundary?.receive(bytes, this.recovering ? message => this.recoveryMessage(message) : undefined);
238
- if (!this.recovering && !this.options.discardOutput)
239
- this.output.enqueue(bytes);
163
+ this.output.enqueue(Buffer.from(f.data, 'base64'));
240
164
  }
241
165
  this.cursor = seq;
242
166
  this.changed();
@@ -281,28 +205,14 @@ export class BrokerTransport {
281
205
  fatal(new NeedError('BROKER_ATTACH_FAILED', '主机会话附着失败;根因未查明', '有效的原员工 SSH/broker 连接'));
282
206
  return;
283
207
  }
284
- // A matching remote receipt can precede the local stdin write callback.
285
- // The protocol ledger, rather than that callback's timing, owns delivery.
286
- if (this.approvalSent || this.unconfirmed.size) {
287
- fatal(new NeedError('INPUT_DELIVERY_UNCONFIRMED', '输入或审批应答尚无协议收件证明时连接中断;不会重发或再次批准', '显式结束原进程并检查项目状态'));
288
- return;
289
- }
290
- this.notice('CONNECTION_LOST', '连接中断,主机可能正在等待你的审批;重连前不发送审批');
291
- void this.options.onDisconnect?.().catch(error => this.fail(error instanceof NeedError ? error : new NeedError('RECONNECT_FAILED', '续接失败,根因未查明', '恢复原连接')));
208
+ // A real disconnect ends this turn; the next turn starts a new agent.
209
+ fatal(new NeedError('CONNECTION_LOST', '主机会话连接已中断,恢复后可在原会话继续。', '当前主机连接'));
292
210
  };
293
211
  child.once('error', () => detached({ layer: 'broker', initiator: 'broker.attachment-error', reason: 'BROKER_ATTACHMENT_ERROR', action: 'failure' }));
294
212
  child.once('close', (code, signal) => detached({ layer: 'broker', initiator: 'broker.attachment-close', reason: 'BROKER_ATTACHMENT_CLOSED', action: 'exit', exitCode: code, signal }));
295
213
  return connected.promise.finally(off);
296
214
  }
297
- async write(bytes) {
298
- this.preparingInputs++;
299
- try {
300
- return await this.exclusive(() => this.send(bytes));
301
- }
302
- finally {
303
- this.preparingInputs--;
304
- }
305
- }
215
+ write(bytes) { return this.exclusive(() => this.send(bytes)); }
306
216
  async send(bytes) {
307
217
  if (!this.online || !this.attachment)
308
218
  throw new NeedError('INPUT_DELIVERY_UNCONFIRMED', '连接不在线,输入没有排队或重放', '恢复连接并确认原会话状态');
@@ -314,22 +224,42 @@ export class BrokerTransport {
314
224
  throw new NeedError('ACP_INPUT_INVALID', 'ACP 输出不是一条完整 JSON 消息', '有效的 ACP SDK 字节输出');
315
225
  }
316
226
  const selected = response.result?.outcome?.outcome === 'selected';
227
+ const prompt = response.method === 'session/prompt', promptPermit = prompt ? this.promptPermit : undefined;
317
228
  const generation = this.epoch;
318
229
  const child = this.attachment;
230
+ const permit = this.permits.get(JSON.stringify(response.id));
231
+ const policyCurrent = () => { try {
232
+ return !permit?.valid || permit.valid();
233
+ }
234
+ catch {
235
+ return false;
236
+ } };
237
+ const currentPrompt = () => {
238
+ let valid = true;
239
+ try {
240
+ valid = !promptPermit || promptPermit();
241
+ }
242
+ catch {
243
+ valid = false;
244
+ }
245
+ if (!valid) {
246
+ this.options.observeClose?.({ layer: 'broker', initiator: 'broker.permission-dispatch-gate', reason: 'PERMISSION_POLICY_CHANGED', operation: 'session/prompt', action: 'failure' });
247
+ throw new NeedError('PERMISSION_POLICY_CHANGED', '派发权限已变化或无法核对,原输入没有重放。', '当前权限代次;已写出部分的结果仍待确认');
248
+ }
249
+ };
319
250
  const eligible = () => {
320
251
  if (!this.online || this.attachment !== child || this.epoch !== generation)
321
252
  throw new NeedError('INPUT_DELIVERY_UNCONFIRMED', '连接已改变,未重放输入或审批', '原连接的有效发送许可');
322
- if (selected && this.permits.get(JSON.stringify(response.id)) !== generation)
323
- throw new NeedError('APPROVAL_STALE', '连接已改变,旧审批未发送', '重连后的本次审批');
253
+ if (selected && (permit?.generation !== generation || this.permits.get(JSON.stringify(response.id)) !== permit || !policyCurrent()))
254
+ throw new NeedError('APPROVAL_STALE', '连接或权限已改变,旧审批未发送', '当前连接和当前权限代次的本次审批');
255
+ if (prompt)
256
+ currentPrompt();
324
257
  };
325
258
  eligible();
326
- await this.options.beforeInput?.();
327
- eligible();
328
- if (selected) {
259
+ if (selected)
329
260
  this.permits.delete(JSON.stringify(response.id));
330
- this.approvalSent = true;
331
- }
332
- this.unconfirmed.add(response.method ?? 'response:' + JSON.stringify(response.id));
261
+ if (prompt && this.promptPermit === promptPermit)
262
+ this.promptPermit = undefined;
333
263
  const chunks = [];
334
264
  if (this.options.leased) {
335
265
  const size = Math.floor((this.options.controlMaxBytes - Buffer.byteLength(JSON.stringify({ type: 'input', data: '' }))) / 4) * 3;
@@ -340,20 +270,20 @@ export class BrokerTransport {
340
270
  }
341
271
  else
342
272
  chunks.push(Buffer.from(bytes));
343
- for (const [index, chunk] of chunks.entries()) {
273
+ for (const chunk of chunks) {
344
274
  if (!this.online || this.attachment !== child || this.epoch !== generation)
345
275
  throw new NeedError('INPUT_DELIVERY_UNCONFIRMED', '分块发送期间断线,未重放输入', '原主机连接');
276
+ if (selected && !policyCurrent())
277
+ throw new NeedError('APPROVAL_STALE', '权限已变化,原审批未继续发送', '当前权限代次的本次审批;已写出部分的结果仍待确认');
278
+ if (prompt)
279
+ currentPrompt();
346
280
  await new Promise((resolve, reject) => {
347
281
  const closed = () => { cleanup(); reject(new NeedError('INPUT_DELIVERY_UNCONFIRMED', '输入送达未确认,未重发', '确认原进程状态')); };
348
282
  const cleanup = () => { child.removeListener('close', closed); };
349
283
  child.once('close', closed);
350
- if (response.method === 'session/prompt' && index === chunks.length - 1)
351
- this.promptSent = true;
352
284
  child.stdin.write(chunk, error => { cleanup(); error ? reject(new NeedError('INPUT_DELIVERY_UNCONFIRMED', '输入送达失败,未重发', '确认原进程状态')) : resolve(); });
353
285
  });
354
286
  }
355
- if (!response.method)
356
- this.boundary?.responded(response.id);
357
287
  }
358
288
  }
359
289
  export function controlNeed(stderr) {
@@ -397,7 +327,6 @@ export function sshIo(target, argv, maxControlBytes) {
397
327
  }) };
398
328
  }
399
329
  export function brokerCommand(t, args) { return `AW_SESSION_ROOT=${shellQuote(t.session.root)} ${shellQuote(t.session.command)} ${args.map(shellQuote).join(' ')}`; }
400
- export function legacyBrokerId(sessionId, agentId = CODEX_AGENT.id) { return (agentId === CODEX_AGENT.id ? 'x' : 'a') + createHash('sha256').update(agentId === CODEX_AGENT.id ? sessionId : agentId + '/' + sessionId).digest('hex').slice(0, 23); }
401
330
  export function brokerId(sessionId, agentId = CODEX_AGENT.id) { return 'r' + createHash('sha256').update(agentId + '/' + sessionId).digest('hex').slice(0, 23); }
402
331
  export function controlJson(text) {
403
332
  try {
@@ -1,44 +1,15 @@
1
1
  import { ModelConfigurationError } from './model-configuration.ts';
2
2
  import type { ConnectTarget } from './provider.ts';
3
3
  export interface CatalogSshEvent {
4
- version: 1;
5
- eventId: string;
6
- stage: 'started' | 'closed';
7
4
  provider: 'claude' | 'codex';
8
- startedAt: string;
9
- endedAt: string | null;
10
- pid: number | null;
11
- command: string;
12
- argv: string[];
13
- target: {
14
- host: string;
15
- username: string;
16
- port: number;
17
- knownHostsFile: string;
18
- proxyCommand: string;
19
- };
20
5
  exitCode: number | null;
21
6
  signal: string | null;
22
7
  stdoutBytes: number;
23
8
  stderrBytes: number;
24
- stderr: string;
25
- stderrSha256: string;
26
- stdoutSha256: string;
27
- hostErrorCode: string | null;
28
- hostErrorMessage: string | null;
29
- elapsedMs: number | null;
30
- stderrUtf8Valid: boolean;
9
+ elapsedMs: number;
31
10
  failureCode: string | null;
32
- stderrComplete: boolean;
33
- processErrorCode?: string | null;
34
- diagnosticWriteError?: string;
35
- connection?: {
36
- instanceId: string;
37
- epoch: number;
38
- phase: string;
39
- } | null;
11
+ processErrorCode: string | null;
40
12
  }
41
- export declare const diagnosticLocation = "\u8BBE\u7F6E \u2192 \u6A21\u578B";
42
13
  /** Preserve ordinary line boundaries; never persist raw structured secrets. */
43
14
  export declare function catalogSafeText(value: string): string;
44
15
  export declare class CatalogSshError extends ModelConfigurationError {
@@ -7,10 +7,8 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
7
7
  return path;
8
8
  };
9
9
  import { spawn } from 'node:child_process';
10
- import { createHash, randomUUID } from 'node:crypto';
11
10
  import { ModelConfigurationError } from "./model-configuration.js";
12
11
  import { sanitizeDisplayLabel, sanitizeObservation } from "./conversation-details.js";
13
- export const diagnosticLocation = '设置 → 模型';
14
12
  /** Strings inside serialized JSON use the same rules as ordinary stderr. */
15
13
  function renderDiagnosticJson(wire) {
16
14
  try {
@@ -57,7 +55,6 @@ export class CatalogSshError extends ModelConfigurationError {
57
55
  }
58
56
  get diagnostic() { return '目录连接技术详情:' + JSON.stringify(this.event); }
59
57
  }
60
- const hash = (value) => createHash('sha256').update(value).digest('hex');
61
58
  /** Each query starts a fresh strict L SSH invocation. No connection pool or retry. */
62
59
  export async function runCatalog(target, command, signal, maxBytes, observe) {
63
60
  const provider = command === '/usr/local/aw-host/bin/aw-model-catalog --provider claude' ? 'claude' : command === '/usr/local/aw-host/bin/aw-model-catalog --provider codex' ? 'codex' : undefined;
@@ -75,29 +72,18 @@ export async function runCatalog(target, command, signal, maxBytes, observe) {
75
72
  throw new ModelConfigurationError('远程主机连接组件缺少可信 SSH 能力');
76
73
  }
77
74
  signal.throwIfAborted();
78
- const safeTarget = { host: catalogSafeText(target.host), username: catalogSafeText(target.username), port: target.port, knownHostsFile: catalogSafeText(target.knownHostsFile), proxyCommand: catalogSafeText(target.proxyCommand) };
79
75
  const tick = performance.now();
80
- const base = { version: 1, eventId: randomUUID(), provider, startedAt: new Date().toISOString(), command, argv: argv.map(catalogSafeText), target: safeTarget };
81
76
  return new Promise((resolve, reject) => {
82
77
  const child = spawn('ssh', argv, { stdio: ['ignore', 'pipe', 'pipe'] });
83
78
  const output = [], diagnostics = [];
84
79
  let total = 0, stdoutBytes = 0, stderrBytes = 0, processErrorCode = null, failure;
85
- const stdoutHash = createHash('sha256'), stderrHash = createHash('sha256');
86
- const event = (stage, extra = {}) => ({ ...base, stage, endedAt: null, pid: child.pid ?? null, exitCode: null, signal: null, stdoutBytes, stderrBytes, stderr: '', stderrSha256: hash(Buffer.alloc(0)), stdoutSha256: hash(Buffer.alloc(0)), hostErrorCode: null, hostErrorMessage: null, elapsedMs: null, stderrUtf8Valid: true, failureCode: null, stderrComplete: true, ...extra });
87
- const first = event('started');
88
- const started = Promise.resolve().then(() => observe?.(first));
89
- started.catch(() => { });
90
80
  const abort = () => { failure ??= { code: 'MODEL_CATALOG_CANCELLED', reason: '连接已改变或目录查询已取消' }; child.kill('SIGTERM'); };
91
81
  signal.addEventListener('abort', abort, { once: true });
92
82
  const receive = (buffer, stdout) => {
93
- if (stdout) {
83
+ if (stdout)
94
84
  stdoutBytes += buffer.length;
95
- stdoutHash.update(buffer);
96
- }
97
- else {
85
+ else
98
86
  stderrBytes += buffer.length;
99
- stderrHash.update(buffer);
100
- }
101
87
  total += buffer.length;
102
88
  if (failure)
103
89
  return;
@@ -114,15 +100,7 @@ export async function runCatalog(target, command, signal, maxBytes, observe) {
114
100
  child.once('close', (code, closeSignal) => {
115
101
  void (async () => {
116
102
  signal.removeEventListener('abort', abort);
117
- let value = '', hostErrorCode = null, hostErrorMessage = null, typed = false, stderrUtf8Valid = true;
118
- let stderr;
119
- try {
120
- stderr = new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(diagnostics));
121
- }
122
- catch {
123
- stderrUtf8Valid = false;
124
- stderr = Buffer.concat(diagnostics).toString('utf8');
125
- }
103
+ let value = '', typed = false;
126
104
  try {
127
105
  value = new TextDecoder('utf-8', { fatal: true }).decode(Buffer.concat(output));
128
106
  }
@@ -130,26 +108,18 @@ export async function runCatalog(target, command, signal, maxBytes, observe) {
130
108
  failure ??= { code: 'MODEL_CATALOG_UTF8_INVALID', reason: '主机目录响应不是有效 UTF-8,未替换模型值' };
131
109
  }
132
110
  try {
133
- const wire = JSON.parse(value);
134
- typed = wire?.ok === false;
135
- if (typed && typeof wire.error?.code === 'string' && /^[A-Z][A-Z0-9_]*$/.test(wire.error.code))
136
- hostErrorCode = wire.error.code;
137
- if (typed && typeof wire.error?.message === 'string')
138
- hostErrorMessage = catalogSafeText(wire.error.message);
111
+ typed = JSON.parse(value)?.ok === false;
139
112
  }
140
113
  catch { }
141
114
  if (!failure && code !== 0 && !typed)
142
- failure = { code: 'SSH_EXIT_NONZERO', reason: '与主机的目录查询连接未完成,请到' + diagnosticLocation + '查看诊断。' };
143
- if (!failure && code === 0 && stderrBytes && !typed)
144
- failure = { code: 'MODEL_CATALOG_STDERR', reason: '目录命令返回了非预期诊断输出,请到' + diagnosticLocation + '查看诊断。' };
145
- const final = event('closed', { endedAt: new Date().toISOString(), exitCode: code ?? null, signal: closeSignal ?? null, stderr: catalogSafeText(stderr), stderrUtf8Valid, elapsedMs: performance.now() - tick, stderrSha256: stderrHash.digest('hex'), stdoutSha256: stdoutHash.digest('hex'), hostErrorCode, hostErrorMessage, failureCode: failure?.code ?? null, processErrorCode, stderrComplete: Buffer.concat(diagnostics).length === stderrBytes });
115
+ failure = { code: 'SSH_EXIT_NONZERO', reason: '模型列表暂时无法读取,请检查连接后重新打开模型菜单。' };
116
+ const final = { provider, exitCode: code ?? null, signal: closeSignal ?? null, stdoutBytes, stderrBytes, elapsedMs: performance.now() - tick, failureCode: failure?.code ?? null, processErrorCode };
117
+ console.info('[remote-agent] model-list ' + provider + ' exit=' + final.exitCode + ' bytes=' + stdoutBytes + ' elapsedMs=' + final.elapsedMs.toFixed(1));
146
118
  try {
147
- await started;
148
- await observe?.(final);
119
+ void Promise.resolve(observe?.(final)).catch(() => console.warn('[remote-agent] 模型列表查询日志未保存'));
149
120
  }
150
121
  catch {
151
- reject(new CatalogSshError((failure?.reason ?? '目录查询未能确认') + ';目录诊断也未能保存,请到' + diagnosticLocation + '查看诊断。', { ...final, diagnosticWriteError: 'CATALOG_DIAGNOSTIC_WRITE_FAILED' }));
152
- return;
122
+ console.warn('[remote-agent] 模型列表查询日志未保存');
153
123
  }
154
124
  if (failure) {
155
125
  reject(new CatalogSshError(failure.reason, final));
@@ -33,6 +33,6 @@ interface Options {
33
33
  shell: (options: Parameters<typeof createEmployeeShell>[0]) => EmployeeShell;
34
34
  };
35
35
  }
36
- /** Local source only. Host and legacy-host remain with the original provider. */
36
+ /** Local source only. Host source remains with the host provider. */
37
37
  export declare function createClaudeLocalProvider(options: Options): LocalRuntimeProvider;
38
38
  export {};