@bitkyc08/opencodex 2.52.0-preview.20260911 → 2.52.0

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 (166) hide show
  1. package/gui/dist/assets/index-CWXut3rG.js +115 -0
  2. package/gui/dist/assets/index-EdoPnm9_.css +1 -0
  3. package/gui/dist/index.html +2 -2
  4. package/gui/dist/provider-icons/devin.svg +49 -0
  5. package/gui/dist/provider-icons/omo.svg +42 -0
  6. package/package.json +3 -1
  7. package/src/AGENTS.md +1 -1
  8. package/src/adapters/cline-pass-deepseek-v4-tool-replay.ts +0 -1
  9. package/src/adapters/command-code.ts +0 -1
  10. package/src/adapters/cursor/checkpoint-store.ts +37 -0
  11. package/src/adapters/cursor/live-transport.ts +74 -2
  12. package/src/adapters/cursor.ts +6 -0
  13. package/src/adapters/devin/cloud-direct/auth.ts +264 -0
  14. package/src/adapters/devin/cloud-direct/catalog.ts +306 -0
  15. package/src/adapters/devin/cloud-direct/chat.ts +1274 -0
  16. package/src/adapters/devin/cloud-direct/index.ts +65 -0
  17. package/src/adapters/devin/cloud-direct/metadata.ts +134 -0
  18. package/src/adapters/devin/cloud-direct/wire.ts +206 -0
  19. package/src/adapters/devin/live-models.ts +133 -0
  20. package/src/adapters/devin-cli/acp.ts +204 -0
  21. package/src/adapters/devin-cli/adapter.ts +345 -0
  22. package/src/adapters/devin-cli/binary.ts +69 -0
  23. package/src/adapters/devin-cli/models.ts +57 -0
  24. package/src/adapters/devin.ts +326 -0
  25. package/src/adapters/google.ts +1 -1
  26. package/src/adapters/openai-chat.ts +31 -10
  27. package/src/adapters/openai-responses.ts +16 -1
  28. package/src/adapters/registry.ts +25 -1
  29. package/src/bridge.ts +8 -2
  30. package/src/claude/inbound-cache-stabilize.ts +130 -0
  31. package/src/claude/inbound.ts +45 -5
  32. package/src/cli/account-auth.ts +60 -1
  33. package/src/cli/account-extended.ts +23 -30
  34. package/src/cli/account.ts +2 -1
  35. package/src/cli/capabilities.ts +50 -8
  36. package/src/cli/config-command.ts +2 -2
  37. package/src/cli/dispatch.ts +14 -2
  38. package/src/cli/export-command.ts +11 -25
  39. package/src/cli/help.ts +2 -2
  40. package/src/cli/opencode.ts +5 -0
  41. package/src/cli/registry.ts +15 -4
  42. package/src/clients/config-export/cline.ts +71 -0
  43. package/src/clients/config-export/contracts.ts +10 -1
  44. package/src/clients/config-export/model-metadata.ts +33 -0
  45. package/src/clients/config-export/zcode.ts +23 -12
  46. package/src/clients/config-export.ts +156 -4
  47. package/src/codex/account-store.ts +7 -2
  48. package/src/codex/auth-context.ts +1 -1
  49. package/src/codex/catalog/effort.ts +4 -3
  50. package/src/codex/catalog/metadata.ts +8 -4
  51. package/src/codex/catalog/native-models.ts +24 -4
  52. package/src/codex/catalog/parsing.ts +19 -1
  53. package/src/codex/catalog/provider-fetch.ts +57 -0
  54. package/src/codex/catalog/sync.ts +1 -1
  55. package/src/codex/context-compat.ts +97 -0
  56. package/src/codex/context-owner.ts +201 -0
  57. package/src/codex/history-provider.ts +161 -14
  58. package/src/codex/inject-coordination.ts +3 -2
  59. package/src/codex/inject.ts +128 -9
  60. package/src/codex/pool-rotation.ts +8 -292
  61. package/src/codex/quota.ts +41 -12
  62. package/src/codex/retired-model-migration.ts +41 -0
  63. package/src/codex/routing.ts +327 -22
  64. package/src/codex/warmup.ts +2 -2
  65. package/src/combos/failover.ts +5 -0
  66. package/src/combos/resolve.ts +11 -15
  67. package/src/config.ts +46 -14
  68. package/src/generated/compatibility-version.json +293 -113
  69. package/src/grok/grpc-web.ts +120 -0
  70. package/src/grok/reset-coupon-ledger.ts +139 -0
  71. package/src/grok/reset-coupons.ts +278 -0
  72. package/src/integrations/catalog-refresh.ts +1 -1
  73. package/src/integrations/cline-document.ts +73 -0
  74. package/src/integrations/cline-io.ts +149 -0
  75. package/src/integrations/cline-transaction.ts +42 -0
  76. package/src/integrations/config-io.ts +21 -2
  77. package/src/integrations/journal.ts +43 -1
  78. package/src/integrations/omp-yaml-source.ts +123 -4
  79. package/src/integrations/ownership-policy.ts +7 -0
  80. package/src/integrations/ownership.ts +36 -0
  81. package/src/integrations/registry.ts +27 -0
  82. package/src/integrations/state.ts +5 -2
  83. package/src/integrations/store.ts +6 -0
  84. package/src/integrations/writer.ts +53 -9
  85. package/src/lab/subject/behavior-fingerprint.ts +1 -1
  86. package/src/lib/abort.ts +36 -0
  87. package/src/lib/local-destinations.ts +1 -1
  88. package/src/lib/upstream-retry.ts +36 -1
  89. package/src/oauth/account-quota-rank.ts +11 -0
  90. package/src/oauth/callback-server.ts +10 -5
  91. package/src/oauth/devin/api-base.ts +63 -0
  92. package/src/oauth/devin/login.ts +1 -0
  93. package/src/oauth/devin/register-user.ts +186 -0
  94. package/src/oauth/devin/types.ts +71 -0
  95. package/src/oauth/devin-cli.ts +149 -0
  96. package/src/oauth/devin.ts +170 -0
  97. package/src/oauth/generic-account-failover.ts +169 -1
  98. package/src/oauth/index.ts +20 -1
  99. package/src/oauth/login-cli.ts +19 -5
  100. package/src/oauth/pool-kernel.ts +321 -0
  101. package/src/oauth/pool-settings-capability.ts +127 -9
  102. package/src/oauth/store.ts +7 -3
  103. package/src/oauth/token-guardian.ts +1 -1
  104. package/src/providers/codebuddy-models.ts +0 -3
  105. package/src/providers/command-code-efforts.ts +23 -4
  106. package/src/providers/default-aliases.ts +4 -0
  107. package/src/providers/derive.ts +8 -0
  108. package/src/providers/devin-cli-authmode-migration.ts +57 -0
  109. package/src/providers/key-failover.ts +175 -0
  110. package/src/providers/model-rename-startup.ts +21 -1
  111. package/src/providers/qoder-models.ts +0 -1
  112. package/src/providers/quota-key-accounts.ts +50 -0
  113. package/src/providers/quota-routing-cache.ts +65 -6
  114. package/src/providers/quota-types.ts +7 -0
  115. package/src/providers/quota.ts +113 -30
  116. package/src/providers/registry.ts +237 -77
  117. package/src/providers/stale-context-window-migration.ts +92 -0
  118. package/src/providers/zai-responses-migration.ts +45 -0
  119. package/src/quota/reset-observer.ts +2 -1
  120. package/src/quota/reset-seen-store.ts +9 -1
  121. package/src/remote-control/crypto.ts +442 -0
  122. package/src/remote-control/host.ts +175 -0
  123. package/src/remote-control/index.ts +100 -0
  124. package/src/remote-control/protocol.ts +200 -0
  125. package/src/remote-control/relay.ts +162 -0
  126. package/src/remote-control/workspace-agent-protocol.ts +246 -0
  127. package/src/remote-control/workspace-rpc-framing.ts +128 -0
  128. package/src/remote-control/workspace-tools.ts +237 -0
  129. package/src/remote-control/workspace-utf8.ts +24 -0
  130. package/src/responses/custom-tool-compat.ts +23 -0
  131. package/src/router.ts +6 -1
  132. package/src/routing/compatibility/behavior.ts +3 -0
  133. package/src/server/adapter-resolve.ts +6 -2
  134. package/src/server/auth-cors.ts +71 -6
  135. package/src/server/chat-completions.ts +5 -3
  136. package/src/server/chat-native.ts +9 -0
  137. package/src/server/claude-messages.ts +25 -30
  138. package/src/server/context-history.ts +207 -0
  139. package/src/server/images.ts +28 -1
  140. package/src/server/index.ts +42 -22
  141. package/src/server/live.ts +2 -1
  142. package/src/server/management/agent-settings-routes.ts +7 -1
  143. package/src/server/management/config-routes.ts +3 -3
  144. package/src/server/management/grok-coupon-routes.ts +287 -0
  145. package/src/server/management/integration-routes.ts +6 -1
  146. package/src/server/management/oauth-account-routes.ts +124 -7
  147. package/src/server/management/provider-routes.ts +77 -2
  148. package/src/server/management/route-registry.ts +6 -1
  149. package/src/server/management-api.ts +7 -0
  150. package/src/server/request-log.ts +4 -0
  151. package/src/server/responses/codex-ws-exchange.ts +90 -14
  152. package/src/server/responses/codex-ws-wire.ts +51 -1
  153. package/src/server/responses/compact.ts +8 -1
  154. package/src/server/responses/core.ts +188 -22
  155. package/src/server/responses/ws-upstream.ts +1 -1
  156. package/src/server/responses-undeclared-tool-guard.ts +261 -12
  157. package/src/server/zai-responses-startup.ts +21 -0
  158. package/src/types/config.ts +33 -3
  159. package/src/types/provider.ts +54 -4
  160. package/src/types/request.ts +1 -1
  161. package/src/types/tools.ts +36 -6
  162. package/src/update/job.ts +33 -11
  163. package/src/vision/plan.ts +40 -37
  164. package/src/web-search/index.ts +1 -0
  165. package/gui/dist/assets/index-BoBRSehJ.css +0 -1
  166. package/gui/dist/assets/index-Dx0xv2EA.js +0 -115
@@ -0,0 +1,246 @@
1
+ import {
2
+ parseRemoteControlClientHello,
3
+ parseRemoteControlHostHello,
4
+ serializeRemoteControlHello,
5
+ } from "./crypto";
6
+ import {
7
+ isRemoteControlCommandProfile,
8
+ isRemoteControlUuid,
9
+ type RemoteControlClientHello,
10
+ type RemoteControlCommandProfile,
11
+ type RemoteControlHostHello,
12
+ } from "./protocol";
13
+ import {
14
+ parseRemoteWorkspaceCapabilities,
15
+ type RemoteWorkspaceCapability,
16
+ } from "./workspace-tools";
17
+
18
+ export const REMOTE_WORKSPACE_AGENT_PROTOCOL_VERSION = 1 as const;
19
+ export const REMOTE_WORKSPACE_AGENT_MAX_CONTROL_BYTES = 96 * 1024;
20
+
21
+ export type RemoteWorkspaceAgentProfile = Extract<RemoteControlCommandProfile, "codex" | "claude" | "pi">;
22
+
23
+ export type RemoteWorkspaceHubMessage =
24
+ | {
25
+ version: typeof REMOTE_WORKSPACE_AGENT_PROTOCOL_VERSION;
26
+ type: "presence_ack";
27
+ capabilities: RemoteWorkspaceCapability[];
28
+ }
29
+ | {
30
+ version: typeof REMOTE_WORKSPACE_AGENT_PROTOCOL_VERSION;
31
+ type: "session_open";
32
+ rootId: string;
33
+ clientHello: RemoteControlClientHello;
34
+ }
35
+ | {
36
+ version: typeof REMOTE_WORKSPACE_AGENT_PROTOCOL_VERSION;
37
+ type: "ciphertext";
38
+ sessionId: string;
39
+ payload: Uint8Array;
40
+ }
41
+ | {
42
+ version: typeof REMOTE_WORKSPACE_AGENT_PROTOCOL_VERSION;
43
+ type: "session_close";
44
+ sessionId: string;
45
+ reason: string;
46
+ };
47
+
48
+ export type RemoteWorkspaceAgentMessage =
49
+ | {
50
+ version: typeof REMOTE_WORKSPACE_AGENT_PROTOCOL_VERSION;
51
+ type: "presence";
52
+ capabilities: RemoteWorkspaceCapability[];
53
+ }
54
+ | {
55
+ version: typeof REMOTE_WORKSPACE_AGENT_PROTOCOL_VERSION;
56
+ type: "session_accept";
57
+ sessionId: string;
58
+ hostHello: RemoteControlHostHello;
59
+ }
60
+ | {
61
+ version: typeof REMOTE_WORKSPACE_AGENT_PROTOCOL_VERSION;
62
+ type: "session_reject";
63
+ sessionId: string;
64
+ reason: string;
65
+ }
66
+ | {
67
+ version: typeof REMOTE_WORKSPACE_AGENT_PROTOCOL_VERSION;
68
+ type: "ciphertext";
69
+ sessionId: string;
70
+ payload: Uint8Array;
71
+ }
72
+ | {
73
+ version: typeof REMOTE_WORKSPACE_AGENT_PROTOCOL_VERSION;
74
+ type: "heartbeat";
75
+ nonce: string;
76
+ };
77
+
78
+ function identifier(value: unknown, label: string): string {
79
+ if (typeof value !== "string" || value.length < 1 || value.length > 256 || /[\x00-\x1f\x7f]/.test(value)) {
80
+ throw new Error(`invalid remote workspace ${label}`);
81
+ }
82
+ return value;
83
+ }
84
+
85
+ function reason(value: unknown): string {
86
+ if (typeof value !== "string" || value.length < 1 || value.length > 512 || /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(value)) {
87
+ throw new Error("invalid remote workspace close reason");
88
+ }
89
+ return value;
90
+ }
91
+
92
+ function exactKeys(value: Record<string, unknown>, keys: readonly string[]): void {
93
+ if (Object.keys(value).length !== keys.length || keys.some(key => !Object.hasOwn(value, key))) {
94
+ throw new Error("invalid remote workspace control message fields");
95
+ }
96
+ }
97
+
98
+ function encodedHello(value: RemoteControlClientHello | RemoteControlHostHello): string {
99
+ return Buffer.from(serializeRemoteControlHello(value)).toString("base64url");
100
+ }
101
+
102
+ function decodedHello(value: unknown, kind: "client"): RemoteControlClientHello;
103
+ function decodedHello(value: unknown, kind: "host"): RemoteControlHostHello;
104
+ function decodedHello(value: unknown, kind: "client" | "host"): RemoteControlClientHello | RemoteControlHostHello {
105
+ if (typeof value !== "string" || value.length < 1 || value.length > 24 * 1024 || !/^[A-Za-z0-9_-]+$/.test(value)) {
106
+ throw new Error("invalid remote workspace handshake encoding");
107
+ }
108
+ const bytes = Buffer.from(value, "base64url");
109
+ return kind === "client" ? parseRemoteControlClientHello(bytes) : parseRemoteControlHostHello(bytes);
110
+ }
111
+
112
+ function payload(value: unknown): Uint8Array {
113
+ if (typeof value !== "string" || value.length < 1 || value.length > 88 * 1024 || !/^[A-Za-z0-9_-]+$/.test(value)) {
114
+ throw new Error("invalid remote workspace ciphertext encoding");
115
+ }
116
+ const decoded = Buffer.from(value, "base64url");
117
+ if (decoded.byteLength < 24 || decoded.byteLength > 64 * 1024) {
118
+ throw new Error("invalid remote workspace ciphertext length");
119
+ }
120
+ return decoded;
121
+ }
122
+
123
+ function parseObject(raw: string | Uint8Array): Record<string, unknown> {
124
+ const bytes = typeof raw === "string" ? Buffer.from(raw, "utf8") : Buffer.from(raw);
125
+ if (bytes.byteLength < 1 || bytes.byteLength > REMOTE_WORKSPACE_AGENT_MAX_CONTROL_BYTES) {
126
+ throw new Error("invalid remote workspace control message length");
127
+ }
128
+ let parsed: unknown;
129
+ try {
130
+ parsed = JSON.parse(bytes.toString("utf8"));
131
+ } catch {
132
+ throw new Error("invalid remote workspace control message JSON");
133
+ }
134
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
135
+ throw new Error("invalid remote workspace control message");
136
+ }
137
+ const value = parsed as Record<string, unknown>;
138
+ if (value.version !== REMOTE_WORKSPACE_AGENT_PROTOCOL_VERSION) {
139
+ throw new Error("unsupported remote workspace agent protocol");
140
+ }
141
+ return value;
142
+ }
143
+
144
+ export function isRemoteWorkspaceAgentProfile(value: unknown): value is RemoteWorkspaceAgentProfile {
145
+ return isRemoteControlCommandProfile(value) && value !== "shell";
146
+ }
147
+
148
+ export function serializeRemoteWorkspaceHubMessage(message: RemoteWorkspaceHubMessage): string {
149
+ const value: Record<string, unknown> = { ...message };
150
+ if (message.type === "session_open") value.clientHello = encodedHello(message.clientHello);
151
+ if (message.type === "ciphertext") value.payload = Buffer.from(message.payload).toString("base64url");
152
+ const encoded = JSON.stringify(value);
153
+ if (Buffer.byteLength(encoded, "utf8") > REMOTE_WORKSPACE_AGENT_MAX_CONTROL_BYTES) {
154
+ throw new Error("remote workspace hub message is too large");
155
+ }
156
+ return encoded;
157
+ }
158
+
159
+ export function serializeRemoteWorkspaceAgentMessage(message: RemoteWorkspaceAgentMessage): string {
160
+ const value: Record<string, unknown> = { ...message };
161
+ if (message.type === "session_accept") value.hostHello = encodedHello(message.hostHello);
162
+ if (message.type === "ciphertext") value.payload = Buffer.from(message.payload).toString("base64url");
163
+ const encoded = JSON.stringify(value);
164
+ if (Buffer.byteLength(encoded, "utf8") > REMOTE_WORKSPACE_AGENT_MAX_CONTROL_BYTES) {
165
+ throw new Error("remote workspace agent message is too large");
166
+ }
167
+ return encoded;
168
+ }
169
+
170
+ export function parseRemoteWorkspaceHubMessage(raw: string | Uint8Array): RemoteWorkspaceHubMessage {
171
+ const value = parseObject(raw);
172
+ if (value.type === "presence_ack") {
173
+ exactKeys(value, ["version", "type", "capabilities"]);
174
+ return {
175
+ version: REMOTE_WORKSPACE_AGENT_PROTOCOL_VERSION,
176
+ type: "presence_ack",
177
+ capabilities: parseRemoteWorkspaceCapabilities(value.capabilities),
178
+ };
179
+ }
180
+ if (value.type === "session_open") {
181
+ exactKeys(value, ["version", "type", "rootId", "clientHello"]);
182
+ const clientHello = decodedHello(value.clientHello, "client");
183
+ if (!isRemoteWorkspaceAgentProfile(clientHello.commandProfile)) {
184
+ throw new Error("invalid remote workspace session profile");
185
+ }
186
+ return {
187
+ version: REMOTE_WORKSPACE_AGENT_PROTOCOL_VERSION,
188
+ type: "session_open",
189
+ rootId: identifier(value.rootId, "root ID"),
190
+ clientHello,
191
+ };
192
+ }
193
+ if (value.type === "ciphertext") {
194
+ exactKeys(value, ["version", "type", "sessionId", "payload"]);
195
+ const sessionId = identifier(value.sessionId, "session ID");
196
+ if (!isRemoteControlUuid(sessionId)) throw new Error("invalid remote workspace session ID");
197
+ return { version: REMOTE_WORKSPACE_AGENT_PROTOCOL_VERSION, type: "ciphertext", sessionId, payload: payload(value.payload) };
198
+ }
199
+ if (value.type === "session_close") {
200
+ exactKeys(value, ["version", "type", "sessionId", "reason"]);
201
+ const sessionId = identifier(value.sessionId, "session ID");
202
+ if (!isRemoteControlUuid(sessionId)) throw new Error("invalid remote workspace session ID");
203
+ return { version: REMOTE_WORKSPACE_AGENT_PROTOCOL_VERSION, type: "session_close", sessionId, reason: reason(value.reason) };
204
+ }
205
+ throw new Error("unsupported remote workspace hub message");
206
+ }
207
+
208
+ export function parseRemoteWorkspaceAgentMessage(raw: string | Uint8Array): RemoteWorkspaceAgentMessage {
209
+ const value = parseObject(raw);
210
+ if (value.type === "presence") {
211
+ exactKeys(value, ["version", "type", "capabilities"]);
212
+ return {
213
+ version: REMOTE_WORKSPACE_AGENT_PROTOCOL_VERSION,
214
+ type: "presence",
215
+ capabilities: parseRemoteWorkspaceCapabilities(value.capabilities),
216
+ };
217
+ }
218
+ if (value.type === "session_accept") {
219
+ exactKeys(value, ["version", "type", "sessionId", "hostHello"]);
220
+ const sessionId = identifier(value.sessionId, "session ID");
221
+ if (!isRemoteControlUuid(sessionId)) throw new Error("invalid remote workspace session ID");
222
+ return {
223
+ version: REMOTE_WORKSPACE_AGENT_PROTOCOL_VERSION,
224
+ type: "session_accept",
225
+ sessionId,
226
+ hostHello: decodedHello(value.hostHello, "host"),
227
+ };
228
+ }
229
+ if (value.type === "session_reject") {
230
+ exactKeys(value, ["version", "type", "sessionId", "reason"]);
231
+ const sessionId = identifier(value.sessionId, "session ID");
232
+ if (!isRemoteControlUuid(sessionId)) throw new Error("invalid remote workspace session ID");
233
+ return { version: REMOTE_WORKSPACE_AGENT_PROTOCOL_VERSION, type: "session_reject", sessionId, reason: reason(value.reason) };
234
+ }
235
+ if (value.type === "ciphertext") {
236
+ exactKeys(value, ["version", "type", "sessionId", "payload"]);
237
+ const sessionId = identifier(value.sessionId, "session ID");
238
+ if (!isRemoteControlUuid(sessionId)) throw new Error("invalid remote workspace session ID");
239
+ return { version: REMOTE_WORKSPACE_AGENT_PROTOCOL_VERSION, type: "ciphertext", sessionId, payload: payload(value.payload) };
240
+ }
241
+ if (value.type === "heartbeat") {
242
+ exactKeys(value, ["version", "type", "nonce"]);
243
+ return { version: REMOTE_WORKSPACE_AGENT_PROTOCOL_VERSION, type: "heartbeat", nonce: identifier(value.nonce, "heartbeat nonce") };
244
+ }
245
+ throw new Error("unsupported remote workspace agent message");
246
+ }
@@ -0,0 +1,128 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { REMOTE_CONTROL_MAX_RELAY_PAYLOAD_BYTES } from "./protocol";
3
+
4
+ const RPC_FRAME_MAGIC = Uint8Array.of(0x4f, 0x43, 0x58, 0x52); // OCXR
5
+ const RPC_FRAME_VERSION = 1;
6
+ const RPC_FRAME_HEADER_BYTES = 30;
7
+ const RPC_FRAME_MESSAGE_ID_BYTES = 16;
8
+ const RPC_FRAME_MESSAGE_ID_OFFSET = 6;
9
+ const RPC_FRAME_TOTAL_BYTES_OFFSET = 22;
10
+ const RPC_FRAME_CHUNK_OFFSET = 26;
11
+ const RPC_ENCRYPTION_OVERHEAD_BYTES = 24;
12
+ const RPC_MAX_PLAINTEXT_FRAME_BYTES = REMOTE_CONTROL_MAX_RELAY_PAYLOAD_BYTES - RPC_ENCRYPTION_OVERHEAD_BYTES;
13
+ const RPC_MAX_CHUNK_BYTES = RPC_MAX_PLAINTEXT_FRAME_BYTES - RPC_FRAME_HEADER_BYTES;
14
+ const RPC_MAX_INCOMPLETE_MESSAGES = 8;
15
+ const RPC_INCOMPLETE_MESSAGE_TTL_MS = 30_000;
16
+
17
+ // A 256 KiB UTF-8 write can expand by up to 6x when JSON escapes control bytes.
18
+ // Keep the wire contract bounded while still carrying every executor-approved text file.
19
+ export const REMOTE_WORKSPACE_RPC_MAX_MESSAGE_BYTES = 2 * 1024 * 1024;
20
+
21
+ interface IncompleteMessage {
22
+ buffer: Uint8Array;
23
+ nextOffset: number;
24
+ timer: ReturnType<typeof setTimeout>;
25
+ }
26
+
27
+ function hasMagic(value: Uint8Array): boolean {
28
+ return RPC_FRAME_MAGIC.every((byte, index) => value[index] === byte);
29
+ }
30
+
31
+ function messageKey(frame: Uint8Array): string {
32
+ return Buffer.from(
33
+ frame.subarray(RPC_FRAME_MESSAGE_ID_OFFSET, RPC_FRAME_MESSAGE_ID_OFFSET + RPC_FRAME_MESSAGE_ID_BYTES),
34
+ ).toString("hex");
35
+ }
36
+
37
+ function writeHeader(frame: Uint8Array, messageId: Uint8Array, totalBytes: number, offset: number): void {
38
+ frame.set(RPC_FRAME_MAGIC, 0);
39
+ frame[4] = RPC_FRAME_VERSION;
40
+ frame[5] = 0;
41
+ frame.set(messageId, RPC_FRAME_MESSAGE_ID_OFFSET);
42
+ const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
43
+ view.setUint32(RPC_FRAME_TOTAL_BYTES_OFFSET, totalBytes);
44
+ view.setUint32(RPC_FRAME_CHUNK_OFFSET, offset);
45
+ }
46
+
47
+ /**
48
+ * Splits one logical RPC message without retaining a second full-size copy.
49
+ * Each yielded plaintext fits one authenticated relay frame.
50
+ */
51
+ export function* frameRemoteWorkspaceRpcMessage(message: Uint8Array): Generator<Uint8Array> {
52
+ if (!(message instanceof Uint8Array)
53
+ || message.byteLength < 1
54
+ || message.byteLength > REMOTE_WORKSPACE_RPC_MAX_MESSAGE_BYTES) {
55
+ throw new Error("invalid remote workspace RPC message length");
56
+ }
57
+ const messageId = randomBytes(RPC_FRAME_MESSAGE_ID_BYTES);
58
+ for (let offset = 0; offset < message.byteLength; offset += RPC_MAX_CHUNK_BYTES) {
59
+ const end = Math.min(offset + RPC_MAX_CHUNK_BYTES, message.byteLength);
60
+ const frame = new Uint8Array(RPC_FRAME_HEADER_BYTES + end - offset);
61
+ writeHeader(frame, messageId, message.byteLength, offset);
62
+ frame.set(message.subarray(offset, end), RPC_FRAME_HEADER_BYTES);
63
+ yield frame;
64
+ }
65
+ }
66
+
67
+ /** Bounded, ordered reassembly for authenticated RPC fragments. */
68
+ export class RemoteWorkspaceRpcReassembler {
69
+ private readonly incomplete = new Map<string, IncompleteMessage>();
70
+
71
+ accept(frame: Uint8Array): Uint8Array | null {
72
+ if (!(frame instanceof Uint8Array)
73
+ || frame.byteLength <= RPC_FRAME_HEADER_BYTES
74
+ || frame.byteLength > RPC_MAX_PLAINTEXT_FRAME_BYTES
75
+ || !hasMagic(frame)
76
+ || frame[4] !== RPC_FRAME_VERSION
77
+ || frame[5] !== 0) {
78
+ throw new Error("invalid remote workspace RPC frame");
79
+ }
80
+ const view = new DataView(frame.buffer, frame.byteOffset, frame.byteLength);
81
+ const totalBytes = view.getUint32(RPC_FRAME_TOTAL_BYTES_OFFSET);
82
+ const offset = view.getUint32(RPC_FRAME_CHUNK_OFFSET);
83
+ const payload = frame.subarray(RPC_FRAME_HEADER_BYTES);
84
+ if (totalBytes < 1
85
+ || totalBytes > REMOTE_WORKSPACE_RPC_MAX_MESSAGE_BYTES
86
+ || offset >= totalBytes
87
+ || payload.byteLength > totalBytes - offset
88
+ || (offset + payload.byteLength < totalBytes && payload.byteLength !== RPC_MAX_CHUNK_BYTES)) {
89
+ throw new Error("invalid remote workspace RPC fragment bounds");
90
+ }
91
+ if (offset === 0 && payload.byteLength === totalBytes) return payload;
92
+
93
+ const key = messageKey(frame);
94
+ let state = this.incomplete.get(key);
95
+ if (!state) {
96
+ if (offset !== 0) throw new Error("remote workspace RPC fragment started out of order");
97
+ if (this.incomplete.size >= RPC_MAX_INCOMPLETE_MESSAGES) {
98
+ throw new Error("remote workspace RPC fragment limit reached");
99
+ }
100
+ const timer = setTimeout(() => this.drop(key), RPC_INCOMPLETE_MESSAGE_TTL_MS);
101
+ timer.unref?.();
102
+ state = { buffer: new Uint8Array(totalBytes), nextOffset: 0, timer };
103
+ this.incomplete.set(key, state);
104
+ } else if (state.buffer.byteLength !== totalBytes || state.nextOffset !== offset) {
105
+ this.drop(key);
106
+ throw new Error("remote workspace RPC fragments are inconsistent or out of order");
107
+ }
108
+
109
+ state.buffer.set(payload, offset);
110
+ state.nextOffset += payload.byteLength;
111
+ if (state.nextOffset !== totalBytes) return null;
112
+ this.incomplete.delete(key);
113
+ clearTimeout(state.timer);
114
+ return state.buffer;
115
+ }
116
+
117
+ clear(): void {
118
+ for (const state of this.incomplete.values()) clearTimeout(state.timer);
119
+ this.incomplete.clear();
120
+ }
121
+
122
+ private drop(key: string): void {
123
+ const state = this.incomplete.get(key);
124
+ if (!state) return;
125
+ clearTimeout(state.timer);
126
+ this.incomplete.delete(key);
127
+ }
128
+ }
@@ -0,0 +1,237 @@
1
+ export const REMOTE_WORKSPACE_TOOL_NAMESPACE = "ocx_remote_workspace" as const;
2
+ export const REMOTE_WORKSPACE_MAX_TOOL_RESULT_BYTES = 256 * 1024;
3
+
4
+ export const REMOTE_WORKSPACE_CAPABILITIES = [
5
+ "workspace.read",
6
+ "workspace.write",
7
+ "workspace.exec",
8
+ ] as const;
9
+
10
+ export type RemoteWorkspaceCapability = typeof REMOTE_WORKSPACE_CAPABILITIES[number];
11
+
12
+ export type RemoteWorkspaceToolName =
13
+ | "list_directory"
14
+ | "read_file"
15
+ | "write_file"
16
+ | "exec";
17
+
18
+ export interface RemoteWorkspaceDynamicToolFunction {
19
+ type: "function";
20
+ name: RemoteWorkspaceToolName;
21
+ description: string;
22
+ inputSchema: Record<string, unknown>;
23
+ }
24
+
25
+ export interface RemoteWorkspaceDynamicToolNamespace {
26
+ type: "namespace";
27
+ name: typeof REMOTE_WORKSPACE_TOOL_NAMESPACE;
28
+ description: string;
29
+ tools: RemoteWorkspaceDynamicToolFunction[];
30
+ }
31
+
32
+ export interface RemoteWorkspaceToolCallParams {
33
+ threadId: string;
34
+ turnId: string;
35
+ callId: string;
36
+ namespace: typeof REMOTE_WORKSPACE_TOOL_NAMESPACE;
37
+ tool: RemoteWorkspaceToolName;
38
+ arguments: unknown;
39
+ }
40
+
41
+ export type RemoteWorkspaceToolResult =
42
+ | { ok: true; value?: unknown; error?: never }
43
+ | { ok: false; error: string; value?: never };
44
+
45
+ const RELATIVE_PATH = {
46
+ type: "string",
47
+ minLength: 1,
48
+ maxLength: 4096,
49
+ description: "Path relative to the workspace root. Absolute paths and parent traversal are rejected.",
50
+ } as const;
51
+
52
+ export const REMOTE_WORKSPACE_DYNAMIC_TOOLS: readonly [RemoteWorkspaceDynamicToolNamespace] = [{
53
+ type: "namespace",
54
+ name: REMOTE_WORKSPACE_TOOL_NAMESPACE,
55
+ description: "Operate only on the selected remote OpenCodex executor workspace.",
56
+ tools: [
57
+ {
58
+ type: "function",
59
+ name: "list_directory",
60
+ description: "List one directory inside the selected remote workspace.",
61
+ inputSchema: {
62
+ type: "object",
63
+ properties: { path: { ...RELATIVE_PATH, default: "." } },
64
+ additionalProperties: false,
65
+ },
66
+ },
67
+ {
68
+ type: "function",
69
+ name: "read_file",
70
+ description: "Read a bounded UTF-8 regular file from the selected remote workspace.",
71
+ inputSchema: {
72
+ type: "object",
73
+ properties: {
74
+ path: RELATIVE_PATH,
75
+ maxBytes: { type: "integer", minimum: 1, maximum: REMOTE_WORKSPACE_MAX_TOOL_RESULT_BYTES },
76
+ },
77
+ required: ["path"],
78
+ additionalProperties: false,
79
+ },
80
+ },
81
+ {
82
+ type: "function",
83
+ name: "write_file",
84
+ description: "Atomically replace a bounded UTF-8 file in the selected remote workspace.",
85
+ inputSchema: {
86
+ type: "object",
87
+ properties: {
88
+ path: RELATIVE_PATH,
89
+ content: { type: "string", maxLength: REMOTE_WORKSPACE_MAX_TOOL_RESULT_BYTES },
90
+ expectedSha256: {
91
+ type: ["string", "null"],
92
+ pattern: "^[0-9a-f]{64}$",
93
+ description: "Expected current file hash, or null when the file must not already exist.",
94
+ },
95
+ },
96
+ required: ["path", "content", "expectedSha256"],
97
+ additionalProperties: false,
98
+ },
99
+ },
100
+ {
101
+ type: "function",
102
+ name: "exec",
103
+ description: "Execute an argv vector in a directory inside the selected remote workspace.",
104
+ inputSchema: {
105
+ type: "object",
106
+ properties: {
107
+ command: {
108
+ type: "array",
109
+ minItems: 1,
110
+ maxItems: 64,
111
+ items: { type: "string", minLength: 1, maxLength: 4096 },
112
+ },
113
+ cwd: { ...RELATIVE_PATH, default: "." },
114
+ timeoutMs: { type: "integer", minimum: 1, maximum: 60_000 },
115
+ },
116
+ required: ["command"],
117
+ additionalProperties: false,
118
+ },
119
+ },
120
+ ],
121
+ }];
122
+
123
+ const TOOL_CAPABILITY: Readonly<Record<RemoteWorkspaceToolName, RemoteWorkspaceCapability>> = {
124
+ list_directory: "workspace.read",
125
+ read_file: "workspace.read",
126
+ write_file: "workspace.write",
127
+ exec: "workspace.exec",
128
+ };
129
+
130
+ export function parseRemoteWorkspaceCapabilities(
131
+ value: unknown,
132
+ fallback: readonly RemoteWorkspaceCapability[] = ["workspace.read", "workspace.write"],
133
+ ): RemoteWorkspaceCapability[] {
134
+ if (value === undefined) return [...fallback];
135
+ if (!Array.isArray(value) || value.length < 1 || value.length > REMOTE_WORKSPACE_CAPABILITIES.length) {
136
+ throw new Error("invalid remote workspace capabilities");
137
+ }
138
+ const result = new Set<RemoteWorkspaceCapability>();
139
+ for (const capability of value) {
140
+ if (!isRemoteWorkspaceCapability(capability)) {
141
+ throw new Error("invalid remote workspace capability");
142
+ }
143
+ result.add(capability);
144
+ }
145
+ if (!result.has("workspace.read")) {
146
+ throw new Error("remote workspace executors must support workspace.read");
147
+ }
148
+ return REMOTE_WORKSPACE_CAPABILITIES.filter(capability => result.has(capability));
149
+ }
150
+
151
+ export function remoteWorkspaceToolsForCapabilities(
152
+ capabilities: readonly RemoteWorkspaceCapability[],
153
+ ): RemoteWorkspaceToolName[] {
154
+ const allowed = new Set(parseRemoteWorkspaceCapabilities(capabilities));
155
+ return REMOTE_WORKSPACE_DYNAMIC_TOOLS[0].tools
156
+ .filter(tool => allowed.has(TOOL_CAPABILITY[tool.name]))
157
+ .map(tool => tool.name);
158
+ }
159
+
160
+ export function remoteWorkspaceDynamicToolsForCapabilities(
161
+ capabilities: readonly RemoteWorkspaceCapability[],
162
+ ): readonly [RemoteWorkspaceDynamicToolNamespace] {
163
+ const names = new Set(remoteWorkspaceToolsForCapabilities(capabilities));
164
+ return [{
165
+ ...REMOTE_WORKSPACE_DYNAMIC_TOOLS[0],
166
+ tools: REMOTE_WORKSPACE_DYNAMIC_TOOLS[0].tools.filter(tool => names.has(tool.name)),
167
+ }];
168
+ }
169
+
170
+ export function remoteWorkspaceCapabilityForTool(tool: RemoteWorkspaceToolName): RemoteWorkspaceCapability {
171
+ return TOOL_CAPABILITY[tool];
172
+ }
173
+
174
+ const TOOL_NAMES: ReadonlySet<string> = new Set(
175
+ REMOTE_WORKSPACE_DYNAMIC_TOOLS[0].tools.map(tool => tool.name),
176
+ );
177
+
178
+ export function isRemoteWorkspaceCapability(value: unknown): value is RemoteWorkspaceCapability {
179
+ return value === "workspace.read" || value === "workspace.write" || value === "workspace.exec";
180
+ }
181
+
182
+ export function isRemoteWorkspaceToolName(value: unknown): value is RemoteWorkspaceToolName {
183
+ return typeof value === "string" && TOOL_NAMES.has(value);
184
+ }
185
+
186
+ function boundedIdentifier(value: unknown): value is string {
187
+ return typeof value === "string" && value.length >= 1 && value.length <= 256 && !/[\x00-\x1f\x7f]/.test(value);
188
+ }
189
+
190
+ export function parseRemoteWorkspaceToolCall(value: unknown): RemoteWorkspaceToolCallParams {
191
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
192
+ throw new Error("invalid remote workspace tool call");
193
+ }
194
+ const raw = value as Record<string, unknown>;
195
+ if (
196
+ !boundedIdentifier(raw.threadId)
197
+ || !boundedIdentifier(raw.turnId)
198
+ || !boundedIdentifier(raw.callId)
199
+ || raw.namespace !== REMOTE_WORKSPACE_TOOL_NAMESPACE
200
+ || !isRemoteWorkspaceToolName(raw.tool)
201
+ ) throw new Error("invalid remote workspace tool call identity");
202
+ return {
203
+ threadId: raw.threadId,
204
+ turnId: raw.turnId,
205
+ callId: raw.callId,
206
+ namespace: REMOTE_WORKSPACE_TOOL_NAMESPACE,
207
+ tool: raw.tool,
208
+ arguments: raw.arguments,
209
+ };
210
+ }
211
+
212
+ export function remoteWorkspaceDeveloperInstructions(
213
+ deviceName: string,
214
+ tools: readonly RemoteWorkspaceToolName[] = REMOTE_WORKSPACE_DYNAMIC_TOOLS[0].tools.map(tool => tool.name),
215
+ ): string {
216
+ const safeName = deviceName.replace(/[\x00-\x1f\x7f]/g, " ").slice(0, 120) || "remote executor";
217
+ const allowed = tools.map(tool => `${REMOTE_WORKSPACE_TOOL_NAMESPACE}.${tool}`).join(", ");
218
+ return [
219
+ `This thread operates on the OpenCodex remote executor named ${JSON.stringify(safeName)}.`,
220
+ `Its available remote tools are: ${allowed}. Use no other tool for filesystem or command work.`,
221
+ "The coordinator filesystem is an empty isolation boundary and is not the user's workspace.",
222
+ "Never use local shell, local file, or local patch tools for this thread.",
223
+ "If a remote tool is unavailable, stop and report that the executor is offline; never fall back locally.",
224
+ ].join(" ");
225
+ }
226
+
227
+ export function remoteWorkspaceCodexDeveloperInstructions(
228
+ deviceName: string,
229
+ tools: readonly RemoteWorkspaceToolName[] = REMOTE_WORKSPACE_DYNAMIC_TOOLS[0].tools.map(tool => tool.name),
230
+ ): string {
231
+ const nestedTools = tools.map(tool => `tools.mcp__${REMOTE_WORKSPACE_TOOL_NAMESPACE}__${tool}`).join(", ");
232
+ return [
233
+ remoteWorkspaceDeveloperInstructions(deviceName, tools),
234
+ `Current Codex versions expose MCP through the functions.exec code-mode tool. Inside it, call only: ${nestedTools}.`,
235
+ "Do not call exec_command, apply_patch, view_image, browser, apps, plugins, or any other nested helper on the Hub.",
236
+ ].join(" ");
237
+ }
@@ -0,0 +1,24 @@
1
+ import { Buffer } from "node:buffer";
2
+
3
+ export function truncateRemoteWorkspaceUtf8(value: string, maximumBytes: number): string {
4
+ if (!Number.isSafeInteger(maximumBytes) || maximumBytes < 0) {
5
+ throw new Error("invalid remote workspace UTF-8 limit");
6
+ }
7
+ if (Buffer.byteLength(value, "utf8") <= maximumBytes) return value;
8
+ let low = 0;
9
+ let high = Math.min(value.length, maximumBytes);
10
+ while (low < high) {
11
+ const middle = Math.ceil((low + high) / 2);
12
+ if (Buffer.byteLength(value.slice(0, middle), "utf8") <= maximumBytes) low = middle;
13
+ else high = middle - 1;
14
+ }
15
+ if (
16
+ low > 0
17
+ && low < value.length
18
+ && value.charCodeAt(low - 1) >= 0xd800
19
+ && value.charCodeAt(low - 1) <= 0xdbff
20
+ && value.charCodeAt(low) >= 0xdc00
21
+ && value.charCodeAt(low) <= 0xdfff
22
+ ) low -= 1;
23
+ return value.slice(0, low);
24
+ }
@@ -261,6 +261,29 @@ export function rewriteRoutedCustomToolsForUpstream(
261
261
  return { body: rewriteForUpstream(body, conversionNames, callIds), names, repairNames };
262
262
  }
263
263
 
264
+ /**
265
+ * A delta result has no tool name. Without its call, lowering cannot tell whether it belongs
266
+ * to a converted function or a native custom tool. Request full replay instead of guessing.
267
+ */
268
+ export function hasUnmappedRoutedCustomToolOutput(
269
+ body: unknown,
270
+ supportsResponsesCustomTools?: boolean,
271
+ ): boolean {
272
+ if (!isPlainObject(body) || !Array.isArray(body.input)) return false;
273
+ if (collectRoutedCustomToolNames(body, supportsResponsesCustomTools).size === 0) return false;
274
+ const callIds = new Set<string>();
275
+ for (const item of body.input) {
276
+ if (isPlainObject(item)
277
+ && (item.type === "custom_tool_call" || item.type === "function_call")
278
+ && typeof item.call_id === "string") callIds.add(item.call_id);
279
+ }
280
+ return body.input.some(item => isPlainObject(item)
281
+ && item.type === "custom_tool_call_output"
282
+ && typeof item.call_id === "string"
283
+ && item.call_id.length > 0
284
+ && !callIds.has(item.call_id));
285
+ }
286
+
264
287
  export function restoreRoutedCustomCalls(
265
288
  value: unknown,
266
289
  names: ReadonlySet<string>,