@bitkyc08/opencodex 2.52.0-preview.20260912 → 2.53.0-preview.20260913

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 (236) hide show
  1. package/gui/dist/assets/index-BBOZWGB6.css +1 -0
  2. package/gui/dist/assets/index-D7ynYo2K.js +128 -0
  3. package/gui/dist/index.html +2 -2
  4. package/native/remote-workspace-helper/Cargo.lock +130 -0
  5. package/native/remote-workspace-helper/Cargo.toml +24 -0
  6. package/native/remote-workspace-helper/src/main.rs +49 -0
  7. package/native/remote-workspace-helper/src/protocol.rs +246 -0
  8. package/native/remote-workspace-helper/src/sandbox/macos.rs +19 -0
  9. package/native/remote-workspace-helper/src/sandbox/mod.rs +77 -0
  10. package/native/remote-workspace-helper/src/sandbox/windows.rs +15 -0
  11. package/package.json +6 -1
  12. package/src/adapters/anthropic-image-normalize.ts +30 -2
  13. package/src/adapters/anthropic.ts +1 -1
  14. package/src/adapters/base.ts +8 -2
  15. package/src/adapters/cursor/cursor-errors.ts +12 -0
  16. package/src/adapters/cursor/thread-continuity.ts +93 -0
  17. package/src/adapters/cursor.ts +104 -73
  18. package/src/adapters/devin/cloud-direct/chat.ts +312 -23
  19. package/src/adapters/devin/cloud-direct/metadata.ts +31 -2
  20. package/src/adapters/devin/live-models.ts +70 -3
  21. package/src/adapters/devin.ts +281 -21
  22. package/src/adapters/google-wire-compiler.ts +14 -6
  23. package/src/adapters/google.ts +22 -8
  24. package/src/adapters/kiro/adapter.ts +316 -0
  25. package/src/adapters/kiro/conversation.ts +136 -0
  26. package/src/adapters/kiro/payload.ts +432 -0
  27. package/src/adapters/kiro/reasoning.ts +56 -0
  28. package/src/adapters/kiro/stream.ts +1153 -0
  29. package/src/adapters/kiro/usage.ts +223 -0
  30. package/src/adapters/kiro/wire.ts +76 -0
  31. package/src/adapters/kiro.ts +8 -2319
  32. package/src/adapters/mimo-free.ts +1 -1
  33. package/src/adapters/openai-chat-images.ts +101 -0
  34. package/src/adapters/openai-chat.ts +201 -181
  35. package/src/adapters/openai-responses.ts +92 -224
  36. package/src/adapters/registry.ts +0 -7
  37. package/src/adapters/run-turn-queue.ts +13 -6
  38. package/src/bridge.ts +14 -15
  39. package/src/chat/inbound.ts +29 -4
  40. package/src/chat/outbound.ts +145 -107
  41. package/src/claude/desktop-profile.ts +4 -6
  42. package/src/cli/account-api.ts +14 -0
  43. package/src/cli/account-extended.ts +1 -1
  44. package/src/cli/account-history.ts +60 -0
  45. package/src/cli/account-main.ts +80 -0
  46. package/src/cli/account.ts +11 -3
  47. package/src/cli/capabilities.ts +113 -0
  48. package/src/cli/catalog.ts +109 -0
  49. package/src/cli/dispatch.ts +9 -0
  50. package/src/cli/help.ts +2 -0
  51. package/src/cli/index.ts +2 -2
  52. package/src/cli/observe.ts +28 -1
  53. package/src/cli/opencode.ts +42 -8
  54. package/src/cli/provider-runtime.ts +11 -1
  55. package/src/cli/provider.ts +22 -2
  56. package/src/cli/registry.ts +21 -0
  57. package/src/cli/remote-workspace.ts +154 -0
  58. package/src/cli/status.ts +39 -7
  59. package/src/cli/usage-report.ts +14 -2
  60. package/src/client/hub-client.ts +34 -0
  61. package/src/client/hub-state.ts +9 -1
  62. package/src/codex/account-store.ts +78 -0
  63. package/src/codex/auth-api.ts +81 -54
  64. package/src/codex/auth-context.ts +45 -16
  65. package/src/codex/catalog/effort.ts +1 -1
  66. package/src/codex/catalog/metadata.ts +3 -6
  67. package/src/codex/catalog/native-models.ts +4 -4
  68. package/src/codex/catalog/parsing.ts +2 -20
  69. package/src/codex/catalog/provider-fetch.ts +10 -1
  70. package/src/codex/catalog/remote.ts +233 -0
  71. package/src/codex/catalog/sync.ts +403 -35
  72. package/src/codex/convergence.ts +1 -1
  73. package/src/codex/history-manifest.ts +36 -0
  74. package/src/codex/history-provider.ts +32 -5
  75. package/src/codex/inject.ts +9 -0
  76. package/src/codex/main-account.ts +113 -0
  77. package/src/codex/main-device-reauth-api.ts +89 -0
  78. package/src/codex/main-device-reauth.ts +217 -0
  79. package/src/codex/native-residue.ts +9 -2
  80. package/src/codex/quota-auto-refresh.ts +3 -2
  81. package/src/codex/quota-capacity.ts +98 -0
  82. package/src/codex/quota-history.ts +160 -0
  83. package/src/codex/quota-types.ts +8 -0
  84. package/src/codex/quota.ts +118 -91
  85. package/src/codex/refresh.ts +2 -1
  86. package/src/codex/routing.ts +90 -17
  87. package/src/codex/sync.ts +33 -4
  88. package/src/combos/request.ts +19 -1
  89. package/src/config/multi-agent-surface.ts +61 -0
  90. package/src/config/provider-validation.ts +176 -0
  91. package/src/config.ts +213 -11
  92. package/src/generated/compatibility-version.json +436 -168
  93. package/src/images/loop.ts +119 -36
  94. package/src/lib/admission.ts +12 -6
  95. package/src/lib/redact.ts +7 -0
  96. package/src/lib/translator-budget.ts +4 -3
  97. package/src/lib/windows-atomic-replace.ts +1 -0
  98. package/src/lib/windows-elevation.ts +1 -1
  99. package/src/oauth/chatgpt-device.ts +62 -5
  100. package/src/oauth/devin/cli-import.ts +130 -0
  101. package/src/oauth/devin.ts +63 -8
  102. package/src/oauth/index.ts +29 -14
  103. package/src/oauth/kiro.ts +18 -6
  104. package/src/oauth/login-cli.ts +9 -1
  105. package/src/oauth/meta-muse-device.ts +464 -0
  106. package/src/oauth/meta-muse.ts +123 -32
  107. package/src/oauth/pool-kernel.ts +9 -0
  108. package/src/oauth/pool-settings-capability.ts +2 -2
  109. package/src/oauth/store.ts +57 -0
  110. package/src/oauth/types.ts +31 -0
  111. package/src/providers/derive.ts +13 -3
  112. package/src/providers/devin-cli-authmode-migration.ts +57 -35
  113. package/src/providers/devin-provider-merge-migration.ts +240 -0
  114. package/src/providers/muse-key-quota.ts +117 -0
  115. package/src/providers/muse-subscription-usage.ts +14 -2
  116. package/src/providers/openai-sidecar.ts +25 -3
  117. package/src/providers/opencode-zen-rate-limit.ts +58 -0
  118. package/src/providers/provider-id-rewrite.ts +20 -5
  119. package/src/providers/quota-types.ts +12 -0
  120. package/src/providers/quota.ts +143 -102
  121. package/src/providers/reasoning-metadata.ts +543 -0
  122. package/src/providers/registry.ts +80 -49
  123. package/src/reasoning-effort.ts +26 -2
  124. package/src/remote/hub-usage.ts +32 -0
  125. package/src/remote-control/index.ts +192 -41
  126. package/src/remote-control/workspace-activation.ts +9 -0
  127. package/src/remote-control/workspace-agent-connection.ts +366 -0
  128. package/src/remote-control/workspace-claude-runtime.ts +243 -0
  129. package/src/remote-control/workspace-codex-runtime.ts +531 -0
  130. package/src/remote-control/workspace-codex-sandbox.ts +115 -0
  131. package/src/remote-control/workspace-command-runner.ts +748 -0
  132. package/src/remote-control/workspace-coordinator.ts +231 -0
  133. package/src/remote-control/workspace-device.ts +585 -0
  134. package/src/remote-control/workspace-executable.ts +43 -0
  135. package/src/remote-control/workspace-executor.ts +397 -0
  136. package/src/remote-control/workspace-hub.ts +519 -0
  137. package/src/remote-control/workspace-pi-runtime.ts +382 -0
  138. package/src/remote-control/workspace-process.ts +129 -0
  139. package/src/remote-control/workspace-rpc.ts +304 -0
  140. package/src/remote-control/workspace-runtime.ts +60 -0
  141. package/src/remote-control/workspace-secret-store.ts +39 -0
  142. package/src/remote-control/workspace-sessions.ts +799 -0
  143. package/src/remote-control/workspace-tool-bridge.ts +192 -0
  144. package/src/responses/code-mode-helper-compat.ts +22 -3
  145. package/src/responses/hosted-tool-policy.ts +0 -1
  146. package/src/responses/muse-tool-name-alias.ts +379 -0
  147. package/src/responses/plaintext-v2-agent-messages.ts +902 -0
  148. package/src/router.ts +7 -0
  149. package/src/routing/compatibility/behavior.ts +0 -1
  150. package/src/server/audio-client.ts +64 -0
  151. package/src/server/audio-dictation.ts +91 -0
  152. package/src/server/audio-live.ts +185 -0
  153. package/src/server/audio-transcriptions.ts +183 -0
  154. package/src/server/audio-upstream.ts +153 -0
  155. package/src/server/auth-cors.ts +61 -2
  156. package/src/server/chat-completions.ts +1 -1
  157. package/src/server/chat-native-sse.ts +92 -48
  158. package/src/server/chat-native.ts +37 -15
  159. package/src/server/hub-usage.ts +57 -0
  160. package/src/server/images.ts +4 -0
  161. package/src/server/index.ts +722 -57
  162. package/src/server/lifecycle.ts +5 -6
  163. package/src/server/live-call-bindings.ts +60 -0
  164. package/src/server/live.ts +12 -1
  165. package/src/server/management/agent-settings-routes.ts +25 -4
  166. package/src/server/management/api-access.ts +37 -0
  167. package/src/server/management/api-key-usage.ts +7 -2
  168. package/src/server/management/config-routes.ts +1 -18
  169. package/src/server/management/context.ts +15 -0
  170. package/src/server/management/logs-usage-routes.ts +2 -0
  171. package/src/server/management/oauth-account-routes.ts +39 -12
  172. package/src/server/management/provider-routes.ts +125 -2
  173. package/src/server/management/remote-workspace-routes.ts +140 -0
  174. package/src/server/management/route-registry.ts +15 -0
  175. package/src/server/management/usage-aggregate-cache.ts +14 -15
  176. package/src/server/management/usage-summary-cache.ts +2 -0
  177. package/src/server/management-api.ts +23 -0
  178. package/src/server/ports.ts +17 -0
  179. package/src/server/relay-eager.ts +4 -1
  180. package/src/server/relay.ts +70 -10
  181. package/src/server/request-decompress.ts +6 -3
  182. package/src/server/responses/agent-task-recovery.ts +25 -32
  183. package/src/server/responses/codex-auth-error.ts +11 -0
  184. package/src/server/responses/codex-ws-exchange.ts +52 -3
  185. package/src/server/responses/codex-ws-wire.ts +55 -0
  186. package/src/server/responses/compact.ts +9 -1
  187. package/src/server/responses/core.ts +337 -73
  188. package/src/server/responses/encrypted-payload.ts +45 -2
  189. package/src/server/responses/ws-upstream.ts +4 -1
  190. package/src/server/responses-self-named-namespace-scrub.ts +1 -3
  191. package/src/server/responses-undeclared-tool-guard.ts +1 -1
  192. package/src/server/search.ts +3 -0
  193. package/src/server/sse-payload-rewrite.ts +136 -51
  194. package/src/server/ws-bridge.ts +35 -1
  195. package/src/service/cli.ts +372 -0
  196. package/src/service/diagnostics.ts +340 -0
  197. package/src/service/guards.ts +303 -0
  198. package/src/service/health.ts +222 -0
  199. package/src/service/launchd.ts +853 -0
  200. package/src/service/orchestration.ts +617 -0
  201. package/src/service/repair.ts +334 -0
  202. package/src/service/state.ts +363 -0
  203. package/src/service/systemd.ts +229 -0
  204. package/src/service/windows-ops.ts +690 -0
  205. package/src/service/windows-scheduler.ts +769 -0
  206. package/src/service/windows-taskxml.ts +613 -0
  207. package/src/service.ts +22 -5550
  208. package/src/storage/cleanup/db.ts +258 -0
  209. package/src/storage/cleanup/execute.ts +358 -0
  210. package/src/storage/cleanup/paths.ts +189 -0
  211. package/src/storage/cleanup/pending.ts +140 -0
  212. package/src/storage/cleanup/preview.ts +292 -0
  213. package/src/storage/cleanup/reconcile.ts +347 -0
  214. package/src/storage/cleanup/restore.ts +932 -0
  215. package/src/storage/cleanup/satellite.ts +474 -0
  216. package/src/storage/cleanup/staging.ts +129 -0
  217. package/src/storage/cleanup/types.ts +98 -0
  218. package/src/storage/cleanup.ts +49 -3127
  219. package/src/types/accounts.ts +2 -0
  220. package/src/types/config.ts +13 -12
  221. package/src/types/provider.ts +37 -0
  222. package/src/types/request.ts +2 -0
  223. package/src/types/tools.ts +17 -5
  224. package/src/types.ts +1 -0
  225. package/src/usage/expected-prices.ts +127 -0
  226. package/src/usage/log.ts +58 -1
  227. package/src/vision/eligibility.ts +13 -2
  228. package/src/web-search/loop.ts +56 -3
  229. package/gui/dist/assets/index-D_t6sCWs.js +0 -115
  230. package/gui/dist/assets/index-EdoPnm9_.css +0 -1
  231. package/src/adapters/devin-cli/acp.ts +0 -204
  232. package/src/adapters/devin-cli/adapter.ts +0 -345
  233. package/src/adapters/devin-cli/binary.ts +0 -69
  234. package/src/adapters/devin-cli/models.ts +0 -57
  235. package/src/oauth/devin-cli.ts +0 -149
  236. package/src/server/responses-reasoning-summary-rewrite.ts +0 -178
@@ -0,0 +1,531 @@
1
+ import { chmodSync, linkSync, mkdirSync, mkdtempSync, realpathSync, symlinkSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { dirname, isAbsolute, join } from "node:path";
4
+ import { resolveCodexRuntime } from "../codex/runtime";
5
+ import { remoteWorkspaceThreadStartParams } from "./workspace-coordinator";
6
+ import { startRemoteWorkspaceToolBridge } from "./workspace-tool-bridge";
7
+ import { truncateRemoteWorkspaceUtf8 } from "./workspace-utf8";
8
+ import { REMOTE_WORKSPACE_TOOL_NAMESPACE } from "./workspace-tools";
9
+ import { findExecutableOnPath } from "./workspace-executable";
10
+ import {
11
+ remoteWorkspaceProcessInvocation,
12
+ removeRemoteWorkspaceIsolation,
13
+ runRemoteWorkspaceCleanupSteps,
14
+ stopRemoteWorkspaceProcess,
15
+ waitForRemoteWorkspaceProcessExit,
16
+ } from "./workspace-process";
17
+ import {
18
+ codexRemotePermissionProfileCompatibility,
19
+ resolveCodexLinuxSandboxBinary,
20
+ } from "./workspace-codex-sandbox";
21
+ import type {
22
+ RemoteWorkspaceRuntimeFactory,
23
+ RemoteWorkspaceRuntimeHandle,
24
+ RemoteWorkspaceSessionEvent,
25
+ } from "./workspace-sessions";
26
+
27
+ const MAX_JSON_LINE_BYTES = 2 * 1024 * 1024;
28
+ const MAX_STDERR_BYTES = 64 * 1024;
29
+ const MAX_BUFFERED_ASSISTANT_ITEMS = 32;
30
+ const MAX_BUFFERED_ASSISTANT_BYTES = 64 * 1024;
31
+ const MAX_EARLY_TURN_COMPLETIONS = 16;
32
+ const START_TIMEOUT_MS = 15_000;
33
+ const REQUEST_TIMEOUT_MS = 60_000;
34
+
35
+ interface JsonRpcMessage {
36
+ id?: string | number;
37
+ method?: string;
38
+ params?: Record<string, unknown>;
39
+ result?: Record<string, unknown>;
40
+ error?: { message?: unknown };
41
+ }
42
+
43
+ interface PendingRpc {
44
+ resolve(message: JsonRpcMessage): void;
45
+ reject(error: Error): void;
46
+ timer: ReturnType<typeof setTimeout>;
47
+ }
48
+
49
+ function parseJsonRpcMessage(value: unknown): JsonRpcMessage {
50
+ const raw = object(value);
51
+ if (!raw) throw new Error("invalid Codex App Server message");
52
+ if (raw.id !== undefined && typeof raw.id !== "string" && typeof raw.id !== "number") {
53
+ throw new Error("invalid Codex App Server message ID");
54
+ }
55
+ if (raw.method !== undefined && typeof raw.method !== "string") {
56
+ throw new Error("invalid Codex App Server method");
57
+ }
58
+ const params = raw.params === undefined ? undefined : object(raw.params);
59
+ const result = raw.result === undefined ? undefined : object(raw.result);
60
+ const error = raw.error === undefined ? undefined : object(raw.error);
61
+ if ((raw.params !== undefined && !params)
62
+ || (raw.result !== undefined && !result)
63
+ || (raw.error !== undefined && !error)) {
64
+ throw new Error("invalid Codex App Server message fields");
65
+ }
66
+ return {
67
+ ...(raw.id !== undefined ? { id: raw.id } : {}),
68
+ ...(typeof raw.method === "string" ? { method: raw.method } : {}),
69
+ ...(params ? { params } : {}),
70
+ ...(result ? { result } : {}),
71
+ ...(error ? { error: { message: error.message } } : {}),
72
+ };
73
+ }
74
+
75
+ function errorMessage(value: unknown, fallback: string): string {
76
+ const raw = value instanceof Error ? value.message : typeof value === "string" ? value : fallback;
77
+ return raw.replace(/[^\x20-\x7e\n\t]/g, " ").slice(0, 4_096) || fallback;
78
+ }
79
+
80
+ function object(value: unknown): Record<string, unknown> | null {
81
+ return value && typeof value === "object" && !Array.isArray(value)
82
+ ? value as Record<string, unknown>
83
+ : null;
84
+ }
85
+
86
+ function nestedString(value: unknown, keys: readonly string[]): string | null {
87
+ let current: unknown = value;
88
+ for (const key of keys) current = object(current)?.[key];
89
+ return typeof current === "string" && current.length > 0 ? current : null;
90
+ }
91
+
92
+ function itemText(value: unknown): string | null {
93
+ const item = object(value);
94
+ if (!item) return null;
95
+ if (typeof item.text === "string" && item.text.length > 0) return item.text;
96
+ if (!Array.isArray(item.content)) return null;
97
+ const parts: string[] = [];
98
+ for (const raw of item.content) {
99
+ const part = object(raw);
100
+ const text = part && typeof part.text === "string" ? part.text : null;
101
+ if (text) parts.push(text);
102
+ }
103
+ return parts.length > 0 ? parts.join("") : null;
104
+ }
105
+
106
+ function appendBoundedUtf8(current: string, delta: string, maximum: number): string {
107
+ const marker = "\n[truncated]";
108
+ if (current.endsWith(marker)) return current;
109
+ const combined = `${current}${delta}`;
110
+ if (Buffer.byteLength(combined, "utf8") <= maximum) return combined;
111
+ const bodyLimit = maximum - Buffer.byteLength(marker, "utf8");
112
+ return `${truncateRemoteWorkspaceUtf8(combined, bodyLimit)}${marker}`;
113
+ }
114
+
115
+ function setBounded<K, V>(map: Map<K, V>, key: K, value: V, maximum: number): void {
116
+ if (!map.has(key) && map.size >= maximum) {
117
+ const oldest = map.keys().next();
118
+ if (!oldest.done) map.delete(oldest.value);
119
+ }
120
+ map.set(key, value);
121
+ }
122
+
123
+ class JsonLineRpcProcess {
124
+ private readonly pending = new Map<string | number, PendingRpc>();
125
+ private nextId = 0;
126
+ private closed = false;
127
+ private closeError: Error | null = null;
128
+
129
+ onRequest: ((message: JsonRpcMessage) => Promise<JsonRpcMessage>) | null = null;
130
+ onNotification: ((message: JsonRpcMessage) => void) | null = null;
131
+ onClose: ((error: Error) => void) | null = null;
132
+
133
+ constructor(private readonly child: Bun.Subprocess<"pipe", "pipe", "pipe">) {
134
+ void this.readStdout();
135
+ void this.drainStderr();
136
+ void child.exited.then(code => this.fail(new Error(`Codex App Server exited with code ${code}`)));
137
+ }
138
+
139
+ request(method: string, params: Record<string, unknown>, timeoutMs = REQUEST_TIMEOUT_MS): Promise<JsonRpcMessage> {
140
+ if (this.closed) return Promise.reject(this.closeError ?? new Error("Codex App Server is closed"));
141
+ const id = ++this.nextId;
142
+ const result = new Promise<JsonRpcMessage>((resolve, reject) => {
143
+ const timer = setTimeout(() => {
144
+ this.pending.delete(id);
145
+ reject(new Error(`Codex App Server ${method} timed out`));
146
+ }, timeoutMs);
147
+ this.pending.set(id, { resolve, reject, timer });
148
+ });
149
+ try {
150
+ this.send({ jsonrpc: "2.0", id, method, params });
151
+ } catch (error) {
152
+ const pending = this.pending.get(id);
153
+ if (pending) {
154
+ clearTimeout(pending.timer);
155
+ this.pending.delete(id);
156
+ pending.reject(error instanceof Error ? error : new Error("Codex App Server write failed"));
157
+ }
158
+ }
159
+ return result;
160
+ }
161
+
162
+ notify(method: string, params: Record<string, unknown>): void {
163
+ this.send({ jsonrpc: "2.0", method, params });
164
+ }
165
+
166
+ async close(): Promise<void> {
167
+ try {
168
+ if (!this.closed) {
169
+ try { this.child.stdin.end(); } catch { /* child already closed */ }
170
+ }
171
+ const graceful = await waitForRemoteWorkspaceProcessExit(this.child, 1_500);
172
+ if (!graceful) {
173
+ await stopRemoteWorkspaceProcess(this.child);
174
+ }
175
+ } finally {
176
+ // Pending callers must settle even if the OS refuses to reap the child.
177
+ this.fail(new Error("Codex App Server session closed"));
178
+ }
179
+ }
180
+
181
+ private send(message: Record<string, unknown>): void {
182
+ if (this.closed) throw this.closeError ?? new Error("Codex App Server is closed");
183
+ const line = `${JSON.stringify(message)}\n`;
184
+ if (Buffer.byteLength(line, "utf8") > MAX_JSON_LINE_BYTES) throw new Error("Codex App Server message is too large");
185
+ this.child.stdin.write(line);
186
+ this.child.stdin.flush();
187
+ }
188
+
189
+ private async readStdout(): Promise<void> {
190
+ const reader = this.child.stdout.getReader();
191
+ const decoder = new TextDecoder("utf-8", { fatal: true });
192
+ let buffer = "";
193
+ try {
194
+ while (true) {
195
+ const next = await reader.read();
196
+ if (next.done) break;
197
+ buffer += decoder.decode(next.value, { stream: true });
198
+ if (Buffer.byteLength(buffer, "utf8") > MAX_JSON_LINE_BYTES && !buffer.includes("\n")) {
199
+ throw new Error("Codex App Server output line is too large");
200
+ }
201
+ let newline = buffer.indexOf("\n");
202
+ while (newline >= 0) {
203
+ const line = buffer.slice(0, newline).replace(/\r$/, "");
204
+ buffer = buffer.slice(newline + 1);
205
+ if (Buffer.byteLength(line, "utf8") > MAX_JSON_LINE_BYTES) throw new Error("Codex App Server output line is too large");
206
+ if (line) this.receive(parseJsonRpcMessage(JSON.parse(line)));
207
+ newline = buffer.indexOf("\n");
208
+ }
209
+ }
210
+ } catch (error) {
211
+ void stopRemoteWorkspaceProcess(this.child).catch(() => {});
212
+ this.fail(new Error(errorMessage(error, "Codex App Server output failed")));
213
+ } finally {
214
+ reader.releaseLock();
215
+ }
216
+ }
217
+
218
+ private async drainStderr(): Promise<void> {
219
+ const reader = this.child.stderr.getReader();
220
+ let retained = 0;
221
+ try {
222
+ while (true) {
223
+ const next = await reader.read();
224
+ if (next.done) break;
225
+ retained = Math.min(MAX_STDERR_BYTES, retained + next.value.byteLength);
226
+ }
227
+ } catch {
228
+ // stdout and the exit code own the user-visible process failure.
229
+ } finally {
230
+ reader.releaseLock();
231
+ void retained;
232
+ }
233
+ }
234
+
235
+ private receive(message: JsonRpcMessage): void {
236
+ if (!message || typeof message !== "object") throw new Error("invalid Codex App Server message");
237
+ if (message.id !== undefined && typeof message.method !== "string") {
238
+ const pending = this.pending.get(message.id);
239
+ if (!pending) return;
240
+ clearTimeout(pending.timer);
241
+ this.pending.delete(message.id);
242
+ if (message.error) pending.reject(new Error(errorMessage(message.error.message, "Codex App Server request failed")));
243
+ else pending.resolve(message);
244
+ return;
245
+ }
246
+ if (typeof message.method !== "string") return;
247
+ if (message.id === undefined) {
248
+ this.onNotification?.(message);
249
+ return;
250
+ }
251
+ const id = message.id;
252
+ const request = this.onRequest;
253
+ if (!request) {
254
+ this.send({ jsonrpc: "2.0", id, error: { code: -32_601, message: "client request handler is unavailable" } });
255
+ return;
256
+ }
257
+ void request(message).then(
258
+ response => this.send({ jsonrpc: "2.0", ...response }),
259
+ error => this.send({
260
+ jsonrpc: "2.0",
261
+ id,
262
+ error: { code: -32_000, message: errorMessage(error, "Remote Workspace tool failed") },
263
+ }),
264
+ );
265
+ }
266
+
267
+ private fail(error: Error): void {
268
+ if (this.closed) return;
269
+ this.closed = true;
270
+ this.closeError = error;
271
+ for (const pending of this.pending.values()) {
272
+ clearTimeout(pending.timer);
273
+ pending.reject(error);
274
+ }
275
+ this.pending.clear();
276
+ this.onClose?.(error);
277
+ }
278
+ }
279
+
280
+ interface ActiveTurn {
281
+ id: string;
282
+ resolve(): void;
283
+ reject(error: Error): void;
284
+ }
285
+
286
+ export interface CodexRemoteWorkspaceRuntimeOptions {
287
+ /** Test seam. Production resolves the configured, trusted Codex runtime. */
288
+ command?: readonly string[];
289
+ env?: Record<string, string | undefined>;
290
+ version?: string;
291
+ }
292
+
293
+ export class CodexRemoteWorkspaceRuntimeFactory implements RemoteWorkspaceRuntimeFactory {
294
+ readonly profile = "codex" as const;
295
+
296
+ constructor(private readonly options: CodexRemoteWorkspaceRuntimeOptions = {}) {}
297
+
298
+ async available(): Promise<{ available: boolean; version?: string; reason?: string }> {
299
+ if (this.options.command && this.options.command.length > 0) {
300
+ return { available: true, version: this.options.version ?? "test" };
301
+ }
302
+ const resolved = resolveCodexRuntime();
303
+ const compatibility = codexRemotePermissionProfileCompatibility();
304
+ if (!compatibility.compatible) return { available: false, reason: compatibility.reason };
305
+ return resolved.runtime.version
306
+ ? { available: true, version: resolved.runtime.version }
307
+ : { available: false, reason: "Codex CLI is not installed or runnable on this Hub." };
308
+ }
309
+
310
+ async start(options: Parameters<RemoteWorkspaceRuntimeFactory["start"]>[0]): Promise<RemoteWorkspaceRuntimeHandle> {
311
+ const command = this.options.command
312
+ ? [...this.options.command]
313
+ : [resolveCodexRuntime().runtime.command];
314
+ if (command.length < 1) throw new Error("Codex CLI is unavailable on this Hub");
315
+ const executablePath = isAbsolute(command[0]!) ? command[0]! : findExecutableOnPath(command[0]!);
316
+ if (!executablePath) throw new Error("Codex CLI executable could not be resolved on this Hub");
317
+ command[0] = executablePath;
318
+ const runtimeDirectory = dirname(realpathSync(executablePath));
319
+ const isolation = mkdtempSync(join(tmpdir(), "ocx-remote-codex-"));
320
+ let processPath = process.env.PATH ?? "/usr/bin:/bin";
321
+ const runtimeReadPaths = [runtimeDirectory];
322
+ try {
323
+ chmodSync(isolation, 0o700);
324
+ if (process.platform === "linux") {
325
+ const native = resolveCodexLinuxSandboxBinary(executablePath);
326
+ if (!native) {
327
+ throw new Error("Codex Remote Workspace could not locate the native Linux permission-profile helper");
328
+ }
329
+ const helperDir = join(isolation, "sandbox-bin");
330
+ mkdirSync(helperDir, { mode: 0o700 });
331
+ const helper = join(helperDir, "codex-linux-sandbox");
332
+ try { linkSync(native, helper); }
333
+ catch { symlinkSync(native, helper); }
334
+ processPath = `${helperDir}:${processPath}`;
335
+ runtimeReadPaths.push(dirname(native), helperDir);
336
+ }
337
+ } catch (error) {
338
+ removeRemoteWorkspaceIsolation(isolation);
339
+ throw error;
340
+ }
341
+ const thread = { id: "" };
342
+ const bridge = (() => {
343
+ try {
344
+ return startRemoteWorkspaceToolBridge({
345
+ coordinator: options.coordinator,
346
+ threadId: () => thread.id,
347
+ tools: options.tools,
348
+ onTool: tool => options.emit("tool", `Running ${tool} on ${options.deviceName}/${options.rootLabel}`),
349
+ });
350
+ } catch (error) {
351
+ removeRemoteWorkspaceIsolation(isolation);
352
+ throw error;
353
+ }
354
+ })();
355
+ const tokenEnvVar = "OCX_REMOTE_WORKSPACE_MCP_TOKEN";
356
+ const mcpPrefix = `mcp_servers.${REMOTE_WORKSPACE_TOOL_NAMESPACE}`;
357
+ const childEnv = { ...process.env, ...this.options.env, PATH: processPath, [tokenEnvVar]: bridge.token };
358
+ const invocation = remoteWorkspaceProcessInvocation([
359
+ ...command,
360
+ "-c", `${mcpPrefix}.url=${JSON.stringify(`${bridge.url}/mcp`)}`,
361
+ "-c", `${mcpPrefix}.bearer_token_env_var=${JSON.stringify(tokenEnvVar)}`,
362
+ "-c", `${mcpPrefix}.required=true`,
363
+ "-c", `${mcpPrefix}.enabled_tools=${JSON.stringify(options.tools)}`,
364
+ "-c", `${mcpPrefix}.default_tools_approval_mode="approve"`,
365
+ "app-server", "--listen", "stdio://",
366
+ ], { env: childEnv });
367
+ let child: Bun.Subprocess<"pipe", "pipe", "pipe">;
368
+ try {
369
+ child = Bun.spawn([invocation.file, ...invocation.args], {
370
+ cwd: isolation,
371
+ env: childEnv,
372
+ stdin: "pipe",
373
+ stdout: "pipe",
374
+ stderr: "pipe",
375
+ ...invocation.options,
376
+ });
377
+ } catch (error) {
378
+ await bridge.stop();
379
+ removeRemoteWorkspaceIsolation(isolation);
380
+ throw error;
381
+ }
382
+ const peer = new JsonLineRpcProcess(child);
383
+ let activeTurn: ActiveTurn | null = null;
384
+ let stopped = false;
385
+ const completedBeforeWait = new Map<string, { status: string; error: string | null }>();
386
+ const assistantDeltas = new Map<string, string>();
387
+ let stopOperation: Promise<void> | null = null;
388
+
389
+ const finishTurn = (turnId: string, status: string, detail: string | null): void => {
390
+ if (!activeTurn || activeTurn.id !== turnId) {
391
+ setBounded(completedBeforeWait, turnId, { status, error: detail }, MAX_EARLY_TURN_COMPLETIONS);
392
+ return;
393
+ }
394
+ const current = activeTurn;
395
+ activeTurn = null;
396
+ assistantDeltas.clear();
397
+ if (status === "completed") current.resolve();
398
+ else current.reject(new Error(detail ?? `Codex turn ${status}`));
399
+ };
400
+
401
+ peer.onRequest = async message => {
402
+ if (message.method !== "item/tool/call" || message.id === undefined) {
403
+ throw new Error("unsupported Codex App Server client request");
404
+ }
405
+ const tool = nestedString(message.params, ["tool"]) ?? "remote tool";
406
+ options.emit("tool", `Running ${tool} on ${options.deviceName}/${options.rootLabel}`);
407
+ return options.coordinator.handle({
408
+ method: "item/tool/call",
409
+ id: message.id,
410
+ params: message.params,
411
+ });
412
+ };
413
+ peer.onNotification = message => {
414
+ const params = message.params ?? {};
415
+ if (message.method === "item/agentMessage/delta") {
416
+ const itemId = nestedString(params, ["itemId"]) ?? nestedString(params, ["item", "id"]);
417
+ const delta = nestedString(params, ["delta"]);
418
+ if (itemId && delta) {
419
+ setBounded(
420
+ assistantDeltas,
421
+ itemId,
422
+ appendBoundedUtf8(assistantDeltas.get(itemId) ?? "", delta, MAX_BUFFERED_ASSISTANT_BYTES),
423
+ MAX_BUFFERED_ASSISTANT_ITEMS,
424
+ );
425
+ }
426
+ return;
427
+ }
428
+ if (message.method === "item/completed") {
429
+ const item = object(params.item);
430
+ const itemId = item && typeof item.id === "string" ? item.id : null;
431
+ const text = itemText(item) ?? (itemId ? assistantDeltas.get(itemId) ?? null : null);
432
+ if (itemId) assistantDeltas.delete(itemId);
433
+ if (text) options.emit("assistant", text);
434
+ return;
435
+ }
436
+ if (message.method === "turn/completed") {
437
+ const turn = object(params.turn);
438
+ const turnId = turn && typeof turn.id === "string" ? turn.id : null;
439
+ if (!turnId) return;
440
+ const status = typeof turn?.status === "string" ? turn.status : "failed";
441
+ const detail = nestedString(turn, ["error", "message"]);
442
+ finishTurn(turnId, status, detail);
443
+ }
444
+ };
445
+ peer.onClose = error => {
446
+ const current = activeTurn;
447
+ activeTurn = null;
448
+ completedBeforeWait.clear();
449
+ assistantDeltas.clear();
450
+ current?.reject(error);
451
+ };
452
+
453
+ try {
454
+ await peer.request("initialize", {
455
+ clientInfo: { name: "opencodex_remote_workspace", title: "OpenCodex Remote Workspace", version: "1" },
456
+ capabilities: { experimentalApi: true },
457
+ }, START_TIMEOUT_MS);
458
+ peer.notify("initialized", {});
459
+ const effective = await peer.request("config/read", { cwd: isolation, includeLayers: false }, START_TIMEOUT_MS);
460
+ const effectiveConfig = object(effective.result?.config) ?? {};
461
+ if (typeof effectiveConfig.sandbox_mode === "string" || effectiveConfig.sandbox_workspace_write) {
462
+ throw new Error("Codex Remote Workspace requires permission profiles; remove legacy sandbox_mode settings from the selected Codex profile first");
463
+ }
464
+ const disabledServerNames = Object.keys(object(effectiveConfig.mcp_servers) ?? {});
465
+ const disabledHookNames = Object.keys(object(effectiveConfig.hooks) ?? {});
466
+ const threadParams = remoteWorkspaceThreadStartParams({
467
+ executorName: options.deviceName,
468
+ coordinatorIsolationPath: isolation,
469
+ tools: options.tools,
470
+ mcp: {
471
+ url: `${bridge.url}/mcp`,
472
+ bearerTokenEnvVar: tokenEnvVar,
473
+ disabledServerNames,
474
+ disabledHookNames,
475
+ hubRuntimeReadPaths: runtimeReadPaths,
476
+ },
477
+ });
478
+ const { ephemeral: _startOnlyEphemeral, ...resumeParams } = threadParams;
479
+ const started = options.resumeThreadId
480
+ ? await peer.request("thread/resume", { ...resumeParams, threadId: options.resumeThreadId }, START_TIMEOUT_MS)
481
+ : await peer.request("thread/start", threadParams, START_TIMEOUT_MS);
482
+ const threadId = nestedString(started.result, ["thread", "id"]);
483
+ if (!threadId) throw new Error("Codex App Server returned no thread ID");
484
+ if (options.resumeThreadId && threadId !== options.resumeThreadId) {
485
+ throw new Error("Codex App Server resumed a different Remote Workspace thread");
486
+ }
487
+ thread.id = threadId;
488
+
489
+ return {
490
+ threadId,
491
+ async prompt(text: string): Promise<void> {
492
+ if (stopped) throw new Error("Codex Remote Workspace session is stopped");
493
+ if (activeTurn) throw new Error("Codex Remote Workspace turn is already active");
494
+ const startedTurn = await peer.request("turn/start", {
495
+ threadId,
496
+ input: [{ type: "text", text }],
497
+ approvalPolicy: "never",
498
+ });
499
+ const turnId = nestedString(startedTurn.result, ["turn", "id"]);
500
+ if (!turnId) throw new Error("Codex App Server returned no turn ID");
501
+ const early = completedBeforeWait.get(turnId);
502
+ if (early) {
503
+ completedBeforeWait.delete(turnId);
504
+ if (early.status === "completed") return;
505
+ throw new Error(early.error ?? `Codex turn ${early.status}`);
506
+ }
507
+ await new Promise<void>((resolve, reject) => { activeTurn = { id: turnId, resolve, reject }; });
508
+ },
509
+ stop(): Promise<void> {
510
+ if (stopOperation) return stopOperation;
511
+ stopped = true;
512
+ const turn = activeTurn;
513
+ stopOperation = runRemoteWorkspaceCleanupSteps([
514
+ async () => {
515
+ if (turn) await peer.request("turn/interrupt", { threadId, turnId: turn.id }, 3_000).catch(() => {});
516
+ },
517
+ () => peer.close(),
518
+ () => bridge.stop(),
519
+ () => removeRemoteWorkspaceIsolation(isolation),
520
+ ]);
521
+ return stopOperation;
522
+ },
523
+ };
524
+ } catch (error) {
525
+ await peer.close().catch(() => {});
526
+ await bridge.stop();
527
+ removeRemoteWorkspaceIsolation(isolation);
528
+ throw error;
529
+ }
530
+ }
531
+ }
@@ -0,0 +1,115 @@
1
+ import { accessSync, constants, existsSync, openSync, closeSync, readFileSync, readSync, realpathSync, statSync } from "node:fs";
2
+ import { arch } from "node:os";
3
+ import { dirname, isAbsolute, join } from "node:path";
4
+ import { inspectCodexShimBackingForCommand } from "../codex/shim";
5
+ import { findExecutableOnPath } from "./workspace-executable";
6
+ import { resolveCodexHomeDir } from "../codex/home";
7
+
8
+ function isNativeExecutable(path: string): boolean {
9
+ let descriptor: number | null = null;
10
+ try {
11
+ descriptor = openSync(path, "r");
12
+ const header = Buffer.alloc(4);
13
+ if (readSync(descriptor, header, 0, header.length, 0) !== header.length) return false;
14
+ return header.equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46]));
15
+ } catch {
16
+ return false;
17
+ } finally {
18
+ if (descriptor !== null) closeSync(descriptor);
19
+ }
20
+ }
21
+
22
+ function packageRootForEntrypoint(path: string): string | null {
23
+ let current = dirname(path);
24
+ for (let depth = 0; depth < 10; depth += 1) {
25
+ const manifest = join(current, "package.json");
26
+ if (existsSync(manifest)) {
27
+ try {
28
+ const parsed = JSON.parse(readFileSync(manifest, "utf8")) as { name?: unknown };
29
+ if (parsed.name === "@openai/codex") return current;
30
+ } catch { /* keep walking */ }
31
+ }
32
+ const parent = dirname(current);
33
+ if (parent === current) break;
34
+ current = parent;
35
+ }
36
+ return null;
37
+ }
38
+
39
+ function checkedNative(path: string): string | null {
40
+ try {
41
+ const canonical = realpathSync(path);
42
+ if (!statSync(canonical).isFile() || !isNativeExecutable(canonical)) return null;
43
+ accessSync(canonical, constants.X_OK);
44
+ return canonical;
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ function generatedShimBacking(path: string): string | null {
51
+ try {
52
+ const source = readFileSync(path, "utf8");
53
+ if (Buffer.byteLength(source, "utf8") > 128 * 1024
54
+ || !source.includes("# opencodex codex autostart shim")) return null;
55
+ const match = /^exec '([^'\r\n]+)' "\$@"\s*$/m.exec(source);
56
+ return match?.[1] && isAbsolute(match[1]) ? match[1] : null;
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Permission profiles invoke the same native Codex binary under argv[0]
64
+ * `codex-linux-sandbox`. npm and OpenCodex shims expose a JS/shell launcher instead,
65
+ * so resolve the package-owned native binary without executing or modifying the install.
66
+ */
67
+ export function resolveCodexLinuxSandboxBinary(command: string): string | null {
68
+ if (process.platform !== "linux") return null;
69
+ const selected = isAbsolute(command) ? command : findExecutableOnPath(command);
70
+ if (!selected) return null;
71
+ const shim = inspectCodexShimBackingForCommand(selected);
72
+ const entrypoint = shim.status === "matched"
73
+ ? shim.backingPath
74
+ : generatedShimBacking(selected) ?? selected;
75
+ const direct = checkedNative(entrypoint);
76
+ if (direct) return direct;
77
+ let canonical: string;
78
+ try { canonical = realpathSync(entrypoint); } catch { return null; }
79
+ const root = packageRootForEntrypoint(canonical);
80
+ if (!root) return null;
81
+ const target = arch() === "arm64" ? "aarch64-unknown-linux-musl" : "x86_64-unknown-linux-musl";
82
+ const packageName = arch() === "arm64" ? "codex-linux-arm64" : "codex-linux-x64";
83
+ const candidates = [
84
+ join(root, "node_modules", "@openai", packageName, "vendor", target, "bin", "codex"),
85
+ join(root, "vendor", target, "bin", "codex"),
86
+ ];
87
+ for (const candidate of candidates) {
88
+ const native = checkedNative(candidate);
89
+ if (native) return native;
90
+ }
91
+ return null;
92
+ }
93
+
94
+ export function codexRemotePermissionProfileCompatibility(
95
+ codexHome = resolveCodexHomeDir(),
96
+ ): { compatible: boolean; reason?: string } {
97
+ const configPath = join(codexHome, "config.toml");
98
+ if (!existsSync(configPath)) return { compatible: true };
99
+ try {
100
+ const metadata = statSync(configPath);
101
+ if (!metadata.isFile() || metadata.size > 4 * 1024 * 1024) {
102
+ return { compatible: false, reason: "Codex config cannot be safely inspected for Remote Workspace permissions." };
103
+ }
104
+ const config = Bun.TOML.parse(readFileSync(configPath, "utf8")) as Record<string, unknown>;
105
+ if (typeof config.sandbox_mode === "string" || config.sandbox_workspace_write !== undefined) {
106
+ return {
107
+ compatible: false,
108
+ reason: "Codex Remote Workspace needs permission profiles, but this Codex config still selects legacy sandbox_mode.",
109
+ };
110
+ }
111
+ return { compatible: true };
112
+ } catch {
113
+ return { compatible: false, reason: "Codex config could not be parsed for Remote Workspace permissions." };
114
+ }
115
+ }