@bitkyc08/opencodex 2.67.0 → 2.68.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 (257) hide show
  1. package/bin/ocx.mjs +3 -0
  2. package/gui/dist/assets/App-DCBismRi.css +1 -0
  3. package/gui/dist/assets/App-mpsibsFj.js +51 -0
  4. package/gui/dist/assets/Tray-Bn8WhKBH.js +1 -0
  5. package/gui/dist/assets/{index-DtNmX7hW.css → index-BDUBS8PW.css} +1 -1
  6. package/gui/dist/assets/index-kUE8tuAK.js +86 -0
  7. package/gui/dist/assets/quota-summary-DL-bNuNL.js +1 -0
  8. package/gui/dist/index.html +21 -2
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic-image-guard.ts +13 -1
  11. package/src/adapters/anthropic-image-normalize.ts +3 -2
  12. package/src/adapters/anthropic-output-schema.ts +36 -0
  13. package/src/adapters/anthropic.ts +10 -2
  14. package/src/adapters/base.ts +7 -0
  15. package/src/adapters/codebuddy/live-models.ts +180 -0
  16. package/src/adapters/coding-agent/protocol.ts +91 -12
  17. package/src/adapters/coding-agent/turn.ts +32 -27
  18. package/src/adapters/devin/cloud-direct/index.ts +1 -0
  19. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +18 -4
  20. package/src/adapters/devin.ts +9 -6
  21. package/src/adapters/google-http.ts +4 -4
  22. package/src/adapters/google.ts +93 -3
  23. package/src/adapters/kiro/adapter.ts +6 -2
  24. package/src/adapters/kiro/stream.ts +12 -2
  25. package/src/adapters/kiro/usage.ts +3 -1
  26. package/src/adapters/kiro-errors.ts +11 -1
  27. package/src/adapters/kiro-events.ts +27 -1
  28. package/src/adapters/kiro-refusal.ts +30 -0
  29. package/src/adapters/kiro-retry.ts +71 -31
  30. package/src/adapters/openai-chat/deepseek-artifact-schema.ts +45 -0
  31. package/src/adapters/openai-chat/messages.ts +31 -4
  32. package/src/adapters/openai-chat/serialized-tool-call-content.ts +52 -7
  33. package/src/adapters/openai-chat/tool-call-id-remint.ts +65 -0
  34. package/src/adapters/openai-chat/tool-schema.ts +6 -2
  35. package/src/adapters/openai-chat.ts +2 -2
  36. package/src/adapters/openai-responses/muse-tool-choice.ts +31 -0
  37. package/src/adapters/openai-responses/passthrough.ts +9 -3
  38. package/src/adapters/opencode-go-additional-tools.ts +19 -10
  39. package/src/adapters/physical-send.ts +10 -3
  40. package/src/adapters/registry.ts +2 -1
  41. package/src/adapters/responses-tool-schema.ts +31 -3
  42. package/src/adapters/run-turn-queue.ts +63 -23
  43. package/src/adapters/unique-tool-call-ids.ts +63 -0
  44. package/src/adapters/xai-web-search.ts +32 -2
  45. package/src/bridge/sse.ts +14 -2
  46. package/src/chat/inbound.ts +20 -6
  47. package/src/claude/agents-inject.ts +4 -0
  48. package/src/claude/desktop-3p.ts +53 -13
  49. package/src/claude/desktop-profile.ts +41 -4
  50. package/src/claude/inbound-model-options.ts +14 -2
  51. package/src/claude/inbound.ts +1 -1
  52. package/src/claude/intercept/connect-proxy.ts +17 -1
  53. package/src/claude/intercept/local-ca.ts +7 -1
  54. package/src/claude/message-threads.ts +28 -0
  55. package/src/cli/account-api.ts +13 -0
  56. package/src/cli/account-auth.ts +49 -2
  57. package/src/cli/account-extended.ts +4 -2
  58. package/src/cli/account.ts +4 -2
  59. package/src/cli/capabilities.ts +17 -0
  60. package/src/cli/claude.ts +3 -1
  61. package/src/cli/dispatch.ts +62 -5
  62. package/src/cli/index.ts +84 -79
  63. package/src/cli/minimax.ts +4 -2
  64. package/src/cli/opencode.ts +6 -1
  65. package/src/cli/restart-handoff.ts +110 -0
  66. package/src/cli/status.ts +51 -0
  67. package/src/client/connect.ts +34 -15
  68. package/src/client/link-ingress.ts +102 -0
  69. package/src/client/link-join.ts +37 -16
  70. package/src/client/link-relay.ts +228 -45
  71. package/src/client/link-state.ts +54 -0
  72. package/src/client/link-status.ts +39 -0
  73. package/src/client/link-teardown.ts +2 -2
  74. package/src/client/link-tunnel.ts +615 -86
  75. package/src/client/machine-api.ts +10 -4
  76. package/src/client/machine-listener.ts +57 -10
  77. package/src/client/runtime.ts +210 -35
  78. package/src/clients/config-export/constants.ts +1 -12
  79. package/src/clients/config-export/contracts.ts +2 -0
  80. package/src/clients/config-export/model-metadata.ts +9 -3
  81. package/src/clients/config-export/omp.ts +1 -1
  82. package/src/clients/config-export/zcode-store.ts +2 -2
  83. package/src/clients/config-export.ts +36 -3
  84. package/src/codex/app-server-processes.ts +32 -0
  85. package/src/codex/app-server-restart-service.ts +20 -3
  86. package/src/codex/catalog/access-programs.ts +50 -0
  87. package/src/codex/catalog/build-entries.ts +13 -0
  88. package/src/codex/catalog/derive-entry.ts +1 -0
  89. package/src/codex/catalog/model-hints.ts +11 -0
  90. package/src/codex/catalog/parsing.ts +6 -2
  91. package/src/codex/catalog/provider-models.ts +81 -0
  92. package/src/codex/catalog/retained-sync.ts +3 -0
  93. package/src/codex/codex-write-lock.ts +2 -2
  94. package/src/codex/convergence.ts +2 -0
  95. package/src/codex/desired-state.ts +14 -4
  96. package/src/codex/home.ts +21 -3
  97. package/src/codex/inject/restore.ts +17 -0
  98. package/src/codex/inject/routing-classify.ts +3 -2
  99. package/src/codex/inject-coordination.ts +4 -1
  100. package/src/codex/inject.ts +8 -1
  101. package/src/codex/journal.ts +6 -2
  102. package/src/codex/management-convergence.ts +9 -0
  103. package/src/codex/model-entitlements.ts +28 -5
  104. package/src/codex/routing/idle-window.ts +58 -0
  105. package/src/codex/routing-drift.ts +134 -0
  106. package/src/codex/routing-healer.ts +419 -0
  107. package/src/codex/routing.ts +10 -0
  108. package/src/codex/runtime.ts +188 -57
  109. package/src/codex/sibling-handoff.ts +71 -0
  110. package/src/codex/sibling-start.ts +139 -0
  111. package/src/codex/sync.ts +5 -3
  112. package/src/combos/failover.ts +32 -5
  113. package/src/combos/request.ts +11 -3
  114. package/src/combos/reset-window.ts +10 -5
  115. package/src/combos/resolve.ts +36 -6
  116. package/src/config/diagnostics.ts +2 -1
  117. package/src/config/load-degrade.ts +2 -0
  118. package/src/config/paths.ts +7 -1
  119. package/src/config/process-state.ts +10 -1
  120. package/src/config/schema/compaction-recovery.ts +15 -0
  121. package/src/config/schema/config-schema.ts +2 -0
  122. package/src/config/schema/leaf-validators.ts +9 -3
  123. package/src/generated/compatibility-version.json +360 -204
  124. package/src/images/loop.ts +47 -32
  125. package/src/integrations/catalog-refresh.ts +4 -0
  126. package/src/integrations/omp-yaml-source.ts +1 -0
  127. package/src/lib/claude-request-projection.ts +101 -0
  128. package/src/lib/local-account-switch-capability.ts +48 -0
  129. package/src/lib/local-upstream.ts +146 -0
  130. package/src/lib/package-tree-integrity.ts +25 -3
  131. package/src/lib/package-tree-retarget.ts +130 -0
  132. package/src/lib/process-control.ts +4 -1
  133. package/src/lib/system-restart-contract.ts +106 -0
  134. package/src/lib/upstream-retry.ts +57 -1
  135. package/src/link/ports.ts +9 -0
  136. package/src/link/ssh-argv.ts +16 -0
  137. package/src/link/ssh-runner.ts +70 -2
  138. package/src/link/tunnel-state.ts +58 -9
  139. package/src/oauth/account-quota-rank.ts +29 -18
  140. package/src/oauth/generic-account-failover.ts +179 -22
  141. package/src/oauth/index.ts +17 -4
  142. package/src/oauth/kiro-account-load.ts +107 -0
  143. package/src/oauth/kiro-device-login.ts +312 -0
  144. package/src/oauth/kiro-terminal-failover.ts +28 -0
  145. package/src/oauth/login-flow-state.ts +7 -1
  146. package/src/oauth/pool-settings-capability.ts +23 -14
  147. package/src/oauth/store.ts +56 -3
  148. package/src/oauth/types.ts +4 -0
  149. package/src/plugins/loader.ts +350 -0
  150. package/src/plugins/upstream-hooks.ts +119 -0
  151. package/src/protocols/encoders/adapter-events.ts +9 -2
  152. package/src/providers/account-quota-disk.ts +42 -4
  153. package/src/providers/codebuddy-models.ts +6 -2
  154. package/src/providers/command-code-efforts.ts +33 -10
  155. package/src/providers/kiro-account-state-disk.ts +44 -0
  156. package/src/providers/kiro-model-catalog.ts +162 -0
  157. package/src/providers/kiro-models.ts +5 -4
  158. package/src/providers/kiro-quota-metrics.ts +35 -0
  159. package/src/providers/kiro-usage.ts +118 -14
  160. package/src/providers/quota/account-cache.ts +43 -7
  161. package/src/providers/quota/antigravity.ts +8 -3
  162. package/src/providers/quota/kiro-account-probe.ts +12 -0
  163. package/src/providers/quota/vendor-probes-key.ts +31 -26
  164. package/src/providers/quota/vendor-probes-oauth.ts +16 -7
  165. package/src/providers/quota-types.ts +14 -0
  166. package/src/providers/quota.ts +21 -21
  167. package/src/providers/registry/entries-extended.ts +5 -3
  168. package/src/providers/request-pacing.ts +410 -12
  169. package/src/remote-control/workspace-codex-runtime.ts +4 -3
  170. package/src/responses/citation-markers.ts +132 -65
  171. package/src/responses/hosted-tool-policy.ts +14 -3
  172. package/src/responses/parser-content.ts +3 -2
  173. package/src/responses/parser.ts +4 -1
  174. package/src/responses/schema.ts +4 -0
  175. package/src/responses/visualization-directives.ts +182 -0
  176. package/src/server/chat-native-sse.ts +24 -12
  177. package/src/server/chat-native.ts +137 -68
  178. package/src/server/claude-messages.ts +147 -12
  179. package/src/server/index/link-listener.ts +7 -0
  180. package/src/server/index/optional-listeners.ts +4 -1
  181. package/src/server/index/package-tree-guard.ts +35 -16
  182. package/src/server/index/serve-options.ts +4 -2
  183. package/src/server/index/startup-warnings.ts +6 -0
  184. package/src/server/index.ts +7 -7
  185. package/src/server/inference/client-encoder-delivery.ts +3 -0
  186. package/src/server/inference/context.ts +30 -2
  187. package/src/server/local-account-switch-auth.ts +79 -0
  188. package/src/server/management/agent-settings-routes.ts +11 -3
  189. package/src/server/management/config-routes.ts +28 -11
  190. package/src/server/management/context.ts +2 -0
  191. package/src/server/management/link-routes.ts +123 -39
  192. package/src/server/management/logs-usage-routes.ts +9 -42
  193. package/src/server/management/model-rows.ts +1 -0
  194. package/src/server/management/oauth-account-routes.ts +67 -12
  195. package/src/server/management/route-registry.ts +5 -5
  196. package/src/server/management/sibling-guard.ts +60 -0
  197. package/src/server/management/storage-log-guard-routes.ts +72 -6
  198. package/src/server/management/system-restart.ts +92 -135
  199. package/src/server/management/system-routes.ts +1 -1
  200. package/src/server/management-api.ts +31 -3
  201. package/src/server/management-auth.ts +3 -0
  202. package/src/server/port-reclaim.ts +111 -1
  203. package/src/server/proxy-liveness.ts +16 -0
  204. package/src/server/relay.ts +16 -15
  205. package/src/server/request-log-filter.ts +28 -27
  206. package/src/server/request-log.ts +7 -3
  207. package/src/server/request-metrics.ts +28 -0
  208. package/src/server/responses/adapter-continuation.ts +151 -21
  209. package/src/server/responses/adapter-delivery.ts +21 -7
  210. package/src/server/responses/adapter-dispatch.ts +239 -51
  211. package/src/server/responses/codex-ws-pool.ts +5 -4
  212. package/src/server/responses/codex-ws-request.ts +4 -1
  213. package/src/server/responses/compact.ts +8 -4
  214. package/src/server/responses/compaction-recovery-policy.ts +116 -0
  215. package/src/server/responses/compaction-recovery.ts +330 -0
  216. package/src/server/responses/core-combo-failure.ts +1 -0
  217. package/src/server/responses/core-combo.ts +5 -1
  218. package/src/server/responses/core-lifetime.ts +22 -0
  219. package/src/server/responses/core-options.ts +16 -1
  220. package/src/server/responses/core.ts +14 -4
  221. package/src/server/responses/empty-completion-guard.ts +2 -0
  222. package/src/server/responses/fetch-helpers.ts +82 -31
  223. package/src/server/responses/passthrough-delivery.ts +3 -2
  224. package/src/server/responses/passthrough-dispatch.ts +12 -2
  225. package/src/server/responses/passthrough-execution.ts +2 -2
  226. package/src/server/responses/request-send-budget.ts +21 -5
  227. package/src/server/responses/request-transport.ts +116 -18
  228. package/src/server/responses/reset-replay.ts +32 -0
  229. package/src/server/responses/run-turn-execution.ts +136 -19
  230. package/src/server/responses/sidecar-execution.ts +41 -8
  231. package/src/server/responses/terminal-guard.ts +2 -0
  232. package/src/server/responses/ws-upstream.ts +44 -4
  233. package/src/server/restart-replacement.ts +382 -0
  234. package/src/server/startup-health-cache.ts +23 -4
  235. package/src/server/stop-teardown.ts +35 -1
  236. package/src/server/system-env.ts +3 -0
  237. package/src/service/windows-taskxml.ts +11 -7
  238. package/src/service/windows-wrapper-exit.ts +8 -0
  239. package/src/stall-timeout.ts +35 -10
  240. package/src/storage/policy-job.ts +4 -0
  241. package/src/storage/scanner.ts +177 -29
  242. package/src/storage/storage-mutation-coordinator.ts +17 -0
  243. package/src/tray/windows-tray.ps1 +50 -23
  244. package/src/types/config.ts +4 -0
  245. package/src/types/provider.ts +5 -1
  246. package/src/types/request.ts +13 -2
  247. package/src/update/index.ts +3 -1
  248. package/src/update/job.ts +6 -2
  249. package/src/update/mise-launcher-target.ts +100 -0
  250. package/src/update/worker-launch.ts +46 -0
  251. package/src/usage/log.ts +2 -0
  252. package/src/web-search/loop.ts +5 -2
  253. package/gui/dist/assets/App-BJsT8Icc.css +0 -1
  254. package/gui/dist/assets/App-D3pNGiN4.js +0 -51
  255. package/gui/dist/assets/Tray-yb05wCjb.js +0 -1
  256. package/gui/dist/assets/index-CouvvtMV.js +0 -86
  257. package/gui/dist/assets/tray-data-CYGdjCJ7.js +0 -1
@@ -1,5 +1,5 @@
1
1
  import type { OcxClaudeCodeConfig } from "../types";
2
- import { isAnthropicOutputSchema } from "../adapters/anthropic-output-schema";
2
+ import { isAnthropicOutputSchema, satisfiesOpenAiStrictSchema } from "../adapters/anthropic-output-schema";
3
3
  import { resolveAlias } from "./alias";
4
4
  import { stripOneMillionMarker } from "./context-windows";
5
5
  import { isUnresolvedDesktop3pAlias, resolveDesktop3pAlias } from "./desktop-3p";
@@ -104,7 +104,19 @@ export function formatFromOutputConfig(outputConfig: unknown): Rec | undefined {
104
104
  || !isRec(format.schema)
105
105
  || !isAnthropicOutputSchema(format.schema)
106
106
  ) return undefined;
107
- return { type: "json_schema", name: "response", schema: format.schema };
107
+ // `strict` is stated rather than left to the destination's default. A schema with an optional
108
+ // property is legal to Anthropic and a hard 400 under OpenAI strict mode ("'required' ... an
109
+ // array including every key in properties"), which takes down every structured-output turn on
110
+ // a route whose canonical target is unavailable. Satisfying strict by adding the missing keys
111
+ // to `required` would change the caller's contract, so the optionality is preserved and the
112
+ // strict claim is dropped instead.
113
+ return {
114
+ type: "json_schema",
115
+ name: "response",
116
+ schema: format.schema,
117
+ // Strict Structured Outputs also needs an object at the root; a root anyOf/oneOf is refused.
118
+ strict: format.schema.type === "object" && satisfiesOpenAiStrictSchema(format.schema),
119
+ };
108
120
  }
109
121
 
110
122
  /**
@@ -513,7 +513,7 @@ function translateAnthropicRequest(
513
513
  // value, so do not attach the similarly named internal catalog sentinel.
514
514
  body.reasoning = { effort: "none" };
515
515
  } else if (isRec(thinking) || outputConfigEffort !== undefined) {
516
- const reasoning: Rec = { summary: "auto" };
516
+ const reasoning: Rec = { summary: isRec(thinking) && thinking.display === "omitted" ? "none" : "auto" };
517
517
  if (outputConfigEffort !== undefined) {
518
518
  // Adaptive wire: /effort arrives as output_config.effort (devlog 080).
519
519
  reasoning.effort = outputConfigEffort;
@@ -32,6 +32,8 @@ export interface ConnectProxyOptions {
32
32
  authToken?: string | (() => string | null);
33
33
  /** Hostnames (lowercase) whose 443 tunnels are spliced onto `interceptPort`. */
34
34
  interceptHosts?: readonly string[];
35
+ /** Optional exact CONNECT authorities (host:port); empty denies all, absent keeps blind relay. */
36
+ allowedTargets?: readonly string[];
35
37
  /** Per-connection override, consulted before interceptHosts; null keeps the default. */
36
38
  selectTunnel?: (host: string, port: number, request: ConnectRequestInfo) => TunnelDecision | null | Promise<TunnelDecision | null>;
37
39
  /** Test seam: dial the real destination for a blind tunnel. */
@@ -67,7 +69,8 @@ function connectRequestInfo(head: string): ConnectRequestInfo {
67
69
  }
68
70
 
69
71
  type ResolvedConnectProxyOptions = Required<Pick<ConnectProxyOptions, "interceptPort" | "interceptHosts" | "dialUpstream">>
70
- & Pick<ConnectProxyOptions, "selectTunnel" | "authToken">;
72
+ & Pick<ConnectProxyOptions, "selectTunnel" | "authToken">
73
+ & { allowedTargets?: ReadonlySet<string> };
71
74
 
72
75
  export interface ConnectProxyHandle {
73
76
  port: number;
@@ -186,6 +189,10 @@ function handleConnection(socket: Socket, options: ResolvedConnectProxyOptions):
186
189
  respond(socket, 403, "Forbidden");
187
190
  return;
188
191
  }
192
+ if (options.allowedTargets && !options.allowedTargets.has(`${target.host}:${target.port}`)) {
193
+ respond(socket, 403, "Forbidden");
194
+ return;
195
+ }
189
196
  const dialFor = (selected: TunnelDecision | null): void => {
190
197
  if (socket.destroyed) return;
191
198
  const choice = selected ?? (target.port === 443 && options.interceptHosts.includes(target.host)
@@ -258,10 +265,19 @@ function handleConnection(socket: Socket, options: ResolvedConnectProxyOptions):
258
265
 
259
266
  /** Bind the CONNECT proxy on 127.0.0.1. Rejects when the port is unavailable. */
260
267
  export function startConnectProxy(port: number, options: ConnectProxyOptions): Promise<ConnectProxyHandle> {
268
+ // Snapshot caller-owned policy before listening. Reuse the request parser's
269
+ // host normalization; malformed policy must not silently disable restrictions.
270
+ const allowedTargets = options.allowedTargets === undefined ? undefined : new Set(options.allowedTargets.map(authority => {
271
+ const target = typeof authority === "string" && !/[\s\r\n]/.test(authority)
272
+ ? parseConnectRequestLine(`CONNECT ${authority} HTTP/1.1`) : null;
273
+ if (!target || /[*/\\?#@%]/.test(target.host)) throw new Error("Invalid CONNECT allowed target");
274
+ return `${target.host}:${target.port}`;
275
+ }));
261
276
  const resolved: ResolvedConnectProxyOptions = {
262
277
  interceptPort: options.interceptPort,
263
278
  authToken: options.authToken,
264
279
  interceptHosts: options.interceptHosts ?? CLAUDE_INTERCEPT_HOSTS,
280
+ allowedTargets,
265
281
  selectTunnel: options.selectTunnel,
266
282
  dialUpstream: options.dialUpstream ?? ((host: string, targetPort: number) => connect({ host, port: targetPort })),
267
283
  };
@@ -193,12 +193,18 @@ export interface LocalInterceptCa extends PemKeyPair {
193
193
 
194
194
  export interface AuthorityOptions {
195
195
  commonName: string;
196
+ /** Optional whole-day lifetime for short-lived authorities; defaults to the existing 3650 days. */
197
+ validityDays?: number;
196
198
  permittedDnsNames?: readonly string[];
197
199
  /** With permittedDnsNames: also exclude every IP address (default true). */
198
200
  excludeAllIpAddresses?: boolean;
199
201
  }
200
202
 
201
203
  export function createCertificateAuthority(options: AuthorityOptions): LocalInterceptCa {
204
+ const validityDays = options.validityDays ?? CA_VALIDITY_DAYS;
205
+ if (!Number.isInteger(validityDays) || validityDays < 1 || validityDays > CA_VALIDITY_DAYS) {
206
+ throw new Error("CA validityDays must be an integer between 1 and 3650");
207
+ }
202
208
  const { publicKey, privateKey } = generateKeyPairSync("ec", { namedCurve: "prime256v1" });
203
209
  const name = distinguishedName(options.commonName);
204
210
  const der = issueCertificate({
@@ -206,7 +212,7 @@ export function createCertificateAuthority(options: AuthorityOptions): LocalInte
206
212
  issuer: name,
207
213
  subjectKey: publicKey,
208
214
  signingKey: privateKey,
209
- validityDays: CA_VALIDITY_DAYS,
215
+ validityDays,
210
216
  extensions: [
211
217
  extension(OID.basicConstraints, true, sequence(boolean(true), tlv(0x02, Uint8Array.of(0)))),
212
218
  // keyCertSign | cRLSign
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Claude Code's message-threads beta, which it only enables against first-party Anthropic, sends
3
+ * a `thread` object on subagent turns. A `continue` carries just the messages after
4
+ * `previous_message_id` and may omit `system` and `tools`, because Anthropic replays them from the
5
+ * stored thread. A translated route has no such store, so translating that delta silently drops the
6
+ * task, the instructions and the earlier turns.
7
+ *
8
+ * Claude Code reads this error code as "threads are unsupported for this model": it resends the
9
+ * same turn with the full conversation and keeps that model stateless for the rest of the session.
10
+ */
11
+ export const MESSAGE_THREAD_UNSUPPORTED_ERROR_CODE = "thread_unsupported_request";
12
+
13
+ export function carriesMessageThread(body: unknown): boolean {
14
+ if (body === null || typeof body !== "object" || Array.isArray(body)) return false;
15
+ const thread = (body as Record<string, unknown>).thread;
16
+ return thread !== null && typeof thread === "object" && !Array.isArray(thread);
17
+ }
18
+
19
+ export function messageThreadUnsupportedResponse(): Response {
20
+ return new Response(JSON.stringify({
21
+ type: "error",
22
+ error: {
23
+ type: "invalid_request_error",
24
+ message: "message threads are not supported on translated routes",
25
+ details: { error_code: MESSAGE_THREAD_UNSUPPORTED_ERROR_CODE },
26
+ },
27
+ }), { status: 400, headers: { "Content-Type": "application/json" } });
28
+ }
@@ -27,6 +27,8 @@ export interface AccountRow {
27
27
  masked?: string;
28
28
  active: boolean;
29
29
  needsReauth?: boolean;
30
+ autoSelectable?: boolean;
31
+ skipReason?: "needs_reauth" | "suspended" | "cooldown" | "quota_exhausted";
30
32
  selectionExcludedReason?: "plan_excluded";
31
33
  selectionExcludedPlan?: string;
32
34
  /** Registered credential that is still excluded from routing until validation completes. */
@@ -343,6 +345,8 @@ interface OAuthAccountDto {
343
345
  email?: string;
344
346
  active?: boolean;
345
347
  needsReauth?: boolean;
348
+ autoSelectable?: boolean;
349
+ skipReason?: unknown;
346
350
  /** Always sent by the management route; explicitly `null` when the tier is unknown. */
347
351
  plan?: string | null;
348
352
  quota?: CodexQuotaDto | null;
@@ -350,6 +354,11 @@ interface OAuthAccountDto {
350
354
  quotaFailure?: unknown;
351
355
  }
352
356
 
357
+ function isKiroSkipReason(value: unknown): value is NonNullable<AccountRow["skipReason"]> {
358
+ return value === "needs_reauth" || value === "suspended"
359
+ || value === "cooldown" || value === "quota_exhausted";
360
+ }
361
+
353
362
  async function fetchOAuthRows(
354
363
  deps: AccountDeps,
355
364
  baseUrl: string,
@@ -376,6 +385,10 @@ async function fetchOAuthRows(
376
385
  email: a.email,
377
386
  active: a.active ?? a.id === activeId,
378
387
  needsReauth: a.needsReauth,
388
+ ...(name === "kiro" && typeof a.autoSelectable === "boolean"
389
+ ? { autoSelectable: a.autoSelectable } : {}),
390
+ ...(name === "kiro" && a.autoSelectable === false && isKiroSkipReason(a.skipReason)
391
+ ? { skipReason: a.skipReason } : {}),
379
392
  // Forward the server's answer verbatim. An absent key means the proxy predates tier
380
393
  // reporting while `null` means it checked and found no tier — collapsing either
381
394
  // direction would destroy the one distinction this field exists to make.
@@ -34,7 +34,7 @@ function writeStdoutFully(text: string): void {
34
34
  }
35
35
 
36
36
  const USAGE = `Usage:
37
- ocx account login <provider> [--id <account-id>] [--reauth] [--device] [--code -] [--no-wait] [--json]
37
+ ocx account login <provider> [--id <account-id>] [--reauth] [--device] [--method builder-id|google|github] [--code -] [--no-wait] [--json]
38
38
  ocx account code <provider> [--flow <flow-id>] [--json] (reads the code from stdin)
39
39
  ocx account cancel <provider> [--flow <flow-id>] [--json] (--flow required for codex)
40
40
  ocx account reset-credits <account-id|main> [--consume --yes [--operation-id <uuid>]] [--json]
@@ -72,6 +72,13 @@ interface LoginStart {
72
72
  deviceCode?: string;
73
73
  /** Whether the host actually opened a browser. Absent from older proxies. */
74
74
  browserLaunch?: "started" | "failed" | "skipped";
75
+ method?: "builder-id" | "google" | "github";
76
+ userCode?: string;
77
+ verificationUri?: string;
78
+ verificationUriComplete?: string;
79
+ expiresAt?: number;
80
+ state?: string;
81
+ warning?: string;
75
82
  }
76
83
 
77
84
  /**
@@ -94,6 +101,14 @@ const STDIN_SENTINEL = "-";
94
101
 
95
102
  /** Providers whose ONLY login is already a device flow; --device is redundant, not wrong. */
96
103
  const DEVICE_NATIVE_PROVIDERS = new Set(["kimi", "nous", "github-copilot"]);
104
+ const stripTerminalControls = (value: string): string => value.replace(/[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u202a-\u202e\u2060-\u2069\ufeff]/g, "");
105
+ export function formatKiroDeviceInstructions(start: { verificationUriComplete?: string; verificationUri?: string; userCode?: string; flowId?: string }): string {
106
+ return [
107
+ stripTerminalControls(start.verificationUriComplete ?? start.verificationUri ?? ""),
108
+ start.userCode ? `User code: ${stripTerminalControls(start.userCode)}` : "",
109
+ start.flowId ? `Flow: ${stripTerminalControls(start.flowId)}` : "",
110
+ ].filter(Boolean).join("\n");
111
+ }
97
112
 
98
113
  const ARGV_WARNING =
99
114
  "warning: the authorization code was passed as a command-line argument, so it is now in your shell history and was visible in the process list while this ran. Pipe it on stdin instead, or pass `-` to read from stdin.";
@@ -128,9 +143,17 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
128
143
  const noWait = takeFlag(args, "--no-wait");
129
144
  const reauth = takeFlag(args, "--reauth");
130
145
  const device = takeFlag(args, "--device");
146
+ const method = takeOption(args, "--method");
131
147
  const id = takeOption(args, "--id");
132
148
  const suppliedCode = takeOptionWithSyntax(args, "--code");
133
149
  if (!provider) throw new CliUsageError("provider is required", USAGE);
150
+ if (method !== undefined) {
151
+ if (provider !== "kiro" || !["builder-id", "google", "github"].includes(method)) {
152
+ throw new CliUsageError("--method requires kiro and builder-id, google, or github", USAGE);
153
+ }
154
+ if (reauth || id) throw new CliUsageError("native Kiro device login only adds accounts; remove and re-add to reauthenticate", USAGE);
155
+ if (device || suppliedCode) throw new CliUsageError("--method cannot be combined with --device or --code", USAGE);
156
+ }
134
157
  // A bare leftover here is plausibly the authorization code itself: this flow takes one
135
158
  // through --code, and a user who pastes it as a positional would otherwise see it echoed
136
159
  // back in the usage error. `ocx login codex` reaches this parser too, so the paste lands
@@ -147,6 +170,30 @@ async function login(argv: string[], deps: RuntimeApiDeps): Promise<void> {
147
170
  // opens the browser flow and polls, and must not block on stdin.
148
171
  const code = await resolveCode(suppliedCode, deps, false);
149
172
 
173
+ if (method) {
174
+ const start = await runtimeRequest<LoginStart>("/api/oauth/login", {
175
+ method: "POST", body: JSON.stringify({ provider: "kiro", method }),
176
+ }, deps);
177
+ if (!wantsJson) {
178
+ const block = formatKiroDeviceInstructions(start);
179
+ if (block) writeStdoutFully(`${block}\n`);
180
+ }
181
+ if (noWait) { printData(start, wantsJson, []); return; }
182
+ if (!start.flowId) throw new CliUsageError("Kiro device login did not return a flow id");
183
+ for (let attempt = 0; attempt < 450; attempt++) {
184
+ await Bun.sleep(2_000);
185
+ const state = await runtimeRequest<LoginStart>(`/api/oauth/status?provider=kiro&flowId=${encodeURIComponent(start.flowId)}`, {}, deps);
186
+ if (state.state === "done") {
187
+ printData(state, wantsJson, ["Logged in to kiro.", ...(state.warning ? [`Warning: ${state.warning}`] : [])]);
188
+ return;
189
+ }
190
+ if (state.state === "failed" || state.state === "expired" || state.state === "cancelled") {
191
+ throw new CliUsageError(`Kiro device login ${state.state}`);
192
+ }
193
+ }
194
+ throw new CliUsageError("Kiro device login timed out");
195
+ }
196
+
150
197
  if (CODEX_NAMES.has(provider)) {
151
198
  const start = await runtimeRequest<LoginStart>("/api/codex-auth/login", {
152
199
  method: "POST",
@@ -290,7 +337,7 @@ async function cancel(argv: string[], deps: RuntimeApiDeps): Promise<void> {
290
337
  }
291
338
  const result = await runtimeRequest(codex ? "/api/codex-auth/login/cancel" : "/api/oauth/login/cancel", {
292
339
  method: "POST",
293
- body: JSON.stringify(codex ? { flowId } : { provider }),
340
+ body: JSON.stringify(codex ? { flowId } : { provider, ...(provider === "kiro" && flowId ? { flowId } : {}) }),
294
341
  }, deps);
295
342
  printData(result, wantsJson, [`Cancelled ${provider} login.`]);
296
343
  }
@@ -45,7 +45,7 @@ const EXTENDED_USAGE = `Usage:
45
45
  ocx account pause <provider> <id|alias|main> [--json]
46
46
  ocx account resume <provider> <id|alias|main> [--json]
47
47
  ocx account pause-exhausted <provider> [--json]
48
- ocx account strategy <provider> [<quota|round-robin|fill-first|reset-first>] [--json]
48
+ ocx account strategy <provider> [<quota|round-robin|fill-first|least-loaded|reset-first>] [--json]
49
49
  ocx account sticky <provider> [<1-100>] [--json]
50
50
  ocx account remove <provider> <id|alias|main> --yes [--json]
51
51
  ocx account clear-cooldown <provider> <id|alias|main> [--json]
@@ -961,7 +961,9 @@ async function poolSetting(
961
961
  console.log(JSON.stringify(payload, null, 2));
962
962
  } else {
963
963
  if (field === "strategy" && autoSwitchThreshold !== undefined) {
964
- const thresholdSummary = strategy === "round-robin"
964
+ const thresholdSummary = strategy === "least-loaded"
965
+ ? "fewest active requests"
966
+ : strategy === "round-robin"
965
967
  ? "threshold not used"
966
968
  : autoSwitchThreshold > 0
967
969
  ? (strategy === "fill-first"
@@ -52,7 +52,7 @@ const ACCOUNT_USAGE = `Usage:
52
52
  ocx account pause <provider> <account-id|alias|main> [--json]
53
53
  ocx account resume <provider> <account-id|alias|main> [--json]
54
54
  ocx account pause-exhausted <provider> [--json]
55
- ocx account strategy <provider> [<quota|round-robin|fill-first|reset-first>] [--json]
55
+ ocx account strategy <provider> [<quota|round-robin|fill-first|least-loaded|reset-first>] [--json]
56
56
  ocx account sticky <provider> [<1-100>] [--json]
57
57
  ocx account remove <provider> <account-or-key-id|alias|main> --yes [--json]
58
58
  ocx account clear-cooldown <provider> <account-id|alias|main> [--json]
@@ -104,7 +104,9 @@ function statusText(row: AccountRow): string {
104
104
  // held out -- so printing only one of the two would hide exactly the confusing case (#2703).
105
105
  if (row.paused) parts.push("paused");
106
106
  if (row.active) parts.push(row.type === "codex" ? "selected" : "active");
107
- if (row.needsReauth) parts.push("needs-reauth");
107
+ if (row.needsReauth && !(row.provider === "kiro" && row.skipReason === "needs_reauth")) parts.push("needs-reauth");
108
+ if (row.provider === "kiro" && row.autoSelectable === false)
109
+ parts.push(row.skipReason ? `not-auto-selected(${row.skipReason})` : "not-auto-selected");
108
110
  if (row.validationPending) parts.push("validation-pending");
109
111
  if (row.selectionExcludedReason === "plan_excluded") {
110
112
  parts.push(`not-auto-selected(plan=${row.selectionExcludedPlan ?? row.plan ?? "unknown"})`);
@@ -386,6 +386,23 @@ export const CAPABILITIES: readonly Capability[] = [
386
386
  "Values accepted by `set` are parsed as JSON when valid, so booleans, numbers, arrays, objects, and null can be passed directly.",
387
387
  ],
388
388
  },
389
+ {
390
+ command: ["account", "login"],
391
+ summary: "Log in to an OAuth provider; Kiro can add a native device account.",
392
+ routes: [
393
+ { method: "POST", path: "/api/oauth/login" },
394
+ { method: "GET", path: "/api/oauth/status" },
395
+ ],
396
+ flags: [
397
+ { name: "--method", value: "string", summary: "For Kiro: builder-id, google, or github device login (add only)." },
398
+ { name: "--reauth", value: "boolean", summary: "Reauthenticate a selected existing account." },
399
+ { name: "--id", value: "string", summary: "Account id for reauthentication." },
400
+ { name: "--no-wait", value: "boolean", summary: "Return after the login flow starts." },
401
+ { name: "--json", value: "boolean", summary: "Emit flow state as JSON." },
402
+ ],
403
+ mutates: true,
404
+ json: "payload",
405
+ },
389
406
  {
390
407
  command: ["account", "history"],
391
408
  summary: "Cached quota observations for one stored Codex pool account.",
package/src/cli/claude.ts CHANGED
@@ -34,6 +34,7 @@ import { aliasForNative, aliasForRoute, legacyAliasForNative, legacyAliasForRout
34
34
  import { desktop3pAlias } from "../claude/desktop-3p";
35
35
  import { inspectDesktopFirstParty } from "../claude/desktop-first-party";
36
36
  import { isClaudeInterceptProxyUrl, type ClaudeInterceptSettingsState } from "../claude/intercept/settings";
37
+ import { withoutSiblingMarker } from "../codex/sibling-start";
37
38
 
38
39
  export interface ClaudeLaunchEnv {
39
40
  [key: string]: string | undefined;
@@ -506,7 +507,8 @@ export async function ensureProxyForClaude(deps: ClaudeProxyEnsureDeps = {}): Pr
506
507
  detached: true,
507
508
  stdio: "ignore",
508
509
  windowsHide: true,
509
- env: withProcessRuntimeProvenance({ ...process.env, OCX_SERVICE: "1" }),
510
+ // An ordinary owner: a stray sibling marker would otherwise mark it before any probe.
511
+ env: withProcessRuntimeProvenance(withoutSiblingMarker({ ...process.env, OCX_SERVICE: "1" })),
510
512
  });
511
513
  child.unref();
512
514
  const deadline = Date.now() + 8_000;
@@ -21,7 +21,9 @@ import {
21
21
  localClientSkipMessage,
22
22
  setIntegrationEnabled,
23
23
  shouldSyncCodexOnStart,
24
+ type LocalClientSkipReason,
24
25
  } from "../codex/desired-state";
26
+ import { siblingSkipMessage } from "../codex/sibling-start";
25
27
  import { syncModelsToCodex } from "../codex/sync";
26
28
  import { collectOrcaCodexHomeDiagnostic } from "../codex/home";
27
29
  import { restoreNativeCodexAsync, type CodexNativeRestoreResult } from "../codex/inject";
@@ -692,13 +694,15 @@ const commandRunners: Record<string, CommandRunner> = {
692
694
  switch (deps.args[1]) {
693
695
  case "install": {
694
696
  const r = installCodexShim();
697
+ const { healthy, summary } = diagnoseCodexShim();
695
698
  const { collectCodexShimReadinessWarnings } = await import("./codex-shim-readiness");
696
- const warnings = diagnoseCodexShim().healthy
699
+ const warnings = healthy
697
700
  ? collectCodexShimReadinessWarnings()
698
701
  : [];
699
702
  console.log(`${r.installed && warnings.length === 0 ? "✅ " : "⚠️ "}${r.message}`);
700
703
  for (const warning of warnings) console.warn(` ${warning}`);
701
- break;
704
+ if (!healthy) console.error(`Codex shim installation is unhealthy: ${summary}`);
705
+ return healthy ? 0 : 1;
702
706
  }
703
707
  case "status":
704
708
  console.log(codexShimStatus());
@@ -999,7 +1003,7 @@ export const DISPATCH_ALIASES: ReadonlyMap<string, string> = aliasTargets;
999
1003
  /** Resolve the runner key for a command, following registry aliases to the
1000
1004
  * canonical runner. Returns undefined when the command is unknown. */
1001
1005
  /** What `handleStart` does about a live proxy it found before binding. */
1002
- export type StartOwnerDecision = "refuse" | "service-stay-out" | "sibling";
1006
+ export type StartOwnerDecision = "refuse" | "service-stay-out" | "sibling" | "await-parent";
1003
1007
 
1004
1008
  /**
1005
1009
  * Pure decision for `handleStart` when the pre-bind probe found a live proxy.
@@ -1011,21 +1015,74 @@ export type StartOwnerDecision = "refuse" | "service-stay-out" | "sibling";
1011
1015
  * decision allows isolated homes on one machine to remain independent.
1012
1016
  * The service wrapper always passes the configured port and keeps its exact
1013
1017
  * stay-out-of-the-way semantics: it never takes the sibling path.
1018
+ *
1019
+ * `"await-parent"` comes first and only for an exact pid match: the live proxy is the draining
1020
+ * process that spawned this start as its restart replacement (`OCX_RESTART_PARENT_PID`, already
1021
+ * checked against the real parent pid). Refusing it would leave no proxy once that parent exits,
1022
+ * so the caller waits for it instead (`src/cli/restart-handoff.ts`). Every other owner, and a
1023
+ * live proxy whose pid could not be verified, keeps the table above.
1014
1024
  */
1015
1025
  export function decideStartWithLiveOwner(input: {
1016
1026
  livePort: number;
1017
1027
  requestedPort: number | undefined;
1018
1028
  ocxService: string | undefined;
1029
+ livePid?: number | null;
1030
+ restartParentPid?: number | null;
1019
1031
  }): StartOwnerDecision {
1032
+ if (input.restartParentPid != null && input.livePid === input.restartParentPid) return "await-parent";
1020
1033
  const sibling = input.requestedPort !== undefined
1021
1034
  && input.requestedPort !== input.livePort
1022
- // Only the exact "1" sentinel is service context — the same check syncCleanup
1035
+ // Only the exact "1" sentinel is service context — the same check the exit teardown
1023
1036
  // uses — so an env value like "0" or "false" cannot reach the stay-out path.
1024
1037
  && input.ocxService !== "1";
1025
1038
  if (sibling) return "sibling";
1026
1039
  return input.ocxService === "1" ? "service-stay-out" : "refuse";
1027
1040
  }
1028
1041
 
1042
+ /** Which shared client state `handleStart`'s exit cleanup tears down. */
1043
+ export interface StartExitTeardown {
1044
+ revertSystemEnv: boolean;
1045
+ restoreNativeCodex: boolean;
1046
+ stripGrokConfig: boolean;
1047
+ }
1048
+
1049
+ /**
1050
+ * Pure exit-teardown decision for `handleStart`'s `syncCleanup`.
1051
+ *
1052
+ * A sibling instance tears down nothing: the Codex routing, the Grok fence and the system env
1053
+ * belong to the live proxy it runs beside, and restoring them would take Codex off a proxy that
1054
+ * is still serving it (`src/codex/sibling-start.ts`). A dashboard drain-and-restart (#563) keeps
1055
+ * everything for the replacement process. Under a service manager — only the exact `"1"`
1056
+ * sentinel — a crash/respawn keeps routing and the fence, and only the environment comes down.
1057
+ * The caller still applies its own external-provider and service-ownership checks.
1058
+ */
1059
+ export function decideStartExitTeardown(input: {
1060
+ sibling: boolean;
1061
+ recycling: boolean;
1062
+ ocxService: string | undefined;
1063
+ }): StartExitTeardown {
1064
+ if (input.sibling || input.recycling) {
1065
+ return { revertSystemEnv: false, restoreNativeCodex: false, stripGrokConfig: false };
1066
+ }
1067
+ const preserveRouting = input.ocxService === "1";
1068
+ return { revertSystemEnv: true, restoreNativeCodex: !preserveRouting, stripGrokConfig: !preserveRouting };
1069
+ }
1070
+
1071
+ /**
1072
+ * The one startup line for "nothing was written to Codex".
1073
+ *
1074
+ * Three very different facts reach it: the user's own OFF switch, a hub declining to rewrite its
1075
+ * own local clients, and a sibling instance leaving the live proxy's routing alone. Printing the
1076
+ * toggle's wording for the gate is what made operators hunt for a switch they never set (#4236).
1077
+ * `port` is the sibling's own bound port, named in its line.
1078
+ */
1079
+ export function startupLeftCodexNativeLine(reason: LocalClientSkipReason, port?: number): string {
1080
+ if (reason === "sibling") return ` ${siblingSkipMessage(port)}`;
1081
+ return reason === "hub-gated"
1082
+ ? ` ${HUB_GATED_SKIP_MESSAGE} Startup left Codex native.`
1083
+ : " Codex integration OFF; startup left Codex native.";
1084
+ }
1085
+
1029
1086
  /** What `chooseListenPort` does when the preferred port stayed busy through prefer-retry. */
1030
1087
  export type BusyPreferredPortDecision =
1031
1088
  | "hop"
@@ -1057,7 +1114,7 @@ export type BusyPreferredPortDecision =
1057
1114
  *
1058
1115
  * Service-wrapper context keeps the semantics `decideStartWithLiveOwner` gives it: a
1059
1116
  * healthy proxy on the port means the port is served, and the wrapper's
1060
- * `if %ERRORLEVEL% NEQ 0` loop must see a zero exit rather than respawn every 5 seconds.
1117
+ * retry loop must receive the intentional stay-out signal rather than respawn every 5 seconds.
1061
1118
  */
1062
1119
  export function decideBusyPreferredPort(input: {
1063
1120
  preferredPort: number;