@ai-workbench/remote-agent 0.8.19 → 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.
- package/README.md +27 -134
- package/dist/acp.d.ts +23 -16
- package/dist/acp.js +209 -226
- package/dist/agents.d.ts +0 -1
- package/dist/agents.js +2 -2
- package/dist/broker-runtime.d.ts +3 -5
- package/dist/broker-runtime.js +45 -342
- package/dist/broker.d.ts +6 -31
- package/dist/broker.js +42 -113
- package/dist/catalog-ssh.d.ts +2 -31
- package/dist/catalog-ssh.js +9 -39
- package/dist/claude-local.d.ts +1 -1
- package/dist/claude-local.js +41 -105
- package/dist/client.js +34 -126
- package/dist/close-diagnostics.d.ts +6 -20
- package/dist/close-diagnostics.js +13 -57
- package/dist/connect-contract.d.ts +4 -8
- package/dist/conversation-details.d.ts +0 -29
- package/dist/conversation-details.js +8 -99
- package/dist/employee-endpoint.d.ts +1 -1
- package/dist/employee-endpoint.js +21 -19
- package/dist/employee-exec.d.ts +4 -3
- package/dist/employee-exec.js +25 -13
- package/dist/employee-info.d.ts +7 -1
- package/dist/employee-info.js +24 -3
- package/dist/employee-location.js +3 -4
- package/dist/employee-mcp.js +44 -69
- package/dist/employee-native.d.ts +1 -3
- package/dist/employee-native.js +6 -27
- package/dist/employee-presentation.d.ts +0 -5
- package/dist/employee-presentation.js +0 -15
- package/dist/employee-provider.d.ts +3 -11
- package/dist/employee-provider.js +69 -205
- package/dist/employee-reasons.js +1 -5
- package/dist/employee-shell.d.ts +2 -1
- package/dist/employee-shell.js +10 -13
- package/dist/employee-ssh.d.ts +1 -3
- package/dist/employee-ssh.js +13 -16
- package/dist/employee-tools.d.ts +5 -5
- package/dist/failure-evidence.d.ts +2 -10
- package/dist/failure-evidence.js +12 -61
- package/dist/host-workspace.d.ts +4 -2
- package/dist/host-workspace.js +6 -2
- package/dist/index.d.ts +13 -43
- package/dist/index.js +192 -484
- package/dist/input.js +2 -3
- package/dist/local-user-turn.js +5 -6
- package/dist/mapping.d.ts +5 -3
- package/dist/mapping.js +31 -36
- package/dist/model-catalog.d.ts +2 -41
- package/dist/model-catalog.js +24 -282
- package/dist/model-configuration.d.ts +0 -3
- package/dist/model-configuration.js +8 -47
- package/dist/model-presentation.d.ts +1 -0
- package/dist/model-presentation.js +2 -2
- package/dist/permissions.d.ts +15 -0
- package/dist/permissions.js +30 -0
- package/dist/preflight-diagnostic.js +11 -22
- package/dist/preflight-payload.d.ts +1 -1
- package/dist/preflight-payload.js +2 -2
- package/dist/presentation.js +21 -41
- package/dist/provider.d.ts +12 -34
- package/dist/provider.js +17 -92
- package/dist/readiness.js +1 -7
- package/dist/remote-details.d.ts +41 -17
- package/dist/remote-details.js +84 -381
- package/dist/workspace-source.d.ts +1 -25
- package/dist/workspace-source.js +1 -138
- package/package.json +11 -12
- package/dist/catalog-diagnostics.d.ts +0 -16
- package/dist/catalog-diagnostics.js +0 -108
- package/dist/host-layout.d.ts +0 -8
- package/dist/host-layout.js +0 -9
- package/dist/job.d.ts +0 -17
- package/dist/job.js +0 -57
- package/dist/model-defaults.d.ts +0 -29
- package/dist/model-defaults.js +0 -62
- package/dist/oneshot.d.ts +0 -13
- package/dist/oneshot.js +0 -78
- package/dist/protocol-boundary.d.ts +0 -13
- package/dist/protocol-boundary.js +0 -49
- package/dist/provider-locale.d.ts +0 -19
- package/dist/provider-locale.js +0 -73
- package/dist/recovery-checkpoint.d.ts +0 -6
- package/dist/recovery-checkpoint.js +0 -71
- package/dist/recovery.d.ts +0 -51
- package/dist/recovery.js +0 -61
- package/dist/sdk-ssh-wrapper.d.ts +0 -2
- package/dist/sdk-ssh-wrapper.js +0 -34
- package/dist/stage-timing.d.ts +0 -31
- package/dist/stage-timing.js +0 -61
- package/dist/workspace-notice.d.ts +0 -47
- package/dist/workspace-notice.js +0 -273
- package/dist/workspace-source-confirmation.d.ts +0 -4
- 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
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
285
|
-
|
|
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
|
-
|
|
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)) !==
|
|
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
|
-
|
|
327
|
-
eligible();
|
|
328
|
-
if (selected) {
|
|
259
|
+
if (selected)
|
|
329
260
|
this.permits.delete(JSON.stringify(response.id));
|
|
330
|
-
|
|
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
|
|
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 {
|
package/dist/catalog-ssh.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 {
|
package/dist/catalog-ssh.js
CHANGED
|
@@ -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
|
-
|
|
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 = '',
|
|
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
|
-
|
|
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: '
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
148
|
-
await observe?.(final);
|
|
119
|
+
void Promise.resolve(observe?.(final)).catch(() => console.warn('[remote-agent] 模型列表查询日志未保存'));
|
|
149
120
|
}
|
|
150
121
|
catch {
|
|
151
|
-
|
|
152
|
-
return;
|
|
122
|
+
console.warn('[remote-agent] 模型列表查询日志未保存');
|
|
153
123
|
}
|
|
154
124
|
if (failure) {
|
|
155
125
|
reject(new CatalogSshError(failure.reason, final));
|
package/dist/claude-local.d.ts
CHANGED
|
@@ -33,6 +33,6 @@ interface Options {
|
|
|
33
33
|
shell: (options: Parameters<typeof createEmployeeShell>[0]) => EmployeeShell;
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
-
/** Local source only. Host
|
|
36
|
+
/** Local source only. Host source remains with the host provider. */
|
|
37
37
|
export declare function createClaudeLocalProvider(options: Options): LocalRuntimeProvider;
|
|
38
38
|
export {};
|