@bitkyc08/opencodex 2.52.0 → 2.53.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 (236) hide show
  1. package/gui/dist/assets/index-BBOZWGB6.css +1 -0
  2. package/gui/dist/assets/index-BlO4Yl6q.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-CWXut3rG.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,192 @@
1
+ import { randomBytes, randomUUID } from "node:crypto";
2
+ import type { RemoteWorkspaceCoordinator } from "./workspace-coordinator";
3
+ import {
4
+ REMOTE_WORKSPACE_DYNAMIC_TOOLS,
5
+ REMOTE_WORKSPACE_TOOL_NAMESPACE,
6
+ isRemoteWorkspaceToolName,
7
+ type RemoteWorkspaceToolName,
8
+ } from "./workspace-tools";
9
+
10
+ const MAX_BRIDGE_BODY_BYTES = 512 * 1024;
11
+ const MAX_BRIDGE_ACTIVE_REQUESTS = 8;
12
+ function json(body: unknown, status = 200): Response {
13
+ return Response.json(body, { status, headers: { "cache-control": "no-store" } });
14
+ }
15
+
16
+ function errorText(value: unknown): string {
17
+ return (value instanceof Error ? value.message : "Remote Workspace tool failed")
18
+ .replace(/[^\x20-\x7e\n\t]/g, " ")
19
+ .slice(0, 4_096);
20
+ }
21
+
22
+ function record(value: unknown): Record<string, unknown> | null {
23
+ return value && typeof value === "object" && !Array.isArray(value)
24
+ ? value as Record<string, unknown>
25
+ : null;
26
+ }
27
+
28
+ async function readBoundedJson(req: Request): Promise<unknown> {
29
+ if (!req.body) throw new Error("invalid JSON");
30
+ const reader = req.body.getReader();
31
+ const chunks: Uint8Array[] = [];
32
+ let total = 0;
33
+ try {
34
+ while (true) {
35
+ const next = await reader.read();
36
+ if (next.done) break;
37
+ total += next.value.byteLength;
38
+ if (total > MAX_BRIDGE_BODY_BYTES) {
39
+ await reader.cancel("request too large").catch(() => {});
40
+ throw new Error("request too large");
41
+ }
42
+ chunks.push(next.value);
43
+ }
44
+ } finally {
45
+ reader.releaseLock();
46
+ }
47
+ const body = new Uint8Array(total);
48
+ let offset = 0;
49
+ for (const chunk of chunks) {
50
+ body.set(chunk, offset);
51
+ offset += chunk.byteLength;
52
+ }
53
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(body));
54
+ }
55
+
56
+ export interface RemoteWorkspaceToolBridge {
57
+ url: string;
58
+ token: string;
59
+ stop(): Promise<void>;
60
+ }
61
+
62
+ /**
63
+ * Loopback-only bridge used by Hub-owned CLIs whose extension boundary is HTTP.
64
+ * The random bearer is passed only to the child process. The model sees tool schemas,
65
+ * never this endpoint or token, and every invocation still goes through the E2EE coordinator.
66
+ */
67
+ export function startRemoteWorkspaceToolBridge(options: {
68
+ coordinator: RemoteWorkspaceCoordinator;
69
+ threadId: string | (() => string);
70
+ tools: readonly RemoteWorkspaceToolName[];
71
+ onTool?: (tool: RemoteWorkspaceToolName) => void;
72
+ }): RemoteWorkspaceToolBridge {
73
+ const token = randomBytes(32).toString("base64url");
74
+ const toolNames = new Set(options.tools);
75
+ const definitions = REMOTE_WORKSPACE_DYNAMIC_TOOLS[0].tools.filter(tool => toolNames.has(tool.name));
76
+ if (definitions.length < 1) throw new Error("Remote Workspace bridge needs at least one tool");
77
+ const invoke = async (tool: unknown, args: unknown): Promise<{ success: boolean; text: string }> => {
78
+ if (!isRemoteWorkspaceToolName(tool) || !toolNames.has(tool)) {
79
+ return { success: false, text: JSON.stringify({ ok: false, error: "unknown Remote Workspace tool" }) };
80
+ }
81
+ options.onTool?.(tool);
82
+ const threadId = typeof options.threadId === "function" ? options.threadId() : options.threadId;
83
+ if (!threadId) return { success: false, text: JSON.stringify({ ok: false, error: "remote workspace thread is not ready" }) };
84
+ const result = await options.coordinator.handle({
85
+ method: "item/tool/call",
86
+ id: randomUUID(),
87
+ params: {
88
+ threadId,
89
+ turnId: randomUUID(),
90
+ callId: randomUUID(),
91
+ namespace: REMOTE_WORKSPACE_TOOL_NAMESPACE,
92
+ tool,
93
+ arguments: args,
94
+ },
95
+ });
96
+ return { success: result.result.success, text: result.result.contentItems[0]!.text };
97
+ };
98
+ let activeRequests = 0;
99
+ const server = Bun.serve({
100
+ hostname: "127.0.0.1",
101
+ port: 0,
102
+ async fetch(req) {
103
+ const url = new URL(req.url);
104
+ if (req.headers.get("origin")) return json({ error: "browser origins are not allowed" }, 403);
105
+ if (req.headers.get("authorization") !== `Bearer ${token}`) return json({ error: "unauthorized" }, 401);
106
+ if (req.method !== "POST" || (url.pathname !== "/invoke" && url.pathname !== "/mcp")) {
107
+ return json({ error: "not found" }, 404);
108
+ }
109
+ if (activeRequests >= MAX_BRIDGE_ACTIVE_REQUESTS) return json({ error: "Remote Workspace bridge is busy" }, 429);
110
+ activeRequests += 1;
111
+ try {
112
+ const length = Number(req.headers.get("content-length") ?? "0");
113
+ if (!Number.isFinite(length) || length > MAX_BRIDGE_BODY_BYTES) return json({ error: "request too large" }, 413);
114
+ let parsed: unknown;
115
+ try { parsed = await readBoundedJson(req); }
116
+ catch (error) {
117
+ return json({ error: error instanceof Error && error.message === "request too large" ? error.message : "invalid JSON" },
118
+ error instanceof Error && error.message === "request too large" ? 413 : 400);
119
+ }
120
+ const body = record(parsed);
121
+ if (!body) return json({ error: "invalid request" }, 400);
122
+
123
+ if (url.pathname === "/invoke") {
124
+ try {
125
+ return json(await invoke(body.tool, body.arguments));
126
+ } catch (error) {
127
+ return json({ success: false, text: JSON.stringify({ ok: false, error: errorText(error) }) }, 502);
128
+ }
129
+ }
130
+
131
+ const id = body.id;
132
+ const method = body.method;
133
+ const params = record(body.params) ?? {};
134
+ if (typeof method !== "string") return json({ jsonrpc: "2.0", id: id ?? null, error: { code: -32_600, message: "invalid MCP request" } });
135
+ if (method === "notifications/initialized") return new Response(null, { status: 202 });
136
+ if (method === "initialize") {
137
+ return json({
138
+ jsonrpc: "2.0",
139
+ id: id ?? null,
140
+ result: {
141
+ protocolVersion: typeof params.protocolVersion === "string" ? params.protocolVersion : "2025-06-18",
142
+ capabilities: { tools: { listChanged: false } },
143
+ serverInfo: { name: "opencodex-remote-workspace", version: "1" },
144
+ },
145
+ });
146
+ }
147
+ if (method === "ping") return json({ jsonrpc: "2.0", id: id ?? null, result: {} });
148
+ if (method === "tools/list") {
149
+ return json({
150
+ jsonrpc: "2.0",
151
+ id: id ?? null,
152
+ result: {
153
+ tools: definitions.map(tool => ({
154
+ name: tool.name,
155
+ description: tool.description,
156
+ inputSchema: tool.inputSchema,
157
+ })),
158
+ },
159
+ });
160
+ }
161
+ if (method === "tools/call") {
162
+ try {
163
+ const called = await invoke(params.name, params.arguments);
164
+ return json({
165
+ jsonrpc: "2.0",
166
+ id: id ?? null,
167
+ result: { content: [{ type: "text", text: called.text }], isError: !called.success },
168
+ });
169
+ } catch (error) {
170
+ return json({
171
+ jsonrpc: "2.0",
172
+ id: id ?? null,
173
+ result: { content: [{ type: "text", text: errorText(error) }], isError: true },
174
+ });
175
+ }
176
+ }
177
+ return json({ jsonrpc: "2.0", id: id ?? null, error: { code: -32_601, message: "MCP method not found" } });
178
+ } finally {
179
+ activeRequests -= 1;
180
+ }
181
+ },
182
+ });
183
+ let stopping: Promise<void> | null = null;
184
+ return {
185
+ url: new URL("/", server.url).toString().replace(/\/$/, ""),
186
+ token,
187
+ stop() {
188
+ stopping ??= server.stop(true);
189
+ return stopping;
190
+ },
191
+ };
192
+ }
@@ -31,7 +31,10 @@ function unwrapPatchInput(value: string): string {
31
31
  */
32
32
  export function compileCodeModeHelperInput(argumentsText: unknown, toolName: string): string {
33
33
  if (typeof argumentsText !== "string") return "";
34
- if (toolName === "apply_patch") {
34
+ const helperName = toolName.startsWith("default.")
35
+ ? toolName.slice("default.".length)
36
+ : toolName;
37
+ if (helperName === "apply_patch") {
35
38
  const patch = normalizeApplyPatchDelimiters(unwrapPatchInput(argumentsText));
36
39
  return `const result = await tools.apply_patch(${JSON.stringify(patch)});\ntext(result);`;
37
40
  }
@@ -43,7 +46,7 @@ export function compileCodeModeHelperInput(argumentsText: unknown, toolName: str
43
46
  }
44
47
  const args: unknown = isPlainObject(parsed) ? { ...parsed } : parsed;
45
48
  if (
46
- toolName === "shell_command"
49
+ helperName === "shell_command"
47
50
  && isPlainObject(args)
48
51
  && typeof args.command === "string"
49
52
  && args.cmd === undefined
@@ -51,9 +54,25 @@ export function compileCodeModeHelperInput(argumentsText: unknown, toolName: str
51
54
  args.cmd = args.command;
52
55
  delete args.command;
53
56
  }
54
- if (toolName === "write_stdin") {
57
+ if (helperName === "write_stdin") {
55
58
  return `const result = await tools.write_stdin(${JSON.stringify(args)});\ntext(result);`;
56
59
  }
60
+ if (helperName === "view_image") {
61
+ // Codex code-mode `exec` exposes `tools.view_image({path, detail?})`; the host answers
62
+ // with a custom_tool_call_output carrying `input_image`, which `image()` surfaces back
63
+ // to the model. Aliases map onto Codex's `path`/`detail`; anything else is passed as
64
+ // data so nested validation can reject it.
65
+ const viewArgs: unknown = isPlainObject(args) ? { ...args } : args;
66
+ if (isPlainObject(viewArgs)) {
67
+ for (const alias of ["file_path", "file", "image_path"]) {
68
+ if (typeof viewArgs.path !== "string" && typeof viewArgs[alias] === "string") {
69
+ viewArgs.path = viewArgs[alias];
70
+ }
71
+ delete viewArgs[alias];
72
+ }
73
+ }
74
+ return `const result = await tools.view_image(${JSON.stringify(viewArgs)});\nif (result && result.image_url) { image(result.image_url); } else { text(result); }`;
75
+ }
57
76
  return `const result = await tools.exec_command(${JSON.stringify(args)});\ntext(result);`;
58
77
  }
59
78
 
@@ -3,7 +3,6 @@ const UNSUPPORTED_HOSTED_TOOLS: ReadonlyArray<{
3
3
  match: (model: string, baseUrl?: string) => boolean;
4
4
  tools: ReadonlySet<string>;
5
5
  }> = [
6
- { match: model => model.includes("codex-spark"), tools: new Set(["image_generation", "tool_search"]) },
7
6
  {
8
7
  match: (model, baseUrl) => model === "grok-4.6"
9
8
  && baseUrl?.replace(/\/+$/, "") === "https://opencode.ai/zen/go/v1",
@@ -0,0 +1,379 @@
1
+ import { createHash } from "node:crypto";
2
+ import { wireToolInnerName } from "./tool-name-aliases";
3
+
4
+ const MUSE_TOOL_NAME_MAX = 64;
5
+ const MUSE_TOOL_NAME_PREFIX = 55;
6
+ const MUSE_TOOL_NAME_HASH_LEN = 8;
7
+ const MUSE_SAFE_NAME = /^[a-zA-Z0-9_-]+$/;
8
+
9
+ export type MuseToolNameAliases = ReadonlyMap<string, string>;
10
+
11
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
12
+ return !!value && typeof value === "object" && !Array.isArray(value);
13
+ }
14
+
15
+ /** Direct Meta Muse / Meta Model Responses host. Path, port, and model id do not matter. */
16
+ export function isMetaAiResponsesDestination(responseUrl: string): boolean {
17
+ try {
18
+ return new URL(responseUrl).hostname.toLowerCase() === "api.meta.ai";
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ function sanitizeMuseToolName(name: string): string {
25
+ return name.replace(/[^a-zA-Z0-9_-]/g, "_");
26
+ }
27
+
28
+ function isPassThroughMuseToolName(name: string): boolean {
29
+ return name.length >= 1
30
+ && name.length <= MUSE_TOOL_NAME_MAX
31
+ && MUSE_SAFE_NAME.test(name)
32
+ && sanitizeMuseToolName(name) === name;
33
+ }
34
+
35
+ function hashSuffix(originalName: string, salt: number): string {
36
+ const hashInput = salt === 0 ? originalName : originalName + "#" + salt;
37
+ return createHash("sha256").update(hashInput).digest("hex").slice(0, MUSE_TOOL_NAME_HASH_LEN);
38
+ }
39
+
40
+ /**
41
+ * Collision-safe wire name for one original tool identity. Pass-through when the name is
42
+ * already `^[a-zA-Z0-9_-]{1,64}$` and unclaimed; otherwise a 55-char sanitized prefix plus
43
+ * an 8-hex sha256 of the ORIGINAL name, salted with `original#N` until unique.
44
+ */
45
+ export function museWireToolName(originalName: string, used?: Set<string>): string {
46
+ if (isPassThroughMuseToolName(originalName) && !(used?.has(originalName))) {
47
+ used?.add(originalName);
48
+ return originalName;
49
+ }
50
+ const base = sanitizeMuseToolName(originalName).slice(0, MUSE_TOOL_NAME_PREFIX) || "tool";
51
+ for (let salt = 0; ; salt++) {
52
+ const candidate = base + "_" + hashSuffix(originalName, salt);
53
+ if (!(used?.has(candidate))) {
54
+ used?.add(candidate);
55
+ return candidate;
56
+ }
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Two-phase claim over a declaration-order name list: conforming <=64 names occupy the
62
+ * collision domain first, then long or charset-unsafe names alias in that same order.
63
+ * `aliases` is wireName -> originalName for identities that actually changed.
64
+ */
65
+ export function buildMuseToolNameAliasPlan(originalNames: readonly string[]): {
66
+ wireByOriginal: Map<string, string>;
67
+ aliases: Map<string, string>;
68
+ } {
69
+ const used = new Set<string>();
70
+ const wireByOriginal = new Map<string, string>();
71
+ const unique: string[] = [];
72
+ const seen = new Set<string>();
73
+ for (const name of originalNames) {
74
+ if (name.length === 0 || seen.has(name)) continue;
75
+ seen.add(name);
76
+ unique.push(name);
77
+ }
78
+ for (const name of unique) {
79
+ if (!isPassThroughMuseToolName(name)) continue;
80
+ used.add(name);
81
+ wireByOriginal.set(name, name);
82
+ }
83
+ for (const name of unique) {
84
+ if (wireByOriginal.has(name)) continue;
85
+ wireByOriginal.set(name, museWireToolName(name, used));
86
+ }
87
+ const aliases = new Map<string, string>();
88
+ for (const [original, wire] of wireByOriginal) {
89
+ if (wire !== original) aliases.set(wire, original);
90
+ }
91
+ return { wireByOriginal, aliases };
92
+ }
93
+
94
+ function addCollectedName(names: string[], seen: Set<string>, name: string | undefined): void {
95
+ if (!name || seen.has(name)) return;
96
+ seen.add(name);
97
+ names.push(name);
98
+ }
99
+
100
+ function collectDeclaredToolName(tool: unknown, names: string[], seen: Set<string>): void {
101
+ addCollectedName(names, seen, wireToolInnerName(tool));
102
+ }
103
+
104
+ function collectToolChoiceNames(choice: unknown, names: string[], seen: Set<string>): void {
105
+ if (!isPlainObject(choice)) return;
106
+ if ((choice.type === "function" || choice.type === "custom") && typeof choice.name === "string") {
107
+ addCollectedName(names, seen, choice.name);
108
+ return;
109
+ }
110
+ if (choice.type !== "allowed_tools" || !Array.isArray(choice.tools)) return;
111
+ for (const tool of choice.tools) {
112
+ if (!isPlainObject(tool)) continue;
113
+ if (tool.type !== "function" && tool.type !== "custom") continue;
114
+ if (typeof tool.name === "string") addCollectedName(names, seen, tool.name);
115
+ }
116
+ }
117
+
118
+ function collectMuseToolNames(body: unknown): string[] {
119
+ if (!isPlainObject(body)) return [];
120
+ const names: string[] = [];
121
+ const seen = new Set<string>();
122
+ if (Array.isArray(body.tools)) {
123
+ for (const tool of body.tools) collectDeclaredToolName(tool, names, seen);
124
+ }
125
+ if (Array.isArray(body.input)) {
126
+ for (const item of body.input) {
127
+ if (!isPlainObject(item)) continue;
128
+ if (item.type === "additional_tools" && Array.isArray(item.tools)) {
129
+ for (const tool of item.tools) collectDeclaredToolName(tool, names, seen);
130
+ continue;
131
+ }
132
+ if (
133
+ (item.type === "function_call" || item.type === "custom_tool_call")
134
+ && typeof item.name === "string"
135
+ ) addCollectedName(names, seen, item.name);
136
+ }
137
+ }
138
+ collectToolChoiceNames(body.tool_choice, names, seen);
139
+ return names;
140
+ }
141
+
142
+ function rewriteDeclaredTool(tool: unknown, wireByOriginal: ReadonlyMap<string, string>): unknown {
143
+ if (!isPlainObject(tool)) return tool;
144
+ const original = wireToolInnerName(tool);
145
+ if (!original) return tool;
146
+ const wire = wireByOriginal.get(original);
147
+ if (wire === undefined || wire === original) return tool;
148
+ let next: Record<string, unknown> = tool;
149
+ if (typeof tool.name === "string" && tool.name === original) {
150
+ next = { ...next, name: wire };
151
+ }
152
+ if (
153
+ tool.type === "function"
154
+ && isPlainObject(tool.function)
155
+ && typeof tool.function.name === "string"
156
+ && tool.function.name === original
157
+ ) {
158
+ next = { ...next, function: { ...tool.function, name: wire } };
159
+ }
160
+ return next;
161
+ }
162
+
163
+ function rewriteToolChoice(choice: unknown, wireByOriginal: ReadonlyMap<string, string>): unknown {
164
+ if (!isPlainObject(choice)) return choice;
165
+ if ((choice.type === "function" || choice.type === "custom") && typeof choice.name === "string") {
166
+ const wire = wireByOriginal.get(choice.name);
167
+ return wire === undefined || wire === choice.name ? choice : { ...choice, name: wire };
168
+ }
169
+ if (choice.type !== "allowed_tools" || !Array.isArray(choice.tools)) return choice;
170
+ let changed = false;
171
+ const tools = choice.tools.map(tool => {
172
+ if (!isPlainObject(tool) || (tool.type !== "function" && tool.type !== "custom")) return tool;
173
+ if (typeof tool.name !== "string") return tool;
174
+ const wire = wireByOriginal.get(tool.name);
175
+ if (wire === undefined || wire === tool.name) return tool;
176
+ changed = true;
177
+ return { ...tool, name: wire };
178
+ });
179
+ return changed ? { ...choice, tools } : choice;
180
+ }
181
+
182
+ function rewriteInputItem(item: unknown, wireByOriginal: ReadonlyMap<string, string>): unknown {
183
+ if (!isPlainObject(item)) return item;
184
+ if (item.type === "additional_tools" && Array.isArray(item.tools)) {
185
+ let changed = false;
186
+ const tools = item.tools.map(tool => {
187
+ const next = rewriteDeclaredTool(tool, wireByOriginal);
188
+ changed ||= next !== tool;
189
+ return next;
190
+ });
191
+ return changed ? { ...item, tools } : item;
192
+ }
193
+ if (
194
+ (item.type === "function_call" || item.type === "custom_tool_call")
195
+ && typeof item.name === "string"
196
+ ) {
197
+ const wire = wireByOriginal.get(item.name);
198
+ return wire === undefined || wire === item.name ? item : { ...item, name: wire };
199
+ }
200
+ return item;
201
+ }
202
+
203
+ /**
204
+ * Keep restoration inside the caller's per-turn authorization boundary, matching
205
+ * `authorizedAliases` in namespace-tool-compat. Upstream sees every declaration even when
206
+ * `tool_choice` narrows what it may call, so a wire name appearing in that catalog is not
207
+ * on its own evidence that restoring it into an executable client name is permitted.
208
+ * The selector is already rewritten to wire names here, so it compares against alias keys.
209
+ */
210
+ function authorizedMuseAliases(
211
+ aliases: Map<string, string>,
212
+ toolChoice: unknown,
213
+ ): Map<string, string> {
214
+ if (toolChoice === undefined || toolChoice === "auto" || toolChoice === "required") return aliases;
215
+ if (toolChoice === "none" || !isPlainObject(toolChoice)) return new Map();
216
+
217
+ const authorized = new Set<string>();
218
+ if (
219
+ (toolChoice.type === "function" || toolChoice.type === "custom")
220
+ && typeof toolChoice.name === "string"
221
+ ) {
222
+ authorized.add(toolChoice.name);
223
+ } else if (toolChoice.type === "allowed_tools" && Array.isArray(toolChoice.tools)) {
224
+ for (const tool of toolChoice.tools) {
225
+ if (!isPlainObject(tool)) continue;
226
+ if (tool.type !== "function" && tool.type !== "custom") continue;
227
+ if (typeof tool.name === "string") authorized.add(tool.name);
228
+ }
229
+ } else {
230
+ // An explicit selector for another tool kind authorizes no function/custom call.
231
+ return new Map();
232
+ }
233
+
234
+ const kept = new Map<string, string>();
235
+ for (const [wire, original] of aliases) {
236
+ if (authorized.has(wire)) kept.set(wire, original);
237
+ }
238
+ return kept;
239
+ }
240
+
241
+ /**
242
+ * Rewrite function/custom tool identities for the Meta Muse 64-char wire limit.
243
+ * Arguments, user text, and schema property names stay untouched.
244
+ */
245
+ export function rewriteMuseToolNamesForUpstream(body: unknown): {
246
+ body: unknown;
247
+ aliases: Map<string, string>;
248
+ } {
249
+ if (!isPlainObject(body)) return { body, aliases: new Map() };
250
+ const { wireByOriginal, aliases } = buildMuseToolNameAliasPlan(collectMuseToolNames(body));
251
+ if (aliases.size === 0) return { body, aliases };
252
+
253
+ let tools = body.tools;
254
+ if (Array.isArray(body.tools)) {
255
+ let changed = false;
256
+ const rewritten = body.tools.map(tool => {
257
+ const next = rewriteDeclaredTool(tool, wireByOriginal);
258
+ changed ||= next !== tool;
259
+ return next;
260
+ });
261
+ if (changed) tools = rewritten;
262
+ }
263
+
264
+ let input = body.input;
265
+ if (Array.isArray(body.input)) {
266
+ let changed = false;
267
+ const rewritten = body.input.map(item => {
268
+ const next = rewriteInputItem(item, wireByOriginal);
269
+ changed ||= next !== item;
270
+ return next;
271
+ });
272
+ if (changed) input = rewritten;
273
+ }
274
+
275
+ const toolChoice = rewriteToolChoice(body.tool_choice, wireByOriginal);
276
+ // Upstream still receives the whole aliased catalog; only what may be restored narrows.
277
+ const restorable = authorizedMuseAliases(aliases, toolChoice);
278
+ if (tools === body.tools && input === body.input && toolChoice === body.tool_choice) {
279
+ return { body, aliases: restorable };
280
+ }
281
+ return {
282
+ body: {
283
+ ...body,
284
+ ...(tools !== body.tools ? { tools } : {}),
285
+ ...(input !== body.input ? { input } : {}),
286
+ ...(toolChoice !== body.tool_choice ? { tool_choice: toolChoice } : {}),
287
+ },
288
+ aliases: restorable,
289
+ };
290
+ }
291
+
292
+ /**
293
+ * Payload shapes whose `name` is a tool identity the client (and the undeclared-tool guard)
294
+ * reads. `response.function_call_arguments.done` carries the name outside any `function_call`
295
+ * item, so a hashed alias there would still reach the guard as an undeclared tool.
296
+ */
297
+ function isMuseToolIdentityType(type: unknown): boolean {
298
+ return type === "function_call"
299
+ || type === "custom_tool_call"
300
+ || type === "function"
301
+ || type === "custom"
302
+ || type === "response.function_call_arguments.done"
303
+ || type === "response.function_call_arguments.delta";
304
+ }
305
+
306
+ function restoreNamedIdentity(
307
+ value: Record<string, unknown>,
308
+ aliases: MuseToolNameAliases,
309
+ ): { value: Record<string, unknown>; changed: boolean } {
310
+ let restored = value;
311
+ let changed = false;
312
+ if (isMuseToolIdentityType(value.type) && typeof restored.name === "string") {
313
+ const original = aliases.get(restored.name);
314
+ if (original && original !== restored.name) {
315
+ restored = { ...restored, name: original };
316
+ changed = true;
317
+ }
318
+ }
319
+ const fn = restored.function;
320
+ if (
321
+ restored.type === "function"
322
+ && isPlainObject(fn)
323
+ && typeof fn.name === "string"
324
+ ) {
325
+ const original = aliases.get(fn.name);
326
+ if (original && original !== fn.name) {
327
+ restored = { ...restored, function: { ...fn, name: original } };
328
+ changed = true;
329
+ }
330
+ }
331
+ return { value: restored, changed };
332
+ }
333
+
334
+ export function restoreMuseToolNames(
335
+ value: unknown,
336
+ aliases: MuseToolNameAliases,
337
+ ): { value: unknown; changed: boolean } {
338
+ if (aliases.size === 0) return { value, changed: false };
339
+ if (Array.isArray(value)) {
340
+ let changed = false;
341
+ const restored = value.map(entry => {
342
+ const result = restoreMuseToolNames(entry, aliases);
343
+ changed ||= result.changed;
344
+ return result.value;
345
+ });
346
+ return changed ? { value: restored, changed: true } : { value, changed: false };
347
+ }
348
+ if (!isPlainObject(value)) return { value, changed: false };
349
+
350
+ let changed = false;
351
+ const restored: Record<string, unknown> = {};
352
+ for (const [key, entry] of Object.entries(value)) {
353
+ const result = restoreMuseToolNames(entry, aliases);
354
+ restored[key] = result.value;
355
+ changed ||= result.changed;
356
+ }
357
+ const node = changed ? restored : value;
358
+ const identity = restoreNamedIdentity(node, aliases);
359
+ if (identity.changed) return { value: identity.value, changed: true };
360
+ return changed ? { value: restored, changed: true } : { value, changed: false };
361
+ }
362
+
363
+ export function restoreMuseToolNamesInJson(text: string, aliases: MuseToolNameAliases): string {
364
+ if (aliases.size === 0) return text;
365
+ let payload: unknown;
366
+ try {
367
+ payload = JSON.parse(text);
368
+ } catch {
369
+ return text;
370
+ }
371
+ const restored = restoreMuseToolNames(payload, aliases);
372
+ return restored.changed ? JSON.stringify(restored.value) : text;
373
+ }
374
+
375
+ export function createMuseToolNameRestoreRewrite(
376
+ aliases: MuseToolNameAliases,
377
+ ): (payload: string) => string {
378
+ return payload => restoreMuseToolNamesInJson(payload, aliases);
379
+ }