ds-harness-remote 0.4.3 → 0.4.4

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.
@@ -1,6 +1,8 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { realpath, stat } from 'node:fs/promises';
2
3
  import { homedir } from 'node:os';
3
- import { isAbsolute, join } from 'node:path';
4
+ import { isAbsolute, join, resolve } from 'node:path';
5
+ import { deriveCodexCwdWorkspaces } from '@dsh-remote/client-core';
4
6
  import { RpcError } from '../safe-error.js';
5
7
  import { CodexAppServerClient, CodexAppServerError, } from './app-server.js';
6
8
  import { isThreadMutation, parseCodexCall, threadIdFromParams, } from './method-policy.js';
@@ -9,6 +11,7 @@ import { paginateCodexNativeHistory, projectCodexNativeHistory } from './virtual
9
11
  const APPROVAL_TTL_MS = 5 * 60_000;
10
12
  const DEFAULT_RESTART_DELAYS_MS = [1_000, 2_000, 4_000, 8_000, 15_000];
11
13
  const CODEX_PAGE_LIMIT = 100;
14
+ const MAX_CODEX_PAGES = 32;
12
15
  const CODEX_HISTORY_PAGE_LIMIT = 25;
13
16
  const MAX_CODEX_HISTORY_PAGES = 64;
14
17
  /**
@@ -25,6 +28,7 @@ export class CodexRemoteDomain {
25
28
  unsubscribeInbound;
26
29
  unsubscribeUnavailable;
27
30
  peers = new Map();
31
+ peerDeviceIds = new Map();
28
32
  turnOwners = new Map();
29
33
  approvals = new Map();
30
34
  approvalExpiryTimer;
@@ -72,6 +76,7 @@ export class CodexRemoteDomain {
72
76
  return undefined;
73
77
  const bridge = new CodexPeerBridge(this, context, publish, this.logger);
74
78
  this.peers.set(context.connectionId, bridge);
79
+ this.peerDeviceIds.set(context.connectionId, context.peerDeviceId);
75
80
  return bridge;
76
81
  }
77
82
  async call(connectionId, input) {
@@ -85,16 +90,16 @@ export class CodexRemoteDomain {
85
90
  return sanitizeProjectList(await this.callUpstream(call.method, call.params));
86
91
  }
87
92
  if (call.method === 'thread/list') {
88
- return filterThreadListByProjectAuthority(sanitizeThreadList(await this.callUpstream(call.method, call.params)), await this.readProjectAuthority());
93
+ const result = sanitizeThreadList(await this.callUpstream(call.method, call.params));
94
+ return filterThreadListByWorkspaceAuthority(result, await this.readWorkspaceAuthority(result.data));
89
95
  }
90
96
  if (call.method === 'thread/start') {
91
97
  const cwd = await this.requireCodexWorkspacePath(call.params.cwd);
92
- const permission = codexPermission(call.params);
98
+ const permission = codexPermission(call.params, 'workspace-write');
93
99
  const result = await this.callUpstream(call.method, {
94
100
  ...permission.params,
95
101
  cwd,
96
- approvalPolicy: permission.approvalPolicy,
97
- sandbox: permission.sandbox,
102
+ ...codexThreadPermissionParams(permission),
98
103
  serviceName: 'deepseek_harness_remote',
99
104
  });
100
105
  if (extractThread(result)?.id === undefined) {
@@ -105,11 +110,15 @@ export class CodexRemoteDomain {
105
110
  const threadId = threadIdFromParams(call.params);
106
111
  if (call.method === 'dsh/sessionHistory') {
107
112
  const thread = await this.readThreadForHistory(connectionId, threadId);
108
- return paginateCodexNativeHistory(projectCodexNativeHistory(thread, `codex:${threadId}`), {
113
+ const page = paginateCodexNativeHistory(projectCodexNativeHistory(thread, `codex:${threadId}`), {
109
114
  beforeSeq: optionalInteger(call.params.beforeSeq),
110
115
  throughSeq: optionalInteger(call.params.throughSeq),
111
116
  maxMessages: optionalInteger(call.params.maxMessages),
112
117
  });
118
+ const activeTurnId = typeof page.activeTurnId === 'string'
119
+ ? page.activeTurnId
120
+ : this.turnOwners.get(threadId)?.turnId;
121
+ return activeTurnId === undefined ? page : { ...page, activeTurnId };
113
122
  }
114
123
  const allowedThread = threadId === undefined ? undefined : await this.readKnownThread(threadId);
115
124
  if (call.method === 'thread/read') {
@@ -125,8 +134,11 @@ export class CodexRemoteDomain {
125
134
  return this.callUpstream(call.method, call.params);
126
135
  }
127
136
  let claimed = false;
137
+ let previousOwner;
128
138
  if (isThreadMutation(call.method) && threadId !== undefined) {
129
- claimed = this.claimTurn(threadId, connectionId, call.method);
139
+ const claim = this.claimTurn(threadId, connectionId, call.method, typeof call.params.turnId === 'string' ? call.params.turnId : undefined);
140
+ claimed = claim.claimed;
141
+ previousOwner = claim.previous;
130
142
  }
131
143
  try {
132
144
  const permission = codexPermission(call.params);
@@ -134,31 +146,20 @@ export class CodexRemoteDomain {
134
146
  ? {
135
147
  ...permission.params,
136
148
  ...(allowedThread.cwd === undefined ? {} : { cwd: allowedThread.cwd }),
137
- approvalPolicy: permission.approvalPolicy,
138
- sandbox: permission.sandbox,
149
+ ...codexThreadPermissionParams(permission),
139
150
  excludeTurns: true,
140
151
  }
141
152
  : call.method === 'thread/fork' && allowedThread !== undefined
142
153
  ? {
143
154
  ...permission.params,
144
155
  ...(allowedThread.cwd === undefined ? {} : { cwd: allowedThread.cwd }),
145
- approvalPolicy: permission.approvalPolicy,
146
- sandbox: permission.sandbox,
156
+ ...codexThreadPermissionParams(permission),
147
157
  }
148
158
  : call.method === 'turn/start'
149
159
  ? {
150
160
  ...permission.params,
151
161
  ...(allowedThread?.cwd === undefined ? {} : { cwd: allowedThread.cwd }),
152
- approvalPolicy: permission.approvalPolicy,
153
- sandboxPolicy: permission.sandbox === 'danger-full-access'
154
- ? { type: 'dangerFullAccess' }
155
- : {
156
- type: 'workspaceWrite',
157
- writableRoots: allowedThread?.cwd === undefined ? [] : [allowedThread.cwd],
158
- networkAccess: false,
159
- excludeTmpdirEnvVar: false,
160
- excludeSlashTmp: false,
161
- },
162
+ ...codexTurnPermissionParams(permission, allowedThread?.cwd),
162
163
  }
163
164
  : permission.params;
164
165
  let result;
@@ -172,7 +173,8 @@ export class CodexRemoteDomain {
172
173
  if (call.method === 'thread/resume'
173
174
  && allowedThread !== undefined
174
175
  && error instanceof RpcError
175
- && error.code === 'CODEX_UPSTREAM_ERROR') {
176
+ && error.code === 'CODEX_UPSTREAM_ERROR'
177
+ && call.params.permissionPreset === undefined) {
176
178
  return { thread: allowedThread.thread };
177
179
  }
178
180
  throw error;
@@ -180,11 +182,18 @@ export class CodexRemoteDomain {
180
182
  if (call.method === 'thread/resume' || call.method === 'thread/fork' || call.method === 'thread/unarchive') {
181
183
  await this.assertResultThreadAllowed(result);
182
184
  }
185
+ if (call.method === 'turn/start' && threadId !== undefined) {
186
+ this.rememberTurnId(threadId, connectionId, extractTurnId(result));
187
+ }
183
188
  return result;
184
189
  }
185
190
  catch (error) {
186
- if (claimed && threadId !== undefined)
187
- this.turnOwners.delete(threadId);
191
+ if (claimed && threadId !== undefined) {
192
+ if (previousOwner === undefined)
193
+ this.turnOwners.delete(threadId);
194
+ else
195
+ this.turnOwners.set(threadId, previousOwner);
196
+ }
188
197
  throw mapAppServerError(error);
189
198
  }
190
199
  }
@@ -204,8 +213,9 @@ export class CodexRemoteDomain {
204
213
  const bridge = this.peers.get(connectionId);
205
214
  if (bridge !== undefined)
206
215
  this.peers.delete(connectionId);
216
+ this.peerDeviceIds.delete(connectionId);
207
217
  for (const [threadId, owner] of this.turnOwners) {
208
- if (owner === connectionId)
218
+ if (owner.connectionId === connectionId)
209
219
  this.turnOwners.delete(threadId);
210
220
  }
211
221
  const appServer = this.appServer;
@@ -228,6 +238,7 @@ export class CodexRemoteDomain {
228
238
  for (const bridge of [...this.peers.values()])
229
239
  await bridge.closeAll();
230
240
  this.peers.clear();
241
+ this.peerDeviceIds.clear();
231
242
  this.turnOwners.clear();
232
243
  if (this.approvalExpiryTimer !== undefined)
233
244
  clearTimeout(this.approvalExpiryTimer);
@@ -359,6 +370,13 @@ export class CodexRemoteDomain {
359
370
  const threadId = extractThreadId(message.params);
360
371
  if (message.method === 'turn/completed' && threadId !== undefined)
361
372
  this.turnOwners.delete(threadId);
373
+ if (message.method === 'turn/started' && threadId !== undefined) {
374
+ const owner = this.turnOwners.get(threadId);
375
+ const turnId = extractTurnId(message.params);
376
+ if (owner !== undefined) {
377
+ this.turnOwners.set(threadId, { ...owner, ...(turnId === undefined ? {} : { turnId }) });
378
+ }
379
+ }
362
380
  if (message.method === 'serverRequest/resolved')
363
381
  this.resolveUpstreamApproval(message.params);
364
382
  if (threadId === undefined)
@@ -378,7 +396,7 @@ export class CodexRemoteDomain {
378
396
  }
379
397
  const threadId = extractThreadId(message.params);
380
398
  const owner = threadId === undefined ? undefined : this.turnOwners.get(threadId);
381
- const peer = owner === undefined ? undefined : this.peers.get(owner);
399
+ const peer = owner === undefined ? undefined : this.peers.get(owner.connectionId);
382
400
  if (threadId === undefined || owner === undefined || peer === undefined || !peer.hasThreadSubscription(threadId)) {
383
401
  await appServer.respond(message.id, { decision: 'decline' });
384
402
  return;
@@ -386,7 +404,7 @@ export class CodexRemoteDomain {
386
404
  const requestHandle = randomUUID();
387
405
  this.approvals.set(requestHandle, {
388
406
  upstreamId: message.id,
389
- connectionId: owner,
407
+ connectionId: owner.connectionId,
390
408
  threadId,
391
409
  method: message.method,
392
410
  expiresAt: Date.now() + APPROVAL_TTL_MS,
@@ -406,7 +424,7 @@ export class CodexRemoteDomain {
406
424
  }
407
425
  }
408
426
  async readKnownThread(threadId) {
409
- const authority = await this.readProjectAuthority();
427
+ const authority = await this.readWorkspaceAuthority();
410
428
  let result;
411
429
  try {
412
430
  result = await this.callUpstream('thread/read', { threadId, includeTurns: false });
@@ -421,7 +439,7 @@ export class CodexRemoteDomain {
421
439
  return { thread: listed, ...(typeof listed.cwd === 'string' && listed.cwd.length > 0 ? { cwd: listed.cwd } : {}) };
422
440
  }
423
441
  const thread = extractThread(result);
424
- if (thread === undefined || thread.id !== threadId || !isThreadAllowedByProjectAuthority(thread, authority)) {
442
+ if (thread === undefined || thread.id !== threadId || !isThreadAllowedByWorkspaceAuthority(thread, authority)) {
425
443
  throw new RpcError('CODEX_THREAD_NOT_ALLOWED', 'The Codex thread is not available through this Remote Host.');
426
444
  }
427
445
  return { thread, ...(typeof thread.cwd === 'string' && thread.cwd.length > 0 ? { cwd: thread.cwd } : {}) };
@@ -532,31 +550,48 @@ export class CodexRemoteDomain {
532
550
  if (thread === undefined || typeof thread.id !== 'string') {
533
551
  throw new RpcError('CODEX_INVALID_RESPONSE', 'Codex App Server returned an invalid thread.');
534
552
  }
535
- if (!isThreadAllowedByProjectAuthority(thread, await this.readProjectAuthority())) {
553
+ if (!isThreadAllowedByWorkspaceAuthority(thread, await this.readWorkspaceAuthority())) {
536
554
  throw new RpcError('CODEX_THREAD_NOT_ALLOWED', 'The Codex thread is not available through this Remote Host.');
537
555
  }
538
556
  }
539
- claimTurn(threadId, connectionId, method) {
557
+ claimTurn(threadId, connectionId, method, turnId) {
540
558
  const owner = this.turnOwners.get(threadId);
559
+ const peerDeviceId = this.peerDeviceIds.get(connectionId) ?? connectionId;
560
+ const nextOwner = { connectionId, peerDeviceId, ...(turnId === undefined ? {} : { turnId }) };
541
561
  if (method === 'turn/interrupt') {
542
562
  if (owner === undefined) {
543
563
  // A replacement connection may recover an already-running persisted
544
564
  // turn. Claim only for this explicit mutation; passive viewers still
545
565
  // remain observers and cannot steal a live connection's lease.
546
- this.turnOwners.set(threadId, connectionId);
547
- return true;
566
+ this.turnOwners.set(threadId, nextOwner);
567
+ return { claimed: true };
548
568
  }
549
- if (owner !== connectionId)
569
+ if (owner.connectionId !== connectionId && owner.peerDeviceId !== peerDeviceId) {
550
570
  throw new RpcError('CODEX_TURN_OWNED', 'Only the connection that started this Codex turn can interrupt it.');
551
- return false;
571
+ }
572
+ if (owner.connectionId !== connectionId) {
573
+ this.turnOwners.set(threadId, { ...owner, connectionId, peerDeviceId, ...(turnId === undefined ? {} : { turnId }) });
574
+ return { claimed: true, previous: owner };
575
+ }
576
+ if (turnId !== undefined && owner.turnId === undefined)
577
+ this.turnOwners.set(threadId, { ...owner, turnId });
578
+ return { claimed: false };
552
579
  }
553
- if (owner !== undefined && owner !== connectionId) {
580
+ if (owner !== undefined && owner.connectionId !== connectionId && owner.peerDeviceId !== peerDeviceId) {
554
581
  throw new RpcError('CODEX_TURN_OWNED', 'Another Remote connection owns the active Codex turn.');
555
582
  }
556
- if (owner === connectionId)
557
- return false;
558
- this.turnOwners.set(threadId, connectionId);
559
- return true;
583
+ if (owner?.connectionId === connectionId)
584
+ return { claimed: false };
585
+ this.turnOwners.set(threadId, owner === undefined ? nextOwner : { ...owner, connectionId, peerDeviceId });
586
+ return { claimed: true, previous: owner };
587
+ }
588
+ rememberTurnId(threadId, connectionId, turnId) {
589
+ if (turnId === undefined)
590
+ return;
591
+ const owner = this.turnOwners.get(threadId);
592
+ if (owner === undefined || owner.connectionId !== connectionId)
593
+ return;
594
+ this.turnOwners.set(threadId, { ...owner, turnId });
560
595
  }
561
596
  hasSubscriber(threadId) {
562
597
  return [...this.peers.values()].some(peer => peer.hasThreadSubscription(threadId));
@@ -609,31 +644,60 @@ export class CodexRemoteDomain {
609
644
  throw mapAppServerError(error);
610
645
  }
611
646
  }
612
- async readProjectAuthority() {
647
+ async readWorkspaceAuthority(listedThreads) {
613
648
  const projectIds = new Set();
614
649
  const roots = [];
615
650
  let cursor;
616
- for (let page = 0; page < 32; page += 1) {
617
- const result = sanitizeProjectList(await this.callUpstream('project/list', {
618
- limit: 100,
619
- ...(cursor === undefined ? {} : { cursor }),
620
- }));
621
- for (const project of result.data) {
622
- const projectRoots = project.roots.map(root => root.path).filter(path => isAbsolute(path));
623
- if (projectRoots.length === 0)
624
- continue;
625
- projectIds.add(project.id);
626
- roots.push(...projectRoots);
651
+ try {
652
+ for (let page = 0; page < MAX_CODEX_PAGES; page += 1) {
653
+ const result = sanitizeProjectList(await this.callUpstream('project/list', {
654
+ limit: CODEX_PAGE_LIMIT,
655
+ ...(cursor === undefined ? {} : { cursor }),
656
+ }));
657
+ for (const project of result.data) {
658
+ const projectRoots = project.roots.map(root => root.path).filter(path => isAbsolute(path));
659
+ if (projectRoots.length === 0)
660
+ continue;
661
+ projectIds.add(project.id);
662
+ roots.push(...projectRoots);
663
+ }
664
+ cursor = typeof result.nextCursor === 'string' && result.nextCursor.length > 0 ? result.nextCursor : undefined;
665
+ if (cursor === undefined)
666
+ break;
627
667
  }
628
- cursor = typeof result.nextCursor === 'string' && result.nextCursor.length > 0 ? result.nextCursor : undefined;
629
- if (cursor === undefined)
630
- break;
668
+ }
669
+ catch (error) {
670
+ if (!isProjectListFallbackError(error))
671
+ throw error;
672
+ }
673
+ if (roots.length > 0)
674
+ return { projectIds, roots };
675
+ const threads = listedThreads === undefined ? [] : [...listedThreads];
676
+ if (listedThreads === undefined) {
677
+ cursor = undefined;
678
+ for (let page = 0; page < MAX_CODEX_PAGES; page += 1) {
679
+ const result = sanitizeThreadList(await this.callUpstream('thread/list', {
680
+ limit: CODEX_PAGE_LIMIT,
681
+ sortKey: 'updated_at',
682
+ sortDirection: 'desc',
683
+ archived: false,
684
+ ...(cursor === undefined ? {} : { cursor }),
685
+ }));
686
+ threads.push(...result.data);
687
+ cursor = typeof result.nextCursor === 'string' && result.nextCursor.length > 0 ? result.nextCursor : undefined;
688
+ if (cursor === undefined)
689
+ break;
690
+ }
691
+ }
692
+ for (const workspace of deriveCodexCwdWorkspaces(threads)) {
693
+ if (isAbsolute(workspace.path))
694
+ roots.push(workspace.path);
631
695
  }
632
696
  return { projectIds, roots };
633
697
  }
634
698
  async findKnownThreadInList(threadId, authority) {
635
699
  let cursor;
636
- for (let page = 0; page < 32; page += 1) {
700
+ for (let page = 0; page < MAX_CODEX_PAGES; page += 1) {
637
701
  const result = sanitizeThreadList(await this.callUpstream('thread/list', {
638
702
  limit: 100,
639
703
  sortKey: 'updated_at',
@@ -643,7 +707,7 @@ export class CodexRemoteDomain {
643
707
  }));
644
708
  const thread = result.data.find(item => item.id === threadId);
645
709
  if (thread !== undefined) {
646
- return isThreadAllowedByProjectAuthority(thread, authority) ? thread : undefined;
710
+ return isThreadAllowedByWorkspaceAuthority(thread, authority) ? thread : undefined;
647
711
  }
648
712
  cursor = typeof result.nextCursor === 'string' && result.nextCursor.length > 0 ? result.nextCursor : undefined;
649
713
  if (cursor === undefined)
@@ -655,16 +719,34 @@ export class CodexRemoteDomain {
655
719
  if (!isAbsolute(path)) {
656
720
  throw new RpcError('CODEX_PATH_NOT_ALLOWED', 'The CodeX working directory is not available as a Workspace.');
657
721
  }
658
- const paths = await this.listCodexProjectWorkspacePaths();
659
- const canonical = paths.get(normalizeCodexPathForCompare(path));
660
- if (canonical === undefined) {
722
+ const paths = await this.listCodexWorkspacePaths();
723
+ const lexicalCandidate = resolve(path);
724
+ let candidate;
725
+ try {
726
+ candidate = await realpath(path);
727
+ if (!(await stat(candidate)).isDirectory())
728
+ throw new Error('not a directory');
729
+ }
730
+ catch {
661
731
  throw new RpcError('CODEX_PATH_NOT_ALLOWED', 'The CodeX working directory is not available as a Workspace.');
662
732
  }
663
- return canonical;
733
+ for (const root of paths.values()) {
734
+ try {
735
+ if (!containsCodexPath(resolve(root), lexicalCandidate))
736
+ continue;
737
+ const canonicalRoot = await realpath(root);
738
+ if (containsCodexPath(canonicalRoot, candidate))
739
+ return lexicalCandidate;
740
+ }
741
+ catch {
742
+ // A stale project or Thread cwd is not authority for a new Thread.
743
+ }
744
+ }
745
+ throw new RpcError('CODEX_PATH_NOT_ALLOWED', 'The CodeX working directory is not available as a Workspace.');
664
746
  }
665
- async listCodexProjectWorkspacePaths() {
747
+ async listCodexWorkspacePaths() {
666
748
  const paths = new Map();
667
- const authority = await this.readProjectAuthority();
749
+ const authority = await this.readWorkspaceAuthority();
668
750
  for (const root of authority.roots)
669
751
  paths.set(normalizeCodexPathForCompare(root), root);
670
752
  return paths;
@@ -749,7 +831,7 @@ function sanitizeThreadList(result) {
749
831
  : {}),
750
832
  };
751
833
  }
752
- function filterThreadListByProjectAuthority(result, authority) {
834
+ function filterThreadListByWorkspaceAuthority(result, authority) {
753
835
  if (!isRecord(result) || !Array.isArray(result.data)) {
754
836
  throw new RpcError('CODEX_INVALID_RESPONSE', 'Codex App Server returned an invalid thread list.');
755
837
  }
@@ -757,7 +839,7 @@ function filterThreadListByProjectAuthority(result, authority) {
757
839
  ...result,
758
840
  data: result.data
759
841
  .map(record => isRecord(record) ? record : undefined)
760
- .filter((thread) => (thread !== undefined && isThreadAllowedByProjectAuthority(thread, authority))),
842
+ .filter((thread) => (thread !== undefined && isThreadAllowedByWorkspaceAuthority(thread, authority))),
761
843
  };
762
844
  }
763
845
  function sanitizeProjectList(result) {
@@ -789,7 +871,7 @@ function sanitizeProjectList(result) {
789
871
  ...(typeof result.nextCursor === 'string' && result.nextCursor.length > 0 ? { nextCursor: result.nextCursor } : { nextCursor: null }),
790
872
  };
791
873
  }
792
- function isThreadAllowedByProjectAuthority(thread, authority) {
874
+ function isThreadAllowedByWorkspaceAuthority(thread, authority) {
793
875
  const projectId = typeof thread.projectId === 'string' ? thread.projectId : undefined;
794
876
  if (projectId !== undefined && authority.projectIds.has(projectId))
795
877
  return true;
@@ -809,6 +891,15 @@ function normalizeCodexPathForCompare(path) {
809
891
  function extractThread(result) {
810
892
  return isRecord(result) && isRecord(result.thread) ? result.thread : undefined;
811
893
  }
894
+ function extractTurnId(result) {
895
+ if (!isRecord(result))
896
+ return undefined;
897
+ if (typeof result.turnId === 'string' && result.turnId.length > 0)
898
+ return result.turnId;
899
+ if (isRecord(result.turn) && typeof result.turn.id === 'string' && result.turn.id.length > 0)
900
+ return result.turn.id;
901
+ return undefined;
902
+ }
812
903
  function extractThreadId(params) {
813
904
  if (!isRecord(params))
814
905
  return undefined;
@@ -823,15 +914,44 @@ function extractThreadId(params) {
823
914
  function optionalInteger(value) {
824
915
  return typeof value === 'number' && Number.isSafeInteger(value) ? value : undefined;
825
916
  }
826
- function codexPermission(params) {
917
+ function codexPermission(params, fallbackPreset) {
827
918
  const { permissionPreset, ...rest } = params;
828
- const fullAccess = permissionPreset === 'danger-full-access';
919
+ const preset = permissionPreset === 'workspace-write' || permissionPreset === 'danger-full-access'
920
+ ? permissionPreset
921
+ : fallbackPreset;
922
+ if (preset === undefined)
923
+ return { params: mapCodexImageInputs(rest) };
924
+ const fullAccess = preset === 'danger-full-access';
829
925
  return {
830
926
  params: mapCodexImageInputs(rest),
831
927
  approvalPolicy: fullAccess ? 'never' : 'on-request',
832
928
  sandbox: fullAccess ? 'danger-full-access' : 'workspace-write',
833
929
  };
834
930
  }
931
+ function codexThreadPermissionParams(permission) {
932
+ if (permission.approvalPolicy === undefined || permission.sandbox === undefined)
933
+ return {};
934
+ return {
935
+ approvalPolicy: permission.approvalPolicy,
936
+ sandbox: permission.sandbox,
937
+ };
938
+ }
939
+ function codexTurnPermissionParams(permission, cwd) {
940
+ if (permission.approvalPolicy === undefined || permission.sandbox === undefined)
941
+ return {};
942
+ return {
943
+ approvalPolicy: permission.approvalPolicy,
944
+ sandboxPolicy: permission.sandbox === 'danger-full-access'
945
+ ? { type: 'dangerFullAccess' }
946
+ : {
947
+ type: 'workspaceWrite',
948
+ writableRoots: cwd === undefined ? [] : [cwd],
949
+ networkAccess: false,
950
+ excludeTmpdirEnvVar: false,
951
+ excludeSlashTmp: false,
952
+ },
953
+ };
954
+ }
835
955
  function mapCodexImageInputs(params) {
836
956
  if (!Array.isArray(params.input))
837
957
  return params;
@@ -875,6 +995,10 @@ function isHistoryReadRecoverable(error) {
875
995
  return error instanceof RpcError
876
996
  && ['METHOD_NOT_ALLOWED', 'METHOD_NOT_FOUND', 'CODEX_UPSTREAM_ERROR', 'CODEX_REQUEST_TIMEOUT', 'CODEX_THREAD_BUSY'].includes(error.code);
877
997
  }
998
+ function isProjectListFallbackError(error) {
999
+ return error instanceof RpcError
1000
+ && ['METHOD_NOT_ALLOWED', 'METHOD_NOT_FOUND', 'CODEX_UPSTREAM_ERROR'].includes(error.code);
1001
+ }
878
1002
  function canTryNextBinary(error) {
879
1003
  return !(error instanceof RpcError) || !['CODEX_AUTH_REQUIRED', 'CODEX_CLOSED'].includes(error.code);
880
1004
  }