@bitkyc08/opencodex 2.50.0 → 2.51.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 (95) hide show
  1. package/bin/ocx.mjs +222 -71
  2. package/gui/dist/assets/{index-C39tnjXO.js → index-D7BdZpZm.js} +1 -1
  3. package/gui/dist/index.html +1 -1
  4. package/package.json +1 -1
  5. package/src/adapters/qoder/adapter.ts +69 -1
  6. package/src/adapters/qoder/scaffold-guard.ts +233 -0
  7. package/src/claude/agents-inject.ts +29 -5
  8. package/src/claude/desktop-3p.ts +31 -3
  9. package/src/claude/gateway-cache.ts +12 -21
  10. package/src/cli/capabilities.ts +28 -0
  11. package/src/cli/claude-agent-startup-sync.ts +26 -1
  12. package/src/cli/claude.ts +138 -20
  13. package/src/cli/config-command.ts +67 -1
  14. package/src/cli/connect.ts +181 -14
  15. package/src/cli/dispatch.ts +53 -9
  16. package/src/cli/doctor.ts +9 -2
  17. package/src/cli/ensure-desired-integrations.ts +10 -0
  18. package/src/cli/gui-pair-client.ts +1 -12
  19. package/src/cli/help.ts +4 -1
  20. package/src/cli/hub.ts +367 -0
  21. package/src/cli/index.ts +94 -30
  22. package/src/cli/launcher-context.ts +1 -1
  23. package/src/cli/registry.ts +43 -3
  24. package/src/cli/status.ts +325 -5
  25. package/src/cli/version-skew.ts +4 -1
  26. package/src/cli.ts +2 -2
  27. package/src/client/catalog-compatibility.ts +192 -0
  28. package/src/client/connect.ts +31 -0
  29. package/src/client/hub-client.ts +52 -0
  30. package/src/client/hub-state.ts +214 -0
  31. package/src/codex/account-usability.ts +48 -12
  32. package/src/codex/auth-api.ts +49 -5
  33. package/src/codex/catalog/effort.ts +67 -8
  34. package/src/codex/catalog/sync.ts +85 -0
  35. package/src/codex/codex-write-lock.ts +11 -2
  36. package/src/codex/desired-state.ts +47 -1
  37. package/src/codex/inject-coordination.ts +10 -5
  38. package/src/codex/inject.ts +26 -10
  39. package/src/codex/loopback-target.ts +45 -0
  40. package/src/codex/routing.ts +48 -1
  41. package/src/codex/runtime.ts +37 -3
  42. package/src/codex/sync.ts +29 -9
  43. package/src/codex/warmup.ts +21 -4
  44. package/src/config/pending-teardown.ts +1 -1
  45. package/src/config.ts +126 -12
  46. package/src/generated/compatibility-version.json +136 -76
  47. package/src/grok/status.ts +9 -1
  48. package/src/integrations/config-io.ts +54 -1
  49. package/src/lib/bun-runtime.ts +1 -1
  50. package/src/lib/gui-pair-capability.ts +27 -0
  51. package/src/lib/local-destinations.ts +162 -0
  52. package/src/lib/package-tree-integrity.ts +1 -1
  53. package/src/lib/process-control.ts +130 -20
  54. package/src/lib/service-secrets.ts +28 -0
  55. package/src/lib/test-home-guard.ts +49 -0
  56. package/src/providers/opencode-go-transport.ts +9 -1
  57. package/src/providers/quota.ts +5 -1
  58. package/src/providers/registry.ts +34 -5
  59. package/src/remote/hub-state.ts +182 -0
  60. package/src/server/auth-cors.ts +5 -0
  61. package/src/server/chat-completions.ts +6 -3
  62. package/src/server/claude-messages.ts +7 -1
  63. package/src/server/hub-state.ts +98 -0
  64. package/src/server/index.ts +124 -6
  65. package/src/server/management/api-access.ts +14 -3
  66. package/src/server/management/config-routes.ts +2 -2
  67. package/src/server/management/cursor-integration-routes.ts +13 -4
  68. package/src/server/proxy-liveness.ts +7 -1
  69. package/src/server/request-log-conversation.ts +41 -1
  70. package/src/server/responses/codex-auth-error.ts +18 -1
  71. package/src/server/responses/codex-ws-exchange.ts +36 -4
  72. package/src/server/responses/codex-ws-wire.ts +75 -4
  73. package/src/server/responses/compact.ts +20 -9
  74. package/src/server/responses/core.ts +57 -10
  75. package/src/server/responses/policy-fallback.ts +7 -1
  76. package/src/server/system-env-shell.ts +14 -2
  77. package/src/server/system-env.ts +106 -14
  78. package/src/service.ts +906 -94
  79. package/src/types/config.ts +57 -4
  80. package/src/update/badge.ts +3 -2
  81. package/src/update/index.ts +317 -64
  82. package/src/update/install-detection.d.mts +6 -0
  83. package/src/update/install-detection.mjs +73 -0
  84. package/src/update/job.ts +101 -49
  85. package/src/update/pnpm-global-install.d.mts +144 -0
  86. package/src/update/pnpm-global-install.mjs +591 -0
  87. package/src/update/pnpm-invocation.d.mts +43 -0
  88. package/src/update/pnpm-invocation.mjs +141 -0
  89. package/src/update/registry-integrity.d.mts +16 -0
  90. package/src/update/registry-integrity.mjs +37 -0
  91. package/src/update/transactional-install.d.mts +1 -1
  92. package/src/update/transactional-install.mjs +101 -7
  93. package/src/update/tray-update-plan.mjs +1 -1
  94. package/src/vision/plan.ts +13 -3
  95. package/src/vision/routed-describe.ts +51 -20
package/src/config.ts CHANGED
@@ -46,6 +46,7 @@ import {
46
46
  MAIN_CODEX_ACCOUNT_NAMESPACE_TARGET,
47
47
  } from "./codex/account-namespace-match";
48
48
  import { isCodexAccountPriorityKey } from "./codex/account-priority";
49
+ import { loopbackCompanionAllowed } from "./codex/loopback-target";
49
50
  import { UPSTREAM_HOST_CIRCUIT_MAX_THRESHOLD } from "./codex/upstream-host-health";
50
51
  import {
51
52
  adoptCustomModelCatalogMigration,
@@ -1036,6 +1037,18 @@ const hubConfigSchema = z.object({
1036
1037
  }
1037
1038
  return origin;
1038
1039
  }).optional(),
1040
+ // Same canonical-origin rule as managementPublicOrigin, and deliberately NOT `.catch`ed:
1041
+ // a mistyped data origin must be rejected at write time, because silently dropping it
1042
+ // makes `ocx hub invite` print the `http://<hostname>:<port>` fallback that the operator
1043
+ // set this field precisely to replace.
1044
+ dataPublicOrigin: z.string().transform((value, ctx) => {
1045
+ const origin = canonicalHttpOrigin(value);
1046
+ if (!origin) {
1047
+ ctx.addIssue({ code: "custom", message: "must be a canonical http(s) origin without credentials, path, query, or fragment" });
1048
+ return z.NEVER;
1049
+ }
1050
+ return origin;
1051
+ }).optional(),
1039
1052
  // A malformed hand edit disables only the optional ingress. Live writes are rejected by
1040
1053
  // managementIngressConfigError before this load-time degradation can hide the mistake.
1041
1054
  managementIngress: z.union([
@@ -1111,6 +1124,17 @@ const clientConnectionSchema = z.object({
1111
1124
  }).optional(),
1112
1125
  }).strict();
1113
1126
 
1127
+ /**
1128
+ * Codex pool selection policy section.
1129
+ *
1130
+ * `.strict()` like its neighbour: a typo in an optional feature section should surface as a
1131
+ * rejected write rather than a silently ignored key that leaves the operator believing they
1132
+ * excluded something.
1133
+ */
1134
+ const codexPoolSchema = z.object({
1135
+ excludedPlans: z.array(z.string().trim().min(1)).optional(),
1136
+ }).strict();
1137
+
1114
1138
  /**
1115
1139
  * Quota-reset notification section.
1116
1140
  *
@@ -1188,13 +1212,16 @@ const configSchema = z.object({
1188
1212
  // is safe: startServer() already falls back to 127.0.0.1 for a missing hostname. Write-time
1189
1213
  // rejection lives in validateConfigCandidate() so bad values still surface to the caller.
1190
1214
  hostname: z.string().trim().min(1).optional().catch(undefined),
1191
- // Discriminated on `enabled` so a disabled entry cannot be forced to carry a port, and an
1192
- // enabled one cannot omit it (#1102). A malformed value degrades to undefined rather than
1193
- // failing the whole parse: this is an opt-in convenience surface, and a hand-edit typo here
1194
- // must never reset providers/apiKeys through the backup-and-defaults repair path.
1215
+ // Discriminated on `enabled` so a disabled entry cannot be forced to carry a port (#1102).
1216
+ // An enabled one MAY omit it: that is the companion form, which binds 127.0.0.1 on the proxy
1217
+ // port and is legal only off a loopback/wildcard bind — a relationship between two fields, so
1218
+ // it is enforced in validateConfigCandidate() and again at startup, not here (#4236).
1219
+ // A malformed value degrades to undefined rather than failing the whole parse: this is an
1220
+ // opt-in convenience surface, and a hand-edit typo here must never reset providers/apiKeys
1221
+ // through the backup-and-defaults repair path.
1195
1222
  unauthenticatedLoopbackListener: z.union([
1196
1223
  z.object({ enabled: z.literal(false) }),
1197
- z.object({ enabled: z.literal(true), port: z.number().int().min(1).max(65535) }),
1224
+ z.object({ enabled: z.literal(true), port: z.number().int().min(1).max(65535).optional() }),
1198
1225
  ]).optional().catch(undefined),
1199
1226
  providers: z.record(z.string(), providerConfigSchema),
1200
1227
  modelPinnedEfforts: modelPinnedEffortsSchema.optional(),
@@ -1249,6 +1276,10 @@ const configSchema = z.object({
1249
1276
  codexDesktopAuthless: z.boolean().optional().catch(undefined),
1250
1277
  codexClientCompaction: z.boolean().optional().catch(undefined),
1251
1278
  pausedCodexAccountIds: z.array(z.string().regex(/^[a-zA-Z0-9._-]{1,64}$/)).optional(),
1279
+ // A malformed policy degrades to "no policy" rather than failing the parse, so a hand-edited
1280
+ // typo cannot trip the backup-and-defaults repair path and wipe providers or pool accounts.
1281
+ // Silently ignoring it would be its own trap, so the write path rejects it and loadConfig warns.
1282
+ codexPool: codexPoolSchema.optional().catch(undefined),
1252
1283
  codexQuotaAutoRefresh: codexQuotaAutoRefreshSchema.optional().catch(undefined),
1253
1284
  codexAccountNamespaces: codexAccountNamespacesSchema.optional(),
1254
1285
  // Selection order is a preference, not a safety control like pause: a malformed
@@ -2160,6 +2191,20 @@ function malformedQuotaResetNotifyWarning(rawParsed: unknown): string | null {
2160
2191
  return `quotaResetNotify${field ? `.${field}` : ""} ignored: invalid quota-reset notification configuration`;
2161
2192
  }
2162
2193
 
2194
+ /**
2195
+ * Same silent-in-the-wrong-direction failure as the notification block: a dropped pool policy means
2196
+ * the accounts the operator meant to exclude keep taking traffic, and the only visible symptom is
2197
+ * traffic going somewhere it was supposed to stop going.
2198
+ */
2199
+ function malformedCodexPoolWarning(rawParsed: unknown): string | null {
2200
+ const raw = rawConfigRecord(rawParsed);
2201
+ if (!raw || !Object.hasOwn(raw, "codexPool")) return null;
2202
+ const result = codexPoolSchema.safeParse(raw.codexPool);
2203
+ if (result.success) return null;
2204
+ const field = result.error.issues[0]?.path.join(".");
2205
+ return `codexPool${field ? `.${field}` : ""} ignored: invalid Codex pool selection policy`;
2206
+ }
2207
+
2163
2208
  /**
2164
2209
  * Warn once per load that the section was dropped.
2165
2210
  *
@@ -2172,6 +2217,18 @@ function warnDegradedQuotaResetNotify(rawParsed: unknown): void {
2172
2217
  if (warning) console.warn(`⚠️ config.json ${warning}. Other settings were preserved.`);
2173
2218
  }
2174
2219
 
2220
+ /**
2221
+ * Warn once per load that the pool policy was dropped.
2222
+ *
2223
+ * `.catch(undefined)` turns a malformed policy into a SUCCESSFUL parse, so without this the proxy
2224
+ * starts, rotates onto the accounts the operator meant to exclude, and prints nothing. The visible
2225
+ * symptom would be traffic going exactly where it was told not to go.
2226
+ */
2227
+ function warnDegradedCodexPool(rawParsed: unknown): void {
2228
+ const warning = malformedCodexPoolWarning(rawParsed);
2229
+ if (warning) console.warn(`⚠️ config.json ${warning}. Other settings were preserved.`);
2230
+ }
2231
+
2175
2232
  type NativeSubagentPersistedField = "injectionModel" | "injectionEffort" | "syncCodexSubagentDefaults";
2176
2233
 
2177
2234
  function rawConfigRecord(rawParsed: unknown): Record<string, unknown> | null {
@@ -2331,6 +2388,7 @@ export function loadConfig(): OcxConfig {
2331
2388
  warnDegradedRuntimeRole(parsed);
2332
2389
  warnDegradedOptionalRemoteBlocks(parsed);
2333
2390
  warnDegradedQuotaResetNotify(parsed);
2391
+ warnDegradedCodexPool(parsed);
2334
2392
  return withRefreshedCostOverlays(normalizeClaudeSubagentEffort(normalizeNativeSubagentSync(config, parsed), parsed));
2335
2393
  }
2336
2394
  // Schema validation failed — merge defaults into the raw object instead of
@@ -2359,6 +2417,7 @@ export function loadConfig(): OcxConfig {
2359
2417
  warnDegradedRuntimeRole(parsed);
2360
2418
  warnDegradedOptionalRemoteBlocks(parsed);
2361
2419
  warnDegradedQuotaResetNotify(parsed);
2420
+ warnDegradedCodexPool(parsed);
2362
2421
  return withRefreshedCostOverlays(normalizeClaudeSubagentEffort(normalizeNativeSubagentSync(config, parsed), parsed));
2363
2422
  }
2364
2423
  // Still failing, but if every complaint is about one or more named entries
@@ -2383,6 +2442,7 @@ export function loadConfig(): OcxConfig {
2383
2442
  warnDegradedRuntimeRole(parsed);
2384
2443
  warnDegradedOptionalRemoteBlocks(parsed);
2385
2444
  warnDegradedQuotaResetNotify(parsed);
2445
+ warnDegradedCodexPool(parsed);
2386
2446
  return withRefreshedCostOverlays(normalizeClaudeSubagentEffort(normalizeNativeSubagentSync(config, parsed), parsed));
2387
2447
  }
2388
2448
  }
@@ -2527,6 +2587,8 @@ function validFileConfigDiagnostics(config: OcxConfig, rawParsed: unknown): Conf
2527
2587
  if (clientWarning) warnings.push(clientWarning);
2528
2588
  const notifyWarning = malformedQuotaResetNotifyWarning(rawParsed);
2529
2589
  if (notifyWarning) warnings.push(notifyWarning);
2590
+ const codexPoolWarning = malformedCodexPoolWarning(rawParsed);
2591
+ if (codexPoolWarning) warnings.push(codexPoolWarning);
2530
2592
  if (syncDisabledReason) {
2531
2593
  warnings.push(`syncCodexSubagentDefaults ignored: ${syncDisabledReason}`);
2532
2594
  }
@@ -2675,6 +2737,21 @@ function quotaResetNotifyError(value: unknown): string | null {
2675
2737
  return `schema_invalid: quotaResetNotify${field ? `.${field}` : ""}: ${issue?.message ?? "invalid configuration"}`;
2676
2738
  }
2677
2739
 
2740
+ /**
2741
+ * The read path degrades a malformed pool policy to undefined, which for an exclusion policy means
2742
+ * the excluded accounts quietly keep serving traffic. Reject it on write so `ocx config set` cannot
2743
+ * create a policy that looks applied and is not.
2744
+ */
2745
+ function codexPoolError(value: unknown): string | null {
2746
+ const raw = rawConfigRecord(value);
2747
+ if (!raw || !Object.hasOwn(raw, "codexPool") || raw.codexPool === undefined) return null;
2748
+ const result = codexPoolSchema.safeParse(raw.codexPool);
2749
+ if (result.success) return null;
2750
+ const issue = result.error.issues[0];
2751
+ const field = issue?.path.join(".");
2752
+ return `schema_invalid: codexPool${field ? `.${field}` : ""}: ${issue?.message ?? "invalid configuration"}`;
2753
+ }
2754
+
2678
2755
  /**
2679
2756
  * Same reasoning as {@link blankHostnameError}, and more urgent: the read path degrades a
2680
2757
  * malformed selection-order map to undefined, which on a write would drop every entry the
@@ -2759,15 +2836,22 @@ function oauthOpenBrowserError(value: unknown): string | null {
2759
2836
 
2760
2837
  /** Validate an in-memory config candidate without touching disk. Used by headless CLI import/set. */
2761
2838
  /**
2762
- * Reject a loopback-listener port that collides with the proxy port (#1102).
2839
+ * Reject a loopback-listener port that collides with the proxy port (#1102), and a port-less
2840
+ * companion listener on a bind address that already owns 127.0.0.1 (#4236).
2841
+ *
2842
+ * The schema can only check the shape of each field on its own; the two ports being distinct —
2843
+ * and the port-less form being compatible with `hostname` — are relationships between fields.
2844
+ * Letting either through would surface as a startup failure after the public listener already
2845
+ * bound, which reads like an unrelated port conflict.
2763
2846
  *
2764
- * The schema can only check the shape of each field on its own; the two ports being distinct
2765
- * is a relationship between them. Letting the pair through would surface as a startup failure
2766
- * after the public listener already bound, which reads like an unrelated port conflict.
2847
+ * Both keys are read from the same candidate, so `ocx config set hostname 127.0.0.1` on a host
2848
+ * whose listener is already the companion form is refused by this same check, with the same
2849
+ * message, rather than breaking the next start.
2767
2850
  *
2768
2851
  * This is write-time only, matching `blankHostnameError`: a live caller can be told the value
2769
2852
  * is wrong, whereas a hand-edited config on the read path degrades to undefined rather than
2770
- * resetting the whole file.
2853
+ * resetting the whole file. `assertLoopbackListenerBindable` repeats the decision at startup so
2854
+ * a hand edit that skipped this boundary fails with the same sentence instead of EADDRINUSE.
2771
2855
  */
2772
2856
  function loopbackListenerPortError(value: unknown): string | null {
2773
2857
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
@@ -2785,17 +2869,46 @@ function loopbackListenerPortError(value: unknown): string | null {
2785
2869
  return "schema_invalid: unauthenticatedLoopbackListener.enabled: must be a boolean";
2786
2870
  }
2787
2871
  if (entry.enabled !== true) return null;
2872
+ const hostname = typeof (value as Record<string, unknown>).hostname === "string"
2873
+ ? (value as Record<string, unknown>).hostname as string
2874
+ : undefined;
2875
+ const proxyPort = (value as Record<string, unknown>).port;
2788
2876
  const listenerPort = entry.port;
2877
+ // The companion form. `port` omitted means "same port as the public listener, on 127.0.0.1",
2878
+ // which only exists as a free address when the public listener is bound somewhere else.
2879
+ if (listenerPort === undefined) {
2880
+ return loopbackCompanionBindError(
2881
+ hostname,
2882
+ typeof proxyPort === "number" ? proxyPort : 10100,
2883
+ );
2884
+ }
2789
2885
  if (typeof listenerPort !== "number" || !Number.isInteger(listenerPort) || listenerPort < 1 || listenerPort > 65535) {
2790
- return "schema_invalid: unauthenticatedLoopbackListener.port: must be an integer port when enabled";
2886
+ return "schema_invalid: unauthenticatedLoopbackListener.port: must be an integer port when enabled, or omitted to share the proxy port";
2791
2887
  }
2792
- const proxyPort = (value as Record<string, unknown>).port;
2793
2888
  if (typeof proxyPort === "number" && proxyPort === listenerPort) {
2794
2889
  return "schema_invalid: unauthenticatedLoopbackListener.port: must differ from the proxy port";
2795
2890
  }
2796
2891
  return null;
2797
2892
  }
2798
2893
 
2894
+ /**
2895
+ * The one sentence both the write boundary and startup use for an impossible companion bind.
2896
+ *
2897
+ * Exported so `startServer` can fail with the identical text: an operator who hand-edited the
2898
+ * file past `validateConfigCandidate` must read the same diagnosis, not EADDRINUSE.
2899
+ */
2900
+ export function loopbackCompanionBindError(
2901
+ hostname: string | undefined,
2902
+ proxyPort: number,
2903
+ ): string | null {
2904
+ if (loopbackCompanionAllowed(hostname)) return null;
2905
+ const bind = (hostname ?? "").trim() || "127.0.0.1";
2906
+ return "schema_invalid: unauthenticatedLoopbackListener: a port-less listener binds "
2907
+ + `127.0.0.1:${proxyPort}, which the public listener on hostname "${bind}" already holds. `
2908
+ + "Either set a distinct unauthenticatedLoopbackListener.port, or remove the listener — a "
2909
+ + "loopback bind already admits local callers without a credential.";
2910
+ }
2911
+
2799
2912
  /**
2800
2913
  * Validate the hub management ingress at the live-write boundary.
2801
2914
  *
@@ -2850,6 +2963,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
2850
2963
  ?? upstreamHostCircuitThresholdError(value)
2851
2964
  ?? agentTaskRecoveryError(value)
2852
2965
  ?? quotaResetNotifyError(value)
2966
+ ?? codexPoolError(value)
2853
2967
  ?? googleAntigravityStaticCatalogVersionError(value)
2854
2968
  ?? codexAccountPrioritiesError(value)
2855
2969
  ?? codexQuotaAutoRefreshError(value)