@bitkyc08/opencodex 2.52.0 → 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-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
@@ -25,6 +25,7 @@
25
25
  import { homedir } from "node:os";
26
26
  import { join } from "node:path";
27
27
  import { sanitizeApiKeyValue } from "../providers/api-keys";
28
+ import { MuseDeviceLoginError, loginMetaMuseDevice } from "./meta-muse-device";
28
29
  import type { OAuthController, OAuthCredentials } from "./types";
29
30
 
30
31
  const MUSE_POINTER_PATH = join(homedir(), ".config", "muse", "auth.json");
@@ -43,6 +44,7 @@ const KEYCHAIN_TIMEOUT_MS = 5_000;
43
44
  */
44
45
  const CONSENT_WARNING = [
45
46
  "Meta scopes the Muse Code credential to the Muse Code CLI.",
47
+ "A device login authenticates as Meta own Muse Code client, which is a stronger claim than reusing a key your CLI already minted, and that grant has not been exercised against Meta from OpenCodex.",
46
48
  "Using it here is UNSUPPORTED: Meta does not authorize subscription coverage outside its own CLI,",
47
49
  "how these calls settle is not observable from the API, and you should treat every call as billable.",
48
50
  "The key you import or paste is copied into OpenCodex's auth store (~/.opencodex/auth.json, 0600).",
@@ -65,6 +67,16 @@ export interface MuseImportDeps {
65
67
  readPointer?: () => Promise<string | null>;
66
68
  readKeychain?: (signal?: AbortSignal) => Promise<string | null>;
67
69
  fetchImpl?: typeof fetch;
70
+ /** Injected so login-order tests exercise the order without running a grant. */
71
+ loginDevice?: (ctrl: OAuthController) => Promise<OAuthCredentials>;
72
+ /** Forwarded into the device grant so no test can reach the network or a real timer. */
73
+ sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
74
+ now?: () => number;
75
+ }
76
+
77
+ export interface MuseLoginOptions {
78
+ /** `"off"` skips the Keychain import; add-account and reauth pass it. */
79
+ importLocal?: "fallback" | "off";
68
80
  }
69
81
 
70
82
  async function defaultReadPointer(): Promise<string | null> {
@@ -111,9 +123,6 @@ async function defaultReadKeychain(signal?: AbortSignal): Promise<string | null>
111
123
  }
112
124
  }
113
125
 
114
- const INSTALL_HINT =
115
- "Install it from https://dev.meta.ai/install.sh, run `muse login`, then retry.";
116
-
117
126
  /**
118
127
  * Where a user without the CLI gets a key by hand.
119
128
  *
@@ -165,40 +174,62 @@ function normalizedEmail(value: unknown): string | undefined {
165
174
  export async function loginMetaMuse(
166
175
  ctrl: OAuthController = {},
167
176
  deps: MuseImportDeps = {},
177
+ options: MuseLoginOptions = {},
168
178
  ): Promise<OAuthCredentials> {
169
179
  // Before ANY read: the CLI has no other warning surface.
170
180
  ctrl.onProgress?.(CONSENT_WARNING);
171
181
 
172
182
  const platform = deps.platform ?? process.platform;
173
- // Off darwin there is no store this importer can read: Meta ships no native Windows
174
- // CLI, and the Linux credential shape has never been measured. That is a limitation
175
- // of the IMPORT, not of the platform ??the same key is visible in Meta's console ??
176
- // so these hosts get a paste field instead of a dead end. The pasted key then goes
177
- // through the identical grammar check and live validation as an imported one.
178
- if (platform !== "darwin") {
179
- // The two platforms are unavailable for DIFFERENT reasons, and saying so matters:
180
- // Meta ships no Windows build at all, while the Linux CLI exists and only its
181
- // credential storage is unmeasured. Collapsing them into "no CLI here" would tell
182
- // a Linux user something false about their own machine.
183
- const reason = platform === "win32"
184
- ? "Meta ships no native Windows Muse Code CLI, so there is no credential to import."
185
- : "The Muse Code CLI runs here, but where it stores its credential has not been measured, "
186
- + "so importing one is refused rather than guessed.";
183
+
184
+ // Import first on darwin, and only for a plain login. A user who already ran `muse
185
+ // login` keeps the zero-interaction path, and a device grant ends in a mint against a
186
+ // rate-limited endpoint, so starting one while a working credential sits on disk spends
187
+ // a request to arrive at the same key. `forceLogin` maps to "off" upstream because
188
+ // reimporting is how an add-account silently re-adds the account the user already has.
189
+ if (platform === "darwin" && options.importLocal !== "off") {
190
+ const imported = await importFromKeychain(ctrl, deps);
191
+ if (imported) return imported;
192
+ }
193
+
194
+ // fetchImpl/sleep/now are FORWARDED. Without this the device module would carry its own
195
+ // deps object and any test reaching this path would call auth.meta.com for real.
196
+ const device = deps.loginDevice
197
+ ?? (c => loginMetaMuseDevice(c, {
198
+ ...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}),
199
+ ...(deps.sleep ? { sleep: deps.sleep } : {}),
200
+ ...(deps.now ? { now: deps.now } : {}),
201
+ }));
202
+ try {
203
+ ctrl.onProgress?.("Starting the Meta device login...");
204
+ return await device(ctrl);
205
+ } catch (error) {
206
+ // A cancelled login must never be answered with a paste prompt.
207
+ if (isCancellation(error, ctrl)) throw error;
208
+ const reason = deviceFailureReason(error);
187
209
  const pasted = await manualKeyCredential(ctrl, reason);
188
- if (pasted === null) {
189
- throw new Error(
190
- `${reason} This client cannot prompt for a key, so run \`ocx login meta-muse\` from the CLI `
191
- + `or the dashboard and paste yours from ${MANUAL_KEY_URL}, `
192
- + "or use the meta-model provider with your own key (META_MODEL_API_KEY).",
193
- );
194
- }
210
+ // No paste surface, or an empty paste. The refusal KEEPS the guidance the previous
211
+ // implementation gave; a device error must not replace it.
212
+ if (pasted === null) throw noPasteSurfaceError(platform, reason);
195
213
  return await validatedMetaMuseCredential(pasted, ctrl, deps, undefined, "manual");
196
214
  }
215
+ }
197
216
 
217
+ /**
218
+ * Adopt the credential the Muse Code CLI already holds, or report that there is none.
219
+ *
220
+ * Returns null for the two conditions that genuinely mean "nothing here to import", so the
221
+ * caller can offer a device grant instead of a dead end. Everything else still throws: a
222
+ * corrupt file, an unmeasured storage backend, a Keychain read that times out, and a
223
+ * Keychain entry that is unreadable or carries no usable key are all cases where a
224
+ * credential probably EXISTS, and silently starting a browser grant would create a second
225
+ * login to work around a permissions dialog.
226
+ */
227
+ async function importFromKeychain(
228
+ ctrl: OAuthController,
229
+ deps: MuseImportDeps,
230
+ ): Promise<OAuthCredentials | null> {
198
231
  const pointerRaw = await (deps.readPointer ?? defaultReadPointer)();
199
- if (pointerRaw === null) {
200
- throw new Error(`Muse Code CLI credential not found at ${MUSE_POINTER_PATH}. ${INSTALL_HINT}`);
201
- }
232
+ if (pointerRaw === null) return null;
202
233
 
203
234
  let pointer: MusePointer;
204
235
  try {
@@ -208,9 +239,7 @@ export async function loginMetaMuse(
208
239
  }
209
240
 
210
241
  const meta = pointer.providers?.meta;
211
- if (!meta || meta.mechanism !== "oauth") {
212
- throw new Error("The Muse Code credential file has no signed-in Meta account. Run `muse login`, then retry.");
213
- }
242
+ if (!meta || meta.mechanism !== "oauth") return null;
214
243
  // A different storage backend is a shape we have not measured; refuse rather than guess.
215
244
  if (meta.storage !== "keychain") {
216
245
  throw new Error(
@@ -218,10 +247,12 @@ export async function loginMetaMuse(
218
247
  );
219
248
  }
220
249
 
250
+ // ctrl.signal is handed to the reader, not dropped: the caller can cancel a Keychain
251
+ // prompt nobody is going to answer.
221
252
  const secretRaw = await (deps.readKeychain ?? defaultReadKeychain)(ctrl.signal);
222
253
  if (secretRaw === null) {
223
254
  throw new Error(
224
- "Could not read the Muse Code credential from the macOS Keychain within 5s. Approve the Keychain prompt, or run `muse login` again.",
255
+ "Could not read the Muse Code credential from the macOS Keychain within 5s. Approve the Keychain prompt, or run `muse login` again, or add the account again to sign in with a browser code instead.",
225
256
  );
226
257
  }
227
258
 
@@ -242,6 +273,57 @@ export async function loginMetaMuse(
242
273
  );
243
274
  }
244
275
 
276
+ /** A cancelled login is not a failed login, and must not be answered with a prompt. */
277
+ function isCancellation(error: unknown, ctrl: OAuthController): boolean {
278
+ if (ctrl.signal?.aborted) return true;
279
+ if (error instanceof MuseDeviceLoginError) return error.kind === "cancelled";
280
+ return error instanceof Error && error.name === "AbortError";
281
+ }
282
+
283
+ /** One line above the paste field saying why it appeared. Never includes a credential. */
284
+ function deviceFailureReason(error: unknown): string {
285
+ if (error instanceof MuseDeviceLoginError) {
286
+ switch (error.kind) {
287
+ case "subscription-inactive":
288
+ return "This Meta account has no active Muse Code subscription.";
289
+ case "entitlement-required":
290
+ return error.message;
291
+ case "mint-rate-limited":
292
+ return "Meta rate-limited the Muse Code key request.";
293
+ case "device-denied":
294
+ return "The browser approval was denied.";
295
+ case "device-expired":
296
+ return "The device code expired before it was approved.";
297
+ default:
298
+ return "The Meta device login did not complete.";
299
+ }
300
+ }
301
+ return "The Meta device login did not complete.";
302
+ }
303
+
304
+ /**
305
+ * The refusal for a host that can neither finish a grant nor accept a paste.
306
+ *
307
+ * Composed, not substituted. A host in this position needs to be told where the key lives,
308
+ * which is what the previous implementation said and what three existing tests assert. The
309
+ * non-darwin clause is retained for the same reason: Meta ships no Windows CLI and the
310
+ * Linux credential shape has never been measured, and collapsing those into one message
311
+ * would tell a Linux user something false about their own machine.
312
+ */
313
+ function noPasteSurfaceError(platform: string, reason: string): Error {
314
+ const platformClause = platform === "darwin"
315
+ ? ""
316
+ : platform === "win32"
317
+ ? " Meta ships no native Windows Muse Code CLI, so there is no credential to import."
318
+ : " The Muse Code CLI runs here, but where it stores its credential has not been measured, "
319
+ + "so importing one is refused rather than guessed.";
320
+ return new Error(
321
+ `${reason}${platformClause} This client cannot prompt for a key, so run \`ocx login meta-muse\` from the CLI `
322
+ + `or the dashboard and paste yours from ${MANUAL_KEY_URL}, `
323
+ + "or use the meta-model provider with your own key (META_MODEL_API_KEY).",
324
+ );
325
+ }
326
+
245
327
  /**
246
328
  * The single gate every credential passes, imported or pasted.
247
329
  *
@@ -332,6 +414,15 @@ export async function refreshMetaMuseToken(
332
414
  access: apiKey,
333
415
  refresh: apiKey,
334
416
  expires: Number.MAX_SAFE_INTEGER,
335
- source: credential?.source === "manual" ? "manual" : "local-cli",
417
+ // Preserve the provenance the slot already recorded. A device login is "oauth";
418
+ // relabelling it "local-cli" would misreport where the credential came from, the same
419
+ // failure this function comment already warns about for "manual".
420
+ source: credential?.source === "manual" || credential?.source === "oauth"
421
+ ? credential.source
422
+ : "local-cli",
423
+ // The account token is not re-derivable: Meta rejects refresh_token grants on this
424
+ // client (001 A). Dropping it here would silently cost the on-demand quota probe with
425
+ // no way back except a full re-login.
426
+ ...(credential?.muse ? { muse: credential.muse } : {}),
336
427
  };
337
428
  }
@@ -41,6 +41,15 @@ export function parseAccountPoolStrategy(raw: unknown): OcxAccountPoolRotationSt
41
41
  return null;
42
42
  }
43
43
 
44
+ /** Codex alone supports ordering by the next shared quota reset. */
45
+ export function parseCodexAccountPoolStrategy(raw: unknown): OcxAccountPoolRotationStrategy | "reset-first" | null {
46
+ return raw === "reset-first" ? raw : parseAccountPoolStrategy(raw);
47
+ }
48
+
49
+ export function normalizeCodexAccountPoolStrategy(raw: unknown): OcxAccountPoolRotationStrategy | "reset-first" {
50
+ return parseCodexAccountPoolStrategy(raw) ?? DEFAULT_STRATEGY;
51
+ }
52
+
44
53
  /** Strict parse for management APIs — returns null instead of defaulting. */
45
54
  export function parseAccountPoolStickyLimit(raw: unknown): number | null {
46
55
  if (typeof raw === "number" && Number.isInteger(raw) && raw >= MIN_STICKY_LIMIT && raw <= MAX_STICKY_LIMIT) {
@@ -1,5 +1,5 @@
1
1
  import { isGenericFailoverProvider } from "./generic-account-failover";
2
- import { parseAccountPoolStickyLimit, parseAccountPoolStrategy } from "./pool-kernel";
2
+ import { parseAccountPoolStickyLimit, parseAccountPoolStrategy, parseCodexAccountPoolStrategy } from "./pool-kernel";
3
3
  import type { OcxConfig, OcxProviderConfig } from "../types";
4
4
 
5
5
  /**
@@ -147,7 +147,7 @@ export function unifiedPoolSettingsDto(
147
147
  // honest answer is "not a field here" rather than a fabricated true.
148
148
  enabled: null,
149
149
  enabledEffective: true,
150
- strategy: parseGenericPoolStrategy(config.accountPoolStrategy) ?? "quota",
150
+ strategy: parseCodexAccountPoolStrategy(config.accountPoolStrategy) ?? "quota",
151
151
  stickyLimit: parseGenericStickyLimit(config.accountPoolStickyLimit) ?? 1,
152
152
  autoSwitchThreshold: parseGenericAutoSwitchThreshold(config.autoSwitchThreshold) ?? 80,
153
153
  quotaWindow: null,
@@ -490,6 +490,30 @@ function normalizeCredential(cred: unknown): OAuthCredentials | null {
490
490
  };
491
491
  }
492
492
  }
493
+ if (candidate.muse && typeof candidate.muse === "object") {
494
+ const muse = candidate.muse;
495
+ const cleanMuse = (value: unknown, max: number): string | undefined => {
496
+ if (typeof value !== "string") return undefined;
497
+ const trimmed = value.trim();
498
+ return trimmed && trimmed.length <= max && !/[\x00-\x1f\x7f]/.test(trimmed) ? trimmed : undefined;
499
+ };
500
+ const oauthAccessToken = cleanMuse(muse.oauthAccessToken, 4096);
501
+ const userId = cleanMuse(muse.userId, 128);
502
+ const tierName = cleanMuse(muse.tierName, 128);
503
+ const mintedAt = typeof muse.mintedAt === "number" && Number.isFinite(muse.mintedAt)
504
+ ? muse.mintedAt
505
+ : undefined;
506
+ // oauthAccessToken is the only load-bearing member: without it there is nothing to
507
+ // mint or probe with, and a row carrying only a tier label would be noise.
508
+ if (oauthAccessToken) {
509
+ normalized.muse = {
510
+ oauthAccessToken,
511
+ ...(userId ? { userId } : {}),
512
+ ...(tierName ? { tierName } : {}),
513
+ ...(mintedAt !== undefined ? { mintedAt } : {}),
514
+ };
515
+ }
516
+ }
493
517
  return normalized;
494
518
  }
495
519
 
@@ -1024,6 +1048,39 @@ export async function replaceProviderAccountSet(
1024
1048
  }, [provider, set]);
1025
1049
  }
1026
1050
 
1051
+ export type ProviderCredentialRekeyOutcome = "moved" | "absent" | "conflict";
1052
+
1053
+ /**
1054
+ * Move a provider's whole account set to a different provider key.
1055
+ *
1056
+ * Used by provider-id merge migrations (e.g. devin-cli -> devin): the
1057
+ * credential itself stays valid under the canonical id, only the slot name is
1058
+ * stale. The move goes through mutateStore so it takes the same file lock and
1059
+ * revision bookkeeping as every other auth.json write.
1060
+ *
1061
+ * Both slots occupied is a REFUSAL, not a merge: two account sets may belong
1062
+ * to different humans, and picking a survivor is a user decision, so the
1063
+ * outcome is reported and both are left in place.
1064
+ *
1065
+ * Orphaned auth.refresh.<provider>.<hash>.json intent files are not moved.
1066
+ * They are keyed by provider name plus an account-id hash, so a file left
1067
+ * under the old id simply never matches a lookup again — harmless litter, and
1068
+ * rewriting them would have to guess at an intent's in-flight state anyway.
1069
+ */
1070
+ export async function rekeyProviderCredentials(
1071
+ from: string,
1072
+ to: string,
1073
+ ): Promise<ProviderCredentialRekeyOutcome> {
1074
+ return await mutateStore(store => {
1075
+ const source = store[from];
1076
+ if (!source) return "absent";
1077
+ if (store[to]) return "conflict";
1078
+ store[to] = source;
1079
+ delete store[from];
1080
+ return "moved";
1081
+ }, [from, to]);
1082
+ }
1083
+
1027
1084
  export async function markAccountNeedsReauth(
1028
1085
  provider: string,
1029
1086
  accountId: string,
@@ -25,6 +25,35 @@ export interface KiroOAuthMetadata {
25
25
  authType?: KiroCredentialAuthType;
26
26
  }
27
27
 
28
+ /**
29
+ * Account-scoped Muse Code data that is NOT the request bearer.
30
+ *
31
+ * The Model API is authenticated by the `LLM|` key in `access`; this token authenticates
32
+ * the Meta ACCOUNT and exists only to mint that key and to read subscription usage
33
+ * (devlog/_plan/260912_muse_device_oauth/002 A). Keeping it out of `access` is what lets
34
+ * every request path stay unchanged.
35
+ *
36
+ * It must never be added to `OAuthAccountSummary` (src/oauth/index.ts:1803) or to
37
+ * `OAuthAccessSnapshot` (src/oauth/index.ts:85-100). Both are hand-built allowlists, and
38
+ * that construction — not a redactor — is what keeps a secret out of a response.
39
+ */
40
+ export interface MuseOAuthMetadata {
41
+ /** Meta account access token from the device grant. Never sent to api.meta.ai/v1. */
42
+ oauthAccessToken: string;
43
+ /**
44
+ * The stable Meta account id. Kept HERE rather than in `accountId` on purpose (wp2
45
+ * audit fold W2): the store keys a slot on `accountId ?? email`, and this provider
46
+ * import path has always supplied email only. Promoting `user_id` to `accountId` would
47
+ * make a device login fail to match the row an imported login already created, giving
48
+ * one human two accounts.
49
+ */
50
+ userId?: string;
51
+ /** Epoch ms of the mint that produced the stored key. */
52
+ mintedAt?: number;
53
+ /** Subscription tier label as Meta reported it. Display only. */
54
+ tierName?: string;
55
+ }
56
+
28
57
  export type OAuthCredentials = {
29
58
  refresh: string;
30
59
  access: string;
@@ -42,6 +71,8 @@ export type OAuthCredentials = {
42
71
  apiBaseUrl?: string;
43
72
  /** Never returned by management APIs; persisted only inside the protected auth-store boundary. */
44
73
  kiro?: KiroOAuthMetadata;
74
+ /** Never returned by management APIs; persisted only inside the protected auth-store boundary. */
75
+ muse?: MuseOAuthMetadata;
45
76
  };
46
77
 
47
78
  /** One logged-in account inside a provider's account set (multiauth). */
@@ -44,6 +44,7 @@ export interface DerivedKeyLoginProvider {
44
44
  autoToolChoiceOnlyModels?: string[];
45
45
  preserveReasoningContentModels?: string[];
46
46
  requiresReasoningPlaceholderModels?: string[];
47
+ showThinkingSummary?: boolean;
47
48
  reasoningSplitModels?: string[];
48
49
  reasoningDetailsModels?: string[];
49
50
  thinkingToggleModels?: string[];
@@ -271,6 +272,7 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
271
272
  ...(entry.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...entry.autoToolChoiceOnlyModels] } : {}),
272
273
  ...(entry.preserveReasoningContentModels ? { preserveReasoningContentModels: [...entry.preserveReasoningContentModels] } : {}),
273
274
  ...(entry.requiresReasoningPlaceholderModels ? { requiresReasoningPlaceholderModels: [...entry.requiresReasoningPlaceholderModels] } : {}),
275
+ ...(entry.showThinkingSummary !== undefined ? { showThinkingSummary: entry.showThinkingSummary } : {}),
274
276
  ...(entry.reasoningSplitModels ? { reasoningSplitModels: [...entry.reasoningSplitModels] } : {}),
275
277
  ...(entry.reasoningDetailsModels ? { reasoningDetailsModels: [...entry.reasoningDetailsModels] } : {}),
276
278
  ...(entry.thinkingToggleModels ? { thinkingToggleModels: [...entry.thinkingToggleModels] } : {}),
@@ -320,6 +322,7 @@ export function deriveKeyLoginMap(): Record<string, DerivedKeyLoginProvider> {
320
322
  ...(entry.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...entry.autoToolChoiceOnlyModels] } : {}),
321
323
  ...(entry.preserveReasoningContentModels ? { preserveReasoningContentModels: [...entry.preserveReasoningContentModels] } : {}),
322
324
  ...(entry.requiresReasoningPlaceholderModels ? { requiresReasoningPlaceholderModels: [...entry.requiresReasoningPlaceholderModels] } : {}),
325
+ ...(entry.showThinkingSummary !== undefined ? { showThinkingSummary: entry.showThinkingSummary } : {}),
323
326
  ...(entry.reasoningSplitModels ? { reasoningSplitModels: [...entry.reasoningSplitModels] } : {}),
324
327
  ...(entry.reasoningDetailsModels ? { reasoningDetailsModels: [...entry.reasoningDetailsModels] } : {}),
325
328
  ...(entry.thinkingToggleModels ? { thinkingToggleModels: [...entry.thinkingToggleModels] } : {}),
@@ -441,13 +444,19 @@ function applyVerbosityDefaults(prov: OcxProviderConfig, entry: ProviderRegistry
441
444
  * was skipped and the reasoning ladder was advertised without summary support — exactly the
442
445
  * inconsistency that makes Codex drop the inbound reasoning object.
443
446
  *
444
- * Deliberately narrow: only the reasoning-summary map, and only via
447
+ * Deliberately narrow: reasoning-summary and effort metadata only, via
445
448
  * `registryEntryForProviderDestination`, which matches fixed key destinations and refuses
446
449
  * templated or overridable base URLs. A custom row keeps its own identity for everything else.
447
450
  */
448
- function enrichReasoningSummariesByDestination(prov: OcxProviderConfig): void {
451
+ function enrichReasoningMetadataByDestination(prov: OcxProviderConfig): void {
449
452
  const destination = registryEntryForProviderDestination(prov);
450
453
  applyReasoningSummaryDefaults(prov, destination?.modelSupportsReasoningSummaries);
454
+ if (destination?.modelReasoningEfforts) {
455
+ prov.modelReasoningEfforts = fillRecordOfArrays(destination.modelReasoningEfforts, prov.modelReasoningEfforts);
456
+ }
457
+ if (prov.reasoningEfforts === undefined && destination?.reasoningEfforts !== undefined) {
458
+ prov.reasoningEfforts = [...destination.reasoningEfforts];
459
+ }
451
460
  }
452
461
 
453
462
  /** Repair the exact low-only ClinePass ladder generated by older key-login presets. */
@@ -467,7 +476,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
467
476
  // `registryEntryForProviderDestination` answers the question that actually matters here —
468
477
  // which vendor endpoint is this row talking to — and is already restricted to fixed key
469
478
  // destinations, so a templated or overridable base URL cannot be claimed by it.
470
- enrichReasoningSummariesByDestination(prov);
479
+ enrichReasoningMetadataByDestination(prov);
471
480
  applyServiceTierModelDefaults(prov, serviceTierModelDefaultsFor(registryEntryForProviderDestination(prov), prov));
472
481
  applyVerbosityDefaults(prov, registryEntryForProviderDestination(prov));
473
482
  return;
@@ -574,6 +583,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
574
583
  if (!prov.thinkingToggleModels && seed.thinkingToggleModels) prov.thinkingToggleModels = [...seed.thinkingToggleModels];
575
584
  if (!prov.thinkingBudgetModels && seed.thinkingBudgetModels) prov.thinkingBudgetModels = [...seed.thinkingBudgetModels];
576
585
  if (prov.escapeBuiltinToolNames === undefined && seed.escapeBuiltinToolNames !== undefined) prov.escapeBuiltinToolNames = seed.escapeBuiltinToolNames;
586
+ if (prov.showThinkingSummary === undefined && seed.showThinkingSummary !== undefined) prov.showThinkingSummary = seed.showThinkingSummary;
577
587
  if (prov.keyOptional === undefined && seed.keyOptional !== undefined) prov.keyOptional = seed.keyOptional;
578
588
  if (prov.freeTier === undefined && seed.freeTier !== undefined) prov.freeTier = seed.freeTier;
579
589
  if (prov.modelSuffixBracketStrip === undefined && seed.modelSuffixBracketStrip !== undefined) prov.modelSuffixBracketStrip = seed.modelSuffixBracketStrip;
@@ -1,23 +1,33 @@
1
1
  /**
2
- * Repair a saved `devin-cli` row that still claims `authMode: "local"`.
2
+ * Repair saved Devin rows written while `devin-cli` was a local ACP provider.
3
3
  *
4
- * `derive.ts` seeds `authMode` from the registry's `authKind`, so every config
5
- * written while `devin-cli` was a local provider carries `"local"`. The registry
6
- * now classifies it as an account provider, and the management write boundary
7
- * fails closed on that mismatch: `auth-cors.ts` rejects `authMode: "local"` when
8
- * the registry entry is not local. Without this, an existing install can no
9
- * longer save provider changes from the dashboard.
4
+ * `devin-cli` used to be a local provider whose adapter spawned `devin acp`;
5
+ * it later became an account provider on the shared `devin` adapter, and the
6
+ * registry id itself has since merged into `devin` outright
7
+ * (devlog/_plan/260913_devin_provider_merge).
10
8
  *
11
- * It rewrites exactly that one field, on exactly that mismatch. A row the user
12
- * retargeted at another adapter is left alone, and so is any other value.
9
+ * Only the adapter repair still lives here. The `authMode` half — rewriting
10
+ * `"local"` to `"oauth"` on the registry-id row — moved into
11
+ * devin-provider-merge-migration.ts, which normalizes the row as it moves the
12
+ * key: the `devin-cli` registry entry is gone, so the PROVIDER_REGISTRY lookup
13
+ * that gated the rewrite here could never fire again.
13
14
  *
14
- * It also WARNS, without changing anything, when the saved row still names the
15
- * ACP adapter. That field no longer selects the transport — `routedProviderConfig`
16
- * pins the adapter from the registry for any row whose name is a registry id — so
17
- * an operator who deliberately chose ACP would otherwise switch transports
18
- * silently. The warning names the custom-id row that still gets them ACP.
15
+ * The adapter repair must keep working for CUSTOM-NAMED rows. The ACP adapter
16
+ * was removed, so `"devin-cli"` is not a constructible adapter id and
17
+ * `createRegisteredAdapter` would throw `Unknown adapter: devin-cli`. The
18
+ * canonical `devin` row gets its adapter pinned from the registry by
19
+ * `routedProviderConfig`, but a row named e.g. `"devin-acp"` has nothing
20
+ * pinning it and would fail every request. So every row naming the retired
21
+ * adapter is rewritten to `devin`, whatever the row is called, and each
22
+ * rewrite is reported.
23
+ *
24
+ * A row carrying the retired ACP identity URL is repointed at the api-server
25
+ * in the same pass. That URL was never a destination — it existed only so
26
+ * provider validation would accept an `http(s)` scheme for a child process —
27
+ * so leaving it in place would turn a constructible adapter into a request
28
+ * that cannot resolve a host.
19
29
  */
20
- import { PROVIDER_REGISTRY } from "./registry";
30
+ import { DEVIN_DEFAULT_API_SERVER } from "../oauth/devin/api-base";
21
31
  import type { OcxConfig } from "../types";
22
32
 
23
33
  export interface DevinCliAuthModeProjection {
@@ -26,32 +36,44 @@ export interface DevinCliAuthModeProjection {
26
36
  warnings: string[];
27
37
  }
28
38
 
29
- const ACP_ESCAPE_HATCH =
30
- 'a custom-named row still reaches it, e.g. "devin-acp": { "adapter": "devin-cli", "baseUrl": "https://cli.devin.ai" }';
39
+ /** The adapter id the removed ACP transport was registered under. */
40
+ const RETIRED_ACP_ADAPTER = "devin-cli";
41
+ /** Identity-only URL the ACP rows carried; never a destination. */
42
+ const RETIRED_ACP_IDENTITY_HOST = "cli.devin.ai";
43
+
44
+ /** Exported for the merge migration, which applies the same rule to the row it moves. */
45
+ export function isRetiredDevinAcpIdentityUrl(baseUrl: string | undefined): boolean {
46
+ if (typeof baseUrl !== "string") return false;
47
+ try {
48
+ return new URL(baseUrl).hostname.toLowerCase() === RETIRED_ACP_IDENTITY_HOST;
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
31
53
 
32
54
  export function projectDevinCliAuthMode(config: OcxConfig): DevinCliAuthModeProjection {
33
55
  const warnings: string[] = [];
34
- const prov = config.providers?.["devin-cli"];
35
- if (!prov) return { config, changed: false, warnings };
36
-
37
- const entry = PROVIDER_REGISTRY.find(row => row.id === "devin-cli");
38
- if (!entry || entry.authKind !== "oauth") return { config, changed: false, warnings };
56
+ let changed = false;
39
57
 
40
- // Non-mutating: the saved adapter no longer decides the transport, so leaving
41
- // it in place preserves nothing except a signal worth reporting once.
42
- if (prov.adapter === "devin-cli") {
58
+ // Every row, not only the registry id: a custom-named row had no registry pin,
59
+ // so after the ACP removal it is the one that cannot construct an adapter.
60
+ for (const [name, row] of Object.entries(config.providers ?? {})) {
61
+ if (!row || row.adapter !== RETIRED_ACP_ADAPTER) continue;
62
+ row.adapter = "devin";
63
+ changed = true;
64
+ let detail = "";
65
+ if (isRetiredDevinAcpIdentityUrl(row.baseUrl)) {
66
+ // The shared default, not a second copy of the host: a migration that
67
+ // hardcodes it would keep writing the old address after the default moves.
68
+ row.baseUrl = DEVIN_DEFAULT_API_SERVER;
69
+ detail = ` and repointed its baseUrl at ${DEVIN_DEFAULT_API_SERVER}`;
70
+ }
43
71
  warnings.push(
44
- 'the saved "devin-cli" row names the ACP adapter, but that provider id now streams over '
45
- + `Cognition's api-server and the adapter is pinned from the registry; for the CLI's own agent loop, ${ACP_ESCAPE_HATCH}.`,
72
+ `rewrote "${name}" adapter ${RETIRED_ACP_ADAPTER} -> devin${detail}: the local ACP transport `
73
+ + "was removed, and Devin now streams over Cognition's api-server with the credential the "
74
+ + "installed CLI already holds.",
46
75
  );
47
76
  }
48
77
 
49
- if (prov.authMode !== "local") return { config, changed: warnings.length > 0 ? false : false, warnings };
50
- prov.authMode = "oauth";
51
- warnings.push(
52
- 'rewrote "devin-cli" authMode local -> oauth: the registry no longer classifies it as local, '
53
- + "and the management write boundary fails closed on the mismatch.",
54
- );
55
- return { config, changed: true, warnings };
78
+ return { config, changed, warnings };
56
79
  }
57
-