ds-harness-remote 0.4.1 → 0.4.3
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 +35 -11
- package/cordis.patch.yml +3 -0
- package/dist/client-runtime.d.ts +28 -0
- package/dist/client-runtime.d.ts.map +1 -1
- package/dist/client-runtime.js +327 -5
- package/dist/client-runtime.js.map +1 -1
- package/dist/client-secure-transport.d.ts.map +1 -1
- package/dist/client-secure-transport.js +9 -2
- package/dist/client-secure-transport.js.map +1 -1
- package/dist/client.github.js +167 -48
- package/dist/client.js +167 -48
- package/dist/client.js.map +1 -1
- package/dist/codex/app-server.d.ts +71 -0
- package/dist/codex/app-server.d.ts.map +1 -0
- package/dist/codex/app-server.js +268 -0
- package/dist/codex/app-server.js.map +1 -0
- package/dist/codex/domain.d.ts +82 -0
- package/dist/codex/domain.d.ts.map +1 -0
- package/dist/codex/domain.js +893 -0
- package/dist/codex/domain.js.map +1 -0
- package/dist/codex/method-policy.d.ts +297 -0
- package/dist/codex/method-policy.d.ts.map +1 -0
- package/dist/codex/method-policy.js +103 -0
- package/dist/codex/method-policy.js.map +1 -0
- package/dist/codex/peer-bridge.d.ts +57 -0
- package/dist/codex/peer-bridge.d.ts.map +1 -0
- package/dist/codex/peer-bridge.js +300 -0
- package/dist/codex/peer-bridge.js.map +1 -0
- package/dist/codex/virtual-harness.d.ts +178 -0
- package/dist/codex/virtual-harness.d.ts.map +1 -0
- package/dist/codex/virtual-harness.js +2515 -0
- package/dist/codex/virtual-harness.js.map +1 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +12 -0
- package/dist/config.js.map +1 -1
- package/dist/control-runtime.d.ts +1 -0
- package/dist/control-runtime.d.ts.map +1 -1
- package/dist/control-runtime.js +22 -0
- package/dist/control-runtime.js.map +1 -1
- package/dist/harness-remote-bridge.d.ts +2 -2
- package/dist/harness-remote-bridge.d.ts.map +1 -1
- package/dist/harness-remote-bridge.js +2 -2
- package/dist/harness-remote-bridge.js.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8550 -3884
- package/dist/index.js.map +4 -4
- package/dist/remote-typert-gateway.d.ts +1 -1
- package/dist/remote-typert-gateway.d.ts.map +1 -1
- package/dist/remote-typert-gateway.js +9 -2
- package/dist/remote-typert-gateway.js.map +1 -1
- package/dist/rpc-router.d.ts +5 -2
- package/dist/rpc-router.d.ts.map +1 -1
- package/dist/rpc-router.js +30 -1
- package/dist/rpc-router.js.map +1 -1
- package/dist/server-connection.d.ts +1 -0
- package/dist/server-connection.d.ts.map +1 -1
- package/dist/server-connection.js +69 -22
- package/dist/server-connection.js.map +1 -1
- package/dist/service.d.ts +20 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +58 -2
- package/dist/service.js.map +1 -1
- package/dist/typert-gateway-contract.d.ts +2 -2
- package/dist/typert-gateway-contract.d.ts.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +19 -23
- package/public.d.ts +4 -0
package/dist/client-runtime.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RemoteClientCore } from '@dsh-remote/client-core';
|
|
1
|
+
import { CodexRemoteClient, RemoteClientCore } from '@dsh-remote/client-core';
|
|
2
2
|
import { AdaptiveTransport, stunOnlyIceServers, } from '@dsh-remote/webrtc';
|
|
3
3
|
import { ApiProxySwitch } from './api-proxy-switch.js';
|
|
4
4
|
import { ClientSecureTransport } from './client-secure-transport.js';
|
|
@@ -6,6 +6,7 @@ import { CONTROL_RPC_PREFIX } from './control-route.js';
|
|
|
6
6
|
import { uuidV7 } from './ids.js';
|
|
7
7
|
import { RemoteHarnessApiProxy } from './remote-api-proxy.js';
|
|
8
8
|
import { RemoteTypertGateway } from './remote-typert-gateway.js';
|
|
9
|
+
import { CodexVirtualHarness, discoverCodexVirtualWorkspaces, } from './codex/virtual-harness.js';
|
|
9
10
|
import { ServerApiError, } from './server-api.js';
|
|
10
11
|
import { TypertGatewaySwitch } from './typert-gateway-switch.js';
|
|
11
12
|
import { loadNodeRtcFactory } from './werift-rtc.js';
|
|
@@ -22,8 +23,10 @@ export class ClientModeRuntime {
|
|
|
22
23
|
identity;
|
|
23
24
|
connected;
|
|
24
25
|
pendingWorkspaceSelection;
|
|
26
|
+
codexVirtual;
|
|
25
27
|
proxySwitch;
|
|
26
28
|
gatewaySwitch;
|
|
29
|
+
codexStreams = new Map();
|
|
27
30
|
closed = false;
|
|
28
31
|
constructor(config, identities, server, apiProxy, typertGateway, logger, host, rtcFactoryProvider = loadNodeRtcFactory) {
|
|
29
32
|
this.config = config;
|
|
@@ -70,6 +73,7 @@ export class ClientModeRuntime {
|
|
|
70
73
|
...(this.pendingWorkspaceSelection === undefined
|
|
71
74
|
? {}
|
|
72
75
|
: { workspaceSelection: { ...this.pendingWorkspaceSelection } }),
|
|
76
|
+
backend: this.codexVirtual === undefined ? 'harness' : 'codex',
|
|
73
77
|
hostAuthorizationAvailable: this.host !== undefined,
|
|
74
78
|
...(this.host === undefined ? {} : { host: this.host.hostStatus() }),
|
|
75
79
|
};
|
|
@@ -141,8 +145,10 @@ export class ClientModeRuntime {
|
|
|
141
145
|
const previous = this.connected;
|
|
142
146
|
this.connected = undefined;
|
|
143
147
|
this.pendingWorkspaceSelection = undefined;
|
|
148
|
+
await this.closeCodexVirtual();
|
|
144
149
|
this.proxySwitch?.selectLocal();
|
|
145
150
|
this.gatewaySwitch.selectLocal();
|
|
151
|
+
await this.closeCodexStreams(previous?.client);
|
|
146
152
|
await previous?.client.close().catch(() => undefined);
|
|
147
153
|
await this.server.revokeCurrentDevice();
|
|
148
154
|
this.identity = await this.identities.reset(this.config.deviceName);
|
|
@@ -161,11 +167,13 @@ export class ClientModeRuntime {
|
|
|
161
167
|
}
|
|
162
168
|
async setMode(mode, targetDeviceId, signal) {
|
|
163
169
|
if (mode === 'local') {
|
|
170
|
+
await this.closeCodexVirtual();
|
|
164
171
|
this.proxySwitch?.selectLocal();
|
|
165
172
|
this.gatewaySwitch.selectLocal();
|
|
166
173
|
const previous = this.connected;
|
|
167
174
|
this.connected = undefined;
|
|
168
175
|
this.pendingWorkspaceSelection = undefined;
|
|
176
|
+
await this.closeCodexStreams(previous?.client);
|
|
169
177
|
await previous?.client.close().catch(() => undefined);
|
|
170
178
|
this.logger.info('Harness target switched', { mode: 'local' });
|
|
171
179
|
return this.status();
|
|
@@ -184,7 +192,9 @@ export class ClientModeRuntime {
|
|
|
184
192
|
const previous = this.connected;
|
|
185
193
|
this.connected = next;
|
|
186
194
|
this.pendingWorkspaceSelection = undefined;
|
|
195
|
+
await this.closeCodexVirtual();
|
|
187
196
|
this.selectRemoteTarget(next);
|
|
197
|
+
await this.closeCodexStreams(previous?.client);
|
|
188
198
|
await previous?.client.close().catch(() => undefined);
|
|
189
199
|
this.logger.info('Harness target switched', { mode: 'remote', targetDeviceId: shortId(next.target.deviceId) });
|
|
190
200
|
return this.status();
|
|
@@ -242,15 +252,58 @@ export class ClientModeRuntime {
|
|
|
242
252
|
});
|
|
243
253
|
workspace = unwrapNativeResult(response);
|
|
244
254
|
}
|
|
255
|
+
await this.closeCodexVirtual();
|
|
245
256
|
this.selectRemoteTarget(remote);
|
|
246
257
|
const workspaceId = workspaceRecordId(workspace.workspace);
|
|
247
258
|
this.pendingWorkspaceSelection = { targetDeviceId: remote.target.deviceId, workspaceId };
|
|
248
259
|
this.logger.info('Remote workspace opened', { targetDeviceId: shortId(remote.target.deviceId) });
|
|
249
260
|
return { ...this.status(), workspace };
|
|
250
261
|
}
|
|
262
|
+
async listCodexWorkspaces(targetDeviceId, signal) {
|
|
263
|
+
const remote = await this.ensureConnected(targetDeviceId, signal);
|
|
264
|
+
remote.features = await probeRemoteHostFeatures(remote.client, remote.clientVersion);
|
|
265
|
+
if (!remote.features.codex) {
|
|
266
|
+
throw new ClientModeError('FEATURE_NOT_SUPPORTED', 'The selected Host does not provide CodeX workspaces.');
|
|
267
|
+
}
|
|
268
|
+
return discoverCodexVirtualWorkspaces(new CodexRemoteClient(remote.client), signal);
|
|
269
|
+
}
|
|
270
|
+
async openCodexWorkspace(targetDeviceId, workspaceId, signal) {
|
|
271
|
+
const remote = await this.ensureConnected(targetDeviceId, signal);
|
|
272
|
+
remote.features = await probeRemoteHostFeatures(remote.client, remote.clientVersion);
|
|
273
|
+
if (!remote.features.codex) {
|
|
274
|
+
throw new ClientModeError('FEATURE_NOT_SUPPORTED', 'The selected Host does not provide CodeX workspaces.');
|
|
275
|
+
}
|
|
276
|
+
this.assertRemoteCompatible(remote);
|
|
277
|
+
const virtual = CodexVirtualHarness.remote(remote.client, {
|
|
278
|
+
deviceId: remote.target.deviceId,
|
|
279
|
+
name: remote.target.name,
|
|
280
|
+
});
|
|
281
|
+
let workspace;
|
|
282
|
+
try {
|
|
283
|
+
workspace = await virtual.selectWorkspace(workspaceId, signal);
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
await virtual.close();
|
|
287
|
+
throw new ClientModeError('WORKSPACE_NOT_FOUND', 'The selected CodeX workspace is no longer available.');
|
|
288
|
+
}
|
|
289
|
+
await this.closeCodexVirtual();
|
|
290
|
+
this.codexVirtual = virtual;
|
|
291
|
+
this.selectCodexTarget(virtual, remote);
|
|
292
|
+
const preferredSessionId = await virtual.preferredSessionId(signal);
|
|
293
|
+
this.pendingWorkspaceSelection = {
|
|
294
|
+
targetDeviceId: remote.target.deviceId,
|
|
295
|
+
workspaceId,
|
|
296
|
+
backend: 'codex',
|
|
297
|
+
...(preferredSessionId === undefined ? {} : { sessionId: preferredSessionId }),
|
|
298
|
+
};
|
|
299
|
+
this.logger.info('CodeX virtual workspace opened', { targetDeviceId: shortId(remote.target.deviceId) });
|
|
300
|
+
return { ...this.status(), workspace };
|
|
301
|
+
}
|
|
251
302
|
consumeWorkspaceSelection(selection) {
|
|
252
303
|
const pending = this.pendingWorkspaceSelection;
|
|
253
|
-
if (pending?.targetDeviceId === selection.targetDeviceId
|
|
304
|
+
if (pending?.targetDeviceId === selection.targetDeviceId
|
|
305
|
+
&& pending.workspaceId === selection.workspaceId
|
|
306
|
+
&& (pending.backend ?? 'harness') === (selection.backend ?? 'harness')) {
|
|
254
307
|
this.pendingWorkspaceSelection = undefined;
|
|
255
308
|
}
|
|
256
309
|
return this.status();
|
|
@@ -262,6 +315,8 @@ export class ClientModeRuntime {
|
|
|
262
315
|
this.proxySwitch?.selectLocal();
|
|
263
316
|
this.gatewaySwitch.selectLocal();
|
|
264
317
|
this.pendingWorkspaceSelection = undefined;
|
|
318
|
+
await this.closeCodexVirtual();
|
|
319
|
+
await this.closeCodexStreams(this.connected?.client);
|
|
265
320
|
await this.connected?.client.close().catch(() => undefined);
|
|
266
321
|
this.connected = undefined;
|
|
267
322
|
this.proxySwitch?.restore();
|
|
@@ -277,6 +332,157 @@ export class ClientModeRuntime {
|
|
|
277
332
|
}
|
|
278
333
|
return remote.client.rpc('fileviewer.call', { endpoint, payload }, signal);
|
|
279
334
|
}
|
|
335
|
+
activeRemote() {
|
|
336
|
+
return this.connected;
|
|
337
|
+
}
|
|
338
|
+
activeCodexRemote() {
|
|
339
|
+
const remote = this.activeRemote();
|
|
340
|
+
if (remote === undefined)
|
|
341
|
+
return undefined;
|
|
342
|
+
if (!remote.features.codex) {
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
return remote;
|
|
346
|
+
}
|
|
347
|
+
async openCodexStream(payload, signal) {
|
|
348
|
+
const remote = this.activeCodexRemote();
|
|
349
|
+
const value = record(payload);
|
|
350
|
+
if (typeof value.streamId !== 'string' || value.streamId.length === 0 || value.streamId.length > 128
|
|
351
|
+
|| typeof value.threadId !== 'string' || value.threadId.length === 0) {
|
|
352
|
+
throw new ClientModeError('INVALID_MESSAGE', 'A Codex stream and thread are required.');
|
|
353
|
+
}
|
|
354
|
+
if (this.codexStreams.has(value.streamId))
|
|
355
|
+
throw new ClientModeError('REQUEST_CONFLICT', 'The Codex stream is already open.');
|
|
356
|
+
let wake = () => undefined;
|
|
357
|
+
const stream = {
|
|
358
|
+
target: remote === undefined ? { kind: 'local' } : { kind: 'remote', client: remote.client },
|
|
359
|
+
frames: [],
|
|
360
|
+
unsubscribe: () => undefined,
|
|
361
|
+
close: async () => {
|
|
362
|
+
if (remote === undefined) {
|
|
363
|
+
await this.host?.codexCloseStream?.({ streamId: value.streamId }).catch(() => undefined);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
await remote.client.rpc('codex.app.stream.close', { streamId: value.streamId }).catch(() => undefined);
|
|
367
|
+
},
|
|
368
|
+
wake: () => wake(),
|
|
369
|
+
};
|
|
370
|
+
if (remote === undefined) {
|
|
371
|
+
const host = this.requireLocalCodex();
|
|
372
|
+
this.codexStreams.set(value.streamId, stream);
|
|
373
|
+
try {
|
|
374
|
+
const result = await host.codexOpenStream({ streamId: value.streamId, threadId: value.threadId }, this.publishLocalCodexFrame, signal);
|
|
375
|
+
return result;
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
this.codexStreams.delete(value.streamId);
|
|
379
|
+
stream.wake();
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
stream.unsubscribe = remote.client.onEvent(event => {
|
|
384
|
+
if (event.event === 'codex.app.frame' && isRecord(event.data) && event.data.streamId === value.streamId) {
|
|
385
|
+
this.appendCodexFrame(stream, event.data);
|
|
386
|
+
}
|
|
387
|
+
if (event.event === 'codex.app.stream.closed' && isRecord(event.data) && event.data.streamId === value.streamId) {
|
|
388
|
+
stream.closed = typeof event.data.reason === 'string' ? event.data.reason : 'closed';
|
|
389
|
+
stream.wake();
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
try {
|
|
393
|
+
// Subscribe before opening the Host stream so the first App Server
|
|
394
|
+
// notification cannot race past the loopback listener.
|
|
395
|
+
await remote.client.rpc('codex.app.stream.open', { streamId: value.streamId, threadId: value.threadId }, signal);
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
stream.unsubscribe();
|
|
399
|
+
throw error;
|
|
400
|
+
}
|
|
401
|
+
this.codexStreams.set(value.streamId, stream);
|
|
402
|
+
return { opened: true, streamId: value.streamId, threadId: value.threadId };
|
|
403
|
+
}
|
|
404
|
+
publishLocalCodexFrame = async (event, data) => {
|
|
405
|
+
const streamId = data.streamId;
|
|
406
|
+
const stream = this.codexStreams.get(streamId);
|
|
407
|
+
if (stream === undefined || stream.target.kind !== 'local')
|
|
408
|
+
return;
|
|
409
|
+
if (event === 'codex.app.frame') {
|
|
410
|
+
this.appendCodexFrame(stream, data);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const closed = data;
|
|
414
|
+
stream.closed = typeof closed.reason === 'string' ? closed.reason : 'closed';
|
|
415
|
+
stream.wake();
|
|
416
|
+
};
|
|
417
|
+
appendCodexFrame(stream, data) {
|
|
418
|
+
if (!isRecord(data) || !isRecord(data.frame) || typeof data.frame.method !== 'string')
|
|
419
|
+
return;
|
|
420
|
+
if (stream.frames.length >= 256) {
|
|
421
|
+
stream.closed = 'overflow';
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
stream.frames.push({ method: data.frame.method, params: data.frame.params });
|
|
425
|
+
}
|
|
426
|
+
stream.wake();
|
|
427
|
+
}
|
|
428
|
+
localCodexAvailable() {
|
|
429
|
+
return this.host?.codexStatus?.().available === true;
|
|
430
|
+
}
|
|
431
|
+
requireLocalCodex() {
|
|
432
|
+
if (!this.localCodexAvailable()
|
|
433
|
+
|| this.host?.codexCall === undefined
|
|
434
|
+
|| this.host.codexRespond === undefined
|
|
435
|
+
|| this.host.codexOpenStream === undefined
|
|
436
|
+
|| this.host.codexCloseStream === undefined) {
|
|
437
|
+
throw new ClientModeError('FEATURE_NOT_SUPPORTED', 'Local CodeX is disabled or unavailable on this Host.');
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
codexCall: this.host.codexCall.bind(this.host),
|
|
441
|
+
codexRespond: this.host.codexRespond.bind(this.host),
|
|
442
|
+
codexOpenStream: this.host.codexOpenStream.bind(this.host),
|
|
443
|
+
codexCloseStream: this.host.codexCloseStream.bind(this.host),
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
async nextCodexFrames(payload, signal) {
|
|
447
|
+
const value = record(payload);
|
|
448
|
+
if (typeof value.streamId !== 'string')
|
|
449
|
+
throw new ClientModeError('INVALID_MESSAGE', 'A Codex stream is required.');
|
|
450
|
+
const stream = this.codexStreams.get(value.streamId);
|
|
451
|
+
if (stream === undefined)
|
|
452
|
+
throw new ClientModeError('STREAM_NOT_FOUND', 'The Codex stream is not open.');
|
|
453
|
+
if (stream.frames.length === 0 && stream.closed === undefined)
|
|
454
|
+
await waitForCodexFrames(stream, signal);
|
|
455
|
+
const frames = stream.frames.splice(0, 100);
|
|
456
|
+
return {
|
|
457
|
+
streamId: value.streamId,
|
|
458
|
+
frames,
|
|
459
|
+
closed: stream.closed !== undefined,
|
|
460
|
+
...(stream.closed === undefined ? {} : { reason: stream.closed }),
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
async closeCodexStream(payload) {
|
|
464
|
+
const value = record(payload);
|
|
465
|
+
if (typeof value.streamId !== 'string')
|
|
466
|
+
throw new ClientModeError('INVALID_MESSAGE', 'A Codex stream is required.');
|
|
467
|
+
const stream = this.codexStreams.get(value.streamId);
|
|
468
|
+
if (stream === undefined)
|
|
469
|
+
return { closed: false, streamId: value.streamId };
|
|
470
|
+
this.codexStreams.delete(value.streamId);
|
|
471
|
+
stream.unsubscribe();
|
|
472
|
+
stream.wake();
|
|
473
|
+
await stream.close();
|
|
474
|
+
return { closed: true, streamId: value.streamId };
|
|
475
|
+
}
|
|
476
|
+
async closeCodexStreams(client) {
|
|
477
|
+
const targets = [...this.codexStreams.entries()].filter(([, stream]) => (client === undefined || (stream.target.kind === 'remote' && stream.target.client === client)));
|
|
478
|
+
await Promise.all(targets.map(async ([streamId, stream]) => {
|
|
479
|
+
this.codexStreams.delete(streamId);
|
|
480
|
+
stream.unsubscribe();
|
|
481
|
+
stream.closed = 'peer-disconnected';
|
|
482
|
+
stream.wake();
|
|
483
|
+
await stream.close();
|
|
484
|
+
}));
|
|
485
|
+
}
|
|
280
486
|
selectRemoteTarget(remote) {
|
|
281
487
|
const target = { deviceId: remote.target.deviceId, name: remote.target.name };
|
|
282
488
|
if (this.gatewaySwitch.supportsCarrier()) {
|
|
@@ -289,6 +495,20 @@ export class ClientModeRuntime {
|
|
|
289
495
|
list: remote.features.commandList,
|
|
290
496
|
}, target);
|
|
291
497
|
}
|
|
498
|
+
selectCodexTarget(virtual, remote) {
|
|
499
|
+
const target = { deviceId: remote.target.deviceId, name: remote.target.name };
|
|
500
|
+
if (this.gatewaySwitch.supportsCarrier()) {
|
|
501
|
+
this.gatewaySwitch.selectRemote(virtual, undefined, target);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
this.proxySwitch.selectRemote(virtual.api, target);
|
|
505
|
+
this.gatewaySwitch.selectRemote(request => virtual.invoke(request), { execute: true, list: true }, target);
|
|
506
|
+
}
|
|
507
|
+
async closeCodexVirtual() {
|
|
508
|
+
const virtual = this.codexVirtual;
|
|
509
|
+
this.codexVirtual = undefined;
|
|
510
|
+
await virtual?.close();
|
|
511
|
+
}
|
|
292
512
|
assertRemoteCompatible(remote) {
|
|
293
513
|
const localRemoteGateway = this.gatewaySwitch.supportsCarrier();
|
|
294
514
|
if ((localRemoteGateway && remote.features.remoteGateway)
|
|
@@ -337,8 +557,14 @@ export class ClientModeRuntime {
|
|
|
337
557
|
targetDeviceId: shortId(target.deviceId),
|
|
338
558
|
attempt,
|
|
339
559
|
reason: diagnosticReason(error),
|
|
340
|
-
...webrtcDiagnosticsLogFields(diagnostics),
|
|
341
560
|
});
|
|
561
|
+
if (diagnostics !== undefined) {
|
|
562
|
+
this.logger.debug('remote Harness WebRTC fallback diagnostics', {
|
|
563
|
+
targetDeviceId: shortId(target.deviceId),
|
|
564
|
+
attempt,
|
|
565
|
+
...webrtcDiagnosticsLogFields(diagnostics),
|
|
566
|
+
});
|
|
567
|
+
}
|
|
342
568
|
},
|
|
343
569
|
});
|
|
344
570
|
const attempts = this.config.forceRelay || rtcFactory === undefined
|
|
@@ -374,6 +600,7 @@ export class ClientModeRuntime {
|
|
|
374
600
|
return;
|
|
375
601
|
this.connected = undefined;
|
|
376
602
|
this.pendingWorkspaceSelection = undefined;
|
|
603
|
+
void this.closeCodexVirtual();
|
|
377
604
|
this.proxySwitch?.selectLocal();
|
|
378
605
|
this.gatewaySwitch.selectLocal();
|
|
379
606
|
void connectedClient.close().catch(() => undefined);
|
|
@@ -390,10 +617,21 @@ export class ClientModeRuntime {
|
|
|
390
617
|
negotiatedCapabilities: connectionDetails.negotiatedCapabilities,
|
|
391
618
|
webRtcEnabled: connectionDetails.webRtcEnabled,
|
|
392
619
|
}),
|
|
393
|
-
...webrtcDiagnosticsLogFields(connectionDetails?.webRtc?.diagnostics),
|
|
394
620
|
});
|
|
621
|
+
if (connectionDetails?.webRtc?.diagnostics !== undefined) {
|
|
622
|
+
this.logger.debug('remote Harness transport diagnostics', {
|
|
623
|
+
targetDeviceId: shortId(target.deviceId),
|
|
624
|
+
...webrtcDiagnosticsLogFields(connectionDetails.webRtc.diagnostics),
|
|
625
|
+
});
|
|
626
|
+
}
|
|
395
627
|
const features = await probeRemoteHostFeatures(connectedClient, serverDevice.clientVersion);
|
|
396
|
-
return {
|
|
628
|
+
return {
|
|
629
|
+
client: connectedClient,
|
|
630
|
+
target,
|
|
631
|
+
transport: connectedTransport,
|
|
632
|
+
features,
|
|
633
|
+
...(serverDevice.clientVersion === undefined ? {} : { clientVersion: serverDevice.clientVersion }),
|
|
634
|
+
};
|
|
397
635
|
}
|
|
398
636
|
catch (error) {
|
|
399
637
|
await client?.close().catch(() => undefined);
|
|
@@ -449,6 +687,12 @@ export class ClientModeRuntime {
|
|
|
449
687
|
throw new ClientModeError('INVALID_MESSAGE', 'A Host is required.');
|
|
450
688
|
return ok(await this.listRemoteWorkspaces(value.targetDeviceId, signal));
|
|
451
689
|
}
|
|
690
|
+
if (endpoint === 'codex.workspaces.list') {
|
|
691
|
+
const value = record(payload);
|
|
692
|
+
if (typeof value.targetDeviceId !== 'string')
|
|
693
|
+
throw new ClientModeError('INVALID_MESSAGE', 'A Host is required.');
|
|
694
|
+
return ok(await this.listCodexWorkspaces(value.targetDeviceId, signal));
|
|
695
|
+
}
|
|
452
696
|
if (endpoint === 'workspace.open') {
|
|
453
697
|
const value = record(payload);
|
|
454
698
|
if (typeof value.targetDeviceId !== 'string' || typeof value.path !== 'string') {
|
|
@@ -456,6 +700,13 @@ export class ClientModeRuntime {
|
|
|
456
700
|
}
|
|
457
701
|
return ok(await this.openRemoteWorkspace(value.targetDeviceId, value.path, signal));
|
|
458
702
|
}
|
|
703
|
+
if (endpoint === 'codex.workspace.open') {
|
|
704
|
+
const value = record(payload);
|
|
705
|
+
if (typeof value.targetDeviceId !== 'string' || typeof value.workspaceId !== 'string') {
|
|
706
|
+
throw new ClientModeError('INVALID_MESSAGE', 'A Host and CodeX Workspace are required.');
|
|
707
|
+
}
|
|
708
|
+
return ok(await this.openCodexWorkspace(value.targetDeviceId, value.workspaceId, signal));
|
|
709
|
+
}
|
|
459
710
|
if (endpoint === 'workspace.selection.consume') {
|
|
460
711
|
const value = record(payload);
|
|
461
712
|
if (typeof value.targetDeviceId !== 'string' || typeof value.workspaceId !== 'string') {
|
|
@@ -464,6 +715,8 @@ export class ClientModeRuntime {
|
|
|
464
715
|
return ok(this.consumeWorkspaceSelection({
|
|
465
716
|
targetDeviceId: value.targetDeviceId,
|
|
466
717
|
workspaceId: value.workspaceId,
|
|
718
|
+
...(value.backend === 'codex' ? { backend: 'codex' } : {}),
|
|
719
|
+
...(typeof value.sessionId === 'string' ? { sessionId: value.sessionId } : {}),
|
|
467
720
|
}));
|
|
468
721
|
}
|
|
469
722
|
if (endpoint === 'fileviewer.stat' || endpoint === 'fileviewer.readRange' || endpoint === 'fileviewer.list') {
|
|
@@ -472,6 +725,47 @@ export class ClientModeRuntime {
|
|
|
472
725
|
: endpoint === 'fileviewer.readRange' ? 'readRange' : 'list';
|
|
473
726
|
return ok(await this.callRemoteFileViewer(method, payload, signal));
|
|
474
727
|
}
|
|
728
|
+
if (endpoint === 'codex.call') {
|
|
729
|
+
const value = record(payload);
|
|
730
|
+
if (typeof value.method !== 'string' || !('params' in value)) {
|
|
731
|
+
throw new ClientModeError('INVALID_MESSAGE', 'A Codex method and params are required.');
|
|
732
|
+
}
|
|
733
|
+
const remote = this.activeCodexRemote();
|
|
734
|
+
if (remote !== undefined)
|
|
735
|
+
return ok(await new CodexRemoteClient(remote.client).request(value.method, value.params, signal));
|
|
736
|
+
const host = this.requireLocalCodex();
|
|
737
|
+
return ok(await host.codexCall(value, signal));
|
|
738
|
+
}
|
|
739
|
+
if (endpoint === 'codex.probe') {
|
|
740
|
+
const local = this.localCodexAvailable();
|
|
741
|
+
let remoteSupported = false;
|
|
742
|
+
const remote = this.activeRemote();
|
|
743
|
+
if (remote !== undefined) {
|
|
744
|
+
try {
|
|
745
|
+
remote.features = await probeRemoteHostFeatures(remote.client, remote.clientVersion);
|
|
746
|
+
remoteSupported = remote.features.codex;
|
|
747
|
+
}
|
|
748
|
+
catch (error) {
|
|
749
|
+
if (!local)
|
|
750
|
+
throw error;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return ok({ supported: local || remoteSupported, local, remote: remoteSupported });
|
|
754
|
+
}
|
|
755
|
+
if (endpoint === 'codex.respond') {
|
|
756
|
+
const value = record(payload);
|
|
757
|
+
const remote = this.activeCodexRemote();
|
|
758
|
+
if (remote !== undefined)
|
|
759
|
+
return ok(await remote.client.rpc('codex.app.respond', value, signal));
|
|
760
|
+
const host = this.requireLocalCodex();
|
|
761
|
+
return ok(await host.codexRespond(value, signal));
|
|
762
|
+
}
|
|
763
|
+
if (endpoint === 'codex.stream.open')
|
|
764
|
+
return ok(await this.openCodexStream(payload, signal));
|
|
765
|
+
if (endpoint === 'codex.stream.next')
|
|
766
|
+
return ok(await this.nextCodexFrames(payload, signal));
|
|
767
|
+
if (endpoint === 'codex.stream.close')
|
|
768
|
+
return ok(await this.closeCodexStream(payload));
|
|
475
769
|
if (endpoint === 'host.account.login') {
|
|
476
770
|
if (this.host === undefined)
|
|
477
771
|
throw new ClientModeError('METHOD_NOT_ALLOWED', 'This plugin is not running as a Host.');
|
|
@@ -666,6 +960,7 @@ export function remoteHostFeatures(clientVersion) {
|
|
|
666
960
|
fileViewer: isVersionAtLeast(clientVersion, REMOTE_FILE_VIEWER_MIN_VERSION),
|
|
667
961
|
apiProxy: true,
|
|
668
962
|
remoteGateway: false,
|
|
963
|
+
codex: false,
|
|
669
964
|
};
|
|
670
965
|
}
|
|
671
966
|
export async function probeRemoteHostFeatures(client, clientVersion) {
|
|
@@ -694,8 +989,35 @@ export async function probeRemoteHostFeatures(client, clientVersion) {
|
|
|
694
989
|
fileViewer: capabilities.has('fileviewer.read.v1'),
|
|
695
990
|
apiProxy,
|
|
696
991
|
remoteGateway,
|
|
992
|
+
codex: capabilities.has('codex.appserver.v1'),
|
|
697
993
|
};
|
|
698
994
|
}
|
|
995
|
+
async function waitForCodexFrames(stream, signal) {
|
|
996
|
+
if (signal?.aborted)
|
|
997
|
+
throw new ClientModeError('RPC_ABORTED', 'The Codex event poll was cancelled.');
|
|
998
|
+
await new Promise((resolve, reject) => {
|
|
999
|
+
const previousWake = stream.wake;
|
|
1000
|
+
const timer = setTimeout(done, 25_000);
|
|
1001
|
+
const onAbort = () => {
|
|
1002
|
+
cleanup();
|
|
1003
|
+
reject(new ClientModeError('RPC_ABORTED', 'The Codex event poll was cancelled.'));
|
|
1004
|
+
};
|
|
1005
|
+
function cleanup() {
|
|
1006
|
+
clearTimeout(timer);
|
|
1007
|
+
stream.wake = previousWake;
|
|
1008
|
+
signal?.removeEventListener('abort', onAbort);
|
|
1009
|
+
}
|
|
1010
|
+
function done() {
|
|
1011
|
+
cleanup();
|
|
1012
|
+
resolve();
|
|
1013
|
+
}
|
|
1014
|
+
stream.wake = () => {
|
|
1015
|
+
previousWake();
|
|
1016
|
+
done();
|
|
1017
|
+
};
|
|
1018
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
699
1021
|
function isVersionAtLeast(value, minimum) {
|
|
700
1022
|
const match = value?.match(/^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
|
|
701
1023
|
if (match === undefined || match === null)
|