ds-harness-remote 0.4.3 → 0.4.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.
Files changed (54) hide show
  1. package/README.md +57 -9
  2. package/bin/ds-harness-remote.js +11 -0
  3. package/cordis.patch.yml +22 -0
  4. package/dist/cli.d.ts +31 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +283 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/client.github.js +31 -15
  9. package/dist/client.js +31 -15
  10. package/dist/client.js.map +1 -1
  11. package/dist/codex/domain.d.ts +6 -2
  12. package/dist/codex/domain.d.ts.map +1 -1
  13. package/dist/codex/domain.js +279 -68
  14. package/dist/codex/domain.js.map +1 -1
  15. package/dist/codex/method-policy.d.ts +14 -7
  16. package/dist/codex/method-policy.d.ts.map +1 -1
  17. package/dist/codex/method-policy.js +3 -0
  18. package/dist/codex/method-policy.js.map +1 -1
  19. package/dist/codex/virtual-harness.d.ts +6 -0
  20. package/dist/codex/virtual-harness.d.ts.map +1 -1
  21. package/dist/codex/virtual-harness.js +161 -23
  22. package/dist/codex/virtual-harness.js.map +1 -1
  23. package/dist/config.d.ts +1 -0
  24. package/dist/config.d.ts.map +1 -1
  25. package/dist/config.js +1 -0
  26. package/dist/config.js.map +1 -1
  27. package/dist/harness-api-bridge.d.ts.map +1 -1
  28. package/dist/harness-api-bridge.js +14 -6
  29. package/dist/harness-api-bridge.js.map +1 -1
  30. package/dist/harness-remote-bridge.d.ts +1 -1
  31. package/dist/harness-remote-bridge.d.ts.map +1 -1
  32. package/dist/harness-remote-bridge.js +1 -0
  33. package/dist/harness-remote-bridge.js.map +1 -1
  34. package/dist/index.d.ts +2 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +1342 -270
  37. package/dist/index.js.map +4 -4
  38. package/dist/server-api.d.ts.map +1 -1
  39. package/dist/server-api.js +15 -2
  40. package/dist/server-api.js.map +1 -1
  41. package/dist/server-credentials.d.ts +2 -2
  42. package/dist/service.d.ts +4 -1
  43. package/dist/service.d.ts.map +1 -1
  44. package/dist/service.js +34 -2
  45. package/dist/service.js.map +1 -1
  46. package/dist/tui-command.d.ts +13 -0
  47. package/dist/tui-command.d.ts.map +1 -0
  48. package/dist/tui-command.js +378 -0
  49. package/dist/tui-command.js.map +1 -0
  50. package/dist/version.d.ts +1 -1
  51. package/dist/version.js +1 -1
  52. package/dsh-plugin.json +41 -0
  53. package/package.json +6 -1
  54. package/public.d.ts +1 -0
@@ -1,4 +1,4 @@
1
- import { CodexRemoteClient, projectCodexThread, } from '@dsh-remote/client-core';
1
+ import { CodexRemoteClient, deriveCodexCwdWorkspaces, projectCodexThread, } from '@dsh-remote/client-core';
2
2
  const CODEX_SESSION_PREFIX = 'codex:';
3
3
  const CODEX_WORKSPACE_PREFIX = 'codex-workspace:';
4
4
  const CODEX_PROVIDER = 'codex';
@@ -151,11 +151,14 @@ export class CodexVirtualHarness {
151
151
  }));
152
152
  }
153
153
  case 'session/selectModel': return business(await this.selectModel(requestArg(args), signal));
154
- case 'session/canOpenWorkspacePath': return business(false);
154
+ case 'session/canOpenWorkspacePath': return business(this.selectedWorkspaceId !== undefined);
155
155
  case 'session/openWorkspacePath': return business(failure('bad-request', 'Opening Host paths is unavailable in CodeX mode.'));
156
+ case 'host/describe': return business(success(await this.describeHost(signal)));
157
+ case 'host/listDirectory': return business(await this.listDirectory(requestArg(args), signal));
158
+ case 'directoryPicker/list': return business(await this.listDirectory(requestArg(args), signal));
156
159
  case 'skills/list': return business(success({ items: [] }));
157
160
  case 'commands/list': return ok(this.commandList(args));
158
- case 'commands/execute': return ok(this.executeCommand(args));
161
+ case 'commands/execute': return ok(await this.executeCommand(args, signal));
159
162
  default: return fail('method-not-found', `CodeX virtual Harness does not implement ${endpoint}.`);
160
163
  }
161
164
  }
@@ -241,6 +244,10 @@ export class CodexVirtualHarness {
241
244
  permissionSelection(sessionId) {
242
245
  return this.selectedPermissions.get(sessionId) ?? CODEX_DEFAULT_PERMISSION;
243
246
  }
247
+ setPermissionSelection(sessionId, preset) {
248
+ this.selectedPermissions.set(sessionId, preset);
249
+ this.publishProjection(sessionId, 'permissions', codexPermissionsProjection(preset), this.nextProjectionSeq());
250
+ }
244
251
  commandList(args) {
245
252
  nativeThreadId(requiredString(args.agentId, 'agentId'));
246
253
  return [{
@@ -249,9 +256,9 @@ export class CodexVirtualHarness {
249
256
  input: { hint: '<preset>' },
250
257
  }];
251
258
  }
252
- executeCommand(args) {
259
+ async executeCommand(args, signal) {
253
260
  const sessionId = requiredString(args.agentId, 'agentId');
254
- nativeThreadId(sessionId);
261
+ const threadId = nativeThreadId(sessionId);
255
262
  const line = requiredString(args.line, 'command line').trim();
256
263
  if (!Array.isArray(args.images))
257
264
  throw new Error('The CodeX command image list is required.');
@@ -275,9 +282,10 @@ export class CodexVirtualHarness {
275
282
  commandId,
276
283
  result: { kind: 'error', text: `unknown preset "${requested}" (available: workspace-write, danger-full-access)` },
277
284
  };
278
- this.selectedPermissions.set(sessionId, requested);
279
- this.publishProjection(sessionId, 'permissions', codexPermissionsProjection(requested), this.nextProjectionSeq());
280
- return { commandId, result: { kind: 'success', text: `preset ${requested}` } };
285
+ const result = await this.client.request('thread/resume', { threadId, permissionPreset: requested }, signal);
286
+ const effective = codexPermissionPresetFromResponse(result) ?? requested;
287
+ this.setPermissionSelection(sessionId, effective);
288
+ return { commandId, result: { kind: 'success', text: `preset ${effective}` } };
281
289
  }
282
290
  sessionTitle(sessionId) {
283
291
  const session = this.catalog?.sessions.find(item => item.id === sessionId);
@@ -429,7 +437,7 @@ export class CodexVirtualHarness {
429
437
  queue,
430
438
  nextSeq: history.cursor + 1,
431
439
  turn: history.nextTurn,
432
- stepOpen: false,
440
+ stepOpen: history.activeTurnId !== undefined,
433
441
  startedItems: new Set(),
434
442
  completedItems: new Set(),
435
443
  streamedBlocks: new Map(),
@@ -439,6 +447,7 @@ export class CodexVirtualHarness {
439
447
  liveToolOutput: new Map(),
440
448
  liveToolResultSeq: new Map(),
441
449
  requestId: this.pendingRequestIds.get(sessionId),
450
+ ...(history.activeTurnId === undefined ? {} : { activeTurnId: history.activeTurnId }),
442
451
  };
443
452
  this.follows.add(follow);
444
453
  queue.push({
@@ -484,6 +493,12 @@ export class CodexVirtualHarness {
484
493
  this.publishProjection(follow.sessionId, 'title', title, seq);
485
494
  return;
486
495
  }
496
+ if (frame.method === 'thread/settings/updated') {
497
+ const preset = codexPermissionPresetFromResponse(params);
498
+ if (preset !== undefined)
499
+ this.setPermissionSelection(follow.sessionId, preset);
500
+ return;
501
+ }
487
502
  if (frame.method === 'turn/started') {
488
503
  const turn = record(params.turn);
489
504
  this.resetLiveTurn(follow);
@@ -889,6 +904,49 @@ export class CodexVirtualHarness {
889
904
  ? failure('workspace-not-found', 'The CodeX virtual Workspace was not found.')
890
905
  : success({ workspace: nativeWorkspace(workspace) });
891
906
  }
907
+ selectedWorkspace(catalog) {
908
+ return this.selectedWorkspaceId === undefined
909
+ ? undefined
910
+ : catalog.workspaces.find(item => item.workspaceId === this.selectedWorkspaceId);
911
+ }
912
+ async describeHost(signal) {
913
+ const catalog = await this.currentCatalog(signal);
914
+ const workspace = this.selectedWorkspace(catalog);
915
+ const models = await this.models(signal).catch(() => undefined);
916
+ return {
917
+ version: 'CodeX Remote',
918
+ cwd: workspace?.path ?? '',
919
+ home: workspace?.path ?? '',
920
+ provider: 'CodeX',
921
+ ...(models === undefined ? {} : { model: models.default.model }),
922
+ attachedSessions: catalog.sessions.length,
923
+ canOpenPath: workspace !== undefined,
924
+ };
925
+ }
926
+ async listDirectory(request, signal) {
927
+ const catalog = await this.currentCatalog(signal);
928
+ const workspace = this.selectedWorkspace(catalog);
929
+ if (workspace === undefined)
930
+ return failure('workspace-not-found', 'The CodeX virtual Workspace was not found.');
931
+ const path = string(request.path) ?? workspace.path;
932
+ if (!containsPath(workspace.path, path)) {
933
+ return failure('workspace-not-found', 'The selected directory is outside the current CodeX virtual Workspace.');
934
+ }
935
+ const listing = record(await this.client.request('dsh/directoryList', { path }, signal));
936
+ if (!isCodexDirectoryListing(listing)) {
937
+ return failure('internal', 'CodeX Remote returned an invalid directory listing.');
938
+ }
939
+ const listedPaths = [
940
+ listing.path,
941
+ listing.home,
942
+ ...listing.crumbs.map(item => item.path),
943
+ ...listing.entries.map(item => item.path),
944
+ ];
945
+ if (listedPaths.some(path => !containsPath(workspace.path, path))) {
946
+ return failure('internal', 'CodeX Remote returned a directory outside the current Workspace.');
947
+ }
948
+ return success(listing);
949
+ }
892
950
  async archiveSession(request, signal) {
893
951
  const sessionId = requiredString(request.sessionId, 'sessionId');
894
952
  await this.client.request('thread/archive', { threadId: nativeThreadId(sessionId) }, signal);
@@ -899,18 +957,23 @@ export class CodexVirtualHarness {
899
957
  async createSession(request, signal) {
900
958
  const catalog = await this.currentCatalog(signal);
901
959
  const workspaceId = string(request.workspaceId) ?? this.selectedWorkspaceId;
960
+ const workspace = workspaceId === undefined ? undefined : catalog.workspaces.find(item => item.workspaceId === workspaceId);
902
961
  const cwd = string(request.cwd)
903
- ?? (workspaceId === undefined ? undefined : catalog.workspaces.find(item => item.workspaceId === workspaceId)?.path);
962
+ ?? workspace?.path;
904
963
  if (cwd === undefined)
905
964
  return failure('workspace-not-found', 'The CodeX virtual Workspace was not found.');
965
+ if (workspace !== undefined && !containsPath(workspace.path, cwd)) {
966
+ return failure('workspace-not-found', 'The selected directory is outside the current CodeX virtual Workspace.');
967
+ }
906
968
  const directory = await this.models(signal);
907
969
  const selection = directory.default;
908
- const permissionPreset = CODEX_DEFAULT_PERMISSION;
970
+ const requestedPreset = CODEX_DEFAULT_PERMISSION;
909
971
  const result = record(await this.client.request('thread/start', {
910
972
  cwd,
911
973
  model: selection.model,
912
- permissionPreset,
974
+ permissionPreset: requestedPreset,
913
975
  }, signal));
976
+ const permissionPreset = codexPermissionPresetFromResponse(result) ?? requestedPreset;
914
977
  const thread = { ...record(result.thread), cwd, turns: array(record(result.thread).turns) };
915
978
  const projected = projectCodexThread(thread);
916
979
  if (projected === undefined)
@@ -932,11 +995,12 @@ export class CodexVirtualHarness {
932
995
  }
933
996
  async forkSession(request, signal) {
934
997
  const sessionId = requiredString(request.sessionId, 'sessionId');
935
- const permissionPreset = this.permissionSelection(sessionId);
998
+ let permissionPreset = this.selectedPermissions.get(sessionId);
936
999
  const result = record(await this.client.request('thread/fork', {
937
1000
  threadId: nativeThreadId(sessionId),
938
- permissionPreset,
1001
+ ...(permissionPreset === undefined ? {} : { permissionPreset }),
939
1002
  }, signal));
1003
+ permissionPreset = codexPermissionPresetFromResponse(result) ?? permissionPreset ?? CODEX_DEFAULT_PERMISSION;
940
1004
  const projected = projectCodexThread(record(result.thread));
941
1005
  if (projected === undefined)
942
1006
  return failure('internal', 'CodeX returned an invalid forked Thread.');
@@ -955,15 +1019,20 @@ export class CodexVirtualHarness {
955
1019
  return failure('attachment-error', 'CodeX virtual Sessions accept text and pasted PNG, JPEG, WebP, or GIF images only.');
956
1020
  const threadId = nativeThreadId(sessionId);
957
1021
  const selection = this.modelSelection(sessionId, await this.models(signal));
958
- const permissionPreset = this.permissionSelection(sessionId);
1022
+ let permissionPreset = this.selectedPermissions.get(sessionId);
959
1023
  const isFreshBlankThread = this.blankThreads.has(threadId) || this.pendingThreads.has(threadId);
960
1024
  await this.ensureRcFollow(sessionId);
961
- if (!isFreshBlankThread)
962
- await this.client.request('thread/resume', {
1025
+ if (!isFreshBlankThread) {
1026
+ const resumeResult = await this.client.request('thread/resume', {
963
1027
  threadId,
964
1028
  model: selection.model,
965
- permissionPreset,
966
1029
  }, signal);
1030
+ const serverPreset = codexPermissionPresetFromResponse(resumeResult);
1031
+ if (permissionPreset === undefined && serverPreset !== undefined) {
1032
+ permissionPreset = serverPreset;
1033
+ this.setPermissionSelection(sessionId, serverPreset);
1034
+ }
1035
+ }
967
1036
  this.pendingRequestIds.set(sessionId, string(request.requestId) ?? '');
968
1037
  const mode = request.mode === 'steer' ? 'turn/steer' : 'turn/start';
969
1038
  if (mode === 'turn/steer') {
@@ -977,7 +1046,7 @@ export class CodexVirtualHarness {
977
1046
  threadId,
978
1047
  input,
979
1048
  ...codexModelParams(selection),
980
- permissionPreset,
1049
+ ...(permissionPreset === undefined ? {} : { permissionPreset }),
981
1050
  }, signal);
982
1051
  }
983
1052
  this.blankThreads.delete(threadId);
@@ -1136,7 +1205,7 @@ export class CodexVirtualHarness {
1136
1205
  queue: new AsyncValueQueue(controller.signal),
1137
1206
  nextSeq: history.cursor + 1,
1138
1207
  turn: history.nextTurn,
1139
- stepOpen: false,
1208
+ stepOpen: history.activeTurnId !== undefined,
1140
1209
  startedItems: new Set(),
1141
1210
  completedItems: new Set(),
1142
1211
  streamedBlocks: new Map(),
@@ -1147,6 +1216,7 @@ export class CodexVirtualHarness {
1147
1216
  liveToolResultSeq: new Map(),
1148
1217
  requestId: this.pendingRequestIds.get(sessionId),
1149
1218
  rcOnly: true,
1219
+ ...(history.activeTurnId === undefined ? {} : { activeTurnId: history.activeTurnId }),
1150
1220
  };
1151
1221
  this.follows.add(follow);
1152
1222
  try {
@@ -1225,7 +1295,10 @@ export class CodexVirtualHarness {
1225
1295
  archiveSession: call('workspace.archiveSession'),
1226
1296
  },
1227
1297
  subagents: {},
1228
- host: {},
1298
+ host: {
1299
+ describe: call('host.describe'),
1300
+ listDirectory: call('host.listDirectory'),
1301
+ },
1229
1302
  skills: { list: call('skills.list') },
1230
1303
  agentPresets: {},
1231
1304
  goals: {},
@@ -1289,7 +1362,7 @@ export class CodexVirtualHarness {
1289
1362
  }
1290
1363
  }
1291
1364
  async function loadCatalog(client, signal, pendingThreads) {
1292
- const projects = await loadCodexProjects(client, signal);
1365
+ let projects = await loadCodexProjects(client, signal);
1293
1366
  const threads = [];
1294
1367
  let cursor;
1295
1368
  for (let page = 0; page < MAX_CODEX_PAGES; page += 1) {
@@ -1318,6 +1391,16 @@ async function loadCatalog(client, signal, pendingThreads) {
1318
1391
  threads.unshift(thread);
1319
1392
  }
1320
1393
  }
1394
+ if (projects.length === 0) {
1395
+ projects = deriveCodexCwdWorkspaces(threads).map(workspace => ({
1396
+ id: workspace.id,
1397
+ name: workspace.name,
1398
+ roots: [{ path: workspace.path }],
1399
+ position: workspace.position,
1400
+ createdAt: workspace.createdAt,
1401
+ updatedAt: workspace.updatedAt,
1402
+ }));
1403
+ }
1321
1404
  const projected = threads.map(thread => {
1322
1405
  const session = projectCodexThread(thread);
1323
1406
  return session === undefined ? undefined : { thread, session };
@@ -1440,6 +1523,7 @@ export function projectCodexNativeHistory(thread, sessionId) {
1440
1523
  const entries = [];
1441
1524
  let seq = 0;
1442
1525
  let turnNumber = 0;
1526
+ const active = activeTurnId(thread.turns);
1443
1527
  const append = (type, data, time = Date.now(), view, sourceEventSeqs) => {
1444
1528
  const eventSeq = seq++;
1445
1529
  entries.push({
@@ -1459,6 +1543,7 @@ export function projectCodexNativeHistory(thread, sessionId) {
1459
1543
  for (const rawTurn of array(thread.turns)) {
1460
1544
  const turn = record(rawTurn);
1461
1545
  turnNumber += 1;
1546
+ const turnActive = isActiveCodexTurn(turn);
1462
1547
  const time = normalizeTime(turn.createdAt) || normalizeTime(turn.startedAt) || normalizeTime(thread.createdAt) || Date.now();
1463
1548
  append('turn/start', { turn: turnNumber }, time);
1464
1549
  append('step/start', { turn: turnNumber, step: 1 }, time);
@@ -1471,6 +1556,8 @@ export function projectCodexNativeHistory(thread, sessionId) {
1471
1556
  toolCallSeq = eventSeq;
1472
1557
  }
1473
1558
  }
1559
+ if (turnActive)
1560
+ continue;
1474
1561
  append('step/end', { turn: turnNumber, step: 1 }, normalizeTime(turn.updatedAt) || normalizeTime(turn.completedAt) || time);
1475
1562
  append('turn/end', {
1476
1563
  turn: turnNumber,
@@ -1490,6 +1577,7 @@ export function projectCodexNativeHistory(thread, sessionId) {
1490
1577
  entries,
1491
1578
  lastSeq: seq - 1,
1492
1579
  nextTurn: turnNumber,
1580
+ ...(active === undefined ? {} : { activeTurnId: active }),
1493
1581
  };
1494
1582
  }
1495
1583
  export function paginateCodexNativeHistory(history, request) {
@@ -1515,6 +1603,7 @@ export function paginateCodexNativeHistory(history, request) {
1515
1603
  nextTurn: history.nextTurn,
1516
1604
  records: window.filter(entry => entry.event.seq >= cut),
1517
1605
  hasMore: window.some(entry => entry.event.seq < cut),
1606
+ ...(history.activeTurnId === undefined ? {} : { activeTurnId: history.activeTurnId }),
1518
1607
  };
1519
1608
  }
1520
1609
  function itemEvents(item, turn, step, requestId, selection = modelSelection(), sessionId = CODEX_SESSION_PREFIX) {
@@ -2155,6 +2244,25 @@ function codexImageLimitsProjection() {
2155
2244
  function isCodexPermissionPreset(value) {
2156
2245
  return value === 'workspace-write' || value === 'danger-full-access';
2157
2246
  }
2247
+ function codexPermissionPresetFromResponse(value) {
2248
+ const root = record(value);
2249
+ return codexPermissionPresetFromPolicyRecord(root)
2250
+ ?? codexPermissionPresetFromPolicyRecord(record(root.threadSettings));
2251
+ }
2252
+ function codexPermissionPresetFromPolicyRecord(value) {
2253
+ const sandbox = value.sandbox ?? value.sandboxPolicy;
2254
+ if (value.approvalPolicy === 'never' && isDangerFullAccessSandbox(sandbox))
2255
+ return 'danger-full-access';
2256
+ if (value.approvalPolicy === 'on-request' && isWorkspaceWriteSandbox(sandbox))
2257
+ return 'workspace-write';
2258
+ return undefined;
2259
+ }
2260
+ function isDangerFullAccessSandbox(value) {
2261
+ return value === 'danger-full-access' || record(value).type === 'dangerFullAccess';
2262
+ }
2263
+ function isWorkspaceWriteSandbox(value) {
2264
+ return value === 'workspace-write' || record(value).type === 'workspaceWrite';
2265
+ }
2158
2266
  function modelCatalog(directory) {
2159
2267
  return {
2160
2268
  default: directory.default,
@@ -2318,6 +2426,19 @@ function nativeThreadId(sessionId) {
2318
2426
  }
2319
2427
  return sessionId.slice(CODEX_SESSION_PREFIX.length);
2320
2428
  }
2429
+ function activeTurnId(value) {
2430
+ if (!Array.isArray(value))
2431
+ return undefined;
2432
+ for (let index = value.length - 1; index >= 0; index -= 1) {
2433
+ const turn = record(value[index]);
2434
+ if (isActiveCodexTurn(turn) && typeof turn.id === 'string' && turn.id.length > 0)
2435
+ return turn.id;
2436
+ }
2437
+ return undefined;
2438
+ }
2439
+ function isActiveCodexTurn(turn) {
2440
+ return turn.status === 'inProgress' || turn.status === 'running';
2441
+ }
2321
2442
  function carrierArgs(payload) {
2322
2443
  return record(record(payload).args);
2323
2444
  }
@@ -2348,6 +2469,22 @@ function business(value) {
2348
2469
  }
2349
2470
  return ok(value);
2350
2471
  }
2472
+ function isCodexDirectoryListing(value) {
2473
+ const listing = record(value);
2474
+ return typeof listing.path === 'string'
2475
+ && typeof listing.home === 'string'
2476
+ && Array.isArray(listing.crumbs)
2477
+ && listing.crumbs.every(isCodexDirectoryEntry)
2478
+ && Array.isArray(listing.entries)
2479
+ && listing.entries.every(isCodexDirectoryEntry)
2480
+ && typeof listing.truncated === 'boolean';
2481
+ }
2482
+ function isCodexDirectoryEntry(value) {
2483
+ const entry = record(value);
2484
+ return typeof entry.name === 'string'
2485
+ && typeof entry.path === 'string'
2486
+ && typeof entry.hidden === 'boolean';
2487
+ }
2351
2488
  function fail(code, message, details = {}) {
2352
2489
  return { ok: false, error: { code, message, details } };
2353
2490
  }
@@ -2413,7 +2550,8 @@ function isCodexNativeHistoryPage(value, sessionId) {
2413
2550
  && integer(page.cursor) !== undefined
2414
2551
  && integer(page.nextTurn) !== undefined
2415
2552
  && Array.isArray(page.records)
2416
- && typeof page.hasMore === 'boolean';
2553
+ && typeof page.hasMore === 'boolean'
2554
+ && (page.activeTurnId === undefined || typeof page.activeTurnId === 'string');
2417
2555
  }
2418
2556
  function truncateCodePoints(value, maximum) {
2419
2557
  return [...value].slice(0, maximum).join('');