@bitkyc08/opencodex 2.84.0 → 2.86.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 (246) hide show
  1. package/README.md +0 -4
  2. package/bin/ocx.mjs +61 -3
  3. package/gui/dist/assets/App-BjlttVLy.js +58 -0
  4. package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DyR9yBee.js} +1 -1
  5. package/gui/dist/assets/index-BMssP4k3.js +94 -0
  6. package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
  7. package/gui/dist/index.html +1 -1
  8. package/package.json +1 -2
  9. package/src/adapters/anthropic/beta-allowlist.ts +87 -8
  10. package/src/adapters/anthropic/client-identity.ts +1 -1
  11. package/src/adapters/anthropic/passthrough.ts +33 -4
  12. package/src/adapters/azure.ts +19 -1
  13. package/src/adapters/client-fingerprint.ts +24 -0
  14. package/src/adapters/coding-agent/turn.ts +22 -9
  15. package/src/adapters/cursor/arg-codec.ts +17 -5
  16. package/src/adapters/cursor/catalog.ts +8 -0
  17. package/src/adapters/cursor/effort-map.ts +3 -0
  18. package/src/adapters/cursor/freeform-args.ts +75 -0
  19. package/src/adapters/cursor/protobuf-events.ts +36 -23
  20. package/src/adapters/google-antigravity-wire.ts +22 -1
  21. package/src/adapters/google.ts +2 -2
  22. package/src/adapters/openai-chat/response-events.ts +59 -1
  23. package/src/adapters/openai-chat.ts +8 -5
  24. package/src/adapters/qoder/adapter.ts +40 -1
  25. package/src/advisor/consult.ts +220 -0
  26. package/src/advisor/context.ts +226 -0
  27. package/src/advisor/disclosure.ts +20 -0
  28. package/src/advisor/runtime.ts +305 -0
  29. package/src/advisor/settings.ts +135 -0
  30. package/src/advisor/state.ts +369 -0
  31. package/src/advisor/synthetic-tool.ts +33 -0
  32. package/src/bridge/errors.ts +5 -0
  33. package/src/chat/inbound.ts +30 -6
  34. package/src/chat/outbound.ts +5 -0
  35. package/src/claude/desktop-3p-library.ts +43 -3
  36. package/src/claude/desktop-3p.ts +22 -8
  37. package/src/claude/desktop-picker-profile.ts +11 -4
  38. package/src/claude/desktop-profile.ts +10 -1
  39. package/src/claude/desktop-remote-store-io.ts +5 -3
  40. package/src/claude/inbound.ts +121 -50
  41. package/src/claude/intercept/local-ca.ts +8 -1
  42. package/src/claude/model-default-settings.ts +154 -0
  43. package/src/claude/model-info.ts +7 -7
  44. package/src/claude/outbound.ts +13 -6
  45. package/src/cli/advisor.ts +151 -0
  46. package/src/cli/auxiliary-listener-preflight.ts +37 -0
  47. package/src/cli/capabilities-agents-routing.ts +18 -0
  48. package/src/cli/capabilities-base.ts +1 -1
  49. package/src/cli/chatgpt-command.ts +35 -14
  50. package/src/cli/claude.ts +46 -6
  51. package/src/cli/dispatch.ts +5 -1
  52. package/src/cli/doctor.ts +9 -0
  53. package/src/cli/ensure-desktop-cold-start.ts +329 -0
  54. package/src/cli/export-command.ts +1 -1
  55. package/src/cli/help.ts +2 -1
  56. package/src/cli/index.ts +13 -4
  57. package/src/cli/integration-plan-dto.ts +2 -0
  58. package/src/cli/registry.ts +15 -3
  59. package/src/cli/restart-scope.ts +3 -1
  60. package/src/clients/config-export/contracts.ts +1 -0
  61. package/src/clients/config-export/model-metadata.ts +3 -2
  62. package/src/clients/config-export/qoder.ts +93 -0
  63. package/src/clients/config-export.ts +17 -4
  64. package/src/codex/auth-api/pool-mode-gate.ts +5 -2
  65. package/src/codex/auth-context.ts +79 -5
  66. package/src/codex/autostart-health.ts +4 -0
  67. package/src/codex/catalog/build-entries.ts +11 -2
  68. package/src/codex/catalog/configured-tiers.ts +50 -0
  69. package/src/codex/catalog/effort.ts +4 -1
  70. package/src/codex/catalog/model-hints.ts +21 -3
  71. package/src/codex/catalog/native-ultrafast.ts +106 -0
  72. package/src/codex/catalog/parsing.ts +5 -0
  73. package/src/codex/catalog/provider-models.ts +4 -0
  74. package/src/codex/catalog/retained-sync.ts +2 -0
  75. package/src/codex/catalog/routed-gather.ts +22 -4
  76. package/src/codex/catalog/vendor-context.ts +82 -0
  77. package/src/codex/config-write-lock.ts +40 -2
  78. package/src/codex/convergence.ts +2 -0
  79. package/src/codex/desktop-app/types.ts +2 -0
  80. package/src/codex/desktop-app/windows.ts +29 -16
  81. package/src/codex/desktop-app-restart.ts +4 -2
  82. package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
  83. package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
  84. package/src/codex/desktop-compatibility/connection-store.ts +87 -0
  85. package/src/codex/desktop-compatibility/installed-build.ts +55 -0
  86. package/src/codex/desktop-compatibility/json-body.ts +29 -0
  87. package/src/codex/desktop-compatibility/native-identity.ts +62 -0
  88. package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
  89. package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
  90. package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
  91. package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
  92. package/src/codex/desktop-compatibility/runtime.ts +173 -0
  93. package/src/codex/desktop-compatibility/service.ts +30 -0
  94. package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
  95. package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
  96. package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
  97. package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
  98. package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
  99. package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
  100. package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
  101. package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
  102. package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
  103. package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
  104. package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
  105. package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
  106. package/src/codex/history-provider.ts +1 -1
  107. package/src/codex/inject/config-write-section.ts +4 -1
  108. package/src/codex/inject/remove.ts +18 -6
  109. package/src/codex/journal.ts +16 -1
  110. package/src/codex/main-account.ts +99 -15
  111. package/src/codex/native-main-refresh-mode.ts +217 -0
  112. package/src/codex/routing/health-store.ts +1 -1
  113. package/src/codex/routing/pool-recovery.ts +79 -0
  114. package/src/codex/shim-diagnostics.ts +6 -0
  115. package/src/codex/shim-overlay.ts +7 -2
  116. package/src/config/diagnostics.ts +2 -1
  117. package/src/config/live-reconcile.ts +13 -0
  118. package/src/config/load-degrade.ts +6 -1
  119. package/src/config/paths.ts +10 -3
  120. package/src/config/schema/config-schema.ts +17 -0
  121. package/src/config/schema/desktop-compatibility.ts +9 -0
  122. package/src/config/schema/leaf-validators.ts +1 -0
  123. package/src/generated/compatibility-version.json +451 -163
  124. package/src/generated/model-metadata.ts +1 -1
  125. package/src/integrations/catalog-refresh.ts +1 -1
  126. package/src/integrations/config-io.ts +20 -2
  127. package/src/integrations/journal.ts +15 -1
  128. package/src/integrations/mutation-plan.ts +1 -0
  129. package/src/integrations/registry.ts +7 -0
  130. package/src/integrations/store.ts +3 -0
  131. package/src/integrations/writer.ts +10 -1
  132. package/src/lib/advisor-activation.ts +16 -0
  133. package/src/lib/background-work.ts +65 -0
  134. package/src/lib/bounded-file-read.ts +58 -0
  135. package/src/lib/bun-path-runtime.d.mts +10 -1
  136. package/src/lib/bun-path-runtime.mjs +62 -3
  137. package/src/lib/desktop-proxy-route.ts +30 -0
  138. package/src/lib/desktop-upstream-tunnel.ts +107 -0
  139. package/src/lib/errors.ts +7 -0
  140. package/src/lib/local-internal-call-capability.ts +55 -0
  141. package/src/lib/package-tree-integrity.ts +2 -2
  142. package/src/lib/socks5-fetch.ts +48 -71
  143. package/src/lib/socks5-handshake.ts +112 -0
  144. package/src/lib/translator-budget.ts +7 -1
  145. package/src/oauth/generic-account-failover.ts +29 -5
  146. package/src/oauth/index.ts +4 -2
  147. package/src/oauth/kiro-device-login.ts +2 -1
  148. package/src/oauth/kiro.ts +7 -1
  149. package/src/protocols/codecs/chat-ir.ts +467 -0
  150. package/src/protocols/codecs/chat.ts +10 -1
  151. package/src/protocols/codecs/messages-ir.ts +500 -0
  152. package/src/protocols/features.ts +10 -2
  153. package/src/protocols/path.ts +11 -1
  154. package/src/protocols/plan-snapshot.ts +39 -16
  155. package/src/protocols/plan.ts +9 -1
  156. package/src/protocols/settings.ts +8 -0
  157. package/src/providers/antigravity-account-roster.ts +102 -0
  158. package/src/providers/antigravity-models.ts +85 -7
  159. package/src/providers/model-rename-fields.ts +2 -0
  160. package/src/providers/model-rename-migration.ts +5 -3
  161. package/src/providers/registry/entries-core.ts +7 -6
  162. package/src/providers/registry/entries-extended.ts +19 -0
  163. package/src/responses/parser.ts +30 -19
  164. package/src/responses/state.ts +32 -29
  165. package/src/routing/history/build-file.ts +92 -0
  166. package/src/routing/history/indexer.ts +248 -216
  167. package/src/routing/history/ingest.ts +238 -0
  168. package/src/routing/history/rebuild-job.ts +111 -0
  169. package/src/routing/history/rebuild-protocol.ts +15 -0
  170. package/src/routing/history/rebuild-worker.ts +34 -0
  171. package/src/server/admission-model-scope.ts +4 -1
  172. package/src/server/antigravity-direct-eligibility.ts +208 -0
  173. package/src/server/auth-cors.ts +4 -0
  174. package/src/server/chat-completions.ts +75 -9
  175. package/src/server/claude-messages.ts +110 -20
  176. package/src/server/index/desktop-compatibility-startup.ts +56 -0
  177. package/src/server/index/serve-options.ts +2 -2
  178. package/src/server/index/startup-warnings.ts +9 -0
  179. package/src/server/index/websocket-handler.ts +9 -0
  180. package/src/server/index.ts +13 -10
  181. package/src/server/inference/client-encoder-delivery.ts +6 -5
  182. package/src/server/login-shell-path.ts +146 -0
  183. package/src/server/management/advisor-routes.ts +191 -0
  184. package/src/server/management/agent-settings-routes.ts +22 -4
  185. package/src/server/management/companion-routes.ts +4 -0
  186. package/src/server/management/config-routes.ts +2 -2
  187. package/src/server/management/context.ts +4 -0
  188. package/src/server/management/desktop-compatibility-routes.ts +46 -0
  189. package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
  190. package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
  191. package/src/server/management/model-routes.ts +2 -2
  192. package/src/server/management/protocol-settings-patch.ts +1 -0
  193. package/src/server/management/route-registry.ts +11 -0
  194. package/src/server/management/sibling-guard.ts +1 -0
  195. package/src/server/management-api.ts +6 -0
  196. package/src/server/messages-antigravity-direct.ts +195 -0
  197. package/src/server/messages-native.ts +5 -1
  198. package/src/server/request-log.ts +7 -0
  199. package/src/server/responses/adapter-continuation.ts +35 -18
  200. package/src/server/responses/adapter-delivery.ts +24 -2
  201. package/src/server/responses/adapter-dispatch.ts +29 -2
  202. package/src/server/responses/advisor-plan-slot.ts +27 -0
  203. package/src/server/responses/advisor-slot.ts +336 -0
  204. package/src/server/responses/code-mode-collab-surface.ts +116 -8
  205. package/src/server/responses/core-combo-failure.ts +10 -1
  206. package/src/server/responses/core-combo.ts +21 -0
  207. package/src/server/responses/core-lifetime.ts +2 -0
  208. package/src/server/responses/core-normalize.ts +11 -0
  209. package/src/server/responses/core-options.ts +46 -0
  210. package/src/server/responses/core-replay.ts +7 -4
  211. package/src/server/responses/core.ts +1 -1
  212. package/src/server/responses/passthrough-delivery.ts +16 -10
  213. package/src/server/responses/passthrough-dispatch.ts +14 -3
  214. package/src/server/responses/request-prepare.ts +68 -5
  215. package/src/server/responses/request-transport.ts +16 -0
  216. package/src/server/responses/run-turn-execution.ts +70 -8
  217. package/src/server/responses/sidecar-execution.ts +78 -3
  218. package/src/server/responses/terminal-guard.ts +2 -1
  219. package/src/server/responses-undeclared-tool-guard.ts +30 -9
  220. package/src/server/startup-action-control.ts +16 -2
  221. package/src/server/startup-health-cache.ts +1 -0
  222. package/src/service/desktop-startup.ts +14 -0
  223. package/src/service/health-sentinel-main.ts +23 -4
  224. package/src/service/health-sentinel-probe.ts +24 -7
  225. package/src/service/logging.ts +72 -0
  226. package/src/tray/windows-tray.ps1 +43 -24
  227. package/src/types/config.ts +63 -6
  228. package/src/types/provider.ts +7 -0
  229. package/src/types/request.ts +15 -0
  230. package/src/types/tools.ts +2 -0
  231. package/src/update/async-check.ts +1 -1
  232. package/src/update/badge.ts +2 -2
  233. package/src/update/index.ts +88 -15
  234. package/src/update/job-supervised-restart.ts +107 -0
  235. package/src/update/job.ts +32 -32
  236. package/src/update/notify.ts +1 -1
  237. package/src/update/refresh-scheduler.ts +2 -2
  238. package/src/update/supervised-recovery.d.mts +27 -0
  239. package/src/update/supervised-recovery.mjs +157 -0
  240. package/src/usage/cache-diagnostic.ts +2 -0
  241. package/src/usage/expected-prices.ts +12 -0
  242. package/src/web-search/gemini-executor.ts +2 -2
  243. package/assets/sponsors/packycode.png +0 -0
  244. package/gui/dist/assets/App-CZtqiGcw.js +0 -56
  245. package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
  246. package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Process-local internal-call capability.
3
+ *
4
+ * Some requests ARE the proxy itself talking to its own data plane: the advisor sidecar's
5
+ * loopback consultation is the first user. Such a request must be recognized as INTERNAL without
6
+ * trusting anything the client controls. A client-supplied header value is not evidence — any
7
+ * external caller can send it — and neither is the peer address: a public listener reached
8
+ * through Docker, WSL, a tunnel, or port forwarding can present as loopback on the last hop, as
9
+ * the local-management attestation notes.
10
+ *
11
+ * The authority is therefore process-owned: a 256-bit random value minted once per process, kept
12
+ * in memory only. It is never written to config or disk, never logged, never returned by the
13
+ * management API, never placed in usage or request metadata, and never forwarded upstream. A new
14
+ * process mints a new value, so a token captured from an older process is worthless. Comparison
15
+ * is timing-safe and shape-checked, reusing the same secret shape as the local attestation
16
+ * module.
17
+ *
18
+ * The same primitive is what the vision-describe fence would need for the same reason; wiring
19
+ * that surface is deliberately left out of this change (recorded as a pre-existing analogous
20
+ * issue) so the change stays scoped.
21
+ */
22
+ import { randomBytes, timingSafeEqual } from "node:crypto";
23
+ import { isLocalAttestationSecret } from "./local-management-attestation";
24
+
25
+ /** Header an internal sidecar presents on its own loopback request. */
26
+ export const ADVISOR_INTERNAL_CAPABILITY_HEADER = "x-opencodex-advisor-internal";
27
+
28
+ let processCapability: string | null = null;
29
+
30
+ /**
31
+ * The current process's internal-call capability. Minted lazily on first use (one random draw,
32
+ * no I/O), so an install that never enables the advisor never generates one.
33
+ */
34
+ export function internalCallCapability(): string {
35
+ if (processCapability === null) processCapability = randomBytes(32).toString("base64url");
36
+ return processCapability;
37
+ }
38
+
39
+ /** Test seam: install a deterministic value, or `null` to force a fresh mint. */
40
+ export function setInternalCallCapabilityForTests(value: string | null): void {
41
+ processCapability = value;
42
+ }
43
+
44
+ /**
45
+ * True only when the supplied header value IS this process's capability. Shape-checked first so
46
+ * a malformed value cannot reach the comparison, then compared in constant time.
47
+ */
48
+ export function isInternalCallCapability(supplied: string | null | undefined): boolean {
49
+ if (typeof supplied !== "string" || !isLocalAttestationSecret(supplied)) return false;
50
+ if (processCapability === null) return false;
51
+ const expected = processCapability;
52
+ const suppliedBytes = Buffer.from(supplied);
53
+ const expectedBytes = Buffer.from(expected);
54
+ return suppliedBytes.length === expectedBytes.length && timingSafeEqual(suppliedBytes, expectedBytes);
55
+ }
@@ -68,7 +68,7 @@ export interface PackageTreeIntegrityOptions {
68
68
  }
69
69
 
70
70
  export type ObservePackageTree = () => PackageTreeObservation | null;
71
- export type PackageTreeRuntimeInstall = "bun" | "mise" | "npm" | "pnpm" | "source";
71
+ export type PackageTreeRuntimeInstall = "bun" | "mise" | "npm" | "pnpm" | "source" | "standalone";
72
72
 
73
73
  const packageManifestUrl = new URL("../../package.json", import.meta.url);
74
74
 
@@ -323,7 +323,7 @@ export function createRuntimePackageTreeIntegrityGuard(
323
323
  now: () => number = Date.now,
324
324
  options: PackageTreeIntegrityOptions = {},
325
325
  ): PackageTreeIntegrityGuard {
326
- if (installer === "source" || isStandaloneBinary()) {
326
+ if (installer === "source" || installer === "standalone" || isStandaloneBinary()) {
327
327
  return { status: () => ({ ok: true }), dispose: () => {} };
328
328
  }
329
329
  // The restart replacement spawns process.execPath, the bundled Bun in an npm install. Read it
@@ -1,6 +1,7 @@
1
1
  import net, { type Socket } from "node:net";
2
2
  import tls, { type TLSSocket } from "node:tls";
3
3
  import { classifyContentCoding, isNullBodyStatus } from "./http-response-semantics";
4
+ import { socks5Credentials, socks5Handshake, Socks5HandshakeError } from "./socks5-handshake";
4
5
 
5
6
  const DEFAULT_SOCKS5_PORT = 1080;
6
7
  const SOCKS5_CONNECT_TIMEOUT_MS = 30_000;
@@ -10,11 +11,6 @@ const MAX_BODY_SLICE_BYTES = 64 * 1024;
10
11
  const MAX_DECODED_BODY_BYTES = 32 * 1024 * 1024;
11
12
  const MAX_STREAM_DECODE_EXPANSION_RATIO = 128;
12
13
  const SOCKS5_VERSION = 0x05;
13
- const SOCKS5_NO_AUTH = 0x00;
14
- const SOCKS5_USER_PASS = 0x02;
15
- const SOCKS5_CONNECT = 0x01;
16
- const SOCKS5_DOMAIN = 0x03;
17
- const SOCKS5_SUCCESS = 0x00;
18
14
  const CRLF = Buffer.from("\r\n");
19
15
  const HEADER_END = Buffer.from("\r\n\r\n");
20
16
 
@@ -22,24 +18,6 @@ export class Socks5FetchError extends Error {
22
18
  override readonly name = "Socks5FetchError";
23
19
  }
24
20
 
25
- function proxyCredentials(proxy: URL): { username?: Uint8Array; password?: Uint8Array } {
26
- if (!proxy.username && !proxy.password) return {};
27
- let username: string;
28
- let password: string;
29
- try {
30
- username = decodeURIComponent(proxy.username);
31
- password = decodeURIComponent(proxy.password);
32
- } catch {
33
- throw new Socks5FetchError("SOCKS5 proxy credentials contain invalid percent encoding");
34
- }
35
- const usernameBytes = new TextEncoder().encode(username);
36
- const passwordBytes = new TextEncoder().encode(password);
37
- if (usernameBytes.byteLength > 255 || passwordBytes.byteLength > 255) {
38
- throw new Socks5FetchError("SOCKS5 proxy credentials must each fit in 255 UTF-8 bytes");
39
- }
40
- return { username: usernameBytes, password: passwordBytes };
41
- }
42
-
43
21
  function validateProxy(proxy: string): URL {
44
22
  let parsed: URL;
45
23
  try {
@@ -55,7 +33,10 @@ function validateProxy(proxy: string): URL {
55
33
  throw new Socks5FetchError("SOCKS5 proxy port is invalid");
56
34
  }
57
35
  if (parsed.search || parsed.hash) throw new Socks5FetchError("SOCKS5 proxy URL must not contain a query or fragment");
58
- proxyCredentials(parsed);
36
+ try { socks5Credentials(parsed); } catch (error) {
37
+ if (error instanceof Socks5HandshakeError) throw new Socks5FetchError(error.message);
38
+ throw error;
39
+ }
59
40
  return parsed;
60
41
  }
61
42
 
@@ -130,6 +111,14 @@ class SocketReader {
130
111
  socket.resume();
131
112
  }
132
113
 
114
+ write(bytes: Uint8Array): void {
115
+ this.socket.write(bytes);
116
+ }
117
+
118
+ readExact(bytes: number, signal?: AbortSignal): Promise<Buffer> {
119
+ return this.read(bytes, signal);
120
+ }
121
+
133
122
  private readonly onData = (chunk: Buffer | string): void => {
134
123
  const value = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
135
124
  if (this.anyWaiters.length > 0) {
@@ -263,7 +252,7 @@ class SocketReader {
263
252
 
264
253
  async function socks5Connect(proxy: string, target: URL, signal?: AbortSignal): Promise<Socket> {
265
254
  const parsedProxy = validateProxy(proxy);
266
- const credentials = proxyCredentials(parsedProxy);
255
+ const credentials = socks5Credentials(parsedProxy);
267
256
  const proxyHost = parsedProxy.hostname.replace(/^\[|\]$/g, "");
268
257
  const socket = await connectSocket(proxyHost, Number(parsedProxy.port) || DEFAULT_SOCKS5_PORT, signal);
269
258
  socket.setTimeout(SOCKS5_CONNECT_TIMEOUT_MS, () => {
@@ -271,49 +260,12 @@ async function socks5Connect(proxy: string, target: URL, signal?: AbortSignal):
271
260
  });
272
261
  const reader = new SocketReader(socket);
273
262
  try {
274
- const methods = credentials.username ? Buffer.from([SOCKS5_NO_AUTH, SOCKS5_USER_PASS]) : Buffer.from([SOCKS5_NO_AUTH]);
275
- socket.write(Buffer.from([SOCKS5_VERSION, methods.byteLength, ...methods]));
276
- const greeting = await reader.read(2, signal);
277
- if (greeting[0] !== SOCKS5_VERSION) throw new Socks5FetchError("SOCKS5 proxy returned an invalid greeting");
278
- if (greeting[1] === SOCKS5_USER_PASS && credentials.username && credentials.password) {
279
- socket.write(Buffer.from([
280
- 0x01,
281
- credentials.username.byteLength,
282
- ...credentials.username,
283
- credentials.password.byteLength,
284
- ...credentials.password,
285
- ]));
286
- const auth = await reader.read(2, signal);
287
- if (auth[0] !== 0x01 || auth[1] !== 0x00) throw new Socks5FetchError("SOCKS5 proxy authentication failed");
288
- } else if (greeting[1] !== SOCKS5_NO_AUTH) {
289
- throw new Socks5FetchError("SOCKS5 proxy does not accept an offered authentication method");
290
- }
291
-
292
- const hostname = new TextEncoder().encode(target.hostname);
293
- if (hostname.byteLength > 255) throw new Socks5FetchError("SOCKS5 target hostname is too long");
294
- const port = targetPort(target);
295
- socket.write(Buffer.from([
296
- SOCKS5_VERSION,
297
- SOCKS5_CONNECT,
298
- 0x00,
299
- SOCKS5_DOMAIN,
300
- hostname.byteLength,
301
- ...hostname,
302
- port >> 8,
303
- port & 0xff,
304
- ]));
305
- const reply = await reader.read(4, signal);
306
- if (reply[0] !== SOCKS5_VERSION) throw new Socks5FetchError("SOCKS5 proxy returned an invalid connect response");
307
- if (reply[1] !== SOCKS5_SUCCESS) throw new Socks5FetchError(`SOCKS5 proxy refused the connection (code ${reply[1]})`);
308
- if (reply[2] !== 0x00 || ![0x01, SOCKS5_DOMAIN, 0x04].includes(reply[3]!)) {
309
- throw new Socks5FetchError("SOCKS5 proxy returned an invalid address type or reserved byte");
310
- }
311
- const addressLength = reply[3] === 0x01 ? 4 : reply[3] === SOCKS5_DOMAIN ? (await reader.read(1, signal))[0]! : 16;
312
- await reader.read(addressLength + 2, signal);
263
+ await socks5Handshake(reader, { host: target.hostname, port: targetPort(target) }, credentials, signal);
313
264
  socket.setTimeout(0);
314
265
  return socket;
315
266
  } catch (error) {
316
267
  socket.destroy();
268
+ if (error instanceof Socks5HandshakeError) throw new Socks5FetchError(error.message);
317
269
  throw error;
318
270
  } finally {
319
271
  reader.dispose();
@@ -341,25 +293,50 @@ function requestHeaders(request: Request, target: URL): { text: string; chunked:
341
293
 
342
294
  async function secureSocket(socket: Socket, target: URL, signal: AbortSignal): Promise<TLSSocket> {
343
295
  return new Promise((resolve, reject) => {
296
+ if (signal.aborted) {
297
+ socket.destroy();
298
+ reject(signal.reason instanceof Error ? signal.reason : new Error("The operation was aborted"));
299
+ return;
300
+ }
344
301
  const tlsSocket = tls.connect({
345
302
  socket,
346
303
  servername: target.hostname,
347
304
  rejectUnauthorized: true,
348
305
  });
349
- const onAbort = () => {
306
+ let settled = false;
307
+ // Absolute deadline: a peer sending occasional TLS bytes must not extend the handshake.
308
+ const timer = setTimeout(() => {
309
+ fail(new Socks5FetchError("SOCKS5 TLS handshake timed out"));
310
+ }, SOCKS5_CONNECT_TIMEOUT_MS);
311
+ const cleanup = () => {
312
+ clearTimeout(timer);
313
+ signal.removeEventListener("abort", onAbort);
314
+ tlsSocket.removeListener("secureConnect", onSecureConnect);
315
+ tlsSocket.removeListener("error", onError);
316
+ tlsSocket.removeListener("close", onClose);
317
+ };
318
+ const fail = (error: Error) => {
319
+ if (settled) return;
320
+ settled = true;
321
+ cleanup();
322
+ // No error argument: pending event observers must not receive a second error.
350
323
  tlsSocket.destroy();
351
- reject(signal.reason instanceof Error ? signal.reason : new Error("The operation was aborted"));
324
+ reject(error);
352
325
  };
326
+ const onAbort = () => fail(signal.reason instanceof Error ? signal.reason : new Error("The operation was aborted"));
327
+ const onError = (error: Error) => fail(error);
328
+ const onClose = () => fail(new Socks5FetchError("SOCKS5 upstream closed during TLS handshake"));
353
329
  const onSecureConnect = () => {
354
- signal.removeEventListener("abort", onAbort);
330
+ if (settled) return;
331
+ settled = true;
332
+ cleanup();
355
333
  resolve(tlsSocket);
356
334
  };
357
335
  tlsSocket.once("secureConnect", onSecureConnect);
358
- tlsSocket.once("error", error => {
359
- signal.removeEventListener("abort", onAbort);
360
- reject(error);
361
- });
336
+ tlsSocket.once("error", onError);
337
+ tlsSocket.once("close", onClose);
362
338
  signal.addEventListener("abort", onAbort, { once: true });
339
+ if (signal.aborted) onAbort();
363
340
  });
364
341
  }
365
342
 
@@ -0,0 +1,112 @@
1
+ /**
2
+ * The SOCKS5 handshake both raw outbound transports share: method negotiation
3
+ * (RFC 1928), the optional RFC 1929 username/password subnegotiation, and the
4
+ * CONNECT exchange.
5
+ *
6
+ * `src/lib/socks5-fetch.ts` (the HTTP tunnel) and
7
+ * `src/lib/desktop-upstream-tunnel.ts` (the native desktop relay's raw
8
+ * dial) each own their socket, their timeouts, and what happens after CONNECT;
9
+ * this module only frames bytes through the reader each hands it, so a
10
+ * credentialed `socks5://` proxy URL authenticates identically whether the caller
11
+ * is a fetch or a raw WebSocket upgrade. When the URL carries credentials the
12
+ * greeting offers NO-AUTH alongside USER-PASS, which keeps an unauthenticated
13
+ * proxy working for a caller that configured more than it needed.
14
+ */
15
+
16
+ export class Socks5HandshakeError extends Error {
17
+ override readonly name = "Socks5HandshakeError";
18
+ }
19
+
20
+ const SOCKS5_VERSION = 0x05;
21
+ const SOCKS5_NO_AUTH = 0x00;
22
+ const SOCKS5_USER_PASS = 0x02;
23
+ const SOCKS5_CONNECT = 0x01;
24
+ const SOCKS5_DOMAIN = 0x03;
25
+ const SOCKS5_SUCCESS = 0x00;
26
+
27
+ /** RFC 1929 credentials decoded from the proxy URL, each bounded to one length byte. */
28
+ export interface Socks5Credentials {
29
+ username?: Uint8Array;
30
+ password?: Uint8Array;
31
+ }
32
+
33
+ export function socks5Credentials(proxy: URL): Socks5Credentials {
34
+ if (!proxy.username && !proxy.password) return {};
35
+ let username: string;
36
+ let password: string;
37
+ try {
38
+ username = decodeURIComponent(proxy.username);
39
+ password = decodeURIComponent(proxy.password);
40
+ } catch {
41
+ throw new Socks5HandshakeError("SOCKS5 proxy credentials contain invalid percent encoding");
42
+ }
43
+ const usernameBytes = new TextEncoder().encode(username);
44
+ const passwordBytes = new TextEncoder().encode(password);
45
+ if (usernameBytes.byteLength > 255 || passwordBytes.byteLength > 255) {
46
+ throw new Socks5HandshakeError("SOCKS5 proxy credentials must each fit in 255 UTF-8 bytes");
47
+ }
48
+ return { username: usernameBytes, password: passwordBytes };
49
+ }
50
+
51
+ /**
52
+ * The slice of a transport's socket reader the handshake needs: exact-length reads
53
+ * that reject on close, on error, or on the transport's own timeout, plus writes.
54
+ * Bytes read past a step stay queued in the transport's reader; the handshake never
55
+ * touches the socket itself.
56
+ */
57
+ export interface Socks5HandshakeReader {
58
+ write(bytes: Uint8Array): void;
59
+ readExact(bytes: number, signal?: AbortSignal): Promise<Uint8Array>;
60
+ }
61
+
62
+ export interface Socks5Target {
63
+ host: string;
64
+ port: number;
65
+ }
66
+
67
+ /** Negotiate methods, authenticate when the proxy picks USER-PASS, and CONNECT to `target`. */
68
+ export async function socks5Handshake(
69
+ reader: Socks5HandshakeReader,
70
+ target: Socks5Target,
71
+ credentials: Socks5Credentials,
72
+ signal?: AbortSignal,
73
+ ): Promise<void> {
74
+ const hostBytes = Buffer.from(target.host, "utf8");
75
+ if (hostBytes.byteLength > 255) throw new Socks5HandshakeError("SOCKS5 target hostname is too long");
76
+ const methods = credentials.username ? [SOCKS5_NO_AUTH, SOCKS5_USER_PASS] : [SOCKS5_NO_AUTH];
77
+ reader.write(Buffer.from([SOCKS5_VERSION, methods.length, ...methods]));
78
+ const greeting = await reader.readExact(2, signal);
79
+ if (greeting[0] !== SOCKS5_VERSION) throw new Socks5HandshakeError("SOCKS5 proxy returned an invalid greeting");
80
+ if (greeting[1] === SOCKS5_USER_PASS && credentials.username && credentials.password) {
81
+ reader.write(Buffer.from([
82
+ 0x01,
83
+ credentials.username.byteLength,
84
+ ...credentials.username,
85
+ credentials.password.byteLength,
86
+ ...credentials.password,
87
+ ]));
88
+ const auth = await reader.readExact(2, signal);
89
+ if (auth[0] !== 0x01 || auth[1] !== 0x00) throw new Socks5HandshakeError("SOCKS5 proxy authentication failed");
90
+ } else if (greeting[1] !== SOCKS5_NO_AUTH) {
91
+ throw new Socks5HandshakeError("SOCKS5 proxy does not accept an offered authentication method");
92
+ }
93
+
94
+ reader.write(Buffer.from([
95
+ SOCKS5_VERSION,
96
+ SOCKS5_CONNECT,
97
+ 0x00,
98
+ SOCKS5_DOMAIN,
99
+ hostBytes.byteLength,
100
+ ...hostBytes,
101
+ target.port >> 8,
102
+ target.port & 0xff,
103
+ ]));
104
+ const reply = await reader.readExact(4, signal);
105
+ if (reply[0] !== SOCKS5_VERSION) throw new Socks5HandshakeError("SOCKS5 proxy returned an invalid connect response");
106
+ if (reply[1] !== SOCKS5_SUCCESS) throw new Socks5HandshakeError(`SOCKS5 proxy refused the connection (code ${reply[1]})`);
107
+ if (reply[2] !== 0x00 || ![0x01, SOCKS5_DOMAIN, 0x04].includes(reply[3]!)) {
108
+ throw new Socks5HandshakeError("SOCKS5 proxy returned an invalid address type or reserved byte");
109
+ }
110
+ const addressLength = reply[3] === 0x01 ? 4 : reply[3] === SOCKS5_DOMAIN ? (await reader.readExact(1, signal))[0]! : 16;
111
+ await reader.readExact(addressLength + 2, signal);
112
+ }
@@ -1,3 +1,5 @@
1
+ import { isNonReplayableResponse, markResponseNonReplayable } from "./upstream-retry";
2
+
1
3
  export const TRANSLATOR_MAX_CALL_ARGUMENT_BYTES = 2 * 1024 * 1024;
2
4
  export const TRANSLATOR_MAX_TURN_BYTES = 32 * 1024 * 1024;
3
5
  export const TRANSLATOR_MAX_SSE_EVENT_BYTES = 32 * 1024 * 1024;
@@ -410,5 +412,9 @@ export function finalizeTranslatorBudgetResponse(response: Response, budget: Tra
410
412
  });
411
413
  if (signal?.aborted) finalize();
412
414
  else signal?.addEventListener("abort", finalize, { once: true });
413
- return new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
415
+ const finalizedResponse = new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
416
+ // The rebuild above drops identity markers. A refusal that must not be replayed
417
+ // stays non-replayable through accounting finalization.
418
+ if (isNonReplayableResponse(response)) markResponseNonReplayable(finalizedResponse);
419
+ return finalizedResponse;
414
420
  }
@@ -41,6 +41,7 @@ import type { KiroRefusalKind } from "../adapters/kiro-refusal";
41
41
  import { kiroAccountEvidence } from "../providers/kiro-usage";
42
42
  import { kiroEvidenceIdentity } from "../providers/kiro-account-state-disk";
43
43
  import { kiroAccountSupportsModel } from "../providers/kiro-model-catalog";
44
+ import { antigravityAccountSupportsModel } from "../providers/antigravity-account-roster";
44
45
  import { ACCOUNT_QUOTA_TTL_MS } from "../providers/quota-wire";
45
46
  import { sweepExpiredOnWrite } from "../lib/state-store-sweeper";
46
47
  import { subscribeOAuthAccountPauseChanges } from "../lib/account-selection-events";
@@ -248,11 +249,31 @@ export function eligibleFailoverAccounts(providerName: string, now = Date.now(),
248
249
  return eligibleIdsIn(getAccountSet(providerName), providerName, now, family);
249
250
  }
250
251
 
251
- /** Model evidence only ranks accounts already eligible and below any configured cap. */
252
+ /** Whether an account's own model listing includes `model`; undefined when unknown. */
253
+ function accountListsModel(providerName: string, accountId: string, model: string): boolean | undefined {
254
+ if (providerName === "kiro") return kiroAccountSupportsModel(accountId, model);
255
+ if (providerName === "google-antigravity") return antigravityAccountSupportsModel(providerName, accountId, model);
256
+ return undefined;
257
+ }
258
+
259
+ /**
260
+ * Model evidence only ranks accounts already eligible and below any configured cap.
261
+ *
262
+ * Kiro and Antigravity both gate models per account. For Antigravity (Claude 4.6 vs 5.5, Argon)
263
+ * an account whose discovered roster lists the model wins; an account whose roster omits it is
264
+ * skipped while any other remains; an account with no roster yet is unknown and stays usable.
265
+ */
252
266
  export function preferKiroModelSupport(
253
267
  providerName: string, ids: string[], model?: string | null, config?: OcxConfig,
254
268
  ): string[] {
255
- if (providerName !== "kiro" || !model) return ids;
269
+ if (!model) return ids;
270
+ if (providerName === "google-antigravity") {
271
+ const positive = ids.filter(id => accountListsModel(providerName, id, model) === true);
272
+ if (positive.length) return positive;
273
+ const notListedAgainst = ids.filter(id => accountListsModel(providerName, id, model) !== false);
274
+ return notListedAgainst.length ? notListedAgainst : ids;
275
+ }
276
+ if (providerName !== "kiro") return ids;
256
277
  const cap = config?.providers?.kiro?.oauthAccountFailover?.maxConcurrentPerAccount;
257
278
  const withRoom = typeof cap === "number" && Number.isInteger(cap) && cap >= 1 && cap <= 100
258
279
  ? ids.filter(id => accountInFlight("kiro", id) < cap) : ids;
@@ -631,10 +652,13 @@ export function preferredInitialAccount(
631
652
  const modelEligible = preferKiroModelSupport(providerName,
632
653
  eligibleFailoverAccounts(providerName, now, classifyModelFamilyForQuota(providerName, requestedModelId)),
633
654
  requestedModelId, config);
634
- const activeLacks = providerName === "kiro" && !!active && !!requestedModelId
635
- && kiroAccountSupportsModel(active, requestedModelId) === false;
655
+ const activeLacks = (providerName === "kiro" || providerName === "google-antigravity")
656
+ && !!active && !!requestedModelId
657
+ && accountListsModel(providerName, active, requestedModelId) === false;
636
658
  const listingSibling = activeLacks
637
- ? modelEligible.find(id => id !== active && kiroModelListingHasRoom(config, id, requestedModelId!))
659
+ ? modelEligible.find(id => id !== active && (providerName === "kiro"
660
+ ? kiroModelListingHasRoom(config, id, requestedModelId!)
661
+ : accountListsModel(providerName, id, requestedModelId!) === true))
638
662
  : undefined;
639
663
  if (listingSibling) return listingSibling;
640
664
 
@@ -49,7 +49,7 @@ import { loginGithubCopilot, refreshGithubCopilotToken, validateCopilotApiBaseUr
49
49
  import { loginCommandCode, refreshCommandCodeToken } from "./command-code";
50
50
  import { loginMetaMuse, refreshMetaMuseToken } from "./meta-muse";
51
51
  import { loginOrcaRouter, orcaRouterInferenceBaseUrl, refreshOrcaRouterKey } from "./orcarouter";
52
- import { ANTIGRAVITY_REQUEST_UA } from "../adapters/google-antigravity-wire";
52
+ import { ANTIGRAVITY_DISCOVERY_UA } from "../adapters/google-antigravity-wire";
53
53
  import { deriveOAuthDefaultModel, deriveOAuthProviderConfig } from "../providers/derive";
54
54
  import { apiKeyPoolEntryId, sanitizeApiKeyValue } from "../providers/api-keys";
55
55
  import { effectiveGoogleMode, getProviderRegistryEntry, mergeRegistryStaticHeaders, providerMatchesRegistryTransport } from "../providers/registry";
@@ -1161,7 +1161,9 @@ export function buildModelsRequest(
1161
1161
  if (effectiveGoogleMode(providerName, effectiveProvider) === "cloud-code-assist") {
1162
1162
  headers.Accept = "application/json";
1163
1163
  headers["Content-Type"] = "application/json";
1164
- headers["User-Agent"] = ANTIGRAVITY_REQUEST_UA;
1164
+ // CLI UA: CCA lists Gemini 4 Argon's wire ids only to the CLI client family. Inference keeps
1165
+ // choosing per wire model (antigravityUserAgentForWireModel).
1166
+ headers["User-Agent"] = ANTIGRAVITY_DISCOVERY_UA;
1165
1167
  if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
1166
1168
  return guarded({
1167
1169
  method: "POST" as const,
@@ -250,9 +250,10 @@ async function poll(flow: Flow, principal: KiroDevicePrincipal): Promise<KiroDev
250
250
  if (data.status === "authorization_pending") return publicView(flow);
251
251
  if (data.status === "expired_token" || data.status === "expired") return terminal(flow, "expired");
252
252
  }
253
+ // The social poll reports a completed grant as status "authorized"; "approved" is kept as an alias.
253
254
  if (result.status !== 200 || Object.hasOwn(data, "error") || error || (builder && (data.status !== undefined || !positive(data.expiresIn) || !isStorableKiroClientPart(flow.clientId)
254
255
  || !isStorableKiroClientPart(flow.clientSecret)))
255
- || (!builder && (data.status !== undefined && data.status !== "approved"))) return terminal(flow, "failed");
256
+ || (!builder && (data.status !== undefined && data.status !== "authorized" && data.status !== "approved"))) return terminal(flow, "failed");
256
257
  if (!code(data.accessToken) || !code(data.refreshToken)
257
258
  || (!builder && (typeof data.profileArn !== "string" || !PROFILE_ARN.test(data.profileArn)))) return terminal(flow, "failed");
258
259
  const credential: OAuthCredentials = {
package/src/oauth/kiro.ts CHANGED
@@ -45,6 +45,10 @@ const KIRO_TERMINAL_REFRESH_ERRORS = new Set([
45
45
  "access_denied",
46
46
  "expired_token",
47
47
  ]);
48
+ // The desktop refresh endpoint rejects a dead refresh token with HTTP 401 and a bare
49
+ // `{"message":"Bad credentials"}` body, without an OAuth `error` code. Only that exact
50
+ // pair is a terminal verdict; it is surfaced under this fixed code, never the raw text.
51
+ const KIRO_DESKTOP_BAD_CREDENTIALS = "bad_credentials";
48
52
 
49
53
  interface ImportedKiroToken {
50
54
  access: string;
@@ -545,12 +549,14 @@ async function kiroTokenRefreshError(response: Response, awsSsoRefresh = false):
545
549
  let oauthError: string | undefined;
546
550
  let refreshAttention = false;
547
551
  try {
548
- const payload = await response.json() as { error?: unknown };
552
+ const payload = await response.json() as { error?: unknown; message?: unknown };
549
553
  // This input-error code is attention evidence only for our AWS SSO refresh request,
550
554
  // never a terminal grant verdict. Do not surface arbitrary upstream descriptions.
551
555
  refreshAttention = awsSsoRefresh && response.status === 400 && payload.error === "invalid_request";
552
556
  if (typeof payload.error === "string" && KIRO_TERMINAL_REFRESH_ERRORS.has(payload.error)) {
553
557
  oauthError = payload.error;
558
+ } else if (!awsSsoRefresh && response.status === 401 && payload.message === "Bad credentials") {
559
+ oauthError = KIRO_DESKTOP_BAD_CREDENTIALS;
554
560
  }
555
561
  } catch {
556
562
  // Error bodies are untrusted and intentionally excluded from the surfaced message.