ds-harness-remote 0.4.26 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/README.md +6 -6
  2. package/dist/acp/adapter.d.ts +19 -0
  3. package/dist/acp/adapter.d.ts.map +1 -0
  4. package/dist/acp/adapter.js +14 -0
  5. package/dist/acp/adapter.js.map +1 -0
  6. package/dist/acp/adapters/antigravity/executor.d.ts +20 -0
  7. package/dist/acp/adapters/antigravity/executor.d.ts.map +1 -0
  8. package/dist/acp/adapters/antigravity/executor.js +99 -0
  9. package/dist/acp/adapters/antigravity/executor.js.map +1 -0
  10. package/dist/acp/adapters/antigravity/image-store.d.ts +9 -0
  11. package/dist/acp/adapters/antigravity/image-store.d.ts.map +1 -0
  12. package/dist/acp/adapters/antigravity/image-store.js +116 -0
  13. package/dist/acp/adapters/antigravity/image-store.js.map +1 -0
  14. package/dist/acp/adapters/antigravity/index.d.ts +5 -0
  15. package/dist/acp/adapters/antigravity/index.d.ts.map +1 -0
  16. package/dist/acp/adapters/antigravity/index.js +5 -0
  17. package/dist/acp/adapters/antigravity/index.js.map +1 -0
  18. package/dist/acp/adapters/antigravity/models.d.ts +35 -0
  19. package/dist/acp/adapters/antigravity/models.d.ts.map +1 -0
  20. package/dist/acp/adapters/antigravity/models.js +59 -0
  21. package/dist/acp/adapters/antigravity/models.js.map +1 -0
  22. package/dist/acp/adapters/antigravity/service.d.ts +35 -0
  23. package/dist/acp/adapters/antigravity/service.d.ts.map +1 -0
  24. package/dist/acp/adapters/antigravity/service.js +67 -0
  25. package/dist/acp/adapters/antigravity/service.js.map +1 -0
  26. package/dist/acp/adapters/antigravity/transcript-loader.d.ts +21 -0
  27. package/dist/acp/adapters/antigravity/transcript-loader.d.ts.map +1 -0
  28. package/dist/acp/adapters/antigravity/transcript-loader.js +356 -0
  29. package/dist/acp/adapters/antigravity/transcript-loader.js.map +1 -0
  30. package/dist/acp/adapters/antigravity/transcript-watcher.d.ts +28 -0
  31. package/dist/acp/adapters/antigravity/transcript-watcher.d.ts.map +1 -0
  32. package/dist/acp/adapters/antigravity/transcript-watcher.js +157 -0
  33. package/dist/acp/adapters/antigravity/transcript-watcher.js.map +1 -0
  34. package/dist/acp/adapters/antigravity/types.d.ts +56 -0
  35. package/dist/acp/adapters/antigravity/types.d.ts.map +1 -0
  36. package/dist/acp/adapters/antigravity/types.js +2 -0
  37. package/dist/acp/adapters/antigravity/types.js.map +1 -0
  38. package/dist/acp/adapters/antigravity-process.d.ts +69 -0
  39. package/dist/acp/adapters/antigravity-process.d.ts.map +1 -0
  40. package/dist/acp/adapters/antigravity-process.js +566 -0
  41. package/dist/acp/adapters/antigravity-process.js.map +1 -0
  42. package/dist/acp/adapters/antigravity.d.ts +8 -0
  43. package/dist/acp/adapters/antigravity.d.ts.map +1 -0
  44. package/dist/acp/adapters/antigravity.js +9 -0
  45. package/dist/acp/adapters/antigravity.js.map +1 -0
  46. package/dist/acp/adapters/cursor-process.d.ts +75 -0
  47. package/dist/acp/adapters/cursor-process.d.ts.map +1 -0
  48. package/dist/acp/adapters/cursor-process.js +273 -0
  49. package/dist/acp/adapters/cursor-process.js.map +1 -0
  50. package/dist/acp/adapters/cursor.d.ts +8 -0
  51. package/dist/acp/adapters/cursor.d.ts.map +1 -0
  52. package/dist/acp/adapters/cursor.js +8 -0
  53. package/dist/acp/adapters/cursor.js.map +1 -0
  54. package/dist/acp/gateway.d.ts +108 -0
  55. package/dist/acp/gateway.d.ts.map +1 -0
  56. package/dist/acp/gateway.js +1040 -0
  57. package/dist/acp/gateway.js.map +1 -0
  58. package/dist/acp/image-content.d.ts +19 -0
  59. package/dist/acp/image-content.d.ts.map +1 -0
  60. package/dist/acp/image-content.js +76 -0
  61. package/dist/acp/image-content.js.map +1 -0
  62. package/dist/acp/method-policy.d.ts +239 -0
  63. package/dist/acp/method-policy.d.ts.map +1 -0
  64. package/dist/acp/method-policy.js +102 -0
  65. package/dist/acp/method-policy.js.map +1 -0
  66. package/dist/acp/peer-bridge.d.ts +59 -0
  67. package/dist/acp/peer-bridge.d.ts.map +1 -0
  68. package/dist/acp/peer-bridge.js +367 -0
  69. package/dist/acp/peer-bridge.js.map +1 -0
  70. package/dist/acp/virtual-harness.d.ts +181 -0
  71. package/dist/acp/virtual-harness.d.ts.map +1 -0
  72. package/dist/acp/virtual-harness.js +1556 -0
  73. package/dist/acp/virtual-harness.js.map +1 -0
  74. package/dist/acp/workspace-tools.d.ts +13 -0
  75. package/dist/acp/workspace-tools.d.ts.map +1 -0
  76. package/dist/acp/workspace-tools.js +46 -0
  77. package/dist/acp/workspace-tools.js.map +1 -0
  78. package/dist/acp.d.ts.map +1 -1
  79. package/dist/acp.js +1 -0
  80. package/dist/acp.js.map +1 -1
  81. package/dist/api-proxy-switch.d.ts +2 -0
  82. package/dist/api-proxy-switch.d.ts.map +1 -1
  83. package/dist/api-proxy-switch.js +9 -0
  84. package/dist/api-proxy-switch.js.map +1 -1
  85. package/dist/cli.d.ts.map +1 -1
  86. package/dist/cli.js +4 -3
  87. package/dist/cli.js.map +1 -1
  88. package/dist/client-runtime.d.ts +25 -1
  89. package/dist/client-runtime.d.ts.map +1 -1
  90. package/dist/client-runtime.js +222 -8
  91. package/dist/client-runtime.js.map +1 -1
  92. package/dist/client.github.js +327 -190
  93. package/dist/client.js +327 -190
  94. package/dist/client.js.map +1 -1
  95. package/dist/codex/domain.d.ts +4 -1
  96. package/dist/codex/domain.d.ts.map +1 -1
  97. package/dist/codex/domain.js +54 -4
  98. package/dist/codex/domain.js.map +1 -1
  99. package/dist/codex/virtual-harness.d.ts.map +1 -1
  100. package/dist/codex/virtual-harness.js +3 -0
  101. package/dist/codex/virtual-harness.js.map +1 -1
  102. package/dist/codex-workspace-bridge.d.ts +7 -105
  103. package/dist/codex-workspace-bridge.d.ts.map +1 -1
  104. package/dist/codex-workspace-bridge.js +10 -498
  105. package/dist/codex-workspace-bridge.js.map +1 -1
  106. package/dist/config.d.ts +28 -10
  107. package/dist/config.d.ts.map +1 -1
  108. package/dist/config.js +49 -5
  109. package/dist/config.js.map +1 -1
  110. package/dist/connection-controller.d.ts.map +1 -1
  111. package/dist/connection-controller.js +7 -4
  112. package/dist/connection-controller.js.map +1 -1
  113. package/dist/control-runtime.d.ts +1 -0
  114. package/dist/control-runtime.d.ts.map +1 -1
  115. package/dist/control-runtime.js +65 -10
  116. package/dist/control-runtime.js.map +1 -1
  117. package/dist/harness-version.d.ts.map +1 -1
  118. package/dist/harness-version.js +16 -3
  119. package/dist/harness-version.js.map +1 -1
  120. package/dist/index.d.ts +4 -0
  121. package/dist/index.d.ts.map +1 -1
  122. package/dist/index.js +13197 -8204
  123. package/dist/index.js.map +4 -4
  124. package/dist/loopback-host.d.ts +2 -2
  125. package/dist/loopback-host.d.ts.map +1 -1
  126. package/dist/loopback-host.js +41 -16
  127. package/dist/loopback-host.js.map +1 -1
  128. package/dist/remote-typert-gateway.d.ts.map +1 -1
  129. package/dist/remote-typert-gateway.js +7 -4
  130. package/dist/remote-typert-gateway.js.map +1 -1
  131. package/dist/rpc-router.d.ts +6 -5
  132. package/dist/rpc-router.d.ts.map +1 -1
  133. package/dist/rpc-router.js +49 -18
  134. package/dist/rpc-router.js.map +1 -1
  135. package/dist/server-api.d.ts +2 -0
  136. package/dist/server-api.d.ts.map +1 -1
  137. package/dist/server-api.js +5 -0
  138. package/dist/server-api.js.map +1 -1
  139. package/dist/server-connection.d.ts.map +1 -1
  140. package/dist/server-connection.js +15 -0
  141. package/dist/server-connection.js.map +1 -1
  142. package/dist/service.d.ts +30 -1
  143. package/dist/service.d.ts.map +1 -1
  144. package/dist/service.js +114 -12
  145. package/dist/service.js.map +1 -1
  146. package/dist/session-format-compat.d.ts +1 -0
  147. package/dist/session-format-compat.d.ts.map +1 -1
  148. package/dist/session-format-compat.js +14 -0
  149. package/dist/session-format-compat.js.map +1 -1
  150. package/dist/tui-command.js +2 -0
  151. package/dist/tui-command.js.map +1 -1
  152. package/dist/typert-gateway-switch.d.ts +2 -0
  153. package/dist/typert-gateway-switch.d.ts.map +1 -1
  154. package/dist/typert-gateway-switch.js +16 -0
  155. package/dist/typert-gateway-switch.js.map +1 -1
  156. package/dist/version.d.ts +1 -1
  157. package/dist/version.d.ts.map +1 -1
  158. package/dist/version.js +1 -1
  159. package/dist/version.js.map +1 -1
  160. package/dist/workspace-bridge.d.ts +117 -0
  161. package/dist/workspace-bridge.d.ts.map +1 -0
  162. package/dist/workspace-bridge.js +512 -0
  163. package/dist/workspace-bridge.js.map +1 -0
  164. package/dsh-plugin.json +1 -1
  165. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import { LoopbackPreview } from './loopback-preview.js';
2
- import { CodexRemoteClient, RemoteClientCore } from '@dsh-remote/client-core';
2
+ import { parseRemoteWorkspaceTypes, } from '@dsh-remote/protocol';
3
+ import { AgentAcpClient, CodexRemoteClient, RemoteClientCore, remoteWorkspaceTypeAvailable } from '@dsh-remote/client-core';
3
4
  import { AdaptiveTransport, stunOnlyIceServers, } from '@dsh-remote/webrtc';
4
5
  import { ApiProxySwitch } from './api-proxy-switch.js';
5
6
  import { ClientSecureTransport } from './client-secure-transport.js';
@@ -9,13 +10,22 @@ import { harnessSessionGeneration } from './harness-version.js';
9
10
  import { RemoteHarnessApiProxy } from './remote-api-proxy.js';
10
11
  import { RemoteTypertGateway } from './remote-typert-gateway.js';
11
12
  import { codexProjectWorkspaceId, CodexVirtualHarness, discoverCodexVirtualWorkspaces, } from './codex/virtual-harness.js';
13
+ import { AcpVirtualHarness, discoverAcpVirtualWorkspaces, } from './acp/virtual-harness.js';
12
14
  import { ServerApiError, } from './server-api.js';
13
15
  import { TypertGatewaySwitch } from './typert-gateway-switch.js';
14
16
  import { loadNodeRtcFactory } from './werift-rtc.js';
17
+ import { safeErrorCode } from './safe-error.js';
15
18
  const REMOTE_COMMAND_LIST_MIN_VERSION = [0, 3, 16];
16
19
  const REMOTE_FILE_VIEWER_MIN_VERSION = [0, 3, 17];
17
20
  const DIRECT_WEBRTC_NEGOTIATE_TIMEOUT_MS = 12_000;
18
21
  const DIRECT_LAN_PROGRESS_DISPLAY_MS = 1_400;
22
+ const HOST_AUTHORIZATION_ERRORS = new Set([
23
+ 'ACCOUNT_AUTH_REQUIRED',
24
+ 'AUTH_INVALID',
25
+ 'DEVICE_OWNERSHIP_REQUIRED',
26
+ 'DEVICE_REVOKED',
27
+ 'TOKEN_EXPIRED',
28
+ ]);
19
29
  export class ClientModeRuntime {
20
30
  config;
21
31
  identities;
@@ -28,6 +38,8 @@ export class ClientModeRuntime {
28
38
  connected;
29
39
  pendingWorkspaceSelection;
30
40
  codexVirtual;
41
+ cursorVirtual;
42
+ acpVirtualBackend = 'cursor';
31
43
  proxySwitch;
32
44
  gatewaySwitch;
33
45
  codexStreams = new Map();
@@ -55,6 +67,39 @@ export class ClientModeRuntime {
55
67
  deviceId: shortId(this.identity.deviceId),
56
68
  fingerprint: this.identity.fingerprint,
57
69
  });
70
+ // A previously authorized Client should make the local Host controllable
71
+ // on startup as well. Do not register an anonymous Client just to probe:
72
+ // only persisted Client credentials opt into this default.
73
+ if (this.config.hostControl?.enabled !== false
74
+ && this.host !== undefined
75
+ && this.server.hasStoredAuthorization !== undefined) {
76
+ try {
77
+ if (await this.server.hasStoredAuthorization()
78
+ && (this.host.hasStoredAuthorization === undefined || !await this.host.hasStoredAuthorization())) {
79
+ await this.authorizeHostByDefault();
80
+ }
81
+ }
82
+ catch (error) {
83
+ this.logger.warn('automatic Host authorization failed', { code: safeErrorCode(error) });
84
+ }
85
+ }
86
+ }
87
+ async authorizeHostByDefault() {
88
+ try {
89
+ if (this.host === undefined)
90
+ return;
91
+ if (this.config.hostControl?.enabled === false)
92
+ return;
93
+ if (this.host.hostStatus().authorized)
94
+ return;
95
+ if (this.host.hasStoredAuthorization !== undefined && await this.host.hasStoredAuthorization())
96
+ return;
97
+ const credentials = await this.server.authenticate(this.requireIdentity());
98
+ await this.host.authorizeHostAsOwned(credentials.accessToken, credentials.account);
99
+ }
100
+ catch (error) {
101
+ this.logger.warn('automatic Host authorization failed', { code: safeErrorCode(error) });
102
+ }
58
103
  }
59
104
  registerControl(connection, webServer) {
60
105
  return registerControlRoute(connection, (endpoint, payload, signal) => this.handleControl(endpoint, payload, signal), webServer);
@@ -87,7 +132,9 @@ export class ClientModeRuntime {
87
132
  ...(this.pendingWorkspaceSelection === undefined
88
133
  ? {}
89
134
  : { workspaceSelection: { ...this.pendingWorkspaceSelection } }),
90
- backend: this.codexVirtual === undefined ? 'harness' : 'codex',
135
+ backend: this.cursorVirtual !== undefined
136
+ ? this.acpVirtualBackend
137
+ : this.codexVirtual === undefined ? 'harness' : 'codex',
91
138
  hostAuthorizationAvailable: this.host !== undefined,
92
139
  ...(this.host === undefined ? {} : { host: this.host.hostStatus() }),
93
140
  };
@@ -122,6 +169,7 @@ export class ClientModeRuntime {
122
169
  };
123
170
  }
124
171
  async devices() {
172
+ this.assertHostAuthorizationForDeviceDiscovery();
125
173
  this.requireIdentity();
126
174
  const serverDevices = await this.server.listDevices();
127
175
  const remoteDevices = serverDevices.filter(device => device.deviceId !== this.host?.hostStatus().deviceId);
@@ -131,6 +179,23 @@ export class ClientModeRuntime {
131
179
  return { ...device, ...presence };
132
180
  }));
133
181
  }
182
+ /**
183
+ * Device discovery is exposed through the local app control route. When this
184
+ * installation also runs a Host, keep that route closed after the Host's
185
+ * Server credential has become terminally invalid. The Client credential can
186
+ * remain usable for a short time after a revoke, so checking only
187
+ * `ClientServerApi.listDevices()` would otherwise leak the device directory
188
+ * from a Host that the user has already been told to re-authorize.
189
+ */
190
+ assertHostAuthorizationForDeviceDiscovery() {
191
+ const status = this.host?.hostStatus();
192
+ if (status === undefined || status.error === undefined || !HOST_AUTHORIZATION_ERRORS.has(status.error))
193
+ return;
194
+ const message = status.error === 'DEVICE_REVOKED'
195
+ ? 'The local Host was revoked on the Server. Sign out and authorize this Host again.'
196
+ : 'The local Host authorization is no longer valid. Sign out and authorize this Host again.';
197
+ throw new ClientModeError(status.error, message);
198
+ }
134
199
  async authorizeClientWithAccount(email, password) {
135
200
  let authorization;
136
201
  try {
@@ -143,6 +208,7 @@ export class ClientModeRuntime {
143
208
  this.server.bindIdentity(this.identity);
144
209
  authorization = await this.server.authorizeWithAccount(this.identity, email, password);
145
210
  }
211
+ await this.authorizeHostByDefault();
146
212
  this.logger.info('Client account authorized');
147
213
  return authorization;
148
214
  }
@@ -158,6 +224,8 @@ export class ClientModeRuntime {
158
224
  });
159
225
  if (result.status === 'complete')
160
226
  this.logger.info('Client account authorized with QR login');
227
+ if (result.status === 'complete')
228
+ await this.authorizeHostByDefault();
161
229
  return result;
162
230
  }
163
231
  async clearClientAuthorization() {
@@ -166,6 +234,7 @@ export class ClientModeRuntime {
166
234
  this.connectionProgress = undefined;
167
235
  this.pendingWorkspaceSelection = undefined;
168
236
  await this.closeCodexVirtual();
237
+ await this.closeCursorVirtual();
169
238
  this.proxySwitch?.selectLocal();
170
239
  await this.closePreview();
171
240
  this.gatewaySwitch.selectLocal();
@@ -189,6 +258,7 @@ export class ClientModeRuntime {
189
258
  async setMode(mode, targetDeviceId, signal) {
190
259
  if (mode === 'local') {
191
260
  await this.closeCodexVirtual();
261
+ await this.closeCursorVirtual();
192
262
  this.proxySwitch?.selectLocal();
193
263
  await this.closePreview();
194
264
  this.gatewaySwitch.selectLocal();
@@ -219,6 +289,7 @@ export class ClientModeRuntime {
219
289
  this.clearConnectionProgress(next.progressRunId);
220
290
  this.pendingWorkspaceSelection = undefined;
221
291
  await this.closeCodexVirtual();
292
+ await this.closeCursorVirtual();
222
293
  this.selectRemoteTarget(next);
223
294
  await this.closeCodexStreams(previous?.client);
224
295
  await previous?.client.close().catch(() => undefined);
@@ -279,6 +350,7 @@ export class ClientModeRuntime {
279
350
  workspace = unwrapNativeResult(response);
280
351
  }
281
352
  await this.closeCodexVirtual();
353
+ await this.closeCursorVirtual();
282
354
  this.selectRemoteTarget(remote, transport);
283
355
  const workspaceId = workspaceRecordId(workspace.workspace);
284
356
  this.pendingWorkspaceSelection = { targetDeviceId: remote.target.deviceId, workspaceId };
@@ -313,6 +385,7 @@ export class ClientModeRuntime {
313
385
  throw new ClientModeError('WORKSPACE_NOT_FOUND', 'The selected CodeX workspace is no longer available.');
314
386
  }
315
387
  await this.closeCodexVirtual();
388
+ await this.closeCursorVirtual();
316
389
  this.codexVirtual = virtual;
317
390
  this.selectCodexTarget(virtual, remote);
318
391
  const preferredSessionId = await virtual.preferredSessionId(signal);
@@ -346,6 +419,81 @@ export class ClientModeRuntime {
346
419
  }
347
420
  return this.openCodexWorkspace(targetDeviceId, codexProjectWorkspaceId(project.id), signal);
348
421
  }
422
+ async listCursorWorkspaces(targetDeviceId, signal, backend = 'cursor') {
423
+ const remote = await this.ensureConnected(targetDeviceId, signal);
424
+ remote.features = await probeRemoteHostFeatures(remote.client, remote.clientVersion);
425
+ if (backend === 'antigravity' ? !remote.features.antigravity : !remote.features.cursor) {
426
+ throw new ClientModeError('FEATURE_NOT_SUPPORTED', `The selected Host does not provide ${backend} workspaces.`);
427
+ }
428
+ if (this.cursorVirtual !== undefined && this.connected?.target.deviceId === targetDeviceId && this.acpVirtualBackend === backend) {
429
+ return this.cursorVirtual.workspaces();
430
+ }
431
+ const acpClient = new AgentAcpClient(remote.client);
432
+ const workspaces = await discoverAcpVirtualWorkspaces(acpClient, backend, signal);
433
+ return workspaces;
434
+ }
435
+ async openCursorWorkspace(targetDeviceId, workspaceId, signal, backend = 'cursor') {
436
+ const remote = await this.ensureConnected(targetDeviceId, signal);
437
+ remote.features = await probeRemoteHostFeatures(remote.client, remote.clientVersion);
438
+ if (backend === 'antigravity' ? !remote.features.antigravity : !remote.features.cursor) {
439
+ throw new ClientModeError('FEATURE_NOT_SUPPORTED', `The selected Host does not provide ${backend} workspaces.`);
440
+ }
441
+ this.assertLocalHarnessCarrierAvailable();
442
+ const virtual = AcpVirtualHarness.remote(remote.client, {
443
+ deviceId: remote.target.deviceId,
444
+ name: remote.target.name,
445
+ }, backend, remote.features.acpModelLoadsSession ? ['agent.acp.antigravity.model-load.v1'] : []);
446
+ let workspace;
447
+ try {
448
+ workspace = await virtual.selectWorkspace(workspaceId);
449
+ }
450
+ catch {
451
+ await virtual.close();
452
+ throw new ClientModeError('WORKSPACE_NOT_FOUND', `The selected ${backend} workspace is no longer available.`);
453
+ }
454
+ await this.closeCodexVirtual();
455
+ await this.closeCursorVirtual();
456
+ this.cursorVirtual = virtual;
457
+ this.acpVirtualBackend = backend;
458
+ this.selectCursorTarget(virtual, remote);
459
+ const preferredSessionId = await virtual.preferredSessionId();
460
+ this.pendingWorkspaceSelection = {
461
+ targetDeviceId: remote.target.deviceId,
462
+ workspaceId: workspace.workspaceId,
463
+ backend,
464
+ ...(preferredSessionId === undefined ? {} : { sessionId: preferredSessionId }),
465
+ };
466
+ this.logger.info(`${backend} virtual workspace opened`, { targetDeviceId: shortId(remote.target.deviceId) });
467
+ return { ...this.status(), workspace };
468
+ }
469
+ async createCursorWorkspace(targetDeviceId, path, signal, backend = 'cursor') {
470
+ const trimmedPath = path.trim();
471
+ if (trimmedPath === '')
472
+ throw new ClientModeError('INVALID_MESSAGE', `A ${backend} project directory is required.`);
473
+ const remote = await this.ensureConnected(targetDeviceId, signal);
474
+ remote.features = await probeRemoteHostFeatures(remote.client, remote.clientVersion);
475
+ if (backend === 'antigravity' ? !remote.features.antigravity : !remote.features.cursor) {
476
+ throw new ClientModeError('FEATURE_NOT_SUPPORTED', `The selected Host does not provide ${backend} workspaces.`);
477
+ }
478
+ this.assertLocalHarnessCarrierAvailable();
479
+ const virtual = AcpVirtualHarness.remote(remote.client, {
480
+ deviceId: remote.target.deviceId,
481
+ name: remote.target.name,
482
+ }, backend, remote.features.acpModelLoadsSession ? ['agent.acp.antigravity.model-load.v1'] : []);
483
+ const workspace = await virtual.selectOrCreateWorkspace(trimmedPath);
484
+ await this.closeCodexVirtual();
485
+ await this.closeCursorVirtual();
486
+ this.cursorVirtual = virtual;
487
+ this.acpVirtualBackend = backend;
488
+ this.selectCursorTarget(virtual, remote);
489
+ this.pendingWorkspaceSelection = {
490
+ targetDeviceId: remote.target.deviceId,
491
+ workspaceId: workspace.workspaceId,
492
+ backend,
493
+ };
494
+ this.logger.info(`${backend} virtual workspace created`, { targetDeviceId: shortId(remote.target.deviceId) });
495
+ return { ...this.status(), workspace };
496
+ }
349
497
  consumeWorkspaceSelection(selection) {
350
498
  const pending = this.pendingWorkspaceSelection;
351
499
  if (pending?.targetDeviceId === selection.targetDeviceId
@@ -365,6 +513,7 @@ export class ClientModeRuntime {
365
513
  this.gatewaySwitch.selectLocal();
366
514
  this.pendingWorkspaceSelection = undefined;
367
515
  await this.closeCodexVirtual();
516
+ await this.closeCursorVirtual();
368
517
  await this.closeCodexStreams(this.connected?.client);
369
518
  await this.connected?.client.close().catch(() => undefined);
370
519
  this.connected = undefined;
@@ -563,6 +712,20 @@ export class ClientModeRuntime {
563
712
  this.codexVirtual = undefined;
564
713
  await virtual?.close();
565
714
  }
715
+ async closeCursorVirtual() {
716
+ const virtual = this.cursorVirtual;
717
+ this.cursorVirtual = undefined;
718
+ await virtual?.close();
719
+ }
720
+ selectCursorTarget(virtual, remote) {
721
+ const target = { deviceId: remote.target.deviceId, name: remote.target.name };
722
+ if (this.gatewaySwitch.supportsCarrier()) {
723
+ this.gatewaySwitch.selectRemote(virtual, undefined, target);
724
+ return;
725
+ }
726
+ this.proxySwitch.selectRemote(virtual.api, target);
727
+ this.gatewaySwitch.selectRemote(request => virtual.invoke(request), { execute: true, list: true }, target);
728
+ }
566
729
  assertRemoteCompatible(remote) {
567
730
  this.selectHarnessRemoteTransport(remote);
568
731
  }
@@ -682,10 +845,11 @@ export class ClientModeRuntime {
682
845
  this.connectionProgress = undefined;
683
846
  this.pendingWorkspaceSelection = undefined;
684
847
  void this.closeCodexVirtual();
685
- this.proxySwitch?.selectLocal();
686
- this.gatewaySwitch.selectLocal();
848
+ void this.closeCursorVirtual();
849
+ this.proxySwitch?.disconnectRemote();
850
+ this.gatewaySwitch.disconnectRemote();
687
851
  void connectedClient.close().catch(() => undefined);
688
- this.logger.warn('remote Harness transport closed; falling back to local mode', {
852
+ this.logger.warn('remote Harness transport closed; keeping Remote authority disconnected', {
689
853
  targetDeviceId: shortId(target.deviceId),
690
854
  });
691
855
  });
@@ -837,6 +1001,46 @@ export class ClientModeRuntime {
837
1001
  }
838
1002
  return ok(await this.createCodexWorkspace(value.targetDeviceId, value.path, signal));
839
1003
  }
1004
+ if (endpoint === 'cursor.workspaces.list') {
1005
+ const value = record(payload);
1006
+ if (typeof value.targetDeviceId !== 'string')
1007
+ throw new ClientModeError('INVALID_MESSAGE', 'A Host is required.');
1008
+ return ok(await this.listCursorWorkspaces(value.targetDeviceId, signal, 'cursor'));
1009
+ }
1010
+ if (endpoint === 'cursor.workspace.open') {
1011
+ const value = record(payload);
1012
+ if (typeof value.targetDeviceId !== 'string' || typeof value.workspaceId !== 'string') {
1013
+ throw new ClientModeError('INVALID_MESSAGE', 'A Host and Cursor Workspace are required.');
1014
+ }
1015
+ return ok(await this.openCursorWorkspace(value.targetDeviceId, value.workspaceId, signal, 'cursor'));
1016
+ }
1017
+ if (endpoint === 'cursor.workspace.create') {
1018
+ const value = record(payload);
1019
+ if (typeof value.targetDeviceId !== 'string' || typeof value.path !== 'string') {
1020
+ throw new ClientModeError('INVALID_MESSAGE', 'A Host and Cursor project directory are required.');
1021
+ }
1022
+ return ok(await this.createCursorWorkspace(value.targetDeviceId, value.path, signal, 'cursor'));
1023
+ }
1024
+ if (endpoint === 'antigravity.workspaces.list') {
1025
+ const value = record(payload);
1026
+ if (typeof value.targetDeviceId !== 'string')
1027
+ throw new ClientModeError('INVALID_MESSAGE', 'A Host is required.');
1028
+ return ok(await this.listCursorWorkspaces(value.targetDeviceId, signal, 'antigravity'));
1029
+ }
1030
+ if (endpoint === 'antigravity.workspace.open') {
1031
+ const value = record(payload);
1032
+ if (typeof value.targetDeviceId !== 'string' || typeof value.workspaceId !== 'string') {
1033
+ throw new ClientModeError('INVALID_MESSAGE', 'A Host and Antigravity Workspace are required.');
1034
+ }
1035
+ return ok(await this.openCursorWorkspace(value.targetDeviceId, value.workspaceId, signal, 'antigravity'));
1036
+ }
1037
+ if (endpoint === 'antigravity.workspace.create') {
1038
+ const value = record(payload);
1039
+ if (typeof value.targetDeviceId !== 'string' || typeof value.path !== 'string') {
1040
+ throw new ClientModeError('INVALID_MESSAGE', 'A Host and Antigravity project directory are required.');
1041
+ }
1042
+ return ok(await this.createCursorWorkspace(value.targetDeviceId, value.path, signal, 'antigravity'));
1043
+ }
840
1044
  if (endpoint === 'workspace.selection.consume') {
841
1045
  const value = record(payload);
842
1046
  if (typeof value.targetDeviceId !== 'string' || typeof value.workspaceId !== 'string') {
@@ -845,7 +1049,7 @@ export class ClientModeRuntime {
845
1049
  return ok(this.consumeWorkspaceSelection({
846
1050
  targetDeviceId: value.targetDeviceId,
847
1051
  workspaceId: value.workspaceId,
848
- ...(value.backend === 'codex' ? { backend: 'codex' } : {}),
1052
+ ...(value.backend === 'codex' || value.backend === 'cursor' || value.backend === 'antigravity' ? { backend: value.backend } : {}),
849
1053
  ...(typeof value.sessionId === 'string' ? { sessionId: value.sessionId } : {}),
850
1054
  }));
851
1055
  }
@@ -1096,6 +1300,8 @@ export function remoteHostFeatures(clientVersion) {
1096
1300
  apiProxy: true,
1097
1301
  remoteGateway: false,
1098
1302
  codex: false,
1303
+ cursor: false,
1304
+ antigravity: false,
1099
1305
  };
1100
1306
  }
1101
1307
  export async function probeRemoteHostFeatures(client, clientVersion) {
@@ -1118,13 +1324,17 @@ export async function probeRemoteHostFeatures(client, clientVersion) {
1118
1324
  const remoteV1 = capabilities.has('harness.remote.v1');
1119
1325
  const remoteV3 = capabilities.has('harness.remote.v3');
1120
1326
  const terminal = capabilities.has('harness.terminal.v1');
1121
- const codex = capabilities.has('codex.appserver.v1');
1327
+ const workspaceTypes = 'workspaceTypes' in value ? parseRemoteWorkspaceTypes(value.workspaceTypes) : undefined;
1328
+ const available = (id, capability) => remoteWorkspaceTypeAvailable(value.capabilities, workspaceTypes, id, capability);
1329
+ const codex = available('codex', 'codex.appserver.v1');
1330
+ const cursor = available('cursor', 'agent.acp.cursor.v1');
1331
+ const antigravity = available('antigravity', 'agent.acp.antigravity.v1');
1122
1332
  if (remoteV1 && remoteV3) {
1123
1333
  throw new ClientModeError('INVALID_MESSAGE', 'The remote Host advertised conflicting Harness Session formats.');
1124
1334
  }
1125
1335
  const sessionFormat = remoteV3 ? 3 : undefined;
1126
1336
  const remoteGateway = remoteV3 || remoteV1;
1127
- if (!apiProxy && !remoteGateway && !codex) {
1337
+ if (!apiProxy && !remoteGateway && !codex && !cursor && !antigravity) {
1128
1338
  throw new ClientModeError('FEATURE_NOT_SUPPORTED', 'The remote Host exposes no supported Harness transport.');
1129
1339
  }
1130
1340
  return {
@@ -1135,6 +1345,10 @@ export async function probeRemoteHostFeatures(client, clientVersion) {
1135
1345
  remoteGateway,
1136
1346
  ...(sessionFormat === undefined ? {} : { sessionFormat }),
1137
1347
  codex,
1348
+ cursor,
1349
+ antigravity,
1350
+ ...(capabilities.has('agent.acp.antigravity.model-load.v1') ? { acpModelLoadsSession: true } : {}),
1351
+ ...(workspaceTypes === undefined ? {} : { workspaceTypes }),
1138
1352
  };
1139
1353
  }
1140
1354
  async function waitForCodexFrames(stream, signal) {