@blue-soda/dsh-remote 0.5.6 → 0.5.8
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.
- package/README.md +14 -3
- package/cordis.patch.yml +30 -30
- package/dist/client.github.js +1 -1
- package/dist/client.js +1 -1
- package/dist/index.js +1216 -396
- package/dist/index.js.map +4 -4
- package/dsh-plugin.json +41 -41
- package/package.json +165 -165
- package/dist/acp.d.ts +0 -75
- package/dist/acp.d.ts.map +0 -1
- package/dist/acp.js +0 -61
- package/dist/acp.js.map +0 -1
- package/dist/api-proxy-switch.d.ts +0 -33
- package/dist/api-proxy-switch.d.ts.map +0 -1
- package/dist/api-proxy-switch.js +0 -109
- package/dist/api-proxy-switch.js.map +0 -1
- package/dist/atomic-file.d.ts +0 -16
- package/dist/atomic-file.d.ts.map +0 -1
- package/dist/atomic-file.js +0 -81
- package/dist/atomic-file.js.map +0 -1
- package/dist/bin/doctor.d.ts +0 -3
- package/dist/bin/doctor.d.ts.map +0 -1
- package/dist/bin/doctor.js +0 -13
- package/dist/bin/doctor.js.map +0 -1
- package/dist/cli.d.ts +0 -32
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -289
- package/dist/cli.js.map +0 -1
- package/dist/client-runtime.d.ts +0 -375
- package/dist/client-runtime.d.ts.map +0 -1
- package/dist/client-runtime.js +0 -1768
- package/dist/client-runtime.js.map +0 -1
- package/dist/client-secure-transport.d.ts +0 -23
- package/dist/client-secure-transport.d.ts.map +0 -1
- package/dist/client-secure-transport.js +0 -144
- package/dist/client-secure-transport.js.map +0 -1
- package/dist/client-target-store.d.ts +0 -40
- package/dist/client-target-store.d.ts.map +0 -1
- package/dist/client-target-store.js +0 -79
- package/dist/client-target-store.js.map +0 -1
- package/dist/client.d.ts +0 -13
- package/dist/client.d.ts.map +0 -1
- package/dist/codex/app-server.d.ts +0 -71
- package/dist/codex/app-server.d.ts.map +0 -1
- package/dist/codex/app-server.js +0 -268
- package/dist/codex/app-server.js.map +0 -1
- package/dist/codex/codex-image-codec.d.ts +0 -7
- package/dist/codex/codex-image-codec.d.ts.map +0 -1
- package/dist/codex/codex-image-codec.js +0 -288
- package/dist/codex/codex-image-codec.js.map +0 -1
- package/dist/codex/domain.d.ts +0 -102
- package/dist/codex/domain.d.ts.map +0 -1
- package/dist/codex/domain.js +0 -1323
- package/dist/codex/domain.js.map +0 -1
- package/dist/codex/method-policy.d.ts +0 -338
- package/dist/codex/method-policy.d.ts.map +0 -1
- package/dist/codex/method-policy.js +0 -141
- package/dist/codex/method-policy.js.map +0 -1
- package/dist/codex/peer-bridge.d.ts +0 -57
- package/dist/codex/peer-bridge.d.ts.map +0 -1
- package/dist/codex/peer-bridge.js +0 -299
- package/dist/codex/peer-bridge.js.map +0 -1
- package/dist/codex/permissions.d.ts +0 -4
- package/dist/codex/permissions.d.ts.map +0 -1
- package/dist/codex/permissions.js +0 -21
- package/dist/codex/permissions.js.map +0 -1
- package/dist/codex/session-id.d.ts +0 -10
- package/dist/codex/session-id.d.ts.map +0 -1
- package/dist/codex/session-id.js +0 -15
- package/dist/codex/session-id.js.map +0 -1
- package/dist/codex/virtual-harness.d.ts +0 -209
- package/dist/codex/virtual-harness.d.ts.map +0 -1
- package/dist/codex/virtual-harness.js +0 -2650
- package/dist/codex/virtual-harness.js.map +0 -1
- package/dist/codex-workspace-bridge.d.ts +0 -108
- package/dist/codex-workspace-bridge.d.ts.map +0 -1
- package/dist/codex-workspace-bridge.js +0 -502
- package/dist/codex-workspace-bridge.js.map +0 -1
- package/dist/config.d.ts +0 -99
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -127
- package/dist/config.js.map +0 -1
- package/dist/connection-controller.d.ts +0 -44
- package/dist/connection-controller.d.ts.map +0 -1
- package/dist/connection-controller.js +0 -185
- package/dist/connection-controller.js.map +0 -1
- package/dist/control-route.d.ts +0 -34
- package/dist/control-route.d.ts.map +0 -1
- package/dist/control-route.js +0 -136
- package/dist/control-route.js.map +0 -1
- package/dist/control-runtime.d.ts +0 -138
- package/dist/control-runtime.d.ts.map +0 -1
- package/dist/control-runtime.js +0 -543
- package/dist/control-runtime.js.map +0 -1
- package/dist/control-stream.d.ts +0 -67
- package/dist/control-stream.d.ts.map +0 -1
- package/dist/control-stream.js +0 -179
- package/dist/control-stream.js.map +0 -1
- package/dist/defaults.d.ts +0 -9
- package/dist/defaults.d.ts.map +0 -1
- package/dist/defaults.js +0 -9
- package/dist/defaults.js.map +0 -1
- package/dist/file-viewer-bridge.d.ts +0 -30
- package/dist/file-viewer-bridge.d.ts.map +0 -1
- package/dist/file-viewer-bridge.js +0 -128
- package/dist/file-viewer-bridge.js.map +0 -1
- package/dist/file-viewer-contract.d.ts +0 -5
- package/dist/file-viewer-contract.d.ts.map +0 -1
- package/dist/file-viewer-contract.js +0 -4
- package/dist/file-viewer-contract.js.map +0 -1
- package/dist/harness-api-bridge.d.ts +0 -81
- package/dist/harness-api-bridge.d.ts.map +0 -1
- package/dist/harness-api-bridge.js +0 -862
- package/dist/harness-api-bridge.js.map +0 -1
- package/dist/harness-api-history.d.ts +0 -3
- package/dist/harness-api-history.d.ts.map +0 -1
- package/dist/harness-api-history.js +0 -59
- package/dist/harness-api-history.js.map +0 -1
- package/dist/harness-remote-bridge.d.ts +0 -60
- package/dist/harness-remote-bridge.d.ts.map +0 -1
- package/dist/harness-remote-bridge.js +0 -540
- package/dist/harness-remote-bridge.js.map +0 -1
- package/dist/harness-version.d.ts +0 -23
- package/dist/harness-version.d.ts.map +0 -1
- package/dist/harness-version.js +0 -95
- package/dist/harness-version.js.map +0 -1
- package/dist/identity-store.d.ts +0 -45
- package/dist/identity-store.d.ts.map +0 -1
- package/dist/identity-store.js +0 -186
- package/dist/identity-store.js.map +0 -1
- package/dist/ids.d.ts +0 -2
- package/dist/ids.d.ts.map +0 -1
- package/dist/ids.js +0 -17
- package/dist/ids.js.map +0 -1
- package/dist/index.d.ts +0 -78
- package/dist/index.d.ts.map +0 -1
- package/dist/logging.d.ts +0 -18
- package/dist/logging.d.ts.map +0 -1
- package/dist/logging.js +0 -30
- package/dist/logging.js.map +0 -1
- package/dist/loopback-host.d.ts +0 -20
- package/dist/loopback-host.d.ts.map +0 -1
- package/dist/loopback-host.js +0 -245
- package/dist/loopback-host.js.map +0 -1
- package/dist/loopback-preview.d.ts +0 -19
- package/dist/loopback-preview.d.ts.map +0 -1
- package/dist/loopback-preview.js +0 -232
- package/dist/loopback-preview.js.map +0 -1
- package/dist/native-rtc-helper.d.ts +0 -14
- package/dist/native-rtc-helper.d.ts.map +0 -1
- package/dist/native-rtc-helper.js +0 -575
- package/dist/native-rtc-helper.js.map +0 -1
- package/dist/remote-api-proxy.d.ts +0 -17
- package/dist/remote-api-proxy.d.ts.map +0 -1
- package/dist/remote-api-proxy.js +0 -451
- package/dist/remote-api-proxy.js.map +0 -1
- package/dist/remote-directory-browser.d.ts +0 -16
- package/dist/remote-directory-browser.d.ts.map +0 -1
- package/dist/remote-directory-browser.js +0 -48
- package/dist/remote-directory-browser.js.map +0 -1
- package/dist/remote-file-content-provider.d.ts +0 -48
- package/dist/remote-file-content-provider.d.ts.map +0 -1
- package/dist/remote-file-content-provider.js +0 -93
- package/dist/remote-file-content-provider.js.map +0 -1
- package/dist/remote-typert-gateway.d.ts +0 -20
- package/dist/remote-typert-gateway.d.ts.map +0 -1
- package/dist/remote-typert-gateway.js +0 -334
- package/dist/remote-typert-gateway.js.map +0 -1
- package/dist/rpc-binary-attachments.d.ts +0 -33
- package/dist/rpc-binary-attachments.d.ts.map +0 -1
- package/dist/rpc-binary-attachments.js +0 -142
- package/dist/rpc-binary-attachments.js.map +0 -1
- package/dist/rpc-router.d.ts +0 -31
- package/dist/rpc-router.d.ts.map +0 -1
- package/dist/rpc-router.js +0 -201
- package/dist/rpc-router.js.map +0 -1
- package/dist/safe-error.d.ts +0 -8
- package/dist/safe-error.d.ts.map +0 -1
- package/dist/safe-error.js +0 -20
- package/dist/safe-error.js.map +0 -1
- package/dist/server-api.d.ts +0 -109
- package/dist/server-api.d.ts.map +0 -1
- package/dist/server-api.js +0 -520
- package/dist/server-api.js.map +0 -1
- package/dist/server-connection.d.ts +0 -79
- package/dist/server-connection.d.ts.map +0 -1
- package/dist/server-connection.js +0 -1023
- package/dist/server-connection.js.map +0 -1
- package/dist/server-credentials.d.ts +0 -55
- package/dist/server-credentials.d.ts.map +0 -1
- package/dist/server-credentials.js +0 -108
- package/dist/server-credentials.js.map +0 -1
- package/dist/service.d.ts +0 -151
- package/dist/service.d.ts.map +0 -1
- package/dist/service.js +0 -448
- package/dist/service.js.map +0 -1
- package/dist/session-format-compat.d.ts +0 -3
- package/dist/session-format-compat.d.ts.map +0 -1
- package/dist/session-format-compat.js +0 -183
- package/dist/session-format-compat.js.map +0 -1
- package/dist/status-stream.d.ts +0 -48
- package/dist/status-stream.d.ts.map +0 -1
- package/dist/status-stream.js +0 -133
- package/dist/status-stream.js.map +0 -1
- package/dist/terminal-policy.d.ts +0 -21
- package/dist/terminal-policy.d.ts.map +0 -1
- package/dist/terminal-policy.js +0 -68
- package/dist/terminal-policy.js.map +0 -1
- package/dist/tui-command.d.ts +0 -13
- package/dist/tui-command.d.ts.map +0 -1
- package/dist/tui-command.js +0 -392
- package/dist/tui-command.js.map +0 -1
- package/dist/typert-gateway-contract.d.ts +0 -59
- package/dist/typert-gateway-contract.d.ts.map +0 -1
- package/dist/typert-gateway-contract.js +0 -2
- package/dist/typert-gateway-contract.js.map +0 -1
- package/dist/typert-gateway-switch.d.ts +0 -85
- package/dist/typert-gateway-switch.d.ts.map +0 -1
- package/dist/typert-gateway-switch.js +0 -448
- package/dist/typert-gateway-switch.js.map +0 -1
- package/dist/types.d.ts +0 -20
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/version.d.ts +0 -12
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js +0 -12
- package/dist/version.js.map +0 -1
- package/dist/werift-rtc.d.ts +0 -121
- package/dist/werift-rtc.d.ts.map +0 -1
- package/dist/werift-rtc.js +0 -490
- package/dist/werift-rtc.js.map +0 -1
package/dist/codex/domain.js
DELETED
|
@@ -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
|