@blue-soda/dsh-remote 0.5.5 → 0.5.7

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 (232) hide show
  1. package/README.md +2 -2
  2. package/cordis.patch.yml +30 -30
  3. package/dist/client.github.js +1 -1
  4. package/dist/client.js +1 -1
  5. package/dist/index.js +371 -80
  6. package/dist/index.js.map +4 -4
  7. package/dsh-plugin.json +41 -41
  8. package/package.json +165 -165
  9. package/dist/acp.d.ts +0 -75
  10. package/dist/acp.d.ts.map +0 -1
  11. package/dist/acp.js +0 -61
  12. package/dist/acp.js.map +0 -1
  13. package/dist/api-proxy-switch.d.ts +0 -33
  14. package/dist/api-proxy-switch.d.ts.map +0 -1
  15. package/dist/api-proxy-switch.js +0 -109
  16. package/dist/api-proxy-switch.js.map +0 -1
  17. package/dist/atomic-file.d.ts +0 -16
  18. package/dist/atomic-file.d.ts.map +0 -1
  19. package/dist/atomic-file.js +0 -81
  20. package/dist/atomic-file.js.map +0 -1
  21. package/dist/bin/doctor.d.ts +0 -3
  22. package/dist/bin/doctor.d.ts.map +0 -1
  23. package/dist/bin/doctor.js +0 -13
  24. package/dist/bin/doctor.js.map +0 -1
  25. package/dist/cli.d.ts +0 -32
  26. package/dist/cli.d.ts.map +0 -1
  27. package/dist/cli.js +0 -289
  28. package/dist/cli.js.map +0 -1
  29. package/dist/client-runtime.d.ts +0 -375
  30. package/dist/client-runtime.d.ts.map +0 -1
  31. package/dist/client-runtime.js +0 -1768
  32. package/dist/client-runtime.js.map +0 -1
  33. package/dist/client-secure-transport.d.ts +0 -23
  34. package/dist/client-secure-transport.d.ts.map +0 -1
  35. package/dist/client-secure-transport.js +0 -144
  36. package/dist/client-secure-transport.js.map +0 -1
  37. package/dist/client-target-store.d.ts +0 -40
  38. package/dist/client-target-store.d.ts.map +0 -1
  39. package/dist/client-target-store.js +0 -79
  40. package/dist/client-target-store.js.map +0 -1
  41. package/dist/client.d.ts +0 -13
  42. package/dist/client.d.ts.map +0 -1
  43. package/dist/codex/app-server.d.ts +0 -71
  44. package/dist/codex/app-server.d.ts.map +0 -1
  45. package/dist/codex/app-server.js +0 -268
  46. package/dist/codex/app-server.js.map +0 -1
  47. package/dist/codex/codex-image-codec.d.ts +0 -7
  48. package/dist/codex/codex-image-codec.d.ts.map +0 -1
  49. package/dist/codex/codex-image-codec.js +0 -288
  50. package/dist/codex/codex-image-codec.js.map +0 -1
  51. package/dist/codex/domain.d.ts +0 -102
  52. package/dist/codex/domain.d.ts.map +0 -1
  53. package/dist/codex/domain.js +0 -1323
  54. package/dist/codex/domain.js.map +0 -1
  55. package/dist/codex/method-policy.d.ts +0 -338
  56. package/dist/codex/method-policy.d.ts.map +0 -1
  57. package/dist/codex/method-policy.js +0 -141
  58. package/dist/codex/method-policy.js.map +0 -1
  59. package/dist/codex/peer-bridge.d.ts +0 -57
  60. package/dist/codex/peer-bridge.d.ts.map +0 -1
  61. package/dist/codex/peer-bridge.js +0 -299
  62. package/dist/codex/peer-bridge.js.map +0 -1
  63. package/dist/codex/permissions.d.ts +0 -4
  64. package/dist/codex/permissions.d.ts.map +0 -1
  65. package/dist/codex/permissions.js +0 -21
  66. package/dist/codex/permissions.js.map +0 -1
  67. package/dist/codex/session-id.d.ts +0 -10
  68. package/dist/codex/session-id.d.ts.map +0 -1
  69. package/dist/codex/session-id.js +0 -15
  70. package/dist/codex/session-id.js.map +0 -1
  71. package/dist/codex/virtual-harness.d.ts +0 -209
  72. package/dist/codex/virtual-harness.d.ts.map +0 -1
  73. package/dist/codex/virtual-harness.js +0 -2650
  74. package/dist/codex/virtual-harness.js.map +0 -1
  75. package/dist/codex-workspace-bridge.d.ts +0 -108
  76. package/dist/codex-workspace-bridge.d.ts.map +0 -1
  77. package/dist/codex-workspace-bridge.js +0 -502
  78. package/dist/codex-workspace-bridge.js.map +0 -1
  79. package/dist/config.d.ts +0 -99
  80. package/dist/config.d.ts.map +0 -1
  81. package/dist/config.js +0 -127
  82. package/dist/config.js.map +0 -1
  83. package/dist/connection-controller.d.ts +0 -44
  84. package/dist/connection-controller.d.ts.map +0 -1
  85. package/dist/connection-controller.js +0 -185
  86. package/dist/connection-controller.js.map +0 -1
  87. package/dist/control-route.d.ts +0 -34
  88. package/dist/control-route.d.ts.map +0 -1
  89. package/dist/control-route.js +0 -136
  90. package/dist/control-route.js.map +0 -1
  91. package/dist/control-runtime.d.ts +0 -138
  92. package/dist/control-runtime.d.ts.map +0 -1
  93. package/dist/control-runtime.js +0 -543
  94. package/dist/control-runtime.js.map +0 -1
  95. package/dist/control-stream.d.ts +0 -67
  96. package/dist/control-stream.d.ts.map +0 -1
  97. package/dist/control-stream.js +0 -179
  98. package/dist/control-stream.js.map +0 -1
  99. package/dist/defaults.d.ts +0 -9
  100. package/dist/defaults.d.ts.map +0 -1
  101. package/dist/defaults.js +0 -9
  102. package/dist/defaults.js.map +0 -1
  103. package/dist/file-viewer-bridge.d.ts +0 -30
  104. package/dist/file-viewer-bridge.d.ts.map +0 -1
  105. package/dist/file-viewer-bridge.js +0 -128
  106. package/dist/file-viewer-bridge.js.map +0 -1
  107. package/dist/file-viewer-contract.d.ts +0 -5
  108. package/dist/file-viewer-contract.d.ts.map +0 -1
  109. package/dist/file-viewer-contract.js +0 -4
  110. package/dist/file-viewer-contract.js.map +0 -1
  111. package/dist/harness-api-bridge.d.ts +0 -81
  112. package/dist/harness-api-bridge.d.ts.map +0 -1
  113. package/dist/harness-api-bridge.js +0 -862
  114. package/dist/harness-api-bridge.js.map +0 -1
  115. package/dist/harness-api-history.d.ts +0 -3
  116. package/dist/harness-api-history.d.ts.map +0 -1
  117. package/dist/harness-api-history.js +0 -59
  118. package/dist/harness-api-history.js.map +0 -1
  119. package/dist/harness-remote-bridge.d.ts +0 -60
  120. package/dist/harness-remote-bridge.d.ts.map +0 -1
  121. package/dist/harness-remote-bridge.js +0 -540
  122. package/dist/harness-remote-bridge.js.map +0 -1
  123. package/dist/harness-version.d.ts +0 -23
  124. package/dist/harness-version.d.ts.map +0 -1
  125. package/dist/harness-version.js +0 -95
  126. package/dist/harness-version.js.map +0 -1
  127. package/dist/identity-store.d.ts +0 -45
  128. package/dist/identity-store.d.ts.map +0 -1
  129. package/dist/identity-store.js +0 -186
  130. package/dist/identity-store.js.map +0 -1
  131. package/dist/ids.d.ts +0 -2
  132. package/dist/ids.d.ts.map +0 -1
  133. package/dist/ids.js +0 -17
  134. package/dist/ids.js.map +0 -1
  135. package/dist/index.d.ts +0 -78
  136. package/dist/index.d.ts.map +0 -1
  137. package/dist/logging.d.ts +0 -18
  138. package/dist/logging.d.ts.map +0 -1
  139. package/dist/logging.js +0 -30
  140. package/dist/logging.js.map +0 -1
  141. package/dist/loopback-host.d.ts +0 -20
  142. package/dist/loopback-host.d.ts.map +0 -1
  143. package/dist/loopback-host.js +0 -245
  144. package/dist/loopback-host.js.map +0 -1
  145. package/dist/loopback-preview.d.ts +0 -19
  146. package/dist/loopback-preview.d.ts.map +0 -1
  147. package/dist/loopback-preview.js +0 -232
  148. package/dist/loopback-preview.js.map +0 -1
  149. package/dist/native-rtc-helper.d.ts +0 -14
  150. package/dist/native-rtc-helper.d.ts.map +0 -1
  151. package/dist/native-rtc-helper.js +0 -575
  152. package/dist/native-rtc-helper.js.map +0 -1
  153. package/dist/remote-api-proxy.d.ts +0 -17
  154. package/dist/remote-api-proxy.d.ts.map +0 -1
  155. package/dist/remote-api-proxy.js +0 -451
  156. package/dist/remote-api-proxy.js.map +0 -1
  157. package/dist/remote-directory-browser.d.ts +0 -16
  158. package/dist/remote-directory-browser.d.ts.map +0 -1
  159. package/dist/remote-directory-browser.js +0 -48
  160. package/dist/remote-directory-browser.js.map +0 -1
  161. package/dist/remote-file-content-provider.d.ts +0 -48
  162. package/dist/remote-file-content-provider.d.ts.map +0 -1
  163. package/dist/remote-file-content-provider.js +0 -93
  164. package/dist/remote-file-content-provider.js.map +0 -1
  165. package/dist/remote-typert-gateway.d.ts +0 -20
  166. package/dist/remote-typert-gateway.d.ts.map +0 -1
  167. package/dist/remote-typert-gateway.js +0 -334
  168. package/dist/remote-typert-gateway.js.map +0 -1
  169. package/dist/rpc-binary-attachments.d.ts +0 -33
  170. package/dist/rpc-binary-attachments.d.ts.map +0 -1
  171. package/dist/rpc-binary-attachments.js +0 -142
  172. package/dist/rpc-binary-attachments.js.map +0 -1
  173. package/dist/rpc-router.d.ts +0 -31
  174. package/dist/rpc-router.d.ts.map +0 -1
  175. package/dist/rpc-router.js +0 -201
  176. package/dist/rpc-router.js.map +0 -1
  177. package/dist/safe-error.d.ts +0 -8
  178. package/dist/safe-error.d.ts.map +0 -1
  179. package/dist/safe-error.js +0 -20
  180. package/dist/safe-error.js.map +0 -1
  181. package/dist/server-api.d.ts +0 -109
  182. package/dist/server-api.d.ts.map +0 -1
  183. package/dist/server-api.js +0 -520
  184. package/dist/server-api.js.map +0 -1
  185. package/dist/server-connection.d.ts +0 -79
  186. package/dist/server-connection.d.ts.map +0 -1
  187. package/dist/server-connection.js +0 -1023
  188. package/dist/server-connection.js.map +0 -1
  189. package/dist/server-credentials.d.ts +0 -55
  190. package/dist/server-credentials.d.ts.map +0 -1
  191. package/dist/server-credentials.js +0 -108
  192. package/dist/server-credentials.js.map +0 -1
  193. package/dist/service.d.ts +0 -151
  194. package/dist/service.d.ts.map +0 -1
  195. package/dist/service.js +0 -448
  196. package/dist/service.js.map +0 -1
  197. package/dist/session-format-compat.d.ts +0 -3
  198. package/dist/session-format-compat.d.ts.map +0 -1
  199. package/dist/session-format-compat.js +0 -183
  200. package/dist/session-format-compat.js.map +0 -1
  201. package/dist/status-stream.d.ts +0 -48
  202. package/dist/status-stream.d.ts.map +0 -1
  203. package/dist/status-stream.js +0 -133
  204. package/dist/status-stream.js.map +0 -1
  205. package/dist/terminal-policy.d.ts +0 -21
  206. package/dist/terminal-policy.d.ts.map +0 -1
  207. package/dist/terminal-policy.js +0 -68
  208. package/dist/terminal-policy.js.map +0 -1
  209. package/dist/tui-command.d.ts +0 -13
  210. package/dist/tui-command.d.ts.map +0 -1
  211. package/dist/tui-command.js +0 -392
  212. package/dist/tui-command.js.map +0 -1
  213. package/dist/typert-gateway-contract.d.ts +0 -59
  214. package/dist/typert-gateway-contract.d.ts.map +0 -1
  215. package/dist/typert-gateway-contract.js +0 -2
  216. package/dist/typert-gateway-contract.js.map +0 -1
  217. package/dist/typert-gateway-switch.d.ts +0 -85
  218. package/dist/typert-gateway-switch.d.ts.map +0 -1
  219. package/dist/typert-gateway-switch.js +0 -448
  220. package/dist/typert-gateway-switch.js.map +0 -1
  221. package/dist/types.d.ts +0 -20
  222. package/dist/types.d.ts.map +0 -1
  223. package/dist/types.js +0 -2
  224. package/dist/types.js.map +0 -1
  225. package/dist/version.d.ts +0 -12
  226. package/dist/version.d.ts.map +0 -1
  227. package/dist/version.js +0 -12
  228. package/dist/version.js.map +0 -1
  229. package/dist/werift-rtc.d.ts +0 -121
  230. package/dist/werift-rtc.d.ts.map +0 -1
  231. package/dist/werift-rtc.js +0 -490
  232. package/dist/werift-rtc.js.map +0 -1
@@ -1,1323 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { accessSync, constants, existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
3
- import { readdir, realpath, stat } from 'node:fs/promises';
4
- import { homedir } from 'node:os';
5
- import { basename, isAbsolute, join, posix, relative, resolve } from 'node:path';
6
- import { deriveCodexCwdWorkspaces } from '@dsh-remote/client-core';
7
- import { RpcError } from '../safe-error.js';
8
- import { CodexAppServerClient, CodexAppServerError, } from './app-server.js';
9
- import { isThreadMutation, parseCodexCall, threadIdFromParams, } from './method-policy.js';
10
- import { CodexPeerBridge } from './peer-bridge.js';
11
- import { codexPermissionPresetFromResponse } from './permissions.js';
12
- import { paginateCodexNativeHistory, projectCodexNativeHistory } from './virtual-harness.js';
13
- const APPROVAL_TTL_MS = 5 * 60_000;
14
- const DEFAULT_RESTART_DELAYS_MS = [1_000, 2_000, 4_000, 8_000, 15_000];
15
- /**
16
- * How long the whole App Server launch may take.
17
- *
18
- * `launchAppServer` tries several binary candidates and each attempt can spend a full request
19
- * timeout waiting for a binary that will never answer, so a missing or broken install used to
20
- * cost that timeout several times over. One budget for all candidates keeps the domain's own
21
- * status honest ('unavailable' in seconds, not tens of seconds) while the existing restart
22
- * backoff keeps trying in the background.
23
- */
24
- const CODEX_START_BUDGET_MS = 5_000;
25
- const CODEX_PAGE_LIMIT = 100;
26
- const MAX_CODEX_PAGES = 32;
27
- const CODEX_HISTORY_PAGE_LIMIT = 25;
28
- const MAX_CODEX_HISTORY_PAGES = 64;
29
- const CODEX_DIRECTORY_ENTRY_LIMIT = 500;
30
- /**
31
- * Optional Codex business domain inside the existing Remote Plugin. It shares
32
- * Remote identity/transport with Harness, but owns its App Server process,
33
- * method policy, subscriptions, leases, and approval handles.
34
- */
35
- export class CodexRemoteDomain {
36
- config;
37
- logger;
38
- createAppServer;
39
- restartDelaysMs;
40
- appServer;
41
- unsubscribeInbound;
42
- unsubscribeUnavailable;
43
- peers = new Map();
44
- peerDeviceIds = new Map();
45
- turnOwners = new Map();
46
- approvals = new Map();
47
- permissionPresets = new Map();
48
- approvalExpiryTimer;
49
- restartTimer;
50
- restartAttempt = 0;
51
- available = false;
52
- closed = false;
53
- state = 'disabled';
54
- unavailableCode;
55
- constructor(config, logger, createAppServer = (binary, targetLogger) => new CodexAppServerClient(binary, targetLogger), restartDelaysMs = DEFAULT_RESTART_DELAYS_MS) {
56
- this.config = config;
57
- this.logger = logger;
58
- this.createAppServer = createAppServer;
59
- this.restartDelaysMs = restartDelaysMs;
60
- }
61
- async start() {
62
- if (this.closed)
63
- throw new RpcError('CODEX_CLOSED', 'The Codex Remote domain is closed.');
64
- if (!this.config.enabled)
65
- return;
66
- try {
67
- this.state = 'starting';
68
- await this.launchWithinBudget();
69
- }
70
- catch (error) {
71
- this.available = false;
72
- this.state = 'unavailable';
73
- this.unavailableCode = errorCode(error);
74
- await this.disposeAppServer(this.appServer);
75
- this.logger.warn('Codex Remote domain unavailable', { code: this.unavailableCode });
76
- }
77
- }
78
- isAvailable() { return this.available && this.appServer?.isReady() === true; }
79
- status() {
80
- return {
81
- enabled: this.config.enabled,
82
- available: this.isAvailable(),
83
- state: this.state,
84
- restartAttempt: this.restartAttempt,
85
- ...(this.unavailableCode === undefined ? {} : { error: this.unavailableCode }),
86
- };
87
- }
88
- /** Resolve a thread cwd for Host-owned workspace/terminal carriers. */
89
- async resolveThreadWorkspace(connectionId, threadId) {
90
- if (!this.peers.has(connectionId))
91
- throw new RpcError('CODEX_THREAD_UNAVAILABLE', 'The CodeX thread is not available on this connection.');
92
- try {
93
- const known = await this.readKnownThread(threadId);
94
- return known.cwd;
95
- }
96
- catch (error) {
97
- if (error instanceof RpcError && error.code === 'CODEX_THREAD_NOT_ALLOWED') {
98
- throw new RpcError('CODEX_THREAD_UNAVAILABLE', 'The CodeX thread is not available.');
99
- }
100
- throw new RpcError('CODEX_THREAD_UNAVAILABLE', 'The CodeX thread is not available.');
101
- }
102
- }
103
- createPeer(context, publish) {
104
- if (!this.config.enabled)
105
- return undefined;
106
- const bridge = new CodexPeerBridge(this, context, publish, this.logger);
107
- this.peers.set(context.connectionId, bridge);
108
- this.peerDeviceIds.set(context.connectionId, context.peerDeviceId);
109
- return bridge;
110
- }
111
- async call(connectionId, input) {
112
- const envelope = parseCallEnvelope(input);
113
- const call = parseCodexCall(envelope.method, envelope.params);
114
- this.requireAppServer();
115
- if (call.method === 'account/read') {
116
- return sanitizeAccount(await this.callUpstream(call.method, call.params));
117
- }
118
- if (call.method === 'project/list') {
119
- return sanitizeProjectList(await this.callUpstream(call.method, call.params));
120
- }
121
- if (call.method === 'project/create') {
122
- const roots = call.params.roots;
123
- const path = await this.requireNewCodexProjectPath(roots[0].path);
124
- return sanitizeProjectCreate(await this.callUpstream(call.method, {
125
- name: call.params.name,
126
- roots: [{ path }],
127
- idempotencyKey: call.params.idempotencyKey,
128
- }));
129
- }
130
- if (call.method === 'thread/list') {
131
- const result = sanitizeThreadList(await this.callUpstream(call.method, call.params));
132
- return filterThreadListByWorkspaceAuthority(result, await this.readWorkspaceAuthority(result.data));
133
- }
134
- if (call.method === 'thread/start') {
135
- const cwd = await this.requireCodexWorkspacePath(call.params.cwd);
136
- const permission = codexPermission(call.params, 'workspace-write');
137
- const result = await this.callUpstream(call.method, {
138
- ...permission.params,
139
- cwd,
140
- ...codexThreadPermissionParams(permission),
141
- serviceName: 'deepseek_harness_remote',
142
- });
143
- if (extractThread(result)?.id === undefined) {
144
- throw new RpcError('CODEX_INVALID_RESPONSE', 'Codex App Server returned an invalid thread.');
145
- }
146
- this.rememberPermission(extractThread(result).id, result);
147
- return result;
148
- }
149
- const threadId = threadIdFromParams(call.params);
150
- if (call.method === 'dsh/sessionHistory') {
151
- const thread = await this.readThreadForHistory(connectionId, threadId);
152
- const page = paginateCodexNativeHistory(projectCodexNativeHistory(thread, `codex:${threadId}`), {
153
- beforeSeq: optionalInteger(call.params.beforeSeq),
154
- throughSeq: optionalInteger(call.params.throughSeq),
155
- maxMessages: optionalInteger(call.params.maxMessages),
156
- });
157
- const activeTurnId = typeof page.activeTurnId === 'string'
158
- ? page.activeTurnId
159
- : this.turnOwners.get(threadId)?.turnId;
160
- return {
161
- ...page,
162
- ...(activeTurnId === undefined ? {} : { activeTurnId }),
163
- permissionPreset: this.permissionPresets.get(threadId) ?? null,
164
- };
165
- }
166
- if (call.method === 'dsh/directoryList') {
167
- return this.listCodexDirectory(call.params.path);
168
- }
169
- const allowedThread = threadId === undefined ? undefined : await this.readKnownThread(threadId);
170
- if (call.method === 'thread/resume' && call.params.permissionPreset !== undefined) {
171
- const owner = this.turnOwners.get(threadId);
172
- if (owner !== undefined && owner.connectionId !== connectionId) {
173
- throw new RpcError('CODEX_THREAD_BUSY', 'Another Remote client is writing to this CodeX thread.');
174
- }
175
- return this.changeThreadPermission(threadId, call.params, allowedThread);
176
- }
177
- if (call.method === 'thread/read') {
178
- // assertThreadAllowed already produced a bounded summary, but make the
179
- // requested read again so includeTurns has its official semantics.
180
- return this.callUpstream(call.method, call.params);
181
- }
182
- if (call.method === 'thread/unsubscribe') {
183
- const bridge = this.peers.get(connectionId);
184
- bridge?.removeThreadSubscriptions(threadId);
185
- if (this.hasSubscriber(threadId))
186
- return { status: 'unsubscribed' };
187
- return this.callUpstream(call.method, call.params);
188
- }
189
- let claimed = false;
190
- let previousOwner;
191
- if (isThreadMutation(call.method) && threadId !== undefined) {
192
- const claim = this.claimTurn(threadId, connectionId, call.method, typeof call.params.turnId === 'string' ? call.params.turnId : undefined);
193
- claimed = claim.claimed;
194
- previousOwner = claim.previous;
195
- }
196
- try {
197
- const permission = codexPermission(call.params);
198
- const upstreamParams = call.method === 'thread/resume' && allowedThread !== undefined
199
- ? {
200
- ...permission.params,
201
- ...(allowedThread.cwd === undefined ? {} : { cwd: allowedThread.cwd }),
202
- ...codexThreadPermissionParams(permission),
203
- excludeTurns: true,
204
- }
205
- : call.method === 'thread/fork' && allowedThread !== undefined
206
- ? {
207
- ...permission.params,
208
- ...(allowedThread.cwd === undefined ? {} : { cwd: allowedThread.cwd }),
209
- ...codexThreadPermissionParams(permission),
210
- }
211
- : call.method === 'turn/start'
212
- ? {
213
- ...permission.params,
214
- ...(allowedThread?.cwd === undefined ? {} : { cwd: allowedThread.cwd }),
215
- ...codexTurnPermissionParams(permission, allowedThread?.cwd),
216
- }
217
- : permission.params;
218
- let result;
219
- try {
220
- result = await this.callUpstream(call.method, upstreamParams);
221
- }
222
- catch (error) {
223
- // App Server rejects thread/resume when this process already has the
224
- // thread loaded. Remote callers should be able to resume defensively
225
- // before every turn, including immediately after thread/start.
226
- if (call.method === 'thread/resume'
227
- && allowedThread !== undefined
228
- && error instanceof RpcError
229
- && error.code === 'CODEX_UPSTREAM_ERROR'
230
- && call.params.permissionPreset === undefined) {
231
- return { thread: allowedThread.thread };
232
- }
233
- throw error;
234
- }
235
- if (call.method === 'thread/resume' || call.method === 'thread/fork' || call.method === 'thread/unarchive') {
236
- await this.assertResultThreadAllowed(result);
237
- }
238
- if (call.method === 'thread/resume' || call.method === 'thread/fork') {
239
- const resultId = extractThread(result)?.id;
240
- if (typeof resultId === 'string') {
241
- this.rememberPermission(resultId, result);
242
- if (call.params.permissionPreset !== undefined)
243
- await this.publishPermission(resultId, result);
244
- }
245
- }
246
- if (call.method === 'turn/start' && threadId !== undefined) {
247
- this.rememberTurnId(threadId, connectionId, extractTurnId(result));
248
- if (call.params.permissionPreset !== undefined) {
249
- await this.publishPermission(threadId, codexTurnPermissionParams(permission, allowedThread?.cwd));
250
- }
251
- }
252
- return result;
253
- }
254
- catch (error) {
255
- if (claimed && threadId !== undefined) {
256
- if (previousOwner === undefined)
257
- this.turnOwners.delete(threadId);
258
- else
259
- this.turnOwners.set(threadId, previousOwner);
260
- }
261
- throw mapAppServerError(error);
262
- }
263
- }
264
- async respond(connectionId, input) {
265
- await this.expireApprovals();
266
- const params = parseRespond(input);
267
- const approval = this.approvals.get(params.requestHandle);
268
- if (approval === undefined || approval.connectionId !== connectionId) {
269
- throw new RpcError('CODEX_APPROVAL_NOT_FOUND', 'The Codex approval is missing, expired, or belongs to another connection.');
270
- }
271
- this.approvals.delete(params.requestHandle);
272
- this.scheduleApprovalExpiry();
273
- await this.requireAppServer().respond(approval.upstreamId, { decision: params.decision });
274
- return { resolved: true };
275
- }
276
- async detachPeer(connectionId) {
277
- const bridge = this.peers.get(connectionId);
278
- if (bridge !== undefined)
279
- this.peers.delete(connectionId);
280
- this.peerDeviceIds.delete(connectionId);
281
- for (const [threadId, owner] of this.turnOwners) {
282
- if (owner.connectionId === connectionId)
283
- this.turnOwners.delete(threadId);
284
- }
285
- const appServer = this.appServer;
286
- const pending = [...this.approvals.entries()].filter(([, approval]) => approval.connectionId === connectionId);
287
- for (const [handle, approval] of pending) {
288
- this.approvals.delete(handle);
289
- await appServer?.respond(approval.upstreamId, { decision: 'decline' }).catch(() => undefined);
290
- }
291
- this.scheduleApprovalExpiry();
292
- }
293
- async close() {
294
- if (this.closed)
295
- return;
296
- this.closed = true;
297
- this.available = false;
298
- this.state = this.config.enabled ? 'unavailable' : 'disabled';
299
- if (this.restartTimer !== undefined)
300
- clearTimeout(this.restartTimer);
301
- this.restartTimer = undefined;
302
- for (const bridge of [...this.peers.values()])
303
- await bridge.closeAll();
304
- this.peers.clear();
305
- this.peerDeviceIds.clear();
306
- this.turnOwners.clear();
307
- this.permissionPresets.clear();
308
- if (this.approvalExpiryTimer !== undefined)
309
- clearTimeout(this.approvalExpiryTimer);
310
- this.approvalExpiryTimer = undefined;
311
- this.approvals.clear();
312
- this.unsubscribeInbound?.();
313
- this.unsubscribeInbound = undefined;
314
- this.unsubscribeUnavailable?.();
315
- this.unsubscribeUnavailable = undefined;
316
- await this.appServer?.close();
317
- this.appServer = undefined;
318
- }
319
- /**
320
- * Launch the App Server within {@link CODEX_START_BUDGET_MS}.
321
- *
322
- * The losing side of the race keeps running until the disposal in start()'s catch stops it, so
323
- * it needs its own handler: an unhandled rejection here would surface as a process-level error
324
- * for what is only an optional feature being unavailable.
325
- * @returns nothing once a candidate is ready.
326
- */
327
- async launchWithinBudget() {
328
- let timer;
329
- const attempt = this.launchAppServer();
330
- attempt.catch(() => undefined);
331
- try {
332
- await Promise.race([
333
- attempt,
334
- new Promise((_resolve, reject) => {
335
- timer = setTimeout(() => {
336
- reject(new RpcError('CODEX_START_TIMEOUT', `Codex App Server did not start within ${CODEX_START_BUDGET_MS}ms.`));
337
- }, CODEX_START_BUDGET_MS);
338
- timer.unref?.();
339
- }),
340
- ]);
341
- }
342
- finally {
343
- if (timer !== undefined)
344
- clearTimeout(timer);
345
- }
346
- }
347
- async launchAppServer() {
348
- let lastError;
349
- for (const binary of codexBinaryCandidates(this.config.binary)) {
350
- try {
351
- await this.launchAppServerCandidate(binary);
352
- return;
353
- }
354
- catch (error) {
355
- lastError = error;
356
- if (!canTryNextBinary(error))
357
- throw error;
358
- }
359
- }
360
- throw lastError ?? new RpcError('CODEX_START_FAILED', 'Codex App Server could not be started.');
361
- }
362
- async launchAppServerCandidate(binary) {
363
- const appServer = this.createAppServer(binary, this.logger);
364
- this.appServer = appServer;
365
- this.unsubscribeInbound = appServer.onInbound(message => {
366
- void this.handleInbound(message).catch(error => {
367
- this.logger.warn('Codex inbound handling failed', { code: errorCode(error) });
368
- });
369
- });
370
- this.unsubscribeUnavailable = appServer.onUnavailable(code => {
371
- if (this.available && this.appServer === appServer) {
372
- void this.handleAppServerUnavailable(appServer, code);
373
- }
374
- });
375
- try {
376
- await appServer.start();
377
- const account = await appServer.call('account/read', { refreshToken: false }, 15_000);
378
- if (!accountCanRun(account)) {
379
- throw new RpcError('CODEX_AUTH_REQUIRED', 'Codex is not signed in on this Host.');
380
- }
381
- if (this.closed || this.appServer !== appServer) {
382
- await appServer.close().catch(() => undefined);
383
- return;
384
- }
385
- this.available = true;
386
- this.state = 'ready';
387
- this.restartAttempt = 0;
388
- this.unavailableCode = undefined;
389
- this.logger.info('Codex Remote domain ready');
390
- }
391
- catch (error) {
392
- await this.disposeAppServer(appServer);
393
- throw error;
394
- }
395
- }
396
- async handleAppServerUnavailable(appServer, code) {
397
- if (this.closed || this.appServer !== appServer)
398
- return;
399
- this.available = false;
400
- this.state = 'restarting';
401
- this.unavailableCode = code;
402
- this.turnOwners.clear();
403
- this.permissionPresets.clear();
404
- this.approvals.clear();
405
- if (this.approvalExpiryTimer !== undefined)
406
- clearTimeout(this.approvalExpiryTimer);
407
- this.approvalExpiryTimer = undefined;
408
- await Promise.all([...this.peers.values()].map(peer => peer.failStreams('failed')));
409
- await this.disposeAppServer(appServer);
410
- this.scheduleRestart();
411
- }
412
- scheduleRestart() {
413
- if (this.closed || this.restartTimer !== undefined)
414
- return;
415
- if (this.restartAttempt >= this.restartDelaysMs.length) {
416
- this.state = 'unavailable';
417
- this.logger.warn('Codex App Server restart attempts exhausted', { attempts: this.restartAttempt });
418
- return;
419
- }
420
- const delayMs = Math.max(0, this.restartDelaysMs[this.restartAttempt] ?? 0);
421
- this.restartAttempt += 1;
422
- this.state = 'restarting';
423
- this.restartTimer = setTimeout(() => {
424
- this.restartTimer = undefined;
425
- void this.restartAfterFailure();
426
- }, delayMs);
427
- this.restartTimer.unref?.();
428
- this.logger.warn('Codex App Server restart scheduled', { attempt: this.restartAttempt, delayMs });
429
- }
430
- async restartAfterFailure() {
431
- if (this.closed)
432
- return;
433
- try {
434
- await this.launchAppServer();
435
- }
436
- catch (error) {
437
- this.available = false;
438
- this.state = 'restarting';
439
- this.unavailableCode = errorCode(error);
440
- this.logger.warn('Codex App Server restart failed', {
441
- attempt: this.restartAttempt,
442
- code: this.unavailableCode,
443
- });
444
- this.scheduleRestart();
445
- }
446
- }
447
- async disposeAppServer(appServer) {
448
- if (appServer === undefined || this.appServer !== appServer)
449
- return;
450
- this.unsubscribeInbound?.();
451
- this.unsubscribeInbound = undefined;
452
- this.unsubscribeUnavailable?.();
453
- this.unsubscribeUnavailable = undefined;
454
- this.appServer = undefined;
455
- await appServer.close().catch(() => undefined);
456
- }
457
- async handleInbound(message) {
458
- if (!this.available)
459
- return;
460
- if (message.kind === 'request') {
461
- await this.handleServerRequest(message);
462
- return;
463
- }
464
- const threadId = extractThreadId(message.params);
465
- if (message.method === 'thread/settings/updated' && threadId !== undefined) {
466
- this.rememberPermission(threadId, message.params);
467
- }
468
- if (message.method === 'thread/closed' && threadId !== undefined)
469
- this.permissionPresets.delete(threadId);
470
- if (message.method === 'turn/completed' && threadId !== undefined)
471
- this.turnOwners.delete(threadId);
472
- if (message.method === 'turn/started' && threadId !== undefined) {
473
- const owner = this.turnOwners.get(threadId);
474
- const turnId = extractTurnId(message.params);
475
- if (owner !== undefined) {
476
- this.turnOwners.set(threadId, { ...owner, ...(turnId === undefined ? {} : { turnId }) });
477
- }
478
- }
479
- if (message.method === 'serverRequest/resolved')
480
- this.resolveUpstreamApproval(message.params);
481
- if (threadId === undefined)
482
- return;
483
- await Promise.all([...this.peers.values()].map(peer => peer.publishInbound(threadId, {
484
- method: message.method,
485
- params: message.params,
486
- })));
487
- }
488
- rememberPermission(threadId, value) {
489
- const preset = codexPermissionPresetFromResponse(value);
490
- if (preset === undefined)
491
- this.permissionPresets.delete(threadId);
492
- else
493
- this.permissionPresets.set(threadId, preset);
494
- }
495
- async changeThreadPermission(threadId, params, allowed) {
496
- const permission = codexPermission(params);
497
- const settings = {
498
- ...codexTurnPermissionParams(permission, allowed.cwd),
499
- ...(params.model === undefined ? {} : { model: params.model }),
500
- };
501
- let result;
502
- try {
503
- // Loaded threads can reject resume or ignore its configuration overrides.
504
- await this.callUpstream('thread/settings/update', { threadId, ...settings });
505
- result = { thread: allowed.thread, ...settings, sandbox: settings.sandboxPolicy };
506
- }
507
- catch {
508
- // Unloaded threads and older App Servers still use the resume contract.
509
- result = await this.callUpstream('thread/resume', {
510
- ...permission.params,
511
- ...(allowed.cwd === undefined ? {} : { cwd: allowed.cwd }),
512
- ...codexThreadPermissionParams(permission),
513
- excludeTurns: true,
514
- });
515
- await this.assertResultThreadAllowed(result);
516
- if (codexPermissionPresetFromResponse(result) !== params.permissionPreset) {
517
- await this.callUpstream('thread/settings/update', { threadId, ...settings });
518
- result = { ...(isRecord(result) ? result : {}), ...settings, sandbox: settings.sandboxPolicy };
519
- }
520
- }
521
- await this.publishPermission(threadId, result);
522
- return result;
523
- }
524
- async publishPermission(threadId, value) {
525
- const source = isRecord(value) ? value : {};
526
- const threadSettings = { approvalPolicy: source.approvalPolicy, sandboxPolicy: source.sandboxPolicy ?? source.sandbox };
527
- await this.handleInbound({ kind: 'notification', method: 'thread/settings/updated', params: { threadId, threadSettings } });
528
- }
529
- async handleServerRequest(message) {
530
- const appServer = this.requireAppServer();
531
- await this.expireApprovals();
532
- if (message.method !== 'item/commandExecution/requestApproval'
533
- && message.method !== 'item/fileChange/requestApproval') {
534
- await appServer.respondError(message.id, -32601, 'This Remote client does not support the server request.');
535
- return;
536
- }
537
- const threadId = extractThreadId(message.params);
538
- const owner = threadId === undefined ? undefined : this.turnOwners.get(threadId);
539
- const peer = owner === undefined ? undefined : this.peers.get(owner.connectionId);
540
- if (threadId === undefined || owner === undefined || peer === undefined || !peer.hasThreadSubscription(threadId)) {
541
- await appServer.respond(message.id, { decision: 'decline' });
542
- return;
543
- }
544
- const requestHandle = randomUUID();
545
- this.approvals.set(requestHandle, {
546
- upstreamId: message.id,
547
- connectionId: owner.connectionId,
548
- threadId,
549
- method: message.method,
550
- expiresAt: Date.now() + APPROVAL_TTL_MS,
551
- });
552
- this.scheduleApprovalExpiry();
553
- try {
554
- await peer.publishInbound(threadId, {
555
- method: message.method,
556
- params: sanitizeApprovalParams(message.params, requestHandle),
557
- });
558
- }
559
- catch (error) {
560
- this.approvals.delete(requestHandle);
561
- this.scheduleApprovalExpiry();
562
- await appServer.respond(message.id, { decision: 'decline' }).catch(() => undefined);
563
- throw error;
564
- }
565
- }
566
- async readKnownThread(threadId) {
567
- const authority = await this.readWorkspaceAuthority();
568
- let result;
569
- try {
570
- result = await this.callUpstream('thread/read', { threadId, includeTurns: false });
571
- }
572
- catch (error) {
573
- if (!isHistoryReadRecoverable(error))
574
- throw error;
575
- const listed = await this.findKnownThreadInList(threadId, authority);
576
- if (listed === undefined) {
577
- throw new RpcError('CODEX_THREAD_NOT_ALLOWED', 'The Codex thread is not available through this Remote Host.');
578
- }
579
- return { thread: listed, ...(typeof listed.cwd === 'string' && listed.cwd.length > 0 ? { cwd: listed.cwd } : {}) };
580
- }
581
- const thread = extractThread(result);
582
- if (thread === undefined || thread.id !== threadId || !isThreadAllowedByWorkspaceAuthority(thread, authority)) {
583
- throw new RpcError('CODEX_THREAD_NOT_ALLOWED', 'The Codex thread is not available through this Remote Host.');
584
- }
585
- return { thread, ...(typeof thread.cwd === 'string' && thread.cwd.length > 0 ? { cwd: thread.cwd } : {}) };
586
- }
587
- async readThreadForHistory(connectionId, threadId) {
588
- const { thread: metadata } = await this.readKnownThread(threadId);
589
- try {
590
- return { ...metadata, turns: await this.readThreadTurns(connectionId, threadId, 'full') };
591
- }
592
- catch (fullError) {
593
- if (!isHistoryReadRecoverable(fullError))
594
- throw fullError;
595
- this.logHistoryFallback(connectionId, 'turns-full', fullError);
596
- }
597
- try {
598
- return { ...metadata, turns: await this.readThreadTurns(connectionId, threadId, 'summary') };
599
- }
600
- catch (summaryError) {
601
- if (!isHistoryReadRecoverable(summaryError))
602
- throw summaryError;
603
- this.logHistoryFallback(connectionId, 'turns-summary', summaryError);
604
- }
605
- try {
606
- const legacyResult = await this.callUpstream('thread/read', { threadId, includeTurns: true });
607
- const legacyThread = extractThread(legacyResult);
608
- if (legacyThread === undefined || legacyThread.id !== threadId) {
609
- throw new RpcError('CODEX_INVALID_RESPONSE', 'Codex App Server returned an invalid Thread history.');
610
- }
611
- return legacyThread;
612
- }
613
- catch (legacyError) {
614
- if (!isHistoryReadRecoverable(legacyError))
615
- throw legacyError;
616
- this.logHistoryFallback(connectionId, 'thread-read-full', legacyError);
617
- return { ...metadata, turns: [] };
618
- }
619
- }
620
- async readThreadTurns(connectionId, threadId, itemsView) {
621
- const turns = [];
622
- let cursor;
623
- for (let page = 0; page < MAX_CODEX_HISTORY_PAGES; page += 1) {
624
- const result = await this.callUpstream('thread/turns/list', {
625
- threadId,
626
- limit: CODEX_HISTORY_PAGE_LIMIT,
627
- sortDirection: 'asc',
628
- itemsView,
629
- ...(cursor === undefined ? {} : { cursor }),
630
- });
631
- const pageResult = isRecord(result) ? result : {};
632
- for (const rawTurn of array(pageResult.data)) {
633
- if (!isRecord(rawTurn))
634
- continue;
635
- const turnId = typeof rawTurn.id === 'string' ? rawTurn.id : undefined;
636
- const items = rawTurn.itemsView === 'full' || turnId === undefined
637
- ? array(rawTurn.items)
638
- : await this.readThreadItems(connectionId, threadId, turnId, array(rawTurn.items));
639
- turns.push({ ...rawTurn, items });
640
- }
641
- cursor = typeof pageResult.nextCursor === 'string' && pageResult.nextCursor.length > 0
642
- ? pageResult.nextCursor
643
- : undefined;
644
- if (cursor === undefined)
645
- break;
646
- }
647
- return turns;
648
- }
649
- async readThreadItems(connectionId, threadId, turnId, fallbackItems) {
650
- const items = [];
651
- let cursor;
652
- try {
653
- for (let page = 0; page < MAX_CODEX_HISTORY_PAGES; page += 1) {
654
- const result = await this.callUpstream('thread/items/list', {
655
- threadId,
656
- turnId,
657
- limit: CODEX_HISTORY_PAGE_LIMIT,
658
- sortDirection: 'asc',
659
- ...(cursor === undefined ? {} : { cursor }),
660
- });
661
- const pageResult = isRecord(result) ? result : {};
662
- for (const entry of array(pageResult.data)) {
663
- if (isRecord(entry) && entry.item !== undefined)
664
- items.push(entry.item);
665
- }
666
- cursor = typeof pageResult.nextCursor === 'string' && pageResult.nextCursor.length > 0
667
- ? pageResult.nextCursor
668
- : undefined;
669
- if (cursor === undefined)
670
- break;
671
- }
672
- }
673
- catch (error) {
674
- if (!isHistoryReadRecoverable(error))
675
- throw error;
676
- this.logHistoryFallback(connectionId, 'items', error);
677
- return fallbackItems;
678
- }
679
- return items;
680
- }
681
- logHistoryFallback(connectionId, stage, error) {
682
- this.logger.warn('Codex history read fallback', {
683
- connectionId: maskId(connectionId),
684
- stage,
685
- code: errorCode(error),
686
- });
687
- }
688
- async assertResultThreadAllowed(result) {
689
- const thread = extractThread(result);
690
- if (thread === undefined || typeof thread.id !== 'string') {
691
- throw new RpcError('CODEX_INVALID_RESPONSE', 'Codex App Server returned an invalid thread.');
692
- }
693
- if (!isThreadAllowedByWorkspaceAuthority(thread, await this.readWorkspaceAuthority())) {
694
- throw new RpcError('CODEX_THREAD_NOT_ALLOWED', 'The Codex thread is not available through this Remote Host.');
695
- }
696
- }
697
- claimTurn(threadId, connectionId, method, turnId) {
698
- const owner = this.turnOwners.get(threadId);
699
- const peerDeviceId = this.peerDeviceIds.get(connectionId) ?? connectionId;
700
- const nextOwner = { connectionId, peerDeviceId, ...(turnId === undefined ? {} : { turnId }) };
701
- if (method === 'turn/interrupt') {
702
- if (owner === undefined) {
703
- // A replacement connection may recover an already-running persisted
704
- // turn. Claim only for this explicit mutation; passive viewers still
705
- // remain observers and cannot steal a live connection's lease.
706
- this.turnOwners.set(threadId, nextOwner);
707
- return { claimed: true };
708
- }
709
- if (owner.connectionId !== connectionId && owner.peerDeviceId !== peerDeviceId) {
710
- throw new RpcError('CODEX_TURN_OWNED', 'Only the connection that started this Codex turn can interrupt it.');
711
- }
712
- if (owner.connectionId !== connectionId) {
713
- this.turnOwners.set(threadId, { ...owner, connectionId, peerDeviceId, ...(turnId === undefined ? {} : { turnId }) });
714
- return { claimed: true, previous: owner };
715
- }
716
- if (turnId !== undefined && owner.turnId === undefined)
717
- this.turnOwners.set(threadId, { ...owner, turnId });
718
- return { claimed: false };
719
- }
720
- if (owner !== undefined && owner.connectionId !== connectionId && owner.peerDeviceId !== peerDeviceId) {
721
- throw new RpcError('CODEX_TURN_OWNED', 'Another Remote connection owns the active Codex turn.');
722
- }
723
- if (owner?.connectionId === connectionId)
724
- return { claimed: false };
725
- this.turnOwners.set(threadId, owner === undefined ? nextOwner : { ...owner, connectionId, peerDeviceId });
726
- return { claimed: true, previous: owner };
727
- }
728
- rememberTurnId(threadId, connectionId, turnId) {
729
- if (turnId === undefined)
730
- return;
731
- const owner = this.turnOwners.get(threadId);
732
- if (owner === undefined || owner.connectionId !== connectionId)
733
- return;
734
- this.turnOwners.set(threadId, { ...owner, turnId });
735
- }
736
- hasSubscriber(threadId) {
737
- return [...this.peers.values()].some(peer => peer.hasThreadSubscription(threadId));
738
- }
739
- resolveUpstreamApproval(params) {
740
- if (!isRecord(params) || (typeof params.requestId !== 'string' && typeof params.requestId !== 'number'))
741
- return;
742
- for (const [handle, approval] of this.approvals) {
743
- if (approval.upstreamId === params.requestId)
744
- this.approvals.delete(handle);
745
- }
746
- }
747
- async expireApprovals() {
748
- const now = Date.now();
749
- const appServer = this.appServer;
750
- for (const [handle, approval] of this.approvals) {
751
- if (approval.expiresAt > now)
752
- continue;
753
- this.approvals.delete(handle);
754
- await appServer?.respond(approval.upstreamId, { decision: 'decline' }).catch(() => undefined);
755
- }
756
- this.scheduleApprovalExpiry();
757
- }
758
- scheduleApprovalExpiry() {
759
- if (this.approvalExpiryTimer !== undefined)
760
- clearTimeout(this.approvalExpiryTimer);
761
- this.approvalExpiryTimer = undefined;
762
- const nextExpiry = Math.min(...[...this.approvals.values()].map(approval => approval.expiresAt));
763
- if (!Number.isFinite(nextExpiry))
764
- return;
765
- this.approvalExpiryTimer = setTimeout(() => {
766
- this.approvalExpiryTimer = undefined;
767
- void this.expireApprovals().catch(error => {
768
- this.logger.warn('Codex approval expiry failed', { code: errorCode(error) });
769
- });
770
- }, Math.max(0, nextExpiry - Date.now()));
771
- this.approvalExpiryTimer.unref?.();
772
- }
773
- requireAppServer() {
774
- if (!this.isAvailable() || this.appServer === undefined) {
775
- throw new RpcError('CODEX_UNAVAILABLE', 'Codex Remote is disabled or unavailable on this Host.');
776
- }
777
- return this.appServer;
778
- }
779
- async callUpstream(method, params) {
780
- try {
781
- return await this.requireAppServer().call(method, params);
782
- }
783
- catch (error) {
784
- throw mapAppServerError(error);
785
- }
786
- }
787
- async readWorkspaceAuthority(listedThreads) {
788
- const projectIds = new Set();
789
- const roots = [];
790
- let cursor;
791
- try {
792
- for (let page = 0; page < MAX_CODEX_PAGES; page += 1) {
793
- const result = sanitizeProjectList(await this.callUpstream('project/list', {
794
- limit: CODEX_PAGE_LIMIT,
795
- ...(cursor === undefined ? {} : { cursor }),
796
- }));
797
- for (const project of result.data) {
798
- const projectRoots = project.roots.map(root => root.path).filter(path => isAbsolute(path));
799
- if (projectRoots.length === 0)
800
- continue;
801
- projectIds.add(project.id);
802
- roots.push(...projectRoots);
803
- }
804
- cursor = typeof result.nextCursor === 'string' && result.nextCursor.length > 0 ? result.nextCursor : undefined;
805
- if (cursor === undefined)
806
- break;
807
- }
808
- }
809
- catch (error) {
810
- if (!isProjectListFallbackError(error))
811
- throw error;
812
- }
813
- if (roots.length > 0)
814
- return { projectIds, roots };
815
- const threads = listedThreads === undefined ? [] : [...listedThreads];
816
- if (listedThreads === undefined) {
817
- cursor = undefined;
818
- for (let page = 0; page < MAX_CODEX_PAGES; page += 1) {
819
- const result = sanitizeThreadList(await this.callUpstream('thread/list', {
820
- limit: CODEX_PAGE_LIMIT,
821
- sortKey: 'updated_at',
822
- sortDirection: 'desc',
823
- archived: false,
824
- ...(cursor === undefined ? {} : { cursor }),
825
- }));
826
- threads.push(...result.data);
827
- cursor = typeof result.nextCursor === 'string' && result.nextCursor.length > 0 ? result.nextCursor : undefined;
828
- if (cursor === undefined)
829
- break;
830
- }
831
- }
832
- for (const workspace of deriveCodexCwdWorkspaces(threads)) {
833
- if (isAbsolute(workspace.path))
834
- roots.push(workspace.path);
835
- }
836
- return { projectIds, roots };
837
- }
838
- async findKnownThreadInList(threadId, authority) {
839
- let cursor;
840
- for (let page = 0; page < MAX_CODEX_PAGES; page += 1) {
841
- const result = sanitizeThreadList(await this.callUpstream('thread/list', {
842
- limit: 100,
843
- sortKey: 'updated_at',
844
- sortDirection: 'desc',
845
- archived: false,
846
- ...(cursor === undefined ? {} : { cursor }),
847
- }));
848
- const thread = result.data.find(item => item.id === threadId);
849
- if (thread !== undefined) {
850
- return isThreadAllowedByWorkspaceAuthority(thread, authority) ? thread : undefined;
851
- }
852
- cursor = typeof result.nextCursor === 'string' && result.nextCursor.length > 0 ? result.nextCursor : undefined;
853
- if (cursor === undefined)
854
- break;
855
- }
856
- return undefined;
857
- }
858
- async requireCodexWorkspacePath(path) {
859
- if (!isAbsolute(path)) {
860
- throw new RpcError('CODEX_PATH_NOT_ALLOWED', 'The CodeX working directory is not available as a Workspace.');
861
- }
862
- const paths = await this.listCodexWorkspacePaths();
863
- const lexicalCandidate = resolve(path);
864
- let candidate;
865
- try {
866
- candidate = await realpath(path);
867
- if (!(await stat(candidate)).isDirectory())
868
- throw new Error('not a directory');
869
- }
870
- catch {
871
- throw new RpcError('CODEX_PATH_NOT_ALLOWED', 'The CodeX working directory is not available as a Workspace.');
872
- }
873
- for (const root of paths.values()) {
874
- try {
875
- if (!containsCodexPath(resolve(root), lexicalCandidate))
876
- continue;
877
- const canonicalRoot = await realpath(root);
878
- if (containsCodexPath(canonicalRoot, candidate))
879
- return lexicalCandidate;
880
- }
881
- catch {
882
- // A stale project or Thread cwd is not authority for a new Thread.
883
- }
884
- }
885
- throw new RpcError('CODEX_PATH_NOT_ALLOWED', 'The CodeX working directory is not available as a Workspace.');
886
- }
887
- async requireNewCodexProjectPath(path) {
888
- if (!isAbsolute(path)) {
889
- throw new RpcError('CODEX_PATH_NOT_ALLOWED', 'The CodeX project directory must be an existing absolute directory.');
890
- }
891
- try {
892
- const canonical = await realpath(resolve(path));
893
- if (!(await stat(canonical)).isDirectory())
894
- throw new Error('not a directory');
895
- return canonical;
896
- }
897
- catch {
898
- throw new RpcError('CODEX_PATH_NOT_ALLOWED', 'The CodeX project directory must be an existing absolute directory.');
899
- }
900
- }
901
- async listCodexDirectory(path) {
902
- const target = await this.resolveCodexDirectory(path);
903
- const rows = await readdir(target.path, { withFileTypes: true }).catch(() => undefined);
904
- if (rows === undefined) {
905
- throw new RpcError('CODEX_PATH_NOT_ALLOWED', 'The CodeX working directory is not available as a Workspace.');
906
- }
907
- const entries = [];
908
- for (const row of rows) {
909
- const child = resolve(target.path, row.name);
910
- let directory = row.isDirectory();
911
- if (!directory && row.isSymbolicLink()) {
912
- directory = await stat(child).then(value => value.isDirectory()).catch(() => false);
913
- }
914
- if (!directory)
915
- continue;
916
- try {
917
- const canonicalChild = await realpath(child);
918
- if (!(await stat(canonicalChild)).isDirectory())
919
- continue;
920
- if (!containsCodexPath(target.canonicalRoot, canonicalChild))
921
- continue;
922
- }
923
- catch {
924
- continue;
925
- }
926
- entries.push({
927
- name: row.name,
928
- path: child,
929
- hidden: process.platform !== 'win32' && row.name.startsWith('.'),
930
- });
931
- }
932
- entries.sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: 'base' }));
933
- return {
934
- path: target.path,
935
- home: target.root,
936
- crumbs: codexDirectoryCrumbs(target.root, target.path),
937
- entries: entries.slice(0, CODEX_DIRECTORY_ENTRY_LIMIT),
938
- truncated: entries.length > CODEX_DIRECTORY_ENTRY_LIMIT,
939
- };
940
- }
941
- async resolveCodexDirectory(path) {
942
- if (!isAbsolute(path)) {
943
- throw new RpcError('CODEX_PATH_NOT_ALLOWED', 'The CodeX working directory is not available as a Workspace.');
944
- }
945
- const authority = await this.readWorkspaceAuthority();
946
- const lexicalCandidate = resolve(path);
947
- let canonicalCandidate;
948
- try {
949
- canonicalCandidate = await realpath(path);
950
- if (!(await stat(canonicalCandidate)).isDirectory())
951
- throw new Error('not a directory');
952
- }
953
- catch {
954
- throw new RpcError('CODEX_PATH_NOT_ALLOWED', 'The CodeX working directory is not available as a Workspace.');
955
- }
956
- for (const root of authority.roots) {
957
- try {
958
- const lexicalRoot = resolve(root);
959
- if (!containsCodexPath(lexicalRoot, lexicalCandidate))
960
- continue;
961
- const canonicalRoot = await realpath(root);
962
- if (containsCodexPath(canonicalRoot, canonicalCandidate)) {
963
- return { path: lexicalCandidate, root: lexicalRoot, canonicalRoot };
964
- }
965
- }
966
- catch {
967
- // A stale project or Thread cwd is not authority for directory browsing.
968
- }
969
- }
970
- throw new RpcError('CODEX_PATH_NOT_ALLOWED', 'The CodeX working directory is not available as a Workspace.');
971
- }
972
- async listCodexWorkspacePaths() {
973
- const paths = new Map();
974
- const authority = await this.readWorkspaceAuthority();
975
- for (const root of authority.roots)
976
- paths.set(normalizeCodexPathForCompare(root), root);
977
- return paths;
978
- }
979
- }
980
- /**
981
- * Prefer Codex bundled with the desktop app when the user kept the default
982
- * command, so an app install works without a global CLI. Explicit binary
983
- * configuration is never rewritten or supplemented.
984
- */
985
- export function codexBinaryCandidates(configured, hostPlatform = process.platform, userHome = homedir()) {
986
- if (configured !== 'codex')
987
- return [configured];
988
- if (hostPlatform === 'win32')
989
- return [...bundledWindowsCodex(userHome), configured];
990
- if (hostPlatform !== 'darwin')
991
- return [configured];
992
- // The platform is a parameter, not necessarily this host: joining with the host flavour would
993
- // return Windows separators for a macOS home and make the darwin branch unreachable from a test
994
- // or tool running elsewhere.
995
- const bundledCandidates = [
996
- '/Applications/ChatGPT.app',
997
- posix.join(userHome, 'Applications', 'ChatGPT.app'),
998
- ].flatMap(chatGptApp => {
999
- const codexCli = posix.join(chatGptApp, 'Contents', 'Resources', 'codex-cli');
1000
- try {
1001
- const manifest = JSON.parse(readFileSync(posix.join(codexCli, 'codex-package.json'), 'utf8'));
1002
- if (!isRecord(manifest) || typeof manifest.entrypoint !== 'string' || manifest.entrypoint.length === 0) {
1003
- return [];
1004
- }
1005
- const candidate = posix.join(codexCli, manifest.entrypoint);
1006
- if (!existsSync(candidate))
1007
- return [];
1008
- accessSync(candidate, constants.X_OK);
1009
- return [candidate];
1010
- }
1011
- catch {
1012
- return [];
1013
- }
1014
- });
1015
- return [...new Set([
1016
- ...bundledCandidates,
1017
- '/Applications/ChatGPT.app/Contents/Resources/codex',
1018
- posix.join(userHome, 'Applications', 'ChatGPT.app', 'Contents', 'Resources', 'codex'),
1019
- configured,
1020
- ])];
1021
- }
1022
- /**
1023
- * Codex shipped with the desktop app on Windows.
1024
- *
1025
- * The app installs each build into its own hash directory under the user's local
1026
- * app data and adds a new one on every update, so the newest directory is the
1027
- * current build. Naming one of those directories in configuration would stop
1028
- * resolving after an update; picking the newest keeps working.
1029
- * @param userHome - the account's home directory, which anchors the app data path.
1030
- * @returns the newest bundled binary, or nothing when the app is not installed.
1031
- */
1032
- function bundledWindowsCodex(userHome) {
1033
- const bin = join(userHome, 'AppData', 'Local', 'OpenAI', 'Codex', 'bin');
1034
- try {
1035
- const newest = readdirSync(bin, { withFileTypes: true })
1036
- .filter(entry => entry.isDirectory())
1037
- .map(entry => join(bin, entry.name, 'codex.exe'))
1038
- .filter(candidate => existsSync(candidate))
1039
- .map(candidate => ({ candidate, modified: statSync(candidate).mtimeMs }))
1040
- .sort((left, right) => right.modified - left.modified);
1041
- const candidate = newest[0]?.candidate;
1042
- return candidate === undefined ? [] : [candidate];
1043
- }
1044
- catch {
1045
- return [];
1046
- }
1047
- }
1048
- function parseCallEnvelope(input) {
1049
- if (!isRecord(input) || typeof input.method !== 'string' || !('params' in input)
1050
- || Object.keys(input).some(key => key !== 'method' && key !== 'params')) {
1051
- throw new RpcError('INVALID_MESSAGE', 'The Codex call envelope is invalid.');
1052
- }
1053
- return { method: input.method, params: input.params };
1054
- }
1055
- function parseRespond(input) {
1056
- if (!isRecord(input)
1057
- || typeof input.requestHandle !== 'string'
1058
- || !['accept', 'decline', 'cancel'].includes(String(input.decision))
1059
- || Object.keys(input).some(key => key !== 'requestHandle' && key !== 'decision')) {
1060
- throw new RpcError('INVALID_MESSAGE', 'The Codex approval response is invalid.');
1061
- }
1062
- return input;
1063
- }
1064
- function accountCanRun(result) {
1065
- if (!isRecord(result) || typeof result.requiresOpenaiAuth !== 'boolean')
1066
- return false;
1067
- return result.requiresOpenaiAuth === false || isRecord(result.account);
1068
- }
1069
- function sanitizeAccount(result) {
1070
- if (!isRecord(result))
1071
- throw new RpcError('CODEX_INVALID_RESPONSE', 'Codex App Server returned invalid account state.');
1072
- const account = isRecord(result.account) ? result.account : undefined;
1073
- return {
1074
- authenticated: account !== undefined || result.requiresOpenaiAuth === false,
1075
- requiresOpenaiAuth: result.requiresOpenaiAuth === true,
1076
- ...(account === undefined ? {} : {
1077
- account: {
1078
- ...(typeof account.type === 'string' ? { type: account.type } : {}),
1079
- ...(typeof account.planType === 'string' ? { planType: account.planType } : {}),
1080
- },
1081
- }),
1082
- };
1083
- }
1084
- function sanitizeThreadList(result) {
1085
- if (!isRecord(result) || !Array.isArray(result.data)) {
1086
- throw new RpcError('CODEX_INVALID_RESPONSE', 'Codex App Server returned an invalid thread list.');
1087
- }
1088
- const data = result.data.flatMap(value => {
1089
- if (!isRecord(value) || typeof value.id !== 'string')
1090
- return [];
1091
- return [{
1092
- id: value.id,
1093
- ...(typeof value.sessionId === 'string' ? { sessionId: value.sessionId } : {}),
1094
- ...(typeof value.projectId === 'string' ? { projectId: value.projectId } : {}),
1095
- ...(typeof value.name === 'string' ? { name: value.name } : {}),
1096
- ...(typeof value.preview === 'string' ? { preview: value.preview } : {}),
1097
- ...(typeof value.cwd === 'string' ? { cwd: value.cwd } : {}),
1098
- ...(typeof value.createdAt === 'number' && Number.isFinite(value.createdAt) ? { createdAt: value.createdAt } : {}),
1099
- ...(typeof value.updatedAt === 'number' && Number.isFinite(value.updatedAt) ? { updatedAt: value.updatedAt } : {}),
1100
- ...(typeof value.archived === 'boolean' ? { archived: value.archived } : {}),
1101
- ...(typeof value.isPinned === 'boolean' ? { isPinned: value.isPinned } : {}),
1102
- ...(isRecord(value.status) ? { status: value.status } : {}),
1103
- }];
1104
- });
1105
- return {
1106
- data,
1107
- ...(typeof result.nextCursor === 'string' && result.nextCursor.length > 0 ? { nextCursor: result.nextCursor } : { nextCursor: null }),
1108
- ...(typeof result.backwardsCursor === 'string' && result.backwardsCursor.length > 0
1109
- ? { backwardsCursor: result.backwardsCursor }
1110
- : {}),
1111
- };
1112
- }
1113
- function filterThreadListByWorkspaceAuthority(result, authority) {
1114
- if (!isRecord(result) || !Array.isArray(result.data)) {
1115
- throw new RpcError('CODEX_INVALID_RESPONSE', 'Codex App Server returned an invalid thread list.');
1116
- }
1117
- return {
1118
- ...result,
1119
- data: result.data
1120
- .map(record => isRecord(record) ? record : undefined)
1121
- .filter((thread) => (thread !== undefined && isThreadAllowedByWorkspaceAuthority(thread, authority))),
1122
- };
1123
- }
1124
- function sanitizeProject(value) {
1125
- if (!isRecord(value) || typeof value.id !== 'string' || typeof value.name !== 'string')
1126
- return undefined;
1127
- const roots = Array.isArray(value.roots)
1128
- ? value.roots.flatMap(root => {
1129
- const path = isRecord(root) && typeof root.path === 'string' && root.path.length > 0 ? root.path : undefined;
1130
- return path === undefined || !isAbsolute(path) ? [] : [{ path }];
1131
- })
1132
- : [];
1133
- if (roots.length === 0)
1134
- return undefined;
1135
- return {
1136
- id: value.id,
1137
- name: value.name,
1138
- roots,
1139
- ...(typeof value.position === 'number' && Number.isFinite(value.position) ? { position: value.position } : {}),
1140
- ...(typeof value.createdAt === 'number' && Number.isFinite(value.createdAt) ? { createdAt: value.createdAt } : {}),
1141
- ...(typeof value.updatedAt === 'number' && Number.isFinite(value.updatedAt) ? { updatedAt: value.updatedAt } : {}),
1142
- };
1143
- }
1144
- function sanitizeProjectList(result) {
1145
- if (!isRecord(result) || !Array.isArray(result.data)) {
1146
- throw new RpcError('CODEX_INVALID_RESPONSE', 'Codex App Server returned an invalid project list.');
1147
- }
1148
- const data = result.data.flatMap(value => sanitizeProject(value) ?? []);
1149
- return {
1150
- data,
1151
- ...(typeof result.nextCursor === 'string' && result.nextCursor.length > 0 ? { nextCursor: result.nextCursor } : { nextCursor: null }),
1152
- };
1153
- }
1154
- function sanitizeProjectCreate(result) {
1155
- const project = isRecord(result) ? sanitizeProject(result.project) : undefined;
1156
- if (project === undefined) {
1157
- throw new RpcError('CODEX_INVALID_RESPONSE', 'Codex App Server returned an invalid created project.');
1158
- }
1159
- return { project };
1160
- }
1161
- function isThreadAllowedByWorkspaceAuthority(thread, authority) {
1162
- const projectId = typeof thread.projectId === 'string' ? thread.projectId : undefined;
1163
- if (projectId !== undefined && authority.projectIds.has(projectId))
1164
- return true;
1165
- const cwd = typeof thread.cwd === 'string' ? thread.cwd : undefined;
1166
- return cwd !== undefined && authority.roots.some(root => containsCodexPath(root, cwd));
1167
- }
1168
- function containsCodexPath(root, candidate) {
1169
- const normalizedRoot = normalizeCodexPathForCompare(root);
1170
- const normalizedCandidate = normalizeCodexPathForCompare(candidate);
1171
- return normalizedCandidate === normalizedRoot
1172
- || normalizedCandidate.startsWith(`${normalizedRoot}/`)
1173
- || normalizedCandidate.startsWith(`${normalizedRoot}\\`);
1174
- }
1175
- function normalizeCodexPathForCompare(path) {
1176
- const trimmed = path.replace(/[\\/]+$/u, '') || path;
1177
- // Windows resolves paths case-insensitively, and Codex reports the same
1178
- // directory in whatever spelling it happens to hold: a lowercased drive letter,
1179
- // a project renamed after its thread was created, or a short 8.3 name such as
1180
- // `SAKAKI~1`. Comparing literally therefore declared a thread's own directory
1181
- // "outside" the authority built from that very directory, which rejected every
1182
- // thread on Windows.
1183
- return process.platform === 'win32' ? trimmed.toLowerCase() : trimmed;
1184
- }
1185
- function codexDirectoryCrumbs(root, path) {
1186
- const crumbs = [{ name: basename(root) || root, path: root, hidden: false }];
1187
- const remainder = relative(root, path);
1188
- if (remainder === '')
1189
- return crumbs;
1190
- let current = root;
1191
- for (const segment of remainder.split(/[\\/]+/u).filter(Boolean)) {
1192
- current = resolve(current, segment);
1193
- crumbs.push({ name: segment, path: current, hidden: false });
1194
- }
1195
- return crumbs;
1196
- }
1197
- function extractThread(result) {
1198
- return isRecord(result) && isRecord(result.thread) ? result.thread : undefined;
1199
- }
1200
- function extractTurnId(result) {
1201
- if (!isRecord(result))
1202
- return undefined;
1203
- if (typeof result.turnId === 'string' && result.turnId.length > 0)
1204
- return result.turnId;
1205
- if (isRecord(result.turn) && typeof result.turn.id === 'string' && result.turn.id.length > 0)
1206
- return result.turn.id;
1207
- return undefined;
1208
- }
1209
- function extractThreadId(params) {
1210
- if (!isRecord(params))
1211
- return undefined;
1212
- if (typeof params.threadId === 'string')
1213
- return params.threadId;
1214
- if (isRecord(params.thread) && typeof params.thread.id === 'string')
1215
- return params.thread.id;
1216
- if (isRecord(params.turn) && typeof params.turn.threadId === 'string')
1217
- return params.turn.threadId;
1218
- return undefined;
1219
- }
1220
- function optionalInteger(value) {
1221
- return typeof value === 'number' && Number.isSafeInteger(value) ? value : undefined;
1222
- }
1223
- function codexPermission(params, fallbackPreset) {
1224
- const { permissionPreset, ...rest } = params;
1225
- const preset = permissionPreset === 'workspace-write' || permissionPreset === 'danger-full-access'
1226
- ? permissionPreset
1227
- : fallbackPreset;
1228
- if (preset === undefined)
1229
- return { params: mapCodexImageInputs(rest) };
1230
- const fullAccess = preset === 'danger-full-access';
1231
- return {
1232
- params: mapCodexImageInputs(rest),
1233
- approvalPolicy: fullAccess ? 'never' : 'on-request',
1234
- sandbox: fullAccess ? 'danger-full-access' : 'workspace-write',
1235
- };
1236
- }
1237
- function codexThreadPermissionParams(permission) {
1238
- if (permission.approvalPolicy === undefined || permission.sandbox === undefined)
1239
- return {};
1240
- return {
1241
- approvalPolicy: permission.approvalPolicy,
1242
- sandbox: permission.sandbox,
1243
- };
1244
- }
1245
- function codexTurnPermissionParams(permission, cwd) {
1246
- if (permission.approvalPolicy === undefined || permission.sandbox === undefined)
1247
- return {};
1248
- return {
1249
- approvalPolicy: permission.approvalPolicy,
1250
- sandboxPolicy: permission.sandbox === 'danger-full-access'
1251
- ? { type: 'dangerFullAccess' }
1252
- : {
1253
- type: 'workspaceWrite',
1254
- writableRoots: cwd === undefined ? [] : [cwd],
1255
- networkAccess: false,
1256
- excludeTmpdirEnvVar: false,
1257
- excludeSlashTmp: false,
1258
- },
1259
- };
1260
- }
1261
- function mapCodexImageInputs(params) {
1262
- if (!Array.isArray(params.input))
1263
- return params;
1264
- return {
1265
- ...params,
1266
- input: params.input.map(value => {
1267
- if (!isRecord(value) || value.type !== 'image'
1268
- || typeof value.mediaType !== 'string' || typeof value.data !== 'string')
1269
- return value;
1270
- return { type: 'image', url: `data:${value.mediaType};base64,${value.data}` };
1271
- }),
1272
- };
1273
- }
1274
- function sanitizeApprovalParams(params, requestHandle) {
1275
- if (!isRecord(params))
1276
- return { requestHandle };
1277
- const safe = { ...params };
1278
- delete safe.proposedExecpolicyAmendment;
1279
- delete safe.additionalPermissions;
1280
- safe.availableDecisions = ['accept', 'decline', 'cancel'];
1281
- safe.requestHandle = requestHandle;
1282
- return safe;
1283
- }
1284
- function mapAppServerError(error) {
1285
- if (error instanceof RpcError)
1286
- return error;
1287
- if (error instanceof CodexAppServerError) {
1288
- if (error.code === 'CODEX_UPSTREAM_ERROR' && isActiveWriterMessage(error.message)) {
1289
- return new RpcError('CODEX_THREAD_BUSY', 'The selected CodeX thread is already active in another CodeX client.');
1290
- }
1291
- return new RpcError(error.code, error.message, undefined, error.code === 'CODEX_REQUEST_TIMEOUT');
1292
- }
1293
- return new RpcError('CODEX_UPSTREAM_ERROR', 'Codex App Server could not complete the request.');
1294
- }
1295
- function errorCode(error) {
1296
- if (error instanceof RpcError || error instanceof CodexAppServerError)
1297
- return error.code;
1298
- return 'CODEX_START_FAILED';
1299
- }
1300
- function isHistoryReadRecoverable(error) {
1301
- return error instanceof RpcError
1302
- && ['METHOD_NOT_ALLOWED', 'METHOD_NOT_FOUND', 'CODEX_UPSTREAM_ERROR', 'CODEX_REQUEST_TIMEOUT', 'CODEX_THREAD_BUSY'].includes(error.code);
1303
- }
1304
- function isProjectListFallbackError(error) {
1305
- return error instanceof RpcError
1306
- && ['METHOD_NOT_ALLOWED', 'METHOD_NOT_FOUND', 'CODEX_UPSTREAM_ERROR'].includes(error.code);
1307
- }
1308
- function canTryNextBinary(error) {
1309
- return !(error instanceof RpcError) || !['CODEX_AUTH_REQUIRED', 'CODEX_CLOSED'].includes(error.code);
1310
- }
1311
- function isRecord(value) {
1312
- return typeof value === 'object' && value !== null && !Array.isArray(value);
1313
- }
1314
- function array(value) {
1315
- return Array.isArray(value) ? value : [];
1316
- }
1317
- function maskId(value) {
1318
- return value.length <= 12 ? value : `${value.slice(0, 8)}…${value.slice(-4)}`;
1319
- }
1320
- function isActiveWriterMessage(message) {
1321
- return message.toLowerCase().includes('active writer');
1322
- }
1323
- //# sourceMappingURL=domain.js.map