@ai-workbench/remote-agent 0.8.1 → 0.8.5
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 +19 -4
- package/dist/acp.d.ts +25 -0
- package/dist/acp.js +159 -17
- package/dist/approval-label.d.ts +6 -0
- package/dist/approval-label.js +45 -0
- package/dist/auth-reason.d.ts +1 -0
- package/dist/auth-reason.js +27 -0
- package/dist/broker-runtime.d.ts +10 -0
- package/dist/broker-runtime.js +302 -0
- package/dist/broker.d.ts +29 -6
- package/dist/broker.js +141 -141
- package/dist/connect-contract.d.ts +2 -1
- package/dist/conversation-details.d.ts +1 -0
- package/dist/conversation-details.js +58 -8
- package/dist/index.d.ts +31 -14
- package/dist/index.js +248 -42
- package/dist/input.d.ts +4 -0
- package/dist/input.js +4 -1
- package/dist/login-diagnostic.d.ts +38 -0
- package/dist/login-diagnostic.js +29 -0
- package/dist/model-catalog.d.ts +55 -0
- package/dist/model-catalog.js +254 -0
- package/dist/model-configuration.d.ts +51 -0
- package/dist/model-configuration.js +212 -0
- package/dist/presentation.js +4 -0
- package/dist/protocol-boundary.d.ts +13 -0
- package/dist/protocol-boundary.js +49 -0
- package/dist/provider.d.ts +21 -3
- package/dist/provider.js +135 -41
- package/dist/readiness.js +7 -5
- package/dist/recovery-checkpoint.d.ts +6 -0
- package/dist/recovery-checkpoint.js +71 -0
- package/dist/recovery.d.ts +34 -0
- package/dist/recovery.js +52 -0
- package/dist/remote-details.d.ts +17 -1
- package/dist/remote-details.js +137 -6
- package/dist/remote.d.ts +3 -0
- package/dist/remote.js +2 -0
- package/dist/rpc-namespace.d.ts +3 -0
- package/dist/rpc-namespace.js +50 -0
- package/dist/stage-timing.d.ts +27 -0
- package/dist/stage-timing.js +39 -0
- package/package.json +4 -4
package/dist/broker.js
CHANGED
|
@@ -1,17 +1,8 @@
|
|
|
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
1
|
import { spawn } from 'node:child_process';
|
|
10
2
|
import { createHash } from 'node:crypto';
|
|
11
|
-
import {
|
|
3
|
+
import { ProtocolBoundary } from "./protocol-boundary.js";
|
|
12
4
|
import { NeedError, shellQuote } from "./remote.js";
|
|
13
5
|
import { CODEX_AGENT } from "./agents.js";
|
|
14
|
-
import { identity, unwrap, validateTarget, sameTarget } from "./provider.js";
|
|
15
6
|
/** One byte stream / SDK app, multiple SSH attachments. Input is never replayed. */
|
|
16
7
|
export class BrokerTransport {
|
|
17
8
|
readable;
|
|
@@ -30,10 +21,23 @@ export class BrokerTransport {
|
|
|
30
21
|
approvalSent = false;
|
|
31
22
|
unconfirmed = new Set();
|
|
32
23
|
failure;
|
|
24
|
+
heartbeat;
|
|
25
|
+
boundary;
|
|
26
|
+
recovering = false;
|
|
27
|
+
promptSent = false;
|
|
28
|
+
preparingInputs = 0;
|
|
29
|
+
inputTail = Promise.resolve();
|
|
30
|
+
exclusive(operation) { const task = this.inputTail.then(operation); this.inputTail = task.catch(() => { }); return task; }
|
|
33
31
|
constructor(options) {
|
|
34
32
|
if (!Number.isSafeInteger(options.maxFrameBytes) || options.maxFrameBytes < 1)
|
|
35
33
|
throw new NeedError('SETTINGS_REQUIRED', '我需要 brokerMaxFrameBytes', '管理员确认 brokerMaxFrameBytes');
|
|
36
34
|
this.options = options;
|
|
35
|
+
this.cursor = options.initialSequence ?? 0;
|
|
36
|
+
if (!Number.isSafeInteger(this.cursor) || this.cursor < 0)
|
|
37
|
+
throw new NeedError('RECOVERY_CURSOR_INVALID', '会话恢复位置无效', '已处理的安全游标');
|
|
38
|
+
if (options.maxMessageBytes)
|
|
39
|
+
this.boundary = new ProtocolBoundary(options.maxMessageBytes);
|
|
40
|
+
this.recovering = !!options.recoverySessionId;
|
|
37
41
|
this.lease.abort();
|
|
38
42
|
this.readable = new ReadableStream({ start: c => { this.output = c; }, pull: () => { this.attachment?.stdout.resume(); }, cancel: () => { this.close(); } });
|
|
39
43
|
this.writable = new WritableStream({ write: bytes => this.write(bytes), close: () => this.close(), abort: () => this.close() });
|
|
@@ -43,11 +47,46 @@ export class BrokerTransport {
|
|
|
43
47
|
get signal() { return this.lease.signal; }
|
|
44
48
|
get lastSequence() { return this.cursor; }
|
|
45
49
|
get error() { return this.failure; }
|
|
50
|
+
get promptInFlight() { return this.promptSent; }
|
|
51
|
+
processedMessage(message) { this.boundary?.consumed(message); }
|
|
52
|
+
checkpointSequence() { return this.online && this.preparingInputs === 0 && !!this.boundary?.complete && !this.approvalSent && this.unconfirmed.size === 0 ? this.cursor : null; }
|
|
53
|
+
async commitCheckpoint(commit) { return this.exclusive(async () => { const cursor = this.checkpointSequence(); if (cursor === null)
|
|
54
|
+
return false; await commit(cursor); return true; }); }
|
|
55
|
+
finishRecovery() { if (!this.boundary?.wholeLine)
|
|
56
|
+
throw new NeedError('RECOVERY_INCOMPLETE_OUTPUT', '原会话输出停在不完整消息中', '安全结束并新建会话'); this.recovering = false; }
|
|
57
|
+
async replayThrough(sequence, signal) {
|
|
58
|
+
const inspect = () => { signal?.throwIfAborted(); if (this.failure)
|
|
59
|
+
throw this.failure; if (this.cursor >= sequence) {
|
|
60
|
+
if (!this.boundary?.wholeLine)
|
|
61
|
+
throw new NeedError('RECOVERY_INCOMPLETE_OUTPUT', '原会话输出不完整', '安全结束并新建会话');
|
|
62
|
+
return true;
|
|
63
|
+
} return false; };
|
|
64
|
+
if (inspect())
|
|
65
|
+
return;
|
|
66
|
+
await new Promise((resolve, reject) => { let off = () => { }; const clear = () => { off(); signal?.removeEventListener('abort', changed); }; const changed = () => { try {
|
|
67
|
+
if (inspect()) {
|
|
68
|
+
clear();
|
|
69
|
+
resolve();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
clear();
|
|
74
|
+
reject(error);
|
|
75
|
+
} }; off = this.onChange(changed); signal?.addEventListener('abort', changed, { once: true }); changed(); });
|
|
76
|
+
}
|
|
77
|
+
stopHeartbeat() { if (this.heartbeat !== undefined)
|
|
78
|
+
clearInterval(this.heartbeat); this.heartbeat = undefined; }
|
|
79
|
+
recoveryMessage(message) {
|
|
80
|
+
const params = message.params;
|
|
81
|
+
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 ?? ''))
|
|
82
|
+
throw new NeedError('RECOVERY_UNSAFE_OUTPUT', '原会话有未确认输出或旧交互,不能代答', '安全结束并新建会话');
|
|
83
|
+
this.options.onRecoveryUpdate?.(message);
|
|
84
|
+
}
|
|
46
85
|
authorizeResponse(id, generation) { this.permits.set(JSON.stringify(id), generation); }
|
|
47
86
|
/** Called only by the ACP protocol layer after a matching result or current-turn output. */
|
|
48
87
|
acknowledge(method) { this.unconfirmed.delete(method); }
|
|
49
88
|
/** A matching terminal prompt response settles all approval replies in that turn. */
|
|
50
|
-
completePrompt() { this.unconfirmed.delete('session/prompt'); this.approvalSent = false; for (const key of this.unconfirmed)
|
|
89
|
+
completePrompt() { this.promptSent = false; this.unconfirmed.delete('session/prompt'); this.approvalSent = false; for (const key of this.unconfirmed)
|
|
51
90
|
if (key.startsWith('response:'))
|
|
52
91
|
this.unconfirmed.delete(key); }
|
|
53
92
|
onChange(fn) { this.listeners.add(fn); return () => this.listeners.delete(fn); }
|
|
@@ -57,6 +96,7 @@ export class BrokerTransport {
|
|
|
57
96
|
fail(error) {
|
|
58
97
|
if (this.ended)
|
|
59
98
|
return;
|
|
99
|
+
this.stopHeartbeat();
|
|
60
100
|
this.failure = error;
|
|
61
101
|
this.ended = true;
|
|
62
102
|
this.attached = false;
|
|
@@ -72,6 +112,7 @@ export class BrokerTransport {
|
|
|
72
112
|
close() {
|
|
73
113
|
if (this.ended)
|
|
74
114
|
return;
|
|
115
|
+
this.stopHeartbeat();
|
|
75
116
|
this.ended = true;
|
|
76
117
|
this.attached = false;
|
|
77
118
|
this.lease.abort();
|
|
@@ -127,6 +168,7 @@ export class BrokerTransport {
|
|
|
127
168
|
child.stdin.on('error', () => { });
|
|
128
169
|
let partial = Buffer.alloc(0);
|
|
129
170
|
let ready = false;
|
|
171
|
+
let attemptClosed = false;
|
|
130
172
|
const connected = Promise.withResolvers();
|
|
131
173
|
// A local end suppresses detached(), so settle the handshake independently.
|
|
132
174
|
const off = this.onChange(() => { if (this.ended)
|
|
@@ -136,6 +178,23 @@ export class BrokerTransport {
|
|
|
136
178
|
if (f.type === 'ready') {
|
|
137
179
|
if (ready)
|
|
138
180
|
throw new NeedError('BROKER_PROTOCOL_INVALID', 'broker 重复 ready', '有效的 broker 帧');
|
|
181
|
+
if (this.options.instanceId && f.instanceId !== this.options.instanceId)
|
|
182
|
+
throw new NeedError('SESSION_INSTANCE_MISMATCH', '主机会话已被替换,未接入或结束替代进程', '当前会话实例');
|
|
183
|
+
if (this.options.leased) {
|
|
184
|
+
if (f.controlMaxBytes !== this.options.controlMaxBytes || !Number.isSafeInteger(f.controlMaxBytes) || f.controlMaxBytes < Buffer.byteLength(JSON.stringify({ type: 'input', data: '' })) + 4)
|
|
185
|
+
throw new NeedError('LEASE_CAPACITY_INVALID', '主机会话容量无法承载租约输入', 'S实际冻结的controlMaxBytes');
|
|
186
|
+
const policy = f.lifecycle;
|
|
187
|
+
if (!policy || typeof policy.enabled !== 'boolean' || policy.enabled && (!Number.isSafeInteger(policy.heartbeatIntervalMs) || policy.heartbeatIntervalMs < 1 || !Number.isSafeInteger(policy.attachLeaseMs) || policy.attachLeaseMs < 3 * policy.heartbeatIntervalMs))
|
|
188
|
+
throw new NeedError('LEASE_POLICY_INVALID', '主机未给出有效客户端租约策略', 'S实际冻结的生命周期策略');
|
|
189
|
+
if (policy.enabled) {
|
|
190
|
+
this.stopHeartbeat();
|
|
191
|
+
this.heartbeat = setInterval(() => { if (this.online && this.attachment === child) {
|
|
192
|
+
child.stdin.write('{"type":"heartbeat"}\n', error => { if (error)
|
|
193
|
+
child.kill('SIGTERM'); });
|
|
194
|
+
} }, policy.heartbeatIntervalMs);
|
|
195
|
+
this.heartbeat.unref?.();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
139
198
|
ready = true;
|
|
140
199
|
this.attached = true;
|
|
141
200
|
this.lease = new AbortController();
|
|
@@ -143,8 +202,17 @@ export class BrokerTransport {
|
|
|
143
202
|
connected.resolve();
|
|
144
203
|
return;
|
|
145
204
|
}
|
|
146
|
-
if (f.type === 'error')
|
|
147
|
-
|
|
205
|
+
if (f.type === 'error') {
|
|
206
|
+
const code = typeof f.code === 'string' && /^[A-Z][A-Z0-9_]{0,63}$/.test(f.code) ? f.code : 'BROKER_PROTOCOL_INVALID';
|
|
207
|
+
const error = new NeedError(code, '主机会话无法续接(' + code + ');没有重放输入或代答审批', '恢复或显式结束原会话');
|
|
208
|
+
if (!ready && f.code === 'ATTACH_BUSY') {
|
|
209
|
+
attemptClosed = true;
|
|
210
|
+
connected.reject(error);
|
|
211
|
+
child.kill('SIGTERM');
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
148
216
|
if (f.type === 'exit') {
|
|
149
217
|
connected.reject(new NeedError('AGENT_EXITED', '主机 agent 已退出', '重新建立会话'));
|
|
150
218
|
this.fail(new NeedError('AGENT_EXITED', '主机 agent 已退出,退出状态不代表本轮完成', '确认主机状态'));
|
|
@@ -157,14 +225,19 @@ export class BrokerTransport {
|
|
|
157
225
|
return;
|
|
158
226
|
if (seq !== this.cursor + 1)
|
|
159
227
|
throw new NeedError('REPLAY_GAP', '主机输出存在缺口,不能安全继续审批', '显式结束或恢复原会话');
|
|
160
|
-
if (f.channel === 'stdout')
|
|
161
|
-
|
|
228
|
+
if (f.channel === 'stdout') {
|
|
229
|
+
const bytes = Buffer.from(f.data, 'base64');
|
|
230
|
+
this.boundary?.receive(bytes, this.recovering ? message => this.recoveryMessage(message) : undefined);
|
|
231
|
+
if (!this.recovering && !this.options.discardOutput)
|
|
232
|
+
this.output.enqueue(bytes);
|
|
233
|
+
}
|
|
162
234
|
this.cursor = seq;
|
|
235
|
+
this.changed();
|
|
163
236
|
if ((this.output.desiredSize ?? 0) <= 0)
|
|
164
237
|
child.stdout.pause();
|
|
165
238
|
};
|
|
166
239
|
child.stdout.on('data', (bytes) => {
|
|
167
|
-
if (this.ended || generation !== this.epoch)
|
|
240
|
+
if (attemptClosed || this.ended || generation !== this.epoch)
|
|
168
241
|
return;
|
|
169
242
|
try {
|
|
170
243
|
partial = Buffer.concat([partial, bytes]);
|
|
@@ -178,7 +251,7 @@ export class BrokerTransport {
|
|
|
178
251
|
if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object')
|
|
179
252
|
throw Error('frame');
|
|
180
253
|
frame(parsed);
|
|
181
|
-
if (this.ended)
|
|
254
|
+
if (this.ended || attemptClosed)
|
|
182
255
|
return;
|
|
183
256
|
}
|
|
184
257
|
if (partial.length > this.options.maxFrameBytes)
|
|
@@ -189,8 +262,9 @@ export class BrokerTransport {
|
|
|
189
262
|
}
|
|
190
263
|
});
|
|
191
264
|
const detached = () => {
|
|
192
|
-
if (generation !== this.epoch || this.ended)
|
|
265
|
+
if (attemptClosed || generation !== this.epoch || this.ended)
|
|
193
266
|
return;
|
|
267
|
+
this.stopHeartbeat();
|
|
194
268
|
this.attached = false;
|
|
195
269
|
this.lease.abort();
|
|
196
270
|
this.changed();
|
|
@@ -212,6 +286,15 @@ export class BrokerTransport {
|
|
|
212
286
|
return connected.promise.finally(off);
|
|
213
287
|
}
|
|
214
288
|
async write(bytes) {
|
|
289
|
+
this.preparingInputs++;
|
|
290
|
+
try {
|
|
291
|
+
return await this.exclusive(() => this.send(bytes));
|
|
292
|
+
}
|
|
293
|
+
finally {
|
|
294
|
+
this.preparingInputs--;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
async send(bytes) {
|
|
215
298
|
if (!this.online || !this.attachment)
|
|
216
299
|
throw new NeedError('INPUT_DELIVERY_UNCONFIRMED', '连接不在线,输入没有排队或重放', '恢复连接并确认原会话状态');
|
|
217
300
|
let response;
|
|
@@ -222,22 +305,46 @@ export class BrokerTransport {
|
|
|
222
305
|
throw new NeedError('ACP_INPUT_INVALID', 'ACP 输出不是一条完整 JSON 消息', '有效的 ACP SDK 字节输出');
|
|
223
306
|
}
|
|
224
307
|
const selected = response.result?.outcome?.outcome === 'selected';
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
this.
|
|
229
|
-
|
|
308
|
+
const generation = this.epoch;
|
|
309
|
+
const child = this.attachment;
|
|
310
|
+
const eligible = () => {
|
|
311
|
+
if (!this.online || this.attachment !== child || this.epoch !== generation)
|
|
312
|
+
throw new NeedError('INPUT_DELIVERY_UNCONFIRMED', '连接已改变,未重放输入或审批', '原连接的有效发送许可');
|
|
313
|
+
if (selected && this.permits.get(JSON.stringify(response.id)) !== generation)
|
|
230
314
|
throw new NeedError('APPROVAL_STALE', '连接已改变,旧审批未发送', '重连后的本次审批');
|
|
315
|
+
};
|
|
316
|
+
eligible();
|
|
317
|
+
await this.options.beforeInput?.();
|
|
318
|
+
eligible();
|
|
319
|
+
if (selected) {
|
|
320
|
+
this.permits.delete(JSON.stringify(response.id));
|
|
231
321
|
this.approvalSent = true;
|
|
232
322
|
}
|
|
233
323
|
this.unconfirmed.add(response.method ?? 'response:' + JSON.stringify(response.id));
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
324
|
+
const chunks = [];
|
|
325
|
+
if (this.options.leased) {
|
|
326
|
+
const size = Math.floor((this.options.controlMaxBytes - Buffer.byteLength(JSON.stringify({ type: 'input', data: '' }))) / 4) * 3;
|
|
327
|
+
if (size < 1)
|
|
328
|
+
throw new NeedError('LEASE_CAPACITY_INVALID', '主机会话容量无法承载输入', '有效controlMaxBytes');
|
|
329
|
+
for (let offset = 0; offset < bytes.byteLength; offset += size)
|
|
330
|
+
chunks.push(Buffer.from(JSON.stringify({ type: 'input', data: Buffer.from(bytes.subarray(offset, offset + size)).toString('base64') }) + '\n'));
|
|
331
|
+
}
|
|
332
|
+
else
|
|
333
|
+
chunks.push(Buffer.from(bytes));
|
|
334
|
+
for (const [index, chunk] of chunks.entries()) {
|
|
335
|
+
if (!this.online || this.attachment !== child || this.epoch !== generation)
|
|
336
|
+
throw new NeedError('INPUT_DELIVERY_UNCONFIRMED', '分块发送期间断线,未重放输入', '原主机连接');
|
|
337
|
+
await new Promise((resolve, reject) => {
|
|
338
|
+
const closed = () => { cleanup(); reject(new NeedError('INPUT_DELIVERY_UNCONFIRMED', '输入送达未确认,未重发', '确认原进程状态')); };
|
|
339
|
+
const cleanup = () => { child.removeListener('close', closed); };
|
|
340
|
+
child.once('close', closed);
|
|
341
|
+
if (response.method === 'session/prompt' && index === chunks.length - 1)
|
|
342
|
+
this.promptSent = true;
|
|
343
|
+
child.stdin.write(chunk, error => { cleanup(); error ? reject(new NeedError('INPUT_DELIVERY_UNCONFIRMED', '输入送达失败,未重发', '确认原进程状态')) : resolve(); });
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
if (!response.method)
|
|
347
|
+
this.boundary?.responded(response.id);
|
|
241
348
|
}
|
|
242
349
|
}
|
|
243
350
|
export function controlNeed(stderr) {
|
|
@@ -277,8 +384,9 @@ export function sshIo(target, argv, maxControlBytes) {
|
|
|
277
384
|
}) };
|
|
278
385
|
}
|
|
279
386
|
export function brokerCommand(t, args) { return `AW_SESSION_ROOT=${shellQuote(t.session.root)} ${shellQuote(t.session.command)} ${args.map(shellQuote).join(' ')}`; }
|
|
280
|
-
export function
|
|
281
|
-
function
|
|
387
|
+
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); }
|
|
388
|
+
export function brokerId(sessionId, agentId = CODEX_AGENT.id) { return 'r' + createHash('sha256').update(agentId + '/' + sessionId).digest('hex').slice(0, 23); }
|
|
389
|
+
export function controlJson(text) {
|
|
282
390
|
try {
|
|
283
391
|
return JSON.parse(text);
|
|
284
392
|
}
|
|
@@ -286,112 +394,4 @@ function controlJson(text) {
|
|
|
286
394
|
throw new NeedError('BROKER_PROTOCOL_INVALID', 'broker 控制响应不是有效 JSON,未复制原始输出', '有效的 broker 控制响应');
|
|
287
395
|
}
|
|
288
396
|
}
|
|
289
|
-
export
|
|
290
|
-
const active = new Set();
|
|
291
|
-
const retained = new Map();
|
|
292
|
-
const open = async (target, project, request) => {
|
|
293
|
-
const settings = request.brokerSettings ?? (typeof configuration === 'function' ? configuration() : configuration);
|
|
294
|
-
if (!request.sessionId)
|
|
295
|
-
throw new NeedError('SESSION_REQUIRED', '我需要当前 DSH 会话标识', 'DSH sessionId');
|
|
296
|
-
if (request.mode === 'one-shot') {
|
|
297
|
-
return { identity: identity(target), cwd: project.path, oneShot: { target, cwd: project.path, ...protectedOneShotLayout(target) } };
|
|
298
|
-
}
|
|
299
|
-
if (![settings.brokerMaxFrameBytes, settings.brokerControlMaxBytes].every(n => Number.isSafeInteger(n) && n > 0))
|
|
300
|
-
throw new NeedError('SETTINGS_REQUIRED', '我需要管理员确认 broker 帧和控制响应大小设置', 'brokerMaxFrameBytes / brokerControlMaxBytes(无默认)');
|
|
301
|
-
let argv = provided?.argv;
|
|
302
|
-
if (!provided?.io && !argv) {
|
|
303
|
-
try {
|
|
304
|
-
const moduleName = '@ai-workbench/connect';
|
|
305
|
-
const supplied = await import(__rewriteRelativeImportExtension(moduleName));
|
|
306
|
-
if (typeof supplied.sshArgs !== 'function')
|
|
307
|
-
throw Error('missing sshArgs');
|
|
308
|
-
argv = supplied.sshArgs;
|
|
309
|
-
}
|
|
310
|
-
catch {
|
|
311
|
-
throw new NeedError('DEPENDENCY_REQUIRED', '我需要 connect 0.3.0 包导出的 sshArgs 来执行可信 SSH', '@ai-workbench/connect 0.3.0');
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
const ioFor = (t) => provided?.io?.(t) ?? sshIo(t, argv, settings.brokerControlMaxBytes);
|
|
315
|
-
const agent = request.agent ?? CODEX_AGENT;
|
|
316
|
-
let io = ioFor(target);
|
|
317
|
-
const id = brokerId(request.sessionId, agent.id);
|
|
318
|
-
const command = (args) => brokerCommand(target, args);
|
|
319
|
-
const list = controlJson(await io.run(command(['list']), request.signal));
|
|
320
|
-
if (!Array.isArray(list) || list.some(s => typeof s?.id !== 'string'))
|
|
321
|
-
throw new NeedError('BROKER_PROTOCOL_INVALID', '主机会话列表无效', '有效的 broker 列表');
|
|
322
|
-
if (list.some(s => s.id === id)) {
|
|
323
|
-
throw new NeedError('ACP_CLIENT_LOST', '我需要管理员确认并结束遗留的主机会话;当前保留原进程,没有代答旧审批', '管理员核对并明确结束对应主机会话');
|
|
324
|
-
}
|
|
325
|
-
const started = controlJson(await io.run(command(['start', id, '--cwd', project.path, '--settings', target.session.settingsPath, '--', agent.command]), request.signal));
|
|
326
|
-
if (started?.protocolVersion !== 1 || started.id !== id || !Number.isSafeInteger(started.pid) || started.pid < 1 || typeof started.socket !== 'string')
|
|
327
|
-
throw new NeedError('BROKER_START_UNCONFIRMED', 'broker 未返回匹配的启动确认,需确认遗留会话;未自动新建替代进程', `确认主机会话 ${id} 的状态`);
|
|
328
|
-
let transport;
|
|
329
|
-
transport = new BrokerTransport({ maxFrameBytes: settings.brokerMaxFrameBytes, attach: after => io.attach(command(['attach', id, '--after', String(after), '--framed'])), onDisconnect: async () => {
|
|
330
|
-
const connect = get();
|
|
331
|
-
if (!connect)
|
|
332
|
-
throw new NeedError('DEPENDENCY_REQUIRED', '我需要连接插件恢复员工连接', 'aiWorkbenchConnect');
|
|
333
|
-
const next = unwrap(await connect.getSshTarget());
|
|
334
|
-
validateTarget(next);
|
|
335
|
-
if (!sameTarget(next, target))
|
|
336
|
-
throw new NeedError('SESSION_CONTEXT_MISMATCH', '重连目标与原会话不一致', '原员工主机、可信SSH参数和broker');
|
|
337
|
-
io = ioFor(next);
|
|
338
|
-
await transport.connect();
|
|
339
|
-
} });
|
|
340
|
-
const aborted = () => transport.fail(new NeedError('ABORTED', '本轮已取消,正在结束主机进程', '确认主机状态'));
|
|
341
|
-
request.signal?.addEventListener('abort', aborted, { once: true });
|
|
342
|
-
if (request.signal?.aborted)
|
|
343
|
-
aborted();
|
|
344
|
-
const dispose = async () => { request.signal?.removeEventListener('abort', aborted); transport.close(); try {
|
|
345
|
-
await io.run(command(['kill', id]));
|
|
346
|
-
}
|
|
347
|
-
catch {
|
|
348
|
-
throw new NeedError('REMOTE_CLEANUP_UNCONFIRMED', '无法确认原主机会话已结束;不会自动新建替代进程', '恢复连接并结束对应主机会话');
|
|
349
|
-
} };
|
|
350
|
-
try {
|
|
351
|
-
await transport.connect();
|
|
352
|
-
request.signal?.throwIfAborted();
|
|
353
|
-
if (request.retainProcess)
|
|
354
|
-
request.signal?.removeEventListener('abort', aborted);
|
|
355
|
-
}
|
|
356
|
-
catch (error) {
|
|
357
|
-
await dispose();
|
|
358
|
-
throw error;
|
|
359
|
-
}
|
|
360
|
-
return { identity: identity(target), cwd: project.path, broker: transport, acpTransport: transport, dispose };
|
|
361
|
-
};
|
|
362
|
-
return async (target, project, request) => {
|
|
363
|
-
const key = (request.agent ?? CODEX_AGENT).id + '/' + identity(target) + '/' + request.sessionId;
|
|
364
|
-
const existing = request.retainProcess ? retained.get(key) : undefined;
|
|
365
|
-
if (existing) {
|
|
366
|
-
const settings = request.brokerSettings ?? (typeof configuration === 'function' ? configuration() : configuration);
|
|
367
|
-
if (existing.cwd !== project.path || existing.settings.brokerMaxFrameBytes !== settings.brokerMaxFrameBytes || existing.settings.brokerControlMaxBytes !== settings.brokerControlMaxBytes || !sameTarget(existing.target, target))
|
|
368
|
-
throw new NeedError('SESSION_CONTEXT_MISMATCH', '会话工作区、可信SSH参数或主机运行设置已改变,请新建会话', '原会话的可信主机、工作区与运行设置');
|
|
369
|
-
if (existing.runtime.broker?.error)
|
|
370
|
-
throw existing.runtime.broker.error;
|
|
371
|
-
return existing.runtime;
|
|
372
|
-
}
|
|
373
|
-
if (active.has(key))
|
|
374
|
-
throw new NeedError('TURN_BUSY', '当前 DSH 会话已有远端运行,不能当作遗留进程结束', '等待当前运行结束');
|
|
375
|
-
active.add(key);
|
|
376
|
-
try {
|
|
377
|
-
const runtime = await open(target, project, request);
|
|
378
|
-
const dispose = runtime.dispose;
|
|
379
|
-
let disposed = false;
|
|
380
|
-
runtime.dispose = async () => { if (disposed)
|
|
381
|
-
return; disposed = true; try {
|
|
382
|
-
await dispose?.();
|
|
383
|
-
}
|
|
384
|
-
finally {
|
|
385
|
-
active.delete(key);
|
|
386
|
-
retained.delete(key);
|
|
387
|
-
} };
|
|
388
|
-
if (request.retainProcess && request.mode === 'session')
|
|
389
|
-
retained.set(key, { runtime, cwd: project.path, settings: { ...(request.brokerSettings ?? (typeof configuration === 'function' ? configuration() : configuration)) }, target });
|
|
390
|
-
return runtime;
|
|
391
|
-
}
|
|
392
|
-
catch (error) {
|
|
393
|
-
active.delete(key);
|
|
394
|
-
throw error;
|
|
395
|
-
}
|
|
396
|
-
};
|
|
397
|
-
}
|
|
397
|
+
export { brokerFactory } from "./broker-runtime.js";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ConnectNeed } from '@ai-workbench/connect/api';
|
|
1
2
|
import type { ConnectTarget } from './provider.ts';
|
|
2
3
|
import type { ContractInfo, Provider, WorkspaceUse, WorkspaceNoticeReceipt as LWorkspaceNoticeReceipt } from '@ai-workbench/connect/api';
|
|
3
4
|
/** 正式L0.7公开类型;消费子集不表示已安装或已通过真实产品门。 */
|
|
@@ -16,7 +17,7 @@ export type ConnectResult<T> = {
|
|
|
16
17
|
code: string;
|
|
17
18
|
message: string;
|
|
18
19
|
requires: string;
|
|
19
|
-
}
|
|
20
|
+
} & Pick<ConnectNeed, 'officialReason'>;
|
|
20
21
|
};
|
|
21
22
|
export interface NativeWorkspaceApi {
|
|
22
23
|
getContractInfo(): ContractInfo;
|
|
@@ -31,6 +31,7 @@ export type DetailSink = {
|
|
|
31
31
|
};
|
|
32
32
|
/** Only objects created by this module are trusted as redaction markers. */
|
|
33
33
|
export declare function isSanitizedRedaction(value: unknown): boolean;
|
|
34
|
+
export declare function sanitizeDisplayLabel(value: string): string;
|
|
34
35
|
/** Sanitize an internal observation without attaching any L wire semantics. */
|
|
35
36
|
export declare function sanitizeObservation(observation: DetailObservation, protectModelMaterial?: boolean): Readonly<SanitizedObservation>;
|
|
36
37
|
/** Internal diagnostic recorder; a sink is not an assertion that an L wire API exists. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { STAGES } from "./stage-timing.js";
|
|
2
3
|
const redactionMarkers = new WeakSet();
|
|
3
4
|
function redaction(fields) {
|
|
4
5
|
const marker = { redacted: true, ...fields };
|
|
@@ -92,7 +93,7 @@ function safeText(value) {
|
|
|
92
93
|
const url = cleanUrls(value);
|
|
93
94
|
return url.changed ? redaction({ reason: 'URL敏感部分', value: url.value }) : value;
|
|
94
95
|
}
|
|
95
|
-
function
|
|
96
|
+
export function sanitizeDisplayLabel(value) {
|
|
96
97
|
if (typeof value !== 'string')
|
|
97
98
|
throw new TypeError('Detail observation labels must be strings');
|
|
98
99
|
// Inspect credentials before removing control characters, so a bearer newline cannot evade detection.
|
|
@@ -254,19 +255,68 @@ function usageStructure(value, key = '') {
|
|
|
254
255
|
return value;
|
|
255
256
|
return opaqueStructure(value);
|
|
256
257
|
}
|
|
258
|
+
/** Only fixed local stage names and finite nonnegative millisecond observations. */
|
|
259
|
+
function timingStructure(value) {
|
|
260
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
261
|
+
return opaqueStructure(value);
|
|
262
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => {
|
|
263
|
+
if (key === 'phase' && typeof child === 'string' && Object.hasOwn(STAGES, child))
|
|
264
|
+
return [key, child];
|
|
265
|
+
if (key === 'clock' && child === 'client-monotonic' || key === 'outcome' && (child === 'ok' || child === 'failed'))
|
|
266
|
+
return [key, child];
|
|
267
|
+
if (['durationMs', 'elapsedMs'].includes(key) && typeof child === 'number' && Number.isFinite(child) && child >= 0)
|
|
268
|
+
return [key, child];
|
|
269
|
+
return [key, opaqueStructure(child)];
|
|
270
|
+
}));
|
|
271
|
+
}
|
|
272
|
+
/** Only our two model-configuration observations approve these exact diagnostic
|
|
273
|
+
* paths. Directory source, config metadata and arbitrary subtrees stay opaque. */
|
|
274
|
+
function modelConfigurationStructure(value, level = 'root') {
|
|
275
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
276
|
+
return opaqueStructure(value);
|
|
277
|
+
if (isSanitizedRedaction(value))
|
|
278
|
+
return value;
|
|
279
|
+
const strings = {
|
|
280
|
+
root: ['resolvedModel', 'requestedEffort', 'reason'], selected: ['id', 'value', 'name', 'resolvedModel', 'effort'],
|
|
281
|
+
actual: ['model', 'effort', 'modelConfigId', 'effortConfigId'], reasoning: ['defaultValue'], effort: ['value', 'name'],
|
|
282
|
+
};
|
|
283
|
+
return Object.fromEntries(Object.entries(value).map(([key, child]) => {
|
|
284
|
+
if (typeof child === 'string' && strings[level].includes(key))
|
|
285
|
+
return [key, child];
|
|
286
|
+
if (level === 'root' && key === 'status' && child === 'remote-configured')
|
|
287
|
+
return [key, child];
|
|
288
|
+
if (level === 'actual' && typeof child === 'boolean' && ['confirmed', 'effortAvailable'].includes(key))
|
|
289
|
+
return [key, child];
|
|
290
|
+
if (level === 'reasoning' && key === 'defaultKnown' && typeof child === 'boolean')
|
|
291
|
+
return [key, child];
|
|
292
|
+
if (child === null && ((level === 'root' && ['actual', 'requestedEffort'].includes(key)) || (level === 'selected' && key === 'resolvedModel') || (level === 'reasoning' && key === 'defaultValue')))
|
|
293
|
+
return [key, null];
|
|
294
|
+
if (level === 'root' && key === 'selected')
|
|
295
|
+
return [key, modelConfigurationStructure(child, 'selected')];
|
|
296
|
+
if (level === 'root' && ['configured', 'actual'].includes(key))
|
|
297
|
+
return [key, modelConfigurationStructure(child, 'actual')];
|
|
298
|
+
if (level === 'selected' && key === 'reasoning')
|
|
299
|
+
return [key, modelConfigurationStructure(child, 'reasoning')];
|
|
300
|
+
if (level === 'reasoning' && key === 'options' && Array.isArray(child))
|
|
301
|
+
return [key, child.map(option => modelConfigurationStructure(option, 'effort'))];
|
|
302
|
+
return [key, opaqueStructure(child)];
|
|
303
|
+
}));
|
|
304
|
+
}
|
|
257
305
|
/** Sanitize an internal observation without attaching any L wire semantics. */
|
|
258
306
|
export function sanitizeObservation(observation, protectModelMaterial = false) {
|
|
259
307
|
const payload = snapshotPayload(observation.payload, new Set(), protectModelMaterial && observation.kind !== 'commands');
|
|
260
308
|
return freezeDeep({
|
|
261
309
|
kind: observation.kind, severity: observation.severity,
|
|
262
|
-
code:
|
|
263
|
-
...(observation.method === undefined ? {} : { method:
|
|
310
|
+
code: sanitizeDisplayLabel(observation.code), summary: sanitizeDisplayLabel(observation.summary),
|
|
311
|
+
...(observation.method === undefined ? {} : { method: sanitizeDisplayLabel(observation.method) }),
|
|
264
312
|
payload: !protectModelMaterial ? payload
|
|
265
|
-
: observation.kind === '
|
|
266
|
-
: observation.kind === '
|
|
267
|
-
: observation.kind === '
|
|
268
|
-
: observation.kind === '
|
|
269
|
-
:
|
|
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),
|
|
270
320
|
});
|
|
271
321
|
}
|
|
272
322
|
function validatedScope(scope) {
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { type AcpHooks } from './acp.ts';
|
|
|
5
5
|
import { type RuntimeProvider } from './provider.ts';
|
|
6
6
|
import { type AgentDefinition } from './agents.ts';
|
|
7
7
|
import { type ImageReader } from './input.ts';
|
|
8
|
+
import type { LoginDiagnostic } from './login-diagnostic.ts';
|
|
9
|
+
import { type ModelDirectory, type HostModel } from './model-catalog.ts';
|
|
8
10
|
export declare const name = "ai-workbench-remote-agent";
|
|
9
11
|
export declare const settingsNamespace = "ai-workbench-codex";
|
|
10
12
|
export declare const inject: string[];
|
|
@@ -47,27 +49,38 @@ type LiveConfig = {
|
|
|
47
49
|
};
|
|
48
50
|
/** DSH volatile settings are handles; adapters freeze their values once per invocation. */
|
|
49
51
|
export declare function readConfig(config: LiveConfig): PluginConfig;
|
|
52
|
+
export type LoginFailureReporter = (sessionId: string | undefined, code: string, diagnostic: LoginDiagnostic) => Promise<{
|
|
53
|
+
eventId: string;
|
|
54
|
+
recorded: true;
|
|
55
|
+
}>;
|
|
50
56
|
export declare class CodexRemoteAdapter extends LlmAdapter {
|
|
51
57
|
private resolveRuntime;
|
|
52
58
|
private approve?;
|
|
53
59
|
private readonly providerName;
|
|
54
60
|
private readonly agent;
|
|
55
61
|
private readonly readImage?;
|
|
62
|
+
private readonly reportLoginFailure?;
|
|
63
|
+
private readonly directory?;
|
|
56
64
|
private config;
|
|
57
65
|
private sessions;
|
|
58
66
|
private turns;
|
|
59
67
|
private lifetime;
|
|
60
|
-
|
|
68
|
+
private shuttingDown;
|
|
69
|
+
private detached;
|
|
70
|
+
private cleanups;
|
|
71
|
+
constructor(config: PluginConfig | (() => PluginConfig), resolveRuntime: RuntimeProvider, approve?: InvocationApproval | undefined, providerName?: () => string, agent?: AgentDefinition, readImage?: ImageReader | undefined, reportLoginFailure?: LoginFailureReporter | undefined, directory?: ModelDirectory | undefined);
|
|
61
72
|
providerInfo(provider: string): {
|
|
62
73
|
id: string;
|
|
63
74
|
name: string;
|
|
64
75
|
};
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
private catalog;
|
|
77
|
+
listModels(provider: string): Promise<(import("@deepseek-ai/dsh-llm").LlmResolvedModelInfo & {
|
|
78
|
+
catalog: HostModel;
|
|
79
|
+
})[]>;
|
|
80
|
+
resolveModel(provider: string, id: string, signal?: AbortSignal): Promise<import("@deepseek-ai/dsh-llm").LlmResolvedModelInfo & {
|
|
81
|
+
catalog: HostModel;
|
|
82
|
+
}>;
|
|
83
|
+
invalidateModelState(): Promise<void>;
|
|
71
84
|
providerRetryPolicy(): {
|
|
72
85
|
mode: "normal";
|
|
73
86
|
maxRetries: number;
|
|
@@ -76,7 +89,9 @@ export declare class CodexRemoteAdapter extends LlmAdapter {
|
|
|
76
89
|
maxDelayMs: number;
|
|
77
90
|
jitterRatio: number;
|
|
78
91
|
};
|
|
92
|
+
private cleanupRuntime;
|
|
79
93
|
disposeSession(sessionId: string): Promise<void>;
|
|
94
|
+
detachSession(sessionId: string): Promise<void>;
|
|
80
95
|
dispose(): Promise<void>;
|
|
81
96
|
stream(options: GenerateOptions): AsyncGenerator<StreamChunk>;
|
|
82
97
|
private generate;
|
|
@@ -84,18 +99,19 @@ export declare class CodexRemoteAdapter extends LlmAdapter {
|
|
|
84
99
|
/** One official registration owns both routes, preserving opaque replay ownership. */
|
|
85
100
|
export declare class RemoteAgentsAdapter extends LlmAdapter {
|
|
86
101
|
private adapters;
|
|
87
|
-
constructor(config: () => PluginConfig, makeRuntime: (agent: AgentDefinition) => RuntimeProvider, approve: InvocationApproval, nameFor: (agent: AgentDefinition) => string, readImage?: ImageReader);
|
|
102
|
+
constructor(config: () => PluginConfig, makeRuntime: (agent: AgentDefinition) => RuntimeProvider, approve: InvocationApproval, nameFor: (agent: AgentDefinition) => string, readImage?: ImageReader, reportLoginFailure?: LoginFailureReporter, directory?: ModelDirectory);
|
|
88
103
|
private adapter;
|
|
89
104
|
providerInfo(provider: string): {
|
|
90
105
|
id: string;
|
|
91
106
|
name: string;
|
|
92
107
|
};
|
|
93
|
-
listModels(provider: string): Promise<{
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
108
|
+
listModels(provider: string): Promise<(import("@deepseek-ai/dsh-llm").LlmResolvedModelInfo & {
|
|
109
|
+
catalog: HostModel;
|
|
110
|
+
})[]>;
|
|
111
|
+
resolveModel(provider: string, model: string, signal?: AbortSignal): Promise<import("@deepseek-ai/dsh-llm").LlmResolvedModelInfo & {
|
|
112
|
+
catalog: HostModel;
|
|
113
|
+
}>;
|
|
114
|
+
invalidateModelState(): Promise<void>;
|
|
99
115
|
providerRetryPolicy(): {
|
|
100
116
|
mode: "normal";
|
|
101
117
|
maxRetries: number;
|
|
@@ -106,6 +122,7 @@ export declare class RemoteAgentsAdapter extends LlmAdapter {
|
|
|
106
122
|
};
|
|
107
123
|
stream(options: GenerateOptions): AsyncGenerator<StreamChunk, any, any>;
|
|
108
124
|
disposeSession(sessionId: string): Promise<void>;
|
|
125
|
+
detachSession(sessionId: string): Promise<void>;
|
|
109
126
|
dispose(): Promise<void>;
|
|
110
127
|
}
|
|
111
128
|
export declare function apply(ctx: Context, live: LiveConfig): void;
|