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.
Files changed (70) hide show
  1. package/README.md +35 -11
  2. package/cordis.patch.yml +3 -0
  3. package/dist/client-runtime.d.ts +28 -0
  4. package/dist/client-runtime.d.ts.map +1 -1
  5. package/dist/client-runtime.js +327 -5
  6. package/dist/client-runtime.js.map +1 -1
  7. package/dist/client-secure-transport.d.ts.map +1 -1
  8. package/dist/client-secure-transport.js +9 -2
  9. package/dist/client-secure-transport.js.map +1 -1
  10. package/dist/client.github.js +167 -48
  11. package/dist/client.js +167 -48
  12. package/dist/client.js.map +1 -1
  13. package/dist/codex/app-server.d.ts +71 -0
  14. package/dist/codex/app-server.d.ts.map +1 -0
  15. package/dist/codex/app-server.js +268 -0
  16. package/dist/codex/app-server.js.map +1 -0
  17. package/dist/codex/domain.d.ts +82 -0
  18. package/dist/codex/domain.d.ts.map +1 -0
  19. package/dist/codex/domain.js +893 -0
  20. package/dist/codex/domain.js.map +1 -0
  21. package/dist/codex/method-policy.d.ts +297 -0
  22. package/dist/codex/method-policy.d.ts.map +1 -0
  23. package/dist/codex/method-policy.js +103 -0
  24. package/dist/codex/method-policy.js.map +1 -0
  25. package/dist/codex/peer-bridge.d.ts +57 -0
  26. package/dist/codex/peer-bridge.d.ts.map +1 -0
  27. package/dist/codex/peer-bridge.js +300 -0
  28. package/dist/codex/peer-bridge.js.map +1 -0
  29. package/dist/codex/virtual-harness.d.ts +178 -0
  30. package/dist/codex/virtual-harness.d.ts.map +1 -0
  31. package/dist/codex/virtual-harness.js +2515 -0
  32. package/dist/codex/virtual-harness.js.map +1 -0
  33. package/dist/config.d.ts +10 -0
  34. package/dist/config.d.ts.map +1 -1
  35. package/dist/config.js +12 -0
  36. package/dist/config.js.map +1 -1
  37. package/dist/control-runtime.d.ts +1 -0
  38. package/dist/control-runtime.d.ts.map +1 -1
  39. package/dist/control-runtime.js +22 -0
  40. package/dist/control-runtime.js.map +1 -1
  41. package/dist/harness-remote-bridge.d.ts +2 -2
  42. package/dist/harness-remote-bridge.d.ts.map +1 -1
  43. package/dist/harness-remote-bridge.js +2 -2
  44. package/dist/harness-remote-bridge.js.map +1 -1
  45. package/dist/index.d.ts +11 -0
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +8550 -3884
  48. package/dist/index.js.map +4 -4
  49. package/dist/remote-typert-gateway.d.ts +1 -1
  50. package/dist/remote-typert-gateway.d.ts.map +1 -1
  51. package/dist/remote-typert-gateway.js +9 -2
  52. package/dist/remote-typert-gateway.js.map +1 -1
  53. package/dist/rpc-router.d.ts +5 -2
  54. package/dist/rpc-router.d.ts.map +1 -1
  55. package/dist/rpc-router.js +30 -1
  56. package/dist/rpc-router.js.map +1 -1
  57. package/dist/server-connection.d.ts +1 -0
  58. package/dist/server-connection.d.ts.map +1 -1
  59. package/dist/server-connection.js +69 -22
  60. package/dist/server-connection.js.map +1 -1
  61. package/dist/service.d.ts +20 -0
  62. package/dist/service.d.ts.map +1 -1
  63. package/dist/service.js +58 -2
  64. package/dist/service.js.map +1 -1
  65. package/dist/typert-gateway-contract.d.ts +2 -2
  66. package/dist/typert-gateway-contract.d.ts.map +1 -1
  67. package/dist/version.d.ts +1 -1
  68. package/dist/version.js +1 -1
  69. package/package.json +19 -23
  70. package/public.d.ts +4 -0
@@ -0,0 +1,893 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { homedir } from 'node:os';
3
+ import { isAbsolute, join } from 'node:path';
4
+ import { RpcError } from '../safe-error.js';
5
+ import { CodexAppServerClient, CodexAppServerError, } from './app-server.js';
6
+ import { isThreadMutation, parseCodexCall, threadIdFromParams, } from './method-policy.js';
7
+ import { CodexPeerBridge } from './peer-bridge.js';
8
+ import { paginateCodexNativeHistory, projectCodexNativeHistory } from './virtual-harness.js';
9
+ const APPROVAL_TTL_MS = 5 * 60_000;
10
+ const DEFAULT_RESTART_DELAYS_MS = [1_000, 2_000, 4_000, 8_000, 15_000];
11
+ const CODEX_PAGE_LIMIT = 100;
12
+ const CODEX_HISTORY_PAGE_LIMIT = 25;
13
+ const MAX_CODEX_HISTORY_PAGES = 64;
14
+ /**
15
+ * Optional Codex business domain inside the existing Remote Plugin. It shares
16
+ * Remote identity/transport with Harness, but owns its App Server process,
17
+ * method policy, subscriptions, leases, and approval handles.
18
+ */
19
+ export class CodexRemoteDomain {
20
+ config;
21
+ logger;
22
+ createAppServer;
23
+ restartDelaysMs;
24
+ appServer;
25
+ unsubscribeInbound;
26
+ unsubscribeUnavailable;
27
+ peers = new Map();
28
+ turnOwners = new Map();
29
+ approvals = new Map();
30
+ approvalExpiryTimer;
31
+ restartTimer;
32
+ restartAttempt = 0;
33
+ available = false;
34
+ closed = false;
35
+ state = 'disabled';
36
+ unavailableCode;
37
+ constructor(config, logger, createAppServer = (binary, targetLogger) => new CodexAppServerClient(binary, targetLogger), restartDelaysMs = DEFAULT_RESTART_DELAYS_MS) {
38
+ this.config = config;
39
+ this.logger = logger;
40
+ this.createAppServer = createAppServer;
41
+ this.restartDelaysMs = restartDelaysMs;
42
+ }
43
+ async start() {
44
+ if (this.closed)
45
+ throw new RpcError('CODEX_CLOSED', 'The Codex Remote domain is closed.');
46
+ if (!this.config.enabled)
47
+ return;
48
+ try {
49
+ this.state = 'starting';
50
+ await this.launchAppServer();
51
+ }
52
+ catch (error) {
53
+ this.available = false;
54
+ this.state = 'unavailable';
55
+ this.unavailableCode = errorCode(error);
56
+ await this.disposeAppServer(this.appServer);
57
+ this.logger.warn('Codex Remote domain unavailable', { code: this.unavailableCode });
58
+ }
59
+ }
60
+ isAvailable() { return this.available && this.appServer?.isReady() === true; }
61
+ status() {
62
+ return {
63
+ enabled: this.config.enabled,
64
+ available: this.isAvailable(),
65
+ state: this.state,
66
+ restartAttempt: this.restartAttempt,
67
+ ...(this.unavailableCode === undefined ? {} : { error: this.unavailableCode }),
68
+ };
69
+ }
70
+ createPeer(context, publish) {
71
+ if (!this.config.enabled)
72
+ return undefined;
73
+ const bridge = new CodexPeerBridge(this, context, publish, this.logger);
74
+ this.peers.set(context.connectionId, bridge);
75
+ return bridge;
76
+ }
77
+ async call(connectionId, input) {
78
+ const envelope = parseCallEnvelope(input);
79
+ const call = parseCodexCall(envelope.method, envelope.params);
80
+ this.requireAppServer();
81
+ if (call.method === 'account/read') {
82
+ return sanitizeAccount(await this.callUpstream(call.method, call.params));
83
+ }
84
+ if (call.method === 'project/list') {
85
+ return sanitizeProjectList(await this.callUpstream(call.method, call.params));
86
+ }
87
+ if (call.method === 'thread/list') {
88
+ return filterThreadListByProjectAuthority(sanitizeThreadList(await this.callUpstream(call.method, call.params)), await this.readProjectAuthority());
89
+ }
90
+ if (call.method === 'thread/start') {
91
+ const cwd = await this.requireCodexWorkspacePath(call.params.cwd);
92
+ const permission = codexPermission(call.params);
93
+ const result = await this.callUpstream(call.method, {
94
+ ...permission.params,
95
+ cwd,
96
+ approvalPolicy: permission.approvalPolicy,
97
+ sandbox: permission.sandbox,
98
+ serviceName: 'deepseek_harness_remote',
99
+ });
100
+ if (extractThread(result)?.id === undefined) {
101
+ throw new RpcError('CODEX_INVALID_RESPONSE', 'Codex App Server returned an invalid thread.');
102
+ }
103
+ return result;
104
+ }
105
+ const threadId = threadIdFromParams(call.params);
106
+ if (call.method === 'dsh/sessionHistory') {
107
+ const thread = await this.readThreadForHistory(connectionId, threadId);
108
+ return paginateCodexNativeHistory(projectCodexNativeHistory(thread, `codex:${threadId}`), {
109
+ beforeSeq: optionalInteger(call.params.beforeSeq),
110
+ throughSeq: optionalInteger(call.params.throughSeq),
111
+ maxMessages: optionalInteger(call.params.maxMessages),
112
+ });
113
+ }
114
+ const allowedThread = threadId === undefined ? undefined : await this.readKnownThread(threadId);
115
+ if (call.method === 'thread/read') {
116
+ // assertThreadAllowed already produced a bounded summary, but make the
117
+ // requested read again so includeTurns has its official semantics.
118
+ return this.callUpstream(call.method, call.params);
119
+ }
120
+ if (call.method === 'thread/unsubscribe') {
121
+ const bridge = this.peers.get(connectionId);
122
+ bridge?.removeThreadSubscriptions(threadId);
123
+ if (this.hasSubscriber(threadId))
124
+ return { status: 'unsubscribed' };
125
+ return this.callUpstream(call.method, call.params);
126
+ }
127
+ let claimed = false;
128
+ if (isThreadMutation(call.method) && threadId !== undefined) {
129
+ claimed = this.claimTurn(threadId, connectionId, call.method);
130
+ }
131
+ try {
132
+ const permission = codexPermission(call.params);
133
+ const upstreamParams = call.method === 'thread/resume' && allowedThread !== undefined
134
+ ? {
135
+ ...permission.params,
136
+ ...(allowedThread.cwd === undefined ? {} : { cwd: allowedThread.cwd }),
137
+ approvalPolicy: permission.approvalPolicy,
138
+ sandbox: permission.sandbox,
139
+ excludeTurns: true,
140
+ }
141
+ : call.method === 'thread/fork' && allowedThread !== undefined
142
+ ? {
143
+ ...permission.params,
144
+ ...(allowedThread.cwd === undefined ? {} : { cwd: allowedThread.cwd }),
145
+ approvalPolicy: permission.approvalPolicy,
146
+ sandbox: permission.sandbox,
147
+ }
148
+ : call.method === 'turn/start'
149
+ ? {
150
+ ...permission.params,
151
+ ...(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
+ }
163
+ : permission.params;
164
+ let result;
165
+ try {
166
+ result = await this.callUpstream(call.method, upstreamParams);
167
+ }
168
+ catch (error) {
169
+ // App Server rejects thread/resume when this process already has the
170
+ // thread loaded. Remote callers should be able to resume defensively
171
+ // before every turn, including immediately after thread/start.
172
+ if (call.method === 'thread/resume'
173
+ && allowedThread !== undefined
174
+ && error instanceof RpcError
175
+ && error.code === 'CODEX_UPSTREAM_ERROR') {
176
+ return { thread: allowedThread.thread };
177
+ }
178
+ throw error;
179
+ }
180
+ if (call.method === 'thread/resume' || call.method === 'thread/fork' || call.method === 'thread/unarchive') {
181
+ await this.assertResultThreadAllowed(result);
182
+ }
183
+ return result;
184
+ }
185
+ catch (error) {
186
+ if (claimed && threadId !== undefined)
187
+ this.turnOwners.delete(threadId);
188
+ throw mapAppServerError(error);
189
+ }
190
+ }
191
+ async respond(connectionId, input) {
192
+ await this.expireApprovals();
193
+ const params = parseRespond(input);
194
+ const approval = this.approvals.get(params.requestHandle);
195
+ if (approval === undefined || approval.connectionId !== connectionId) {
196
+ throw new RpcError('CODEX_APPROVAL_NOT_FOUND', 'The Codex approval is missing, expired, or belongs to another connection.');
197
+ }
198
+ this.approvals.delete(params.requestHandle);
199
+ this.scheduleApprovalExpiry();
200
+ await this.requireAppServer().respond(approval.upstreamId, { decision: params.decision });
201
+ return { resolved: true };
202
+ }
203
+ async detachPeer(connectionId) {
204
+ const bridge = this.peers.get(connectionId);
205
+ if (bridge !== undefined)
206
+ this.peers.delete(connectionId);
207
+ for (const [threadId, owner] of this.turnOwners) {
208
+ if (owner === connectionId)
209
+ this.turnOwners.delete(threadId);
210
+ }
211
+ const appServer = this.appServer;
212
+ const pending = [...this.approvals.entries()].filter(([, approval]) => approval.connectionId === connectionId);
213
+ for (const [handle, approval] of pending) {
214
+ this.approvals.delete(handle);
215
+ await appServer?.respond(approval.upstreamId, { decision: 'decline' }).catch(() => undefined);
216
+ }
217
+ this.scheduleApprovalExpiry();
218
+ }
219
+ async close() {
220
+ if (this.closed)
221
+ return;
222
+ this.closed = true;
223
+ this.available = false;
224
+ this.state = this.config.enabled ? 'unavailable' : 'disabled';
225
+ if (this.restartTimer !== undefined)
226
+ clearTimeout(this.restartTimer);
227
+ this.restartTimer = undefined;
228
+ for (const bridge of [...this.peers.values()])
229
+ await bridge.closeAll();
230
+ this.peers.clear();
231
+ this.turnOwners.clear();
232
+ if (this.approvalExpiryTimer !== undefined)
233
+ clearTimeout(this.approvalExpiryTimer);
234
+ this.approvalExpiryTimer = undefined;
235
+ this.approvals.clear();
236
+ this.unsubscribeInbound?.();
237
+ this.unsubscribeInbound = undefined;
238
+ this.unsubscribeUnavailable?.();
239
+ this.unsubscribeUnavailable = undefined;
240
+ await this.appServer?.close();
241
+ this.appServer = undefined;
242
+ }
243
+ async launchAppServer() {
244
+ let lastError;
245
+ for (const binary of codexBinaryCandidates(this.config.binary)) {
246
+ try {
247
+ await this.launchAppServerCandidate(binary);
248
+ return;
249
+ }
250
+ catch (error) {
251
+ lastError = error;
252
+ if (!canTryNextBinary(error))
253
+ throw error;
254
+ }
255
+ }
256
+ throw lastError ?? new RpcError('CODEX_START_FAILED', 'Codex App Server could not be started.');
257
+ }
258
+ async launchAppServerCandidate(binary) {
259
+ const appServer = this.createAppServer(binary, this.logger);
260
+ this.appServer = appServer;
261
+ this.unsubscribeInbound = appServer.onInbound(message => {
262
+ void this.handleInbound(message).catch(error => {
263
+ this.logger.warn('Codex inbound handling failed', { code: errorCode(error) });
264
+ });
265
+ });
266
+ this.unsubscribeUnavailable = appServer.onUnavailable(code => {
267
+ if (this.available && this.appServer === appServer) {
268
+ void this.handleAppServerUnavailable(appServer, code);
269
+ }
270
+ });
271
+ try {
272
+ await appServer.start();
273
+ const account = await appServer.call('account/read', { refreshToken: false }, 15_000);
274
+ if (!accountCanRun(account)) {
275
+ throw new RpcError('CODEX_AUTH_REQUIRED', 'Codex is not signed in on this Host.');
276
+ }
277
+ if (this.closed || this.appServer !== appServer) {
278
+ await appServer.close().catch(() => undefined);
279
+ return;
280
+ }
281
+ this.available = true;
282
+ this.state = 'ready';
283
+ this.restartAttempt = 0;
284
+ this.unavailableCode = undefined;
285
+ this.logger.info('Codex Remote domain ready');
286
+ }
287
+ catch (error) {
288
+ await this.disposeAppServer(appServer);
289
+ throw error;
290
+ }
291
+ }
292
+ async handleAppServerUnavailable(appServer, code) {
293
+ if (this.closed || this.appServer !== appServer)
294
+ return;
295
+ this.available = false;
296
+ this.state = 'restarting';
297
+ this.unavailableCode = code;
298
+ this.turnOwners.clear();
299
+ this.approvals.clear();
300
+ if (this.approvalExpiryTimer !== undefined)
301
+ clearTimeout(this.approvalExpiryTimer);
302
+ this.approvalExpiryTimer = undefined;
303
+ await Promise.all([...this.peers.values()].map(peer => peer.failStreams('failed')));
304
+ await this.disposeAppServer(appServer);
305
+ this.scheduleRestart();
306
+ }
307
+ scheduleRestart() {
308
+ if (this.closed || this.restartTimer !== undefined)
309
+ return;
310
+ if (this.restartAttempt >= this.restartDelaysMs.length) {
311
+ this.state = 'unavailable';
312
+ this.logger.warn('Codex App Server restart attempts exhausted', { attempts: this.restartAttempt });
313
+ return;
314
+ }
315
+ const delayMs = Math.max(0, this.restartDelaysMs[this.restartAttempt] ?? 0);
316
+ this.restartAttempt += 1;
317
+ this.state = 'restarting';
318
+ this.restartTimer = setTimeout(() => {
319
+ this.restartTimer = undefined;
320
+ void this.restartAfterFailure();
321
+ }, delayMs);
322
+ this.restartTimer.unref?.();
323
+ this.logger.warn('Codex App Server restart scheduled', { attempt: this.restartAttempt, delayMs });
324
+ }
325
+ async restartAfterFailure() {
326
+ if (this.closed)
327
+ return;
328
+ try {
329
+ await this.launchAppServer();
330
+ }
331
+ catch (error) {
332
+ this.available = false;
333
+ this.state = 'restarting';
334
+ this.unavailableCode = errorCode(error);
335
+ this.logger.warn('Codex App Server restart failed', {
336
+ attempt: this.restartAttempt,
337
+ code: this.unavailableCode,
338
+ });
339
+ this.scheduleRestart();
340
+ }
341
+ }
342
+ async disposeAppServer(appServer) {
343
+ if (appServer === undefined || this.appServer !== appServer)
344
+ return;
345
+ this.unsubscribeInbound?.();
346
+ this.unsubscribeInbound = undefined;
347
+ this.unsubscribeUnavailable?.();
348
+ this.unsubscribeUnavailable = undefined;
349
+ this.appServer = undefined;
350
+ await appServer.close().catch(() => undefined);
351
+ }
352
+ async handleInbound(message) {
353
+ if (!this.available)
354
+ return;
355
+ if (message.kind === 'request') {
356
+ await this.handleServerRequest(message);
357
+ return;
358
+ }
359
+ const threadId = extractThreadId(message.params);
360
+ if (message.method === 'turn/completed' && threadId !== undefined)
361
+ this.turnOwners.delete(threadId);
362
+ if (message.method === 'serverRequest/resolved')
363
+ this.resolveUpstreamApproval(message.params);
364
+ if (threadId === undefined)
365
+ return;
366
+ await Promise.all([...this.peers.values()].map(peer => peer.publishInbound(threadId, {
367
+ method: message.method,
368
+ params: message.params,
369
+ })));
370
+ }
371
+ async handleServerRequest(message) {
372
+ const appServer = this.requireAppServer();
373
+ await this.expireApprovals();
374
+ if (message.method !== 'item/commandExecution/requestApproval'
375
+ && message.method !== 'item/fileChange/requestApproval') {
376
+ await appServer.respondError(message.id, -32601, 'This Remote client does not support the server request.');
377
+ return;
378
+ }
379
+ const threadId = extractThreadId(message.params);
380
+ const owner = threadId === undefined ? undefined : this.turnOwners.get(threadId);
381
+ const peer = owner === undefined ? undefined : this.peers.get(owner);
382
+ if (threadId === undefined || owner === undefined || peer === undefined || !peer.hasThreadSubscription(threadId)) {
383
+ await appServer.respond(message.id, { decision: 'decline' });
384
+ return;
385
+ }
386
+ const requestHandle = randomUUID();
387
+ this.approvals.set(requestHandle, {
388
+ upstreamId: message.id,
389
+ connectionId: owner,
390
+ threadId,
391
+ method: message.method,
392
+ expiresAt: Date.now() + APPROVAL_TTL_MS,
393
+ });
394
+ this.scheduleApprovalExpiry();
395
+ try {
396
+ await peer.publishInbound(threadId, {
397
+ method: message.method,
398
+ params: sanitizeApprovalParams(message.params, requestHandle),
399
+ });
400
+ }
401
+ catch (error) {
402
+ this.approvals.delete(requestHandle);
403
+ this.scheduleApprovalExpiry();
404
+ await appServer.respond(message.id, { decision: 'decline' }).catch(() => undefined);
405
+ throw error;
406
+ }
407
+ }
408
+ async readKnownThread(threadId) {
409
+ const authority = await this.readProjectAuthority();
410
+ let result;
411
+ try {
412
+ result = await this.callUpstream('thread/read', { threadId, includeTurns: false });
413
+ }
414
+ catch (error) {
415
+ if (!isHistoryReadRecoverable(error))
416
+ throw error;
417
+ const listed = await this.findKnownThreadInList(threadId, authority);
418
+ if (listed === undefined) {
419
+ throw new RpcError('CODEX_THREAD_NOT_ALLOWED', 'The Codex thread is not available through this Remote Host.');
420
+ }
421
+ return { thread: listed, ...(typeof listed.cwd === 'string' && listed.cwd.length > 0 ? { cwd: listed.cwd } : {}) };
422
+ }
423
+ const thread = extractThread(result);
424
+ if (thread === undefined || thread.id !== threadId || !isThreadAllowedByProjectAuthority(thread, authority)) {
425
+ throw new RpcError('CODEX_THREAD_NOT_ALLOWED', 'The Codex thread is not available through this Remote Host.');
426
+ }
427
+ return { thread, ...(typeof thread.cwd === 'string' && thread.cwd.length > 0 ? { cwd: thread.cwd } : {}) };
428
+ }
429
+ async readThreadForHistory(connectionId, threadId) {
430
+ const { thread: metadata } = await this.readKnownThread(threadId);
431
+ try {
432
+ return { ...metadata, turns: await this.readThreadTurns(connectionId, threadId, 'full') };
433
+ }
434
+ catch (fullError) {
435
+ if (!isHistoryReadRecoverable(fullError))
436
+ throw fullError;
437
+ this.logHistoryFallback(connectionId, 'turns-full', fullError);
438
+ }
439
+ try {
440
+ return { ...metadata, turns: await this.readThreadTurns(connectionId, threadId, 'summary') };
441
+ }
442
+ catch (summaryError) {
443
+ if (!isHistoryReadRecoverable(summaryError))
444
+ throw summaryError;
445
+ this.logHistoryFallback(connectionId, 'turns-summary', summaryError);
446
+ }
447
+ try {
448
+ const legacyResult = await this.callUpstream('thread/read', { threadId, includeTurns: true });
449
+ const legacyThread = extractThread(legacyResult);
450
+ if (legacyThread === undefined || legacyThread.id !== threadId) {
451
+ throw new RpcError('CODEX_INVALID_RESPONSE', 'Codex App Server returned an invalid Thread history.');
452
+ }
453
+ return legacyThread;
454
+ }
455
+ catch (legacyError) {
456
+ if (!isHistoryReadRecoverable(legacyError))
457
+ throw legacyError;
458
+ this.logHistoryFallback(connectionId, 'thread-read-full', legacyError);
459
+ return { ...metadata, turns: [] };
460
+ }
461
+ }
462
+ async readThreadTurns(connectionId, threadId, itemsView) {
463
+ const turns = [];
464
+ let cursor;
465
+ for (let page = 0; page < MAX_CODEX_HISTORY_PAGES; page += 1) {
466
+ const result = await this.callUpstream('thread/turns/list', {
467
+ threadId,
468
+ limit: CODEX_HISTORY_PAGE_LIMIT,
469
+ sortDirection: 'asc',
470
+ itemsView,
471
+ ...(cursor === undefined ? {} : { cursor }),
472
+ });
473
+ const pageResult = isRecord(result) ? result : {};
474
+ for (const rawTurn of array(pageResult.data)) {
475
+ if (!isRecord(rawTurn))
476
+ continue;
477
+ const turnId = typeof rawTurn.id === 'string' ? rawTurn.id : undefined;
478
+ const items = rawTurn.itemsView === 'full' || turnId === undefined
479
+ ? array(rawTurn.items)
480
+ : await this.readThreadItems(connectionId, threadId, turnId, array(rawTurn.items));
481
+ turns.push({ ...rawTurn, items });
482
+ }
483
+ cursor = typeof pageResult.nextCursor === 'string' && pageResult.nextCursor.length > 0
484
+ ? pageResult.nextCursor
485
+ : undefined;
486
+ if (cursor === undefined)
487
+ break;
488
+ }
489
+ return turns;
490
+ }
491
+ async readThreadItems(connectionId, threadId, turnId, fallbackItems) {
492
+ const items = [];
493
+ let cursor;
494
+ try {
495
+ for (let page = 0; page < MAX_CODEX_HISTORY_PAGES; page += 1) {
496
+ const result = await this.callUpstream('thread/items/list', {
497
+ threadId,
498
+ turnId,
499
+ limit: CODEX_HISTORY_PAGE_LIMIT,
500
+ sortDirection: 'asc',
501
+ ...(cursor === undefined ? {} : { cursor }),
502
+ });
503
+ const pageResult = isRecord(result) ? result : {};
504
+ for (const entry of array(pageResult.data)) {
505
+ if (isRecord(entry) && entry.item !== undefined)
506
+ items.push(entry.item);
507
+ }
508
+ cursor = typeof pageResult.nextCursor === 'string' && pageResult.nextCursor.length > 0
509
+ ? pageResult.nextCursor
510
+ : undefined;
511
+ if (cursor === undefined)
512
+ break;
513
+ }
514
+ }
515
+ catch (error) {
516
+ if (!isHistoryReadRecoverable(error))
517
+ throw error;
518
+ this.logHistoryFallback(connectionId, 'items', error);
519
+ return fallbackItems;
520
+ }
521
+ return items;
522
+ }
523
+ logHistoryFallback(connectionId, stage, error) {
524
+ this.logger.warn('Codex history read fallback', {
525
+ connectionId: maskId(connectionId),
526
+ stage,
527
+ code: errorCode(error),
528
+ });
529
+ }
530
+ async assertResultThreadAllowed(result) {
531
+ const thread = extractThread(result);
532
+ if (thread === undefined || typeof thread.id !== 'string') {
533
+ throw new RpcError('CODEX_INVALID_RESPONSE', 'Codex App Server returned an invalid thread.');
534
+ }
535
+ if (!isThreadAllowedByProjectAuthority(thread, await this.readProjectAuthority())) {
536
+ throw new RpcError('CODEX_THREAD_NOT_ALLOWED', 'The Codex thread is not available through this Remote Host.');
537
+ }
538
+ }
539
+ claimTurn(threadId, connectionId, method) {
540
+ const owner = this.turnOwners.get(threadId);
541
+ if (method === 'turn/interrupt') {
542
+ if (owner === undefined) {
543
+ // A replacement connection may recover an already-running persisted
544
+ // turn. Claim only for this explicit mutation; passive viewers still
545
+ // remain observers and cannot steal a live connection's lease.
546
+ this.turnOwners.set(threadId, connectionId);
547
+ return true;
548
+ }
549
+ if (owner !== connectionId)
550
+ throw new RpcError('CODEX_TURN_OWNED', 'Only the connection that started this Codex turn can interrupt it.');
551
+ return false;
552
+ }
553
+ if (owner !== undefined && owner !== connectionId) {
554
+ throw new RpcError('CODEX_TURN_OWNED', 'Another Remote connection owns the active Codex turn.');
555
+ }
556
+ if (owner === connectionId)
557
+ return false;
558
+ this.turnOwners.set(threadId, connectionId);
559
+ return true;
560
+ }
561
+ hasSubscriber(threadId) {
562
+ return [...this.peers.values()].some(peer => peer.hasThreadSubscription(threadId));
563
+ }
564
+ resolveUpstreamApproval(params) {
565
+ if (!isRecord(params) || (typeof params.requestId !== 'string' && typeof params.requestId !== 'number'))
566
+ return;
567
+ for (const [handle, approval] of this.approvals) {
568
+ if (approval.upstreamId === params.requestId)
569
+ this.approvals.delete(handle);
570
+ }
571
+ }
572
+ async expireApprovals() {
573
+ const now = Date.now();
574
+ const appServer = this.appServer;
575
+ for (const [handle, approval] of this.approvals) {
576
+ if (approval.expiresAt > now)
577
+ continue;
578
+ this.approvals.delete(handle);
579
+ await appServer?.respond(approval.upstreamId, { decision: 'decline' }).catch(() => undefined);
580
+ }
581
+ this.scheduleApprovalExpiry();
582
+ }
583
+ scheduleApprovalExpiry() {
584
+ if (this.approvalExpiryTimer !== undefined)
585
+ clearTimeout(this.approvalExpiryTimer);
586
+ this.approvalExpiryTimer = undefined;
587
+ const nextExpiry = Math.min(...[...this.approvals.values()].map(approval => approval.expiresAt));
588
+ if (!Number.isFinite(nextExpiry))
589
+ return;
590
+ this.approvalExpiryTimer = setTimeout(() => {
591
+ this.approvalExpiryTimer = undefined;
592
+ void this.expireApprovals().catch(error => {
593
+ this.logger.warn('Codex approval expiry failed', { code: errorCode(error) });
594
+ });
595
+ }, Math.max(0, nextExpiry - Date.now()));
596
+ this.approvalExpiryTimer.unref?.();
597
+ }
598
+ requireAppServer() {
599
+ if (!this.isAvailable() || this.appServer === undefined) {
600
+ throw new RpcError('CODEX_UNAVAILABLE', 'Codex Remote is disabled or unavailable on this Host.');
601
+ }
602
+ return this.appServer;
603
+ }
604
+ async callUpstream(method, params) {
605
+ try {
606
+ return await this.requireAppServer().call(method, params);
607
+ }
608
+ catch (error) {
609
+ throw mapAppServerError(error);
610
+ }
611
+ }
612
+ async readProjectAuthority() {
613
+ const projectIds = new Set();
614
+ const roots = [];
615
+ 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);
627
+ }
628
+ cursor = typeof result.nextCursor === 'string' && result.nextCursor.length > 0 ? result.nextCursor : undefined;
629
+ if (cursor === undefined)
630
+ break;
631
+ }
632
+ return { projectIds, roots };
633
+ }
634
+ async findKnownThreadInList(threadId, authority) {
635
+ let cursor;
636
+ for (let page = 0; page < 32; page += 1) {
637
+ const result = sanitizeThreadList(await this.callUpstream('thread/list', {
638
+ limit: 100,
639
+ sortKey: 'updated_at',
640
+ sortDirection: 'desc',
641
+ archived: false,
642
+ ...(cursor === undefined ? {} : { cursor }),
643
+ }));
644
+ const thread = result.data.find(item => item.id === threadId);
645
+ if (thread !== undefined) {
646
+ return isThreadAllowedByProjectAuthority(thread, authority) ? thread : undefined;
647
+ }
648
+ cursor = typeof result.nextCursor === 'string' && result.nextCursor.length > 0 ? result.nextCursor : undefined;
649
+ if (cursor === undefined)
650
+ break;
651
+ }
652
+ return undefined;
653
+ }
654
+ async requireCodexWorkspacePath(path) {
655
+ if (!isAbsolute(path)) {
656
+ throw new RpcError('CODEX_PATH_NOT_ALLOWED', 'The CodeX working directory is not available as a Workspace.');
657
+ }
658
+ const paths = await this.listCodexProjectWorkspacePaths();
659
+ const canonical = paths.get(normalizeCodexPathForCompare(path));
660
+ if (canonical === undefined) {
661
+ throw new RpcError('CODEX_PATH_NOT_ALLOWED', 'The CodeX working directory is not available as a Workspace.');
662
+ }
663
+ return canonical;
664
+ }
665
+ async listCodexProjectWorkspacePaths() {
666
+ const paths = new Map();
667
+ const authority = await this.readProjectAuthority();
668
+ for (const root of authority.roots)
669
+ paths.set(normalizeCodexPathForCompare(root), root);
670
+ return paths;
671
+ }
672
+ }
673
+ /**
674
+ * Prefer Codex bundled with the current ChatGPT desktop app on macOS when the
675
+ * user kept the default command. Explicit binary configuration is never
676
+ * rewritten or supplemented.
677
+ */
678
+ export function codexBinaryCandidates(configured, hostPlatform = process.platform, userHome = homedir()) {
679
+ if (configured !== 'codex' || hostPlatform !== 'darwin')
680
+ return [configured];
681
+ return [...new Set([
682
+ '/Applications/ChatGPT.app/Contents/Resources/codex',
683
+ join(userHome, 'Applications', 'ChatGPT.app', 'Contents', 'Resources', 'codex'),
684
+ configured,
685
+ ])];
686
+ }
687
+ function parseCallEnvelope(input) {
688
+ if (!isRecord(input) || typeof input.method !== 'string' || !('params' in input)
689
+ || Object.keys(input).some(key => key !== 'method' && key !== 'params')) {
690
+ throw new RpcError('INVALID_MESSAGE', 'The Codex call envelope is invalid.');
691
+ }
692
+ return { method: input.method, params: input.params };
693
+ }
694
+ function parseRespond(input) {
695
+ if (!isRecord(input)
696
+ || typeof input.requestHandle !== 'string'
697
+ || !['accept', 'decline', 'cancel'].includes(String(input.decision))
698
+ || Object.keys(input).some(key => key !== 'requestHandle' && key !== 'decision')) {
699
+ throw new RpcError('INVALID_MESSAGE', 'The Codex approval response is invalid.');
700
+ }
701
+ return input;
702
+ }
703
+ function accountCanRun(result) {
704
+ if (!isRecord(result) || typeof result.requiresOpenaiAuth !== 'boolean')
705
+ return false;
706
+ return result.requiresOpenaiAuth === false || isRecord(result.account);
707
+ }
708
+ function sanitizeAccount(result) {
709
+ if (!isRecord(result))
710
+ throw new RpcError('CODEX_INVALID_RESPONSE', 'Codex App Server returned invalid account state.');
711
+ const account = isRecord(result.account) ? result.account : undefined;
712
+ return {
713
+ authenticated: account !== undefined || result.requiresOpenaiAuth === false,
714
+ requiresOpenaiAuth: result.requiresOpenaiAuth === true,
715
+ ...(account === undefined ? {} : {
716
+ account: {
717
+ ...(typeof account.type === 'string' ? { type: account.type } : {}),
718
+ ...(typeof account.planType === 'string' ? { planType: account.planType } : {}),
719
+ },
720
+ }),
721
+ };
722
+ }
723
+ function sanitizeThreadList(result) {
724
+ if (!isRecord(result) || !Array.isArray(result.data)) {
725
+ throw new RpcError('CODEX_INVALID_RESPONSE', 'Codex App Server returned an invalid thread list.');
726
+ }
727
+ const data = result.data.flatMap(value => {
728
+ if (!isRecord(value) || typeof value.id !== 'string')
729
+ return [];
730
+ return [{
731
+ id: value.id,
732
+ ...(typeof value.sessionId === 'string' ? { sessionId: value.sessionId } : {}),
733
+ ...(typeof value.projectId === 'string' ? { projectId: value.projectId } : {}),
734
+ ...(typeof value.name === 'string' ? { name: value.name } : {}),
735
+ ...(typeof value.preview === 'string' ? { preview: value.preview } : {}),
736
+ ...(typeof value.cwd === 'string' ? { cwd: value.cwd } : {}),
737
+ ...(typeof value.createdAt === 'number' && Number.isFinite(value.createdAt) ? { createdAt: value.createdAt } : {}),
738
+ ...(typeof value.updatedAt === 'number' && Number.isFinite(value.updatedAt) ? { updatedAt: value.updatedAt } : {}),
739
+ ...(typeof value.archived === 'boolean' ? { archived: value.archived } : {}),
740
+ ...(typeof value.isPinned === 'boolean' ? { isPinned: value.isPinned } : {}),
741
+ ...(isRecord(value.status) ? { status: value.status } : {}),
742
+ }];
743
+ });
744
+ return {
745
+ data,
746
+ ...(typeof result.nextCursor === 'string' && result.nextCursor.length > 0 ? { nextCursor: result.nextCursor } : { nextCursor: null }),
747
+ ...(typeof result.backwardsCursor === 'string' && result.backwardsCursor.length > 0
748
+ ? { backwardsCursor: result.backwardsCursor }
749
+ : {}),
750
+ };
751
+ }
752
+ function filterThreadListByProjectAuthority(result, authority) {
753
+ if (!isRecord(result) || !Array.isArray(result.data)) {
754
+ throw new RpcError('CODEX_INVALID_RESPONSE', 'Codex App Server returned an invalid thread list.');
755
+ }
756
+ return {
757
+ ...result,
758
+ data: result.data
759
+ .map(record => isRecord(record) ? record : undefined)
760
+ .filter((thread) => (thread !== undefined && isThreadAllowedByProjectAuthority(thread, authority))),
761
+ };
762
+ }
763
+ function sanitizeProjectList(result) {
764
+ if (!isRecord(result) || !Array.isArray(result.data)) {
765
+ throw new RpcError('CODEX_INVALID_RESPONSE', 'Codex App Server returned an invalid project list.');
766
+ }
767
+ const data = result.data.flatMap(value => {
768
+ if (!isRecord(value) || typeof value.id !== 'string' || typeof value.name !== 'string')
769
+ return [];
770
+ const roots = Array.isArray(value.roots)
771
+ ? value.roots.flatMap(root => {
772
+ const path = isRecord(root) && typeof root.path === 'string' && root.path.length > 0 ? root.path : undefined;
773
+ return path === undefined || !isAbsolute(path) ? [] : [{ path }];
774
+ })
775
+ : [];
776
+ if (roots.length === 0)
777
+ return [];
778
+ return [{
779
+ id: value.id,
780
+ name: value.name,
781
+ roots,
782
+ ...(typeof value.position === 'number' && Number.isFinite(value.position) ? { position: value.position } : {}),
783
+ ...(typeof value.createdAt === 'number' && Number.isFinite(value.createdAt) ? { createdAt: value.createdAt } : {}),
784
+ ...(typeof value.updatedAt === 'number' && Number.isFinite(value.updatedAt) ? { updatedAt: value.updatedAt } : {}),
785
+ }];
786
+ });
787
+ return {
788
+ data,
789
+ ...(typeof result.nextCursor === 'string' && result.nextCursor.length > 0 ? { nextCursor: result.nextCursor } : { nextCursor: null }),
790
+ };
791
+ }
792
+ function isThreadAllowedByProjectAuthority(thread, authority) {
793
+ const projectId = typeof thread.projectId === 'string' ? thread.projectId : undefined;
794
+ if (projectId !== undefined && authority.projectIds.has(projectId))
795
+ return true;
796
+ const cwd = typeof thread.cwd === 'string' ? thread.cwd : undefined;
797
+ return cwd !== undefined && authority.roots.some(root => containsCodexPath(root, cwd));
798
+ }
799
+ function containsCodexPath(root, candidate) {
800
+ const normalizedRoot = normalizeCodexPathForCompare(root);
801
+ const normalizedCandidate = normalizeCodexPathForCompare(candidate);
802
+ return normalizedCandidate === normalizedRoot
803
+ || normalizedCandidate.startsWith(`${normalizedRoot}/`)
804
+ || normalizedCandidate.startsWith(`${normalizedRoot}\\`);
805
+ }
806
+ function normalizeCodexPathForCompare(path) {
807
+ return path.replace(/[\\/]+$/u, '') || path;
808
+ }
809
+ function extractThread(result) {
810
+ return isRecord(result) && isRecord(result.thread) ? result.thread : undefined;
811
+ }
812
+ function extractThreadId(params) {
813
+ if (!isRecord(params))
814
+ return undefined;
815
+ if (typeof params.threadId === 'string')
816
+ return params.threadId;
817
+ if (isRecord(params.thread) && typeof params.thread.id === 'string')
818
+ return params.thread.id;
819
+ if (isRecord(params.turn) && typeof params.turn.threadId === 'string')
820
+ return params.turn.threadId;
821
+ return undefined;
822
+ }
823
+ function optionalInteger(value) {
824
+ return typeof value === 'number' && Number.isSafeInteger(value) ? value : undefined;
825
+ }
826
+ function codexPermission(params) {
827
+ const { permissionPreset, ...rest } = params;
828
+ const fullAccess = permissionPreset === 'danger-full-access';
829
+ return {
830
+ params: mapCodexImageInputs(rest),
831
+ approvalPolicy: fullAccess ? 'never' : 'on-request',
832
+ sandbox: fullAccess ? 'danger-full-access' : 'workspace-write',
833
+ };
834
+ }
835
+ function mapCodexImageInputs(params) {
836
+ if (!Array.isArray(params.input))
837
+ return params;
838
+ return {
839
+ ...params,
840
+ input: params.input.map(value => {
841
+ if (!isRecord(value) || value.type !== 'image'
842
+ || typeof value.mediaType !== 'string' || typeof value.data !== 'string')
843
+ return value;
844
+ return { type: 'image', url: `data:${value.mediaType};base64,${value.data}` };
845
+ }),
846
+ };
847
+ }
848
+ function sanitizeApprovalParams(params, requestHandle) {
849
+ if (!isRecord(params))
850
+ return { requestHandle };
851
+ const safe = { ...params };
852
+ delete safe.proposedExecpolicyAmendment;
853
+ delete safe.additionalPermissions;
854
+ safe.availableDecisions = ['accept', 'decline', 'cancel'];
855
+ safe.requestHandle = requestHandle;
856
+ return safe;
857
+ }
858
+ function mapAppServerError(error) {
859
+ if (error instanceof RpcError)
860
+ return error;
861
+ if (error instanceof CodexAppServerError) {
862
+ if (error.code === 'CODEX_UPSTREAM_ERROR' && isActiveWriterMessage(error.message)) {
863
+ return new RpcError('CODEX_THREAD_BUSY', 'The selected CodeX thread is already active in another CodeX client.');
864
+ }
865
+ return new RpcError(error.code, error.message, undefined, error.code === 'CODEX_REQUEST_TIMEOUT');
866
+ }
867
+ return new RpcError('CODEX_UPSTREAM_ERROR', 'Codex App Server could not complete the request.');
868
+ }
869
+ function errorCode(error) {
870
+ if (error instanceof RpcError || error instanceof CodexAppServerError)
871
+ return error.code;
872
+ return 'CODEX_START_FAILED';
873
+ }
874
+ function isHistoryReadRecoverable(error) {
875
+ return error instanceof RpcError
876
+ && ['METHOD_NOT_ALLOWED', 'METHOD_NOT_FOUND', 'CODEX_UPSTREAM_ERROR', 'CODEX_REQUEST_TIMEOUT', 'CODEX_THREAD_BUSY'].includes(error.code);
877
+ }
878
+ function canTryNextBinary(error) {
879
+ return !(error instanceof RpcError) || !['CODEX_AUTH_REQUIRED', 'CODEX_CLOSED'].includes(error.code);
880
+ }
881
+ function isRecord(value) {
882
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
883
+ }
884
+ function array(value) {
885
+ return Array.isArray(value) ? value : [];
886
+ }
887
+ function maskId(value) {
888
+ return value.length <= 12 ? value : `${value.slice(0, 8)}…${value.slice(-4)}`;
889
+ }
890
+ function isActiveWriterMessage(message) {
891
+ return message.toLowerCase().includes('active writer');
892
+ }
893
+ //# sourceMappingURL=domain.js.map