@iislee/opencodex 2.31.0 → 2.35.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 (261) hide show
  1. package/README.md +3 -3
  2. package/bin/ocx.mjs +99 -70
  3. package/gui/dist/assets/index-Be1sDpWw.css +1 -0
  4. package/gui/dist/assets/index-D4OKgUV4.js +114 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +3 -1
  7. package/src/adapters/anthropic.ts +104 -15
  8. package/src/adapters/base.ts +29 -1
  9. package/src/adapters/command-code.ts +42 -6
  10. package/src/adapters/cursor/call-id.ts +44 -0
  11. package/src/adapters/cursor/catalog.ts +541 -0
  12. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  13. package/src/adapters/cursor/cursor-errors.ts +15 -0
  14. package/src/adapters/cursor/discovery.ts +69 -18
  15. package/src/adapters/cursor/effort-map.ts +79 -1
  16. package/src/adapters/cursor/envelope-echo.ts +290 -0
  17. package/src/adapters/cursor/live-models.ts +7 -2
  18. package/src/adapters/cursor/live-transport.ts +14 -0
  19. package/src/adapters/cursor/message-mapper.ts +4 -1
  20. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  21. package/src/adapters/cursor/native-exec-fs.ts +2 -2
  22. package/src/adapters/cursor/native-exec-network.ts +1 -1
  23. package/src/adapters/cursor/native-exec-shell.ts +5 -2
  24. package/src/adapters/cursor/native-exec.ts +20 -0
  25. package/src/adapters/cursor/protobuf-request.ts +68 -14
  26. package/src/adapters/cursor/request-builder.ts +40 -10
  27. package/src/adapters/cursor/tool-definitions.ts +14 -4
  28. package/src/adapters/cursor/tool-result-normalize.ts +28 -5
  29. package/src/adapters/cursor/types.ts +20 -0
  30. package/src/adapters/cursor.ts +209 -38
  31. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  32. package/src/adapters/google-antigravity-replay.ts +173 -25
  33. package/src/adapters/google-antigravity-wire.ts +5 -0
  34. package/src/adapters/google-errors.ts +44 -12
  35. package/src/adapters/google.ts +136 -24
  36. package/src/adapters/image.ts +1 -1
  37. package/src/adapters/kiro-constants.ts +27 -0
  38. package/src/adapters/kiro-tools.ts +55 -16
  39. package/src/adapters/kiro.ts +182 -23
  40. package/src/adapters/openai-chat.ts +300 -242
  41. package/src/adapters/openai-responses.ts +353 -27
  42. package/src/adapters/run-turn-queue.ts +36 -1
  43. package/src/adapters/tool-catalog-nudge.ts +4 -3
  44. package/src/adapters/xai-tool-schema.ts +436 -0
  45. package/src/adapters/xai-web-search.ts +17 -16
  46. package/src/bridge.ts +90 -37
  47. package/src/chat/inbound.ts +14 -1
  48. package/src/chat/outbound.ts +15 -7
  49. package/src/claude/agents-inject.ts +8 -1
  50. package/src/claude/context-windows.ts +16 -9
  51. package/src/claude/outbound.ts +24 -11
  52. package/src/cli/access.ts +46 -3
  53. package/src/cli/account-api.ts +116 -19
  54. package/src/cli/account-extended.ts +262 -29
  55. package/src/cli/account-main.ts +12 -12
  56. package/src/cli/account.ts +69 -15
  57. package/src/cli/agent.ts +8 -1
  58. package/src/cli/alias.ts +66 -0
  59. package/src/cli/capabilities-command.ts +94 -0
  60. package/src/cli/capabilities.ts +496 -0
  61. package/src/cli/claude-desktop.ts +31 -11
  62. package/src/cli/claude.ts +26 -1
  63. package/src/cli/dispatch.ts +256 -28
  64. package/src/cli/doctor.ts +126 -14
  65. package/src/cli/help.ts +17 -6
  66. package/src/cli/index.ts +39 -9
  67. package/src/cli/init.ts +1 -0
  68. package/src/cli/inspect.ts +230 -0
  69. package/src/cli/models-runtime.ts +95 -0
  70. package/src/cli/models.ts +26 -10
  71. package/src/cli/observe.ts +31 -8
  72. package/src/cli/provider-runtime.ts +16 -2
  73. package/src/cli/provider.ts +2 -1
  74. package/src/cli/registry.ts +49 -10
  75. package/src/cli/runtime-api.ts +51 -7
  76. package/src/cli/status.ts +41 -1
  77. package/src/cli/storage.ts +234 -0
  78. package/src/cli/system-command.ts +16 -0
  79. package/src/cli/system-restart-client.ts +1 -1
  80. package/src/cli/usage-report.ts +184 -0
  81. package/src/cli/v2.ts +34 -10
  82. package/src/cli/version-skew.ts +46 -0
  83. package/src/clients/config-export.ts +85 -3
  84. package/src/clients/effective-status.ts +30 -2
  85. package/src/clients/probes/cline.ts +81 -0
  86. package/src/codex/account-label.ts +21 -0
  87. package/src/codex/account-pause.ts +2 -1
  88. package/src/codex/account-priority.ts +3 -2
  89. package/src/codex/app-server-processes.ts +83 -7
  90. package/src/codex/auth-api.ts +50 -10
  91. package/src/codex/auth-context.ts +21 -18
  92. package/src/codex/autostart-health.ts +16 -0
  93. package/src/codex/catalog/aggregation.ts +18 -0
  94. package/src/codex/catalog/effort.ts +18 -3
  95. package/src/codex/catalog/metadata.ts +27 -1
  96. package/src/codex/catalog/native-models.ts +5 -2
  97. package/src/codex/catalog/parsing.ts +72 -27
  98. package/src/codex/catalog/provider-fetch.ts +187 -31
  99. package/src/codex/catalog/sync.ts +131 -5
  100. package/src/codex/catalog.ts +1 -1
  101. package/src/codex/codex-write-lock.ts +3 -1
  102. package/src/codex/convergence-types.ts +1 -1
  103. package/src/codex/convergence.ts +27 -2
  104. package/src/codex/desired-state.ts +2 -2
  105. package/src/codex/desktop-app-restart.ts +355 -0
  106. package/src/codex/history-job.ts +32 -3
  107. package/src/codex/history-manifest.ts +112 -0
  108. package/src/codex/history-migration-guardian.ts +5 -5
  109. package/src/codex/history-provider.ts +825 -247
  110. package/src/codex/history-worker.ts +8 -5
  111. package/src/codex/inject-coordination.ts +83 -0
  112. package/src/codex/inject.ts +63 -22
  113. package/src/codex/injected-marker.ts +1 -1
  114. package/src/codex/internal/history-writer.ts +4 -3
  115. package/src/codex/log-guard/inspect.ts +22 -4
  116. package/src/codex/model-entitlements.ts +9 -2
  117. package/src/codex/native-profile-startup.ts +157 -27
  118. package/src/codex/native-residue.ts +26 -33
  119. package/src/codex/prompt-layers.ts +371 -25
  120. package/src/codex/prompt-text-probe.ts +238 -0
  121. package/src/codex/quota.ts +91 -10
  122. package/src/codex/routing.ts +9 -0
  123. package/src/codex/shim.ts +56 -3
  124. package/src/codex/subagent-model-fallback.ts +104 -25
  125. package/src/codex/transition-state.ts +116 -8
  126. package/src/combos/failover.ts +27 -0
  127. package/src/combos/types.ts +10 -0
  128. package/src/compatibility/index.ts +26 -0
  129. package/src/compatibility/manifest.ts +253 -0
  130. package/src/compatibility/openai-responses.ts +121 -0
  131. package/src/config/atomic-write.ts +219 -0
  132. package/src/config/paths.ts +40 -0
  133. package/src/config/process-state.ts +309 -0
  134. package/src/config/provider-validation.ts +177 -0
  135. package/src/config/rebase-provenance.ts +68 -0
  136. package/src/config.ts +206 -824
  137. package/src/generated/compatibility-version.json +373 -213
  138. package/src/images/loop.ts +5 -4
  139. package/src/images/plan.ts +5 -4
  140. package/src/integrations/ownership-policy.ts +160 -0
  141. package/src/integrations/ownership.ts +46 -2
  142. package/src/integrations/registry.ts +7 -0
  143. package/src/integrations/state.ts +79 -7
  144. package/src/integrations/writer.ts +26 -2
  145. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  146. package/src/lab/fabric/producer-child.ts +1 -1
  147. package/src/lib/admin-secrets.ts +24 -0
  148. package/src/lib/bounded-body.ts +14 -2
  149. package/src/lib/config-ownership.ts +20 -0
  150. package/src/lib/errors.ts +36 -3
  151. package/src/lib/package-tree-integrity.ts +101 -0
  152. package/src/lib/process-control.ts +2 -1
  153. package/src/lib/service-secrets.ts +15 -0
  154. package/src/lib/tool-argument-integers.ts +56 -5
  155. package/src/oauth/callback-server.ts +22 -2
  156. package/src/oauth/cline.ts +203 -0
  157. package/src/oauth/generic-account-failover.ts +231 -0
  158. package/src/oauth/health.ts +1 -1
  159. package/src/oauth/index.ts +65 -6
  160. package/src/oauth/key-providers.ts +10 -1
  161. package/src/oauth/kimi.ts +9 -1
  162. package/src/oauth/kiro.ts +45 -0
  163. package/src/oauth/local-token-detect.ts +2 -0
  164. package/src/oauth/open-browser-choice.ts +26 -0
  165. package/src/oauth/store.ts +14 -5
  166. package/src/oauth/types.ts +15 -0
  167. package/src/providers/auto-compact-budget.ts +65 -0
  168. package/src/providers/command-code-efforts.ts +38 -6
  169. package/src/providers/context-cap.ts +4 -3
  170. package/src/providers/default-aliases.ts +65 -0
  171. package/src/providers/derive.ts +28 -0
  172. package/src/providers/fastwire.ts +7 -1
  173. package/src/providers/label.ts +34 -1
  174. package/src/providers/model-presets.ts +119 -0
  175. package/src/providers/new-model-policy.ts +146 -0
  176. package/src/providers/provider-id-rewrite.ts +2 -1
  177. package/src/providers/quota.ts +65 -7
  178. package/src/providers/registry.ts +184 -69
  179. package/src/providers/slug-codec.ts +52 -0
  180. package/src/providers/xai-transport.ts +21 -0
  181. package/src/reasoning-effort.ts +19 -2
  182. package/src/responses/apply-patch-envelope.ts +63 -0
  183. package/src/responses/code-mode-helper-compat.ts +50 -0
  184. package/src/responses/custom-tool-compat.ts +157 -39
  185. package/src/responses/namespace-tool-compat.ts +84 -4
  186. package/src/responses/parser.ts +7 -2
  187. package/src/responses/reasoning-replay-cache.ts +81 -3
  188. package/src/responses/schema.ts +5 -1
  189. package/src/responses/state.ts +94 -4
  190. package/src/responses/thought-signature-replay.ts +17 -0
  191. package/src/responses/turn-termination.ts +107 -0
  192. package/src/router.ts +40 -2
  193. package/src/routing/analytics.ts +1 -0
  194. package/src/routing/quota.ts +10 -0
  195. package/src/server/auth-cors.ts +27 -7
  196. package/src/server/chat-completions.ts +24 -16
  197. package/src/server/chat-native-sse.ts +3 -3
  198. package/src/server/chat-native.ts +30 -11
  199. package/src/server/claude-messages.ts +1 -1
  200. package/src/server/index.ts +179 -32
  201. package/src/server/lifecycle.ts +52 -1
  202. package/src/server/local-management-read-client.ts +1 -1
  203. package/src/server/local-provider-reload-client.ts +1 -1
  204. package/src/server/management/agent-settings-routes.ts +45 -23
  205. package/src/server/management/codex-prompt-routes.ts +570 -0
  206. package/src/server/management/combo-routes.ts +2 -1
  207. package/src/server/management/config-routes.ts +59 -12
  208. package/src/server/management/context.ts +10 -1
  209. package/src/server/management/logs-usage-routes.ts +30 -24
  210. package/src/server/management/model-routes.ts +274 -4
  211. package/src/server/management/model-rows.ts +4 -0
  212. package/src/server/management/native-integration-routes.ts +2 -1
  213. package/src/server/management/oauth-account-routes.ts +10 -4
  214. package/src/server/management/provider-capability-config.ts +1 -1
  215. package/src/server/management/provider-routes.ts +81 -11
  216. package/src/server/management/route-registry.ts +332 -0
  217. package/src/server/management/routing-profile-routes.ts +2 -2
  218. package/src/server/management/system-restart.ts +1 -1
  219. package/src/server/management-api.ts +18 -16
  220. package/src/server/port-reclaim.ts +1 -1
  221. package/src/server/proxy-liveness.ts +29 -5
  222. package/src/server/relay-eager.ts +147 -20
  223. package/src/server/relay.ts +251 -19
  224. package/src/server/request-log-conversation.ts +51 -0
  225. package/src/server/request-log.ts +70 -16
  226. package/src/server/responses/codex-auth-error.ts +55 -0
  227. package/src/server/responses/collaboration.ts +37 -3
  228. package/src/server/responses/combo-stream-preflight.ts +178 -0
  229. package/src/server/responses/compact.ts +6 -21
  230. package/src/server/responses/core.ts +810 -208
  231. package/src/server/responses/empty-completion-guard.ts +63 -6
  232. package/src/server/responses/fetch-helpers.ts +17 -102
  233. package/src/server/responses/input-admission.ts +3 -1
  234. package/src/server/responses/passthrough-error.ts +33 -9
  235. package/src/server/responses/responses-field-backfill.ts +105 -13
  236. package/src/server/responses/ws-upstream.ts +75 -2
  237. package/src/server/responses-custom-tool-repair.ts +87 -6
  238. package/src/server/responses-terminal-repair.ts +25 -4
  239. package/src/server/responses-undeclared-tool-guard.ts +241 -18
  240. package/src/server/sse-frame-buffer.ts +31 -4
  241. package/src/server/ws-bridge.ts +14 -2
  242. package/src/service.ts +42 -4
  243. package/src/storage/policy-job.ts +14 -4
  244. package/src/storage/policy.ts +88 -23
  245. package/src/types/config.ts +66 -0
  246. package/src/types/provider.ts +69 -0
  247. package/src/types/request.ts +17 -4
  248. package/src/types/tools.ts +116 -11
  249. package/src/types.ts +3 -1
  250. package/src/update/index.ts +5 -4
  251. package/src/update/job.ts +3 -1
  252. package/src/update/transactional-install.mjs +8 -1
  253. package/src/usage/expected-prices.ts +43 -24
  254. package/src/usage/log.ts +63 -12
  255. package/src/usage/summary.ts +211 -8
  256. package/src/vision/describe.ts +18 -13
  257. package/src/web-search/exa-executor.ts +40 -9
  258. package/src/web-search/executor.ts +10 -3
  259. package/src/web-search/loop.ts +5 -4
  260. package/gui/dist/assets/index-BF38heuV.js +0 -104
  261. package/gui/dist/assets/index-DMiI18Kv.css +0 -1
@@ -63,6 +63,7 @@ export async function listManagementModelRows(config: OcxConfig): Promise<Manage
63
63
  disabled: disabled.has(`${selector}/${slug}`) || disabled.has(slug),
64
64
  contextWindow: undefined,
65
65
  maxInputTokens: undefined,
66
+ autoCompactTokenLimit: undefined,
66
67
  })))
67
68
  : [];
68
69
  const native: ManagementModelRow[] = [...nativeRows, ...accountNativeRows].map(row => {
@@ -82,6 +83,9 @@ export async function listManagementModelRows(config: OcxConfig): Promise<Manage
82
83
  // 1.05M). Dropping it here made /api/models describe a native row as if the whole
83
84
  // window were usable as input, which is the claim the measurement disproved.
84
85
  ...(row.maxInputTokens !== undefined ? { maxInputTokens: row.maxInputTokens } : {}),
86
+ ...(row.autoCompactTokenLimit !== undefined
87
+ ? { autoCompactTokenLimit: row.autoCompactTokenLimit }
88
+ : {}),
85
89
  };
86
90
  });
87
91
  const customModels: ManagementModelRow[] = (config.customModels ?? []).map(cm => {
@@ -17,7 +17,8 @@
17
17
  * Design of record: devlog/_fin/260803_integrations_toggle_all/030 (routes),
18
18
  * 011 (Claude Code), 012 (Grok).
19
19
  */
20
- import { loadConfig, readRuntimePort, saveConfigPreservingClaudeCode } from "../../config";
20
+ import { loadConfig, saveConfigPreservingClaudeCode } from "../../config";
21
+ import { readRuntimePort } from "../../config/process-state";
21
22
  import { desktopVisibleNativeSlugs, filterCatalogVisibleModels, nativeContextLimits, nativeOpenAiContextWindow, visibleNativeSlugs } from "../../codex/catalog";
22
23
  import { providerContextCap } from "../../providers/context-cap";
23
24
  import { OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
@@ -136,7 +136,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
136
136
  // the provider's loopback callback server (inside this process) captures the redirect in the
137
137
  // background, then the credential is persisted. The GUI opens the URL and polls /api/oauth/status.
138
138
  if (url.pathname === "/api/oauth/login" && req.method === "POST") {
139
- const body = await readManagementJsonBodyOr(req, {}) as { provider?: string; addAccount?: boolean; accountId?: string; reauth?: boolean };
139
+ const body = await readManagementJsonBodyOr(req, {}) as { provider?: string; addAccount?: boolean; accountId?: string; reauth?: boolean; openBrowser?: unknown };
140
140
  const provider = (body.provider ?? "").trim().toLowerCase();
141
141
  if (!isPublicOAuthProvider(provider)) return jsonResponse({ error: "unknown oauth provider" }, 400);
142
142
  const namespaceCollision = codexAccountNamespaceProviderCollisionError(config.codexAccountNamespaces, provider);
@@ -167,9 +167,15 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
167
167
  reconcileLiveStateStores();
168
168
  },
169
169
  });
170
- if (authUrl && !deviceCode) {
171
- // Open the browser server-side (the proxy runs on the user's machine) the GUI's
172
- // window.open is popup-blocked because it runs after an await, not a direct click.
170
+ // Open the browser server-side (the proxy runs on the user's machine) — the GUI's
171
+ // window.open is popup-blocked because it runs after an await, not a direct click.
172
+ //
173
+ // The operator can decline, which is the only way to finish a login in a
174
+ // browser profile other than the OS default, or on a different machine
175
+ // than the proxy. Declining changes nothing else: the URL is still
176
+ // returned below and every login surface renders it with a copy button.
177
+ const { shouldOpenBrowserForLogin } = await import("../../oauth/open-browser-choice");
178
+ if (authUrl && !deviceCode && shouldOpenBrowserForLogin(body.openBrowser, config)) {
173
179
  const { openUrl } = await import("../../lib/open-url");
174
180
  openUrl(authUrl);
175
181
  }
@@ -1,4 +1,4 @@
1
- import { booleanRecordConfigError } from "../../config";
1
+ import { booleanRecordConfigError } from "../../config/provider-validation";
2
2
  import type { OcxConfig } from "../../types";
3
3
 
4
4
  /**
@@ -36,7 +36,7 @@ import { ProviderOutboundPolicyError, providerOutboundGet, providerOutboundPost,
36
36
  import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
37
37
  import { parseAntigravityAvailableModels } from "../../providers/antigravity-models";
38
38
  import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
39
- import { deriveProviderPresets, enrichProviderFromRegistry } from "../../providers/derive";
39
+ import { deriveProviderPresets, enrichProviderFromRegistry, providerConfigSeed } from "../../providers/derive";
40
40
  import { effectiveGoogleMode, providerCodexAccountMode, providerMatchesRegistryTransport } from "../../providers/registry";
41
41
  import {
42
42
  extractModelEnvelopeRows,
@@ -59,6 +59,7 @@ import {
59
59
  getProviderLiveModelCount,
60
60
  } from "../../codex/model-cache";
61
61
  import { DEFAULT_PROVIDER_CONTEXT_CAP, globalContextCapValue, providerContextCap, providerContextCaps, setAllProviderContextCaps, setGlobalContextCapValue, setProviderContextCap } from "../../providers/context-cap";
62
+ import { modelAutoCompactTokenLimitsConfigError } from "../../providers/auto-compact-budget";
62
63
  import { resolveCodexHomeDir } from "../../codex/home";
63
64
  import { readUsageEntries } from "../../usage/log";
64
65
  import { getUsageDebugLogEntries } from "../../usage/debug";
@@ -271,6 +272,29 @@ function applyProviderPatchFields(
271
272
  }
272
273
  touched = true;
273
274
  }
275
+ if (Object.hasOwn(rawBody, "modelAutoCompactTokenLimits")) {
276
+ const value = rawBody.modelAutoCompactTokenLimits;
277
+ const error = modelAutoCompactTokenLimitsConfigError(value, {
278
+ allowTombstones: true,
279
+ requireNativeIds: name === "openai",
280
+ });
281
+ if (error) return { error };
282
+ if (value === null) {
283
+ delete next.modelAutoCompactTokenLimits;
284
+ } else {
285
+ const budgets: Record<string, number> = Object.assign(
286
+ Object.create(null) as Record<string, number>,
287
+ next.modelAutoCompactTokenLimits ?? {},
288
+ );
289
+ for (const [model, budget] of Object.entries(value as Record<string, number | null>)) {
290
+ if (budget === null) delete budgets[model];
291
+ else budgets[model] = budget;
292
+ }
293
+ if (Object.keys(budgets).length > 0) next.modelAutoCompactTokenLimits = budgets;
294
+ else delete next.modelAutoCompactTokenLimits;
295
+ }
296
+ touched = true;
297
+ }
274
298
  if (Object.hasOwn(rawBody, "modelSupportsServiceTier")) {
275
299
  const value = rawBody.modelSupportsServiceTier;
276
300
  if (value === null) {
@@ -374,6 +398,28 @@ function applyProviderPatchFields(
374
398
  return { next, touched, editorTouched, enablingOpenAi, headersTouched };
375
399
  }
376
400
 
401
+ /** Validate the canonical OpenAI soft-budget overlay against a fresh registry seed. */
402
+ function canonicalOpenAiBudgetPatchError(
403
+ provider: OcxProviderConfig,
404
+ rawBody: Record<string, unknown>,
405
+ keys: string[],
406
+ config: OcxConfig,
407
+ ): string | null {
408
+ if (!isCanonicalOpenAiForwardProvider(provider)) {
409
+ return "provider openai must be the canonical built-in provider";
410
+ }
411
+ const entry = getProviderRegistryEntry("openai");
412
+ if (!entry) return "provider openai registry seed is unavailable";
413
+ const seed = providerConfigSeed(entry);
414
+ if (provider.codexAccountMode !== undefined) seed.codexAccountMode = provider.codexAccountMode;
415
+ if (provider.modelAutoCompactTokenLimits !== undefined) {
416
+ seed.modelAutoCompactTokenLimits = { ...provider.modelAutoCompactTokenLimits };
417
+ }
418
+ const applied = applyProviderPatchFields("openai", seed, rawBody, keys, config);
419
+ if ("error" in applied) return applied.error;
420
+ return providerManagementConfigError("openai", applied.next);
421
+ }
422
+
377
423
  export async function handleProviderRoutes(ctx: ManagementContext): Promise<Response | null> {
378
424
  const { req, url, config, deps, principal, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
379
425
 
@@ -410,6 +456,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
410
456
  models: p.models ?? [],
411
457
  contextWindow: p.contextWindow,
412
458
  modelContextWindows: p.modelContextWindows,
459
+ modelAutoCompactTokenLimits: p.modelAutoCompactTokenLimits,
413
460
  modelSupportsServiceTier: p.modelSupportsServiceTier,
414
461
  noStructuredOutputModels: p.noStructuredOutputModels,
415
462
  upstreamHttpVersion: p.upstreamHttpVersion,
@@ -541,6 +588,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
541
588
  // call can never fire.
542
589
  const submittedContextWindow = Object.hasOwn(prov, "contextWindow");
543
590
  const submittedModelContextWindows = Object.hasOwn(prov, "modelContextWindows");
591
+ const submittedModelAutoCompactTokenLimits = Object.hasOwn(prov, "modelAutoCompactTokenLimits");
544
592
  const submittedRequestPacing = Object.hasOwn(prov, "requestPacing");
545
593
  enrichProviderFromCatalog(name, prov);
546
594
  const { saveConfigPreservingClaudeCode: save } = await import("../../config");
@@ -553,6 +601,12 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
553
601
  // erase hand-edited per-model prices from Logs/Usage estimates.
554
602
  const existingCosts = config.providers[name]?.modelCosts;
555
603
  if (existingCosts && !prov.modelCosts) prov.modelCosts = existingCosts;
604
+ // And to the per-provider account-failover opt-out (#2568d). `ProviderPayload` has no
605
+ // member for it either, so an add/edit save structurally cannot carry it — and dropping it
606
+ // silently ENABLES rotation, because activation is presence-driven once the knob is gone.
607
+ // An overwrite must not spend a second subscription account's quota as a side effect.
608
+ const existingFailover = config.providers[name]?.oauthAccountFailover;
609
+ if (existingFailover && !prov.oauthAccountFailover) prov.oauthAccountFailover = existingFailover;
556
610
  // ...and to hand-edited context windows. `ProviderPayload` (gui/src/provider-payload.ts)
557
611
  // has no member for either field, so the add/edit form structurally cannot send them:
558
612
  // absence in the request means "not carried", never "the user deleted it". Deletion goes
@@ -573,6 +627,11 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
573
627
  ? { ...existing.modelContextWindows, ...(prov.modelContextWindows ?? {}) }
574
628
  : { ...existing.modelContextWindows };
575
629
  }
630
+ if (existing?.modelAutoCompactTokenLimits) {
631
+ prov.modelAutoCompactTokenLimits = submittedModelAutoCompactTokenLimits
632
+ ? { ...existing.modelAutoCompactTokenLimits, ...(prov.modelAutoCompactTokenLimits ?? {}) }
633
+ : { ...existing.modelAutoCompactTokenLimits };
634
+ }
576
635
  config.providers[name] = stripRegistryOnlyStaticHeaders(name, prov);
577
636
  if (body.setDefault === true) config.defaultProvider = name;
578
637
  save(config);
@@ -596,6 +655,9 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
596
655
  const keys = Object.keys(rawBody);
597
656
  const hasMode = Object.hasOwn(rawBody, "codexAccountMode");
598
657
  const hasSetDefault = Object.hasOwn(rawBody, "setDefault");
658
+ const canonicalBudgetOnly = name === "openai"
659
+ && keys.length === 1
660
+ && keys[0] === "modelAutoCompactTokenLimits";
599
661
 
600
662
  // codexAccountMode keeps its dedicated side-effect path (quota cache clear, thread map
601
663
  // clear, pool prime) and is mutually exclusive with every other patch field.
@@ -658,12 +720,16 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
658
720
 
659
721
  const pacingOnly = keys.every(key => key === "requestPacing");
660
722
  if (applied.editorTouched && !pacingOnly) {
661
- const providerError = providerManagementConfigError(name, next);
723
+ const providerError = canonicalBudgetOnly
724
+ ? canonicalOpenAiBudgetPatchError(next, rawBody, keys, config)
725
+ : providerManagementConfigError(name, next);
662
726
  if (providerError) return jsonResponse({ error: providerError }, 400);
663
- const serviceTierError = providerServiceTierConfigError(name, next);
664
- if (serviceTierError) return jsonResponse({ error: serviceTierError }, 400);
665
- const resolvedError = await providerDestinationResolvedError(name, next);
666
- if (resolvedError) return jsonResponse({ error: resolvedError }, 400);
727
+ if (!canonicalBudgetOnly) {
728
+ const serviceTierError = providerServiceTierConfigError(name, next);
729
+ if (serviceTierError) return jsonResponse({ error: serviceTierError }, 400);
730
+ const resolvedError = await providerDestinationResolvedError(name, next);
731
+ if (resolvedError) return jsonResponse({ error: resolvedError }, 400);
732
+ }
667
733
  } else if (applied.enablingOpenAi) {
668
734
  // Same DNS gate as POST: Clash fake-IP only. Never honor a persisted
669
735
  // allowPrivateNetwork on this path — it must not bypass the built-in guard.
@@ -687,15 +753,19 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
687
753
  return;
688
754
  }
689
755
  if (replay.editorTouched && !pacingOnly) {
690
- const syncError = providerManagementConfigError(name, replay.next);
756
+ const syncError = canonicalBudgetOnly
757
+ ? canonicalOpenAiBudgetPatchError(replay.next, rawBody, keys, config)
758
+ : providerManagementConfigError(name, replay.next);
691
759
  if (syncError) {
692
760
  replayError = syncError;
693
761
  return;
694
762
  }
695
- const serviceTierError = providerServiceTierConfigError(name, replay.next);
696
- if (serviceTierError) {
697
- replayError = serviceTierError;
698
- return;
763
+ if (!canonicalBudgetOnly) {
764
+ const serviceTierError = providerServiceTierConfigError(name, replay.next);
765
+ if (serviceTierError) {
766
+ replayError = serviceTierError;
767
+ return;
768
+ }
699
769
  }
700
770
  } else if (replay.enablingOpenAi && !isCanonicalOpenAiForwardProvider(replay.next)) {
701
771
  replayError = "provider openai must be the canonical built-in provider";
@@ -0,0 +1,332 @@
1
+ /**
2
+ * Declared inventory of every reachable management route.
3
+ *
4
+ * DECLARED, not harvested. A grep cannot see this surface: 18 routes are registered
5
+ * through a regex, an `endsWith`, a `pathname.slice`, a prefix decode, a path constant, or a
6
+ * negated `pathname !== "…"` guard, and two of those are live routes whose only textual
7
+ * trace is the negated form. For `GET /api/storage` an equality scan finds solely the dead
8
+ * shadowed copy in `logs-usage-routes.ts` and never the live one.
9
+ *
10
+ * This module is pure DATA and must stay that way. It is imported by
11
+ * `src/server/management-api.ts`, which `tests/core-lab-boundary.test.ts` protects: a user
12
+ * with one provider and no Lab must execute no Lab code. Route paths are strings, so
13
+ * declaring `/api/lab/status` here creates no module edge. Never import a handler, and
14
+ * never import anything from `src/lab/`. The `module` field names the owning file as text
15
+ * for exactly this reason.
16
+ *
17
+ * Reconciliation lives in `tests/management-route-registry.test.ts`, which resolves
18
+ * `(method, path)` pairs from source and fails loudly on a route whose method it cannot
19
+ * determine. Adding a route without declaring it here fails that test.
20
+ */
21
+
22
+ export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD";
23
+
24
+ /**
25
+ * Why a route has no CLI verb. Every value is a claim about the route that a reviewer
26
+ * can check, not a way to quiet the parity test.
27
+ */
28
+ export type ExemptionReason =
29
+ /** Requires a dashboard browser session. Includes the user-consent star boundary. */
30
+ | "session-only"
31
+ /** Deliberately returns 405; there is nothing to drive. */
32
+ | "disabled"
33
+ /** Gated on a process-scoped capability principal, not an operator action. */
34
+ | "capability-principal"
35
+ /** A test seam, not an operator capability. */
36
+ | "test-seam"
37
+ /** The CLI reaches the same data through a local transport instead of HTTP. */
38
+ | "local-transport"
39
+ /** Unreachable in the live dispatch order; delete rather than expose. */
40
+ | "dead"
41
+ /**
42
+ * A verb is owed but belongs to a later work-phase. BOUNDED: requires `owner` and
43
+ * `ownerDoc`, and the parity test asserts that tracked doc exists and names the route.
44
+ * The doc is deliberately a repository file rather than the goalplan, which is
45
+ * gitignored -- a test reading machine-local state passes here and finds nothing in CI.
46
+ */
47
+ | "deferred-verb";
48
+
49
+ export interface RouteExemption {
50
+ readonly reason: ExemptionReason;
51
+ /** Free text; required, because an exemption nobody justified is how a gate erodes. */
52
+ readonly why: string;
53
+ /** Work-phase that owes the verb. Required for `deferred-verb`. */
54
+ readonly owner?: string;
55
+ /** Tracked doc naming the route. Required for `deferred-verb`. */
56
+ readonly ownerDoc?: string;
57
+ }
58
+
59
+ /** How a route is registered, for routes an equality scan cannot see. */
60
+ export type NonLiteralMechanism =
61
+ | "negated-guard"
62
+ | "path-constant"
63
+ | "prefix-decode"
64
+ | "slice"
65
+ | "ends-with"
66
+ | "regex";
67
+
68
+ export interface ManagementRoute {
69
+ readonly method: HttpMethod;
70
+ readonly path: string;
71
+ /** Owning source file, repo-relative without the `src/` prefix or `.ts` suffix. */
72
+ readonly module: string;
73
+ readonly mutates: boolean;
74
+ /** Set when the route is not recoverable from an equality scan of its own file. */
75
+ readonly mechanism?: NonLiteralMechanism;
76
+ readonly exempt?: RouteExemption;
77
+ }
78
+
79
+
80
+ /** Every reachable management route. */
81
+ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
82
+ // server/management-api
83
+ { method: "POST", path: "/api/stop", module: "server/management-api", mutates: true },
84
+ // codex/auth-api
85
+ { method: "DELETE", path: "/api/codex-auth/accounts", module: "codex/auth-api", mutates: true },
86
+ { method: "GET", path: "/api/codex-auth/accounts", module: "codex/auth-api", mutates: false },
87
+ { method: "GET", path: "/api/codex-auth/active", module: "codex/auth-api", mutates: false },
88
+ { method: "GET", path: "/api/codex-auth/login-status", module: "codex/auth-api", mutates: false },
89
+ { method: "GET", path: "/api/codex-auth/quota", module: "codex/auth-api", mutates: false },
90
+ { method: "GET", path: "/api/codex-auth/reset-credits", module: "codex/auth-api", mutates: false },
91
+ { method: "PATCH", path: "/api/codex-auth/pool-strategy", module: "codex/auth-api", mutates: true },
92
+ { method: "POST", path: "/api/codex-auth/accounts", module: "codex/auth-api", mutates: true },
93
+ { method: "POST", path: "/api/codex-auth/accounts/clear-cooldown", module: "codex/auth-api", mutates: true },
94
+ { method: "POST", path: "/api/codex-auth/login", module: "codex/auth-api", mutates: true },
95
+ { method: "POST", path: "/api/codex-auth/login/cancel", module: "codex/auth-api", mutates: true },
96
+ { method: "POST", path: "/api/codex-auth/login/code", module: "codex/auth-api", mutates: true },
97
+ { method: "POST", path: "/api/codex-auth/reset-credits/consume", module: "codex/auth-api", mutates: true },
98
+ { method: "PUT", path: "/api/codex-auth/accounts/alias", module: "codex/auth-api", mutates: true },
99
+ { method: "PUT", path: "/api/codex-auth/accounts/pause", module: "codex/auth-api", mutates: true },
100
+ { method: "PUT", path: "/api/codex-auth/accounts/pause-exhausted", module: "codex/auth-api", mutates: true },
101
+ { method: "PUT", path: "/api/codex-auth/accounts/priority", module: "codex/auth-api", mutates: true },
102
+ { method: "PUT", path: "/api/codex-auth/active", module: "codex/auth-api", mutates: true },
103
+ { method: "PUT", path: "/api/codex-auth/auto-switch", module: "codex/auth-api", mutates: true },
104
+ { method: "PUT", path: "/api/codex-auth/failover", module: "codex/auth-api", mutates: true },
105
+ { method: "PUT", path: "/api/codex-auth/pool-strategy", module: "codex/auth-api", mutates: true },
106
+ // codex/native-profile-api
107
+ { method: "GET", path: "/api/native-main-profiles", module: "codex/native-profile-api", mutates: false },
108
+ { method: "GET", path: "/api/native-main-profiles/doctor", module: "codex/native-profile-api", mutates: false },
109
+ { method: "POST", path: "/api/native-main-profiles/recover", module: "codex/native-profile-api", mutates: true },
110
+ { method: "POST", path: "/api/native-main-profiles/register", module: "codex/native-profile-api", mutates: true },
111
+ { method: "POST", path: "/api/native-main-profiles/stage", module: "codex/native-profile-api", mutates: true },
112
+ { method: "POST", path: "/api/native-main-profiles/stage/cancel", module: "codex/native-profile-api", mutates: true },
113
+ { method: "POST", path: "/api/native-main-profiles/stage/finish", module: "codex/native-profile-api", mutates: true },
114
+ { method: "POST", path: "/api/native-main-profiles/stage/heartbeat", module: "codex/native-profile-api", mutates: true },
115
+ { method: "POST", path: "/api/native-main-profiles/switch", module: "codex/native-profile-api", mutates: true },
116
+ // server/management/agent-settings-routes
117
+ { method: "GET", path: "/api/claude-code", module: "server/management/agent-settings-routes", mutates: false },
118
+ { method: "GET", path: "/api/claude-desktop", module: "server/management/agent-settings-routes", mutates: false },
119
+ { method: "GET", path: "/api/claude-desktop/status", module: "server/management/agent-settings-routes", mutates: false },
120
+ { method: "GET", path: "/api/codex-auth/features/default-mode-request-user-input", module: "server/management/agent-settings-routes", mutates: false },
121
+ { method: "GET", path: "/api/effort-caps", module: "server/management/agent-settings-routes", mutates: false },
122
+ { method: "GET", path: "/api/grok", module: "server/management/agent-settings-routes", mutates: false },
123
+ { method: "GET", path: "/api/injection-model", module: "server/management/agent-settings-routes", mutates: false },
124
+ { method: "GET", path: "/api/pi", module: "server/management/agent-settings-routes", mutates: false },
125
+ { method: "GET", path: "/api/pi/extensions", module: "server/management/agent-settings-routes", mutates: false },
126
+ { method: "GET", path: "/api/pi/packages", module: "server/management/agent-settings-routes", mutates: false },
127
+ { method: "GET", path: "/api/pi/settings", module: "server/management/agent-settings-routes", mutates: false },
128
+ { method: "GET", path: "/api/subagent-model-fallback", module: "server/management/agent-settings-routes", mutates: false },
129
+ { method: "GET", path: "/api/subagent-models", module: "server/management/agent-settings-routes", mutates: false },
130
+ { method: "GET", path: "/api/v2", module: "server/management/agent-settings-routes", mutates: false },
131
+ { method: "POST", path: "/api/claude-desktop/apply", module: "server/management/agent-settings-routes", mutates: true },
132
+ { method: "POST", path: "/api/grok/apply", module: "server/management/agent-settings-routes", mutates: true },
133
+ { method: "POST", path: "/api/pi/apply", module: "server/management/agent-settings-routes", mutates: true },
134
+ { method: "POST", path: "/api/pi/packages/install", module: "server/management/agent-settings-routes", mutates: true },
135
+ { method: "POST", path: "/api/pi/packages/remove", module: "server/management/agent-settings-routes", mutates: true },
136
+ { method: "POST", path: "/api/pi/remove", module: "server/management/agent-settings-routes", mutates: true },
137
+ { method: "PUT", path: "/api/claude-code", module: "server/management/agent-settings-routes", mutates: true },
138
+ { method: "PUT", path: "/api/claude-desktop", module: "server/management/agent-settings-routes", mutates: true },
139
+ { method: "PUT", path: "/api/codex-auth/features/default-mode-request-user-input", module: "server/management/agent-settings-routes", mutates: true },
140
+ { method: "PUT", path: "/api/effort-caps", module: "server/management/agent-settings-routes", mutates: true },
141
+ { method: "PUT", path: "/api/grok/selection", module: "server/management/agent-settings-routes", mutates: true },
142
+ { method: "PUT", path: "/api/injection-model", module: "server/management/agent-settings-routes", mutates: true },
143
+ { method: "PUT", path: "/api/pi/settings", module: "server/management/agent-settings-routes", mutates: true },
144
+ { method: "PUT", path: "/api/subagent-model-fallback", module: "server/management/agent-settings-routes", mutates: true },
145
+ { method: "PUT", path: "/api/subagent-models", module: "server/management/agent-settings-routes", mutates: true },
146
+ { method: "PUT", path: "/api/v2", module: "server/management/agent-settings-routes", mutates: true },
147
+ // server/management/clients-routes
148
+ { method: "GET", path: "/api/clients/status", module: "server/management/clients-routes", mutates: false },
149
+ // server/management/cloud-sync-routes
150
+ { method: "GET", path: "/api/cloud-sync/status", module: "server/management/cloud-sync-routes", mutates: false },
151
+ { method: "POST", path: "/api/cloud-sync/client-id", module: "server/management/cloud-sync-routes", mutates: true },
152
+ { method: "POST", path: "/api/cloud-sync/login/poll", module: "server/management/cloud-sync-routes", mutates: true },
153
+ { method: "POST", path: "/api/cloud-sync/login/start", module: "server/management/cloud-sync-routes", mutates: true },
154
+ { method: "POST", path: "/api/cloud-sync/logout", module: "server/management/cloud-sync-routes", mutates: true },
155
+ { method: "POST", path: "/api/cloud-sync/pull", module: "server/management/cloud-sync-routes", mutates: true },
156
+ { method: "POST", path: "/api/cloud-sync/push", module: "server/management/cloud-sync-routes", mutates: true },
157
+ // server/management/codex-prompt-routes
158
+ { method: "GET", path: "/api/codex-prompt", module: "server/management/codex-prompt-routes", mutates: false },
159
+ { method: "GET", path: "/api/codex-prompt/text", module: "server/management/codex-prompt-routes", mutates: false },
160
+ { method: "POST", path: "/api/codex-prompt/adopt", module: "server/management/codex-prompt-routes", mutates: true, exempt: { reason: "session-only", why: "Prompt adoption requires the gui-session principal (codex-prompt-routes.ts:298)." } },
161
+ { method: "POST", path: "/api/codex-prompt/repair", module: "server/management/codex-prompt-routes", mutates: true, exempt: { reason: "session-only", why: "Prompt repair requires the gui-session principal (codex-prompt-routes.ts:298)." } },
162
+ { method: "PUT", path: "/api/codex-prompt/base", module: "server/management/codex-prompt-routes", mutates: true, exempt: { reason: "session-only", why: "Base prompt write requires the gui-session principal (codex-prompt-routes.ts:298)." } },
163
+ { method: "PUT", path: "/api/codex-prompt/base/select", module: "server/management/codex-prompt-routes", mutates: true, exempt: { reason: "session-only", why: "Base prompt selection requires the gui-session principal (codex-prompt-routes.ts:298)." } },
164
+ { method: "PUT", path: "/api/codex-prompt/custom", module: "server/management/codex-prompt-routes", mutates: true, exempt: { reason: "session-only", why: "Custom prompt write requires the gui-session principal (codex-prompt-routes.ts:298)." } },
165
+ { method: "PUT", path: "/api/codex-prompt/toggle", module: "server/management/codex-prompt-routes", mutates: true, exempt: { reason: "session-only", why: "Prompt toggle requires the gui-session principal (codex-prompt-routes.ts:298)." } },
166
+ // server/management/combo-routes
167
+ { method: "DELETE", path: "/api/combos", module: "server/management/combo-routes", mutates: true },
168
+ { method: "GET", path: "/api/combos", module: "server/management/combo-routes", mutates: false },
169
+ { method: "PUT", path: "/api/combos", module: "server/management/combo-routes", mutates: true },
170
+ // server/management/config-routes
171
+ { method: "GET", path: "/api/config", module: "server/management/config-routes", mutates: false },
172
+ { method: "GET", path: "/api/diagnostics/project-config", module: "server/management/config-routes", mutates: false },
173
+ { method: "GET", path: "/api/settings", module: "server/management/config-routes", mutates: false },
174
+ { method: "GET", path: "/api/shadow-call-settings", module: "server/management/config-routes", mutates: false },
175
+ { method: "GET", path: "/api/sidecar-settings", module: "server/management/config-routes", mutates: false },
176
+ { method: "GET", path: "/api/startup-health", module: "server/management/config-routes", mutates: false },
177
+ { method: "GET", path: "/api/update/check", module: "server/management/config-routes", mutates: false },
178
+ { method: "GET", path: "/api/update/status", module: "server/management/config-routes", mutates: false },
179
+ { method: "GET", path: "/api/windows-tray", module: "server/management/config-routes", mutates: false },
180
+ { method: "POST", path: "/api/startup-action", module: "server/management/config-routes", mutates: true },
181
+ { method: "POST", path: "/api/sync", module: "server/management/config-routes", mutates: true },
182
+ { method: "POST", path: "/api/update/run", module: "server/management/config-routes", mutates: true },
183
+ { method: "POST", path: "/api/windows-tray", module: "server/management/config-routes", mutates: true },
184
+ { method: "PUT", path: "/api/config", module: "server/management/config-routes", mutates: true, exempt: { reason: "disabled", why: "Returns 405 by design; provider changes go through POST /api/providers." } },
185
+ { method: "PUT", path: "/api/settings", module: "server/management/config-routes", mutates: true },
186
+ { method: "PUT", path: "/api/shadow-call-settings", module: "server/management/config-routes", mutates: true },
187
+ { method: "PUT", path: "/api/sidecar-settings", module: "server/management/config-routes", mutates: true },
188
+ // server/management/integration-routes
189
+ { method: "GET", path: "/api/client-integrations", module: "server/management/integration-routes", mutates: false },
190
+ { method: "GET", path: "/api/client-integrations/journal", module: "server/management/integration-routes", mutates: false },
191
+ { method: "POST", path: "/api/client-integrations/restore", module: "server/management/integration-routes", mutates: true },
192
+ // server/management/lab-automation-routes
193
+ { method: "GET", path: "/api/lab/automation", module: "server/management/lab-automation-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
194
+ { method: "GET", path: "/api/lab/automation/runs", module: "server/management/lab-automation-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
195
+ { method: "POST", path: "/api/lab/automation/run", module: "server/management/lab-automation-routes", mutates: true, exempt: { reason: "deferred-verb", why: "Lab automation run has no CLI verb yet. A local SQLite read cannot drive it, so local-transport does not apply.", owner: "wp7", ownerDoc: "devlog/_plan/260828_ocx_agentic_control/060_phase_gui_parity.md" } },
196
+ { method: "PUT", path: "/api/lab/automation", module: "server/management/lab-automation-routes", mutates: true, exempt: { reason: "deferred-verb", why: "Lab automation config update has no CLI verb yet. A local SQLite read cannot drive it, so local-transport does not apply.", owner: "wp7", ownerDoc: "devlog/_plan/260828_ocx_agentic_control/060_phase_gui_parity.md" } },
197
+ // server/management/lab-routes
198
+ { method: "GET", path: "/api/lab/artifacts", module: "server/management/lab-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
199
+ { method: "GET", path: "/api/lab/catalog", module: "server/management/lab-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
200
+ { method: "GET", path: "/api/lab/events", module: "server/management/lab-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
201
+ { method: "GET", path: "/api/lab/observations", module: "server/management/lab-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
202
+ { method: "GET", path: "/api/lab/production-signals", module: "server/management/lab-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
203
+ { method: "GET", path: "/api/lab/public/community", module: "server/management/lab-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
204
+ { method: "GET", path: "/api/lab/status", module: "server/management/lab-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
205
+ { method: "GET", path: "/api/lab/subjects", module: "server/management/lab-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
206
+ { method: "GET", path: "/api/lab/verdicts", module: "server/management/lab-routes", mutates: false, exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
207
+ { method: "POST", path: "/api/lab/public/community/import", module: "server/management/lab-routes", mutates: true, exempt: { reason: "deferred-verb", why: "Community evidence import has no CLI verb yet. A local SQLite read cannot drive it, so local-transport does not apply.", owner: "wp7", ownerDoc: "devlog/_plan/260828_ocx_agentic_control/060_phase_gui_parity.md" } },
208
+ { method: "POST", path: "/api/lab/public/export", module: "server/management/lab-routes", mutates: true, exempt: { reason: "deferred-verb", why: "Public evidence export has no CLI verb yet. A local SQLite read cannot drive it, so local-transport does not apply.", owner: "wp7", ownerDoc: "devlog/_plan/260828_ocx_agentic_control/060_phase_gui_parity.md" } },
209
+ { method: "POST", path: "/api/lab/public/preview", module: "server/management/lab-routes", mutates: true, exempt: { reason: "deferred-verb", why: "Public evidence preview has no CLI verb yet. A local SQLite read cannot drive it, so local-transport does not apply.", owner: "wp7", ownerDoc: "devlog/_plan/260828_ocx_agentic_control/060_phase_gui_parity.md" } },
210
+ { method: "POST", path: "/api/lab/public/verify", module: "server/management/lab-routes", mutates: true, exempt: { reason: "deferred-verb", why: "Public evidence verification has no CLI verb yet. A local SQLite read cannot drive it, so local-transport does not apply.", owner: "wp7", ownerDoc: "devlog/_plan/260828_ocx_agentic_control/060_phase_gui_parity.md" } },
211
+ // server/management/logs-usage-routes
212
+ { method: "GET", path: "/api/claude/inbound-debug", module: "server/management/logs-usage-routes", mutates: false },
213
+ { method: "GET", path: "/api/debug", module: "server/management/logs-usage-routes", mutates: false },
214
+ { method: "GET", path: "/api/debug/injection-logs", module: "server/management/logs-usage-routes", mutates: false },
215
+ { method: "GET", path: "/api/debug/logs", module: "server/management/logs-usage-routes", mutates: false },
216
+ { method: "GET", path: "/api/debug/usage-logs", module: "server/management/logs-usage-routes", mutates: false },
217
+ { method: "GET", path: "/api/logs", module: "server/management/logs-usage-routes", mutates: false },
218
+ { method: "GET", path: "/api/storage/cleanup-policy", module: "server/management/logs-usage-routes", mutates: false },
219
+ { method: "GET", path: "/api/storage/cleanup-policy/test-stream", module: "server/management/logs-usage-routes", mutates: false, exempt: { reason: "test-seam", why: "Opt-in streaming seam declared at src/storage/policy-job.ts:71." } },
220
+ { method: "GET", path: "/api/storage/trash", module: "server/management/logs-usage-routes", mutates: false },
221
+ { method: "GET", path: "/api/storage/trash/restore/test-stream", module: "server/management/logs-usage-routes", mutates: false, exempt: { reason: "test-seam", why: "Opt-in streaming seam declared at src/storage/restore-job.ts:34." } },
222
+ { method: "GET", path: "/api/usage", module: "server/management/logs-usage-routes", mutates: false },
223
+ { method: "POST", path: "/api/storage/cleanup", module: "server/management/logs-usage-routes", mutates: true },
224
+ { method: "POST", path: "/api/storage/cleanup-policy/run", module: "server/management/logs-usage-routes", mutates: true },
225
+ { method: "POST", path: "/api/storage/cleanup/preview", module: "server/management/logs-usage-routes", mutates: true },
226
+ { method: "POST", path: "/api/storage/trash/restore", module: "server/management/logs-usage-routes", mutates: true },
227
+ { method: "POST", path: "/api/usage/ingest", module: "server/management/logs-usage-routes", mutates: true },
228
+ { method: "PUT", path: "/api/debug", module: "server/management/logs-usage-routes", mutates: true },
229
+ { method: "PUT", path: "/api/storage/cleanup-policy", module: "server/management/logs-usage-routes", mutates: true },
230
+ // server/management/model-routes
231
+ { method: "GET", path: "/api/aliases", module: "server/management/model-routes", mutates: false },
232
+ { method: "GET", path: "/api/catalog", module: "server/management/model-routes", mutates: false },
233
+ { method: "GET", path: "/api/client-config", module: "server/management/model-routes", mutates: false },
234
+ { method: "GET", path: "/api/custom-models", module: "server/management/model-routes", mutates: false },
235
+ { method: "GET", path: "/api/model-discovery", module: "server/management/model-routes", mutates: false },
236
+ { method: "GET", path: "/api/model-presets", module: "server/management/model-routes", mutates: false },
237
+ { method: "GET", path: "/api/models", module: "server/management/model-routes", mutates: false },
238
+ { method: "GET", path: "/api/selected-models", module: "server/management/model-routes", mutates: false },
239
+ { method: "POST", path: "/api/custom-models", module: "server/management/model-routes", mutates: true },
240
+ { method: "POST", path: "/api/model-discovery/acknowledge", module: "server/management/model-routes", mutates: true },
241
+ { method: "PUT", path: "/api/default-aliases", module: "server/management/model-routes", mutates: true },
242
+ { method: "PUT", path: "/api/disabled-models", module: "server/management/model-routes", mutates: true },
243
+ { method: "PUT", path: "/api/model-discovery", module: "server/management/model-routes", mutates: true },
244
+ { method: "PUT", path: "/api/model-presets", module: "server/management/model-routes", mutates: true },
245
+ { method: "PUT", path: "/api/model-visibility", module: "server/management/model-routes", mutates: true },
246
+ { method: "PUT", path: "/api/selected-models", module: "server/management/model-routes", mutates: true },
247
+ // server/management/native-integration-routes
248
+ { method: "GET", path: "/api/native-integrations", module: "server/management/native-integration-routes", mutates: false },
249
+ { method: "PUT", path: "/api/native-integrations/claude", module: "server/management/native-integration-routes", mutates: true },
250
+ { method: "PUT", path: "/api/native-integrations/claude-desktop", module: "server/management/native-integration-routes", mutates: true },
251
+ { method: "PUT", path: "/api/native-integrations/codex", module: "server/management/native-integration-routes", mutates: true },
252
+ { method: "PUT", path: "/api/native-integrations/grok", module: "server/management/native-integration-routes", mutates: true },
253
+ // server/management/oauth-account-routes
254
+ { method: "DELETE", path: "/api/keys", module: "server/management/oauth-account-routes", mutates: true },
255
+ { method: "DELETE", path: "/api/oauth/accounts", module: "server/management/oauth-account-routes", mutates: true },
256
+ { method: "DELETE", path: "/api/providers/keys", module: "server/management/oauth-account-routes", mutates: true },
257
+ { method: "GET", path: "/api/key-providers", module: "server/management/oauth-account-routes", mutates: false },
258
+ { method: "GET", path: "/api/keys", module: "server/management/oauth-account-routes", mutates: false },
259
+ { method: "GET", path: "/api/oauth/accounts", module: "server/management/oauth-account-routes", mutates: false },
260
+ { method: "GET", path: "/api/oauth/accounts/pool", module: "server/management/oauth-account-routes", mutates: false },
261
+ { method: "GET", path: "/api/oauth/providers", module: "server/management/oauth-account-routes", mutates: false },
262
+ { method: "GET", path: "/api/oauth/status", module: "server/management/oauth-account-routes", mutates: false },
263
+ { method: "GET", path: "/api/providers/keys", module: "server/management/oauth-account-routes", mutates: false },
264
+ { method: "PATCH", path: "/api/keys", module: "server/management/oauth-account-routes", mutates: true },
265
+ { method: "PATCH", path: "/api/oauth/accounts/pool", module: "server/management/oauth-account-routes", mutates: true },
266
+ { method: "POST", path: "/api/keys", module: "server/management/oauth-account-routes", mutates: true },
267
+ { method: "POST", path: "/api/oauth/accounts/clear-cooldown", module: "server/management/oauth-account-routes", mutates: true },
268
+ { method: "POST", path: "/api/oauth/accounts/import", module: "server/management/oauth-account-routes", mutates: true },
269
+ { method: "POST", path: "/api/oauth/login", module: "server/management/oauth-account-routes", mutates: true },
270
+ { method: "POST", path: "/api/oauth/login/cancel", module: "server/management/oauth-account-routes", mutates: true },
271
+ { method: "POST", path: "/api/oauth/login/code", module: "server/management/oauth-account-routes", mutates: true },
272
+ { method: "POST", path: "/api/oauth/logout", module: "server/management/oauth-account-routes", mutates: true },
273
+ { method: "POST", path: "/api/providers/keys", module: "server/management/oauth-account-routes", mutates: true },
274
+ { method: "PUT", path: "/api/oauth/accounts/active", module: "server/management/oauth-account-routes", mutates: true },
275
+ { method: "PUT", path: "/api/oauth/accounts/alias", module: "server/management/oauth-account-routes", mutates: true },
276
+ { method: "PUT", path: "/api/oauth/accounts/pool", module: "server/management/oauth-account-routes", mutates: true },
277
+ { method: "PUT", path: "/api/providers/keys/active", module: "server/management/oauth-account-routes", mutates: true },
278
+ { method: "PUT", path: "/api/providers/keys/alias", module: "server/management/oauth-account-routes", mutates: true },
279
+ // server/management/provider-routes
280
+ { method: "DELETE", path: "/api/providers", module: "server/management/provider-routes", mutates: true },
281
+ { method: "GET", path: "/api/provider-context-caps", module: "server/management/provider-routes", mutates: false },
282
+ { method: "GET", path: "/api/provider-presets", module: "server/management/provider-routes", mutates: false },
283
+ { method: "GET", path: "/api/provider-quotas", module: "server/management/provider-routes", mutates: false },
284
+ { method: "GET", path: "/api/provider-request-pacing", module: "server/management/provider-routes", mutates: false },
285
+ { method: "GET", path: "/api/providers", module: "server/management/provider-routes", mutates: false },
286
+ { method: "PATCH", path: "/api/providers", module: "server/management/provider-routes", mutates: true },
287
+ { method: "POST", path: "/api/providers", module: "server/management/provider-routes", mutates: true },
288
+ { method: "POST", path: "/api/providers/refresh-models", module: "server/management/provider-routes", mutates: true },
289
+ { method: "POST", path: "/api/providers/test", module: "server/management/provider-routes", mutates: true },
290
+ { method: "PUT", path: "/api/provider-context-caps", module: "server/management/provider-routes", mutates: true },
291
+ // server/management/request-history-routes
292
+ { method: "GET", path: "/api/request-history", module: "server/management/request-history-routes", mutates: false },
293
+ // server/management/routing-analytics-routes
294
+ // server/management/routing-profile-routes
295
+ { method: "DELETE", path: "/api/routing-profiles", module: "server/management/routing-profile-routes", mutates: true },
296
+ { method: "GET", path: "/api/routing-profiles", module: "server/management/routing-profile-routes", mutates: false },
297
+ { method: "POST", path: "/api/routing-profiles/dry-run", module: "server/management/routing-profile-routes", mutates: true },
298
+ { method: "PUT", path: "/api/routing-profiles", module: "server/management/routing-profile-routes", mutates: true },
299
+ // server/management/sidebar-routes
300
+ { method: "GET", path: "/api/github/star", module: "server/management/sidebar-routes", mutates: false },
301
+ { method: "GET", path: "/api/update/badge", module: "server/management/sidebar-routes", mutates: false },
302
+ { method: "POST", path: "/api/github/star", module: "server/management/sidebar-routes", mutates: true, exempt: { reason: "session-only", why: "User-consent boundary in AGENTS_INSTALL.md: starring spends the user's identity. Must never gain a CLI verb." } },
303
+ // server/management/storage-log-guard-routes
304
+ { method: "GET", path: "/api/storage/codex-logs", module: "server/management/storage-log-guard-routes", mutates: false },
305
+ { method: "POST", path: "/api/storage/codex-logs/compact", module: "server/management/storage-log-guard-routes", mutates: true },
306
+ { method: "POST", path: "/api/storage/codex-logs/protect", module: "server/management/storage-log-guard-routes", mutates: true },
307
+ { method: "POST", path: "/api/storage/codex-logs/repair", module: "server/management/storage-log-guard-routes", mutates: true },
308
+ { method: "POST", path: "/api/storage/codex-logs/unprotect", module: "server/management/storage-log-guard-routes", mutates: true },
309
+ // server/management/system-routes
310
+ { method: "GET", path: "/api/system/memory", module: "server/management/system-routes", mutates: false },
311
+ { method: "GET", path: "/api/system/windows-replace-retries", module: "server/management/system-routes", mutates: false },
312
+ { method: "POST", path: "/api/system/restart", module: "server/management/system-routes", mutates: true },
313
+ // --- Routes an equality scan of their own file cannot see (18). ---
314
+ // Each carries `mechanism`; the reconciliation test counts these separately.
315
+ { method: "GET", path: "/api/storage", module: "server/management/storage-log-guard-routes", mutates: false, mechanism: "negated-guard" },
316
+ { method: "GET", path: "/api/routing-analytics", module: "server/management/routing-analytics-routes", mutates: false, mechanism: "negated-guard" },
317
+ { method: "GET", path: "/api/system/codex-app-server", module: "server/management/system-routes", mutates: false, mechanism: "path-constant" },
318
+ { method: "POST", path: "/api/system/codex-restart", module: "server/management/system-routes", mutates: true, mechanism: "path-constant" },
319
+ { method: "POST", path: "/api/providers/reload", module: "server/management/provider-routes", mutates: true, mechanism: "path-constant", exempt: { reason: "capability-principal", why: "Gated on the local-provider-reload-capability principal (provider-routes.ts:467), not an operator action." } },
320
+ { method: "GET", path: "/api/client-integrations/{clientId}", module: "server/management/integration-routes", mutates: false, mechanism: "prefix-decode" },
321
+ { method: "PUT", path: "/api/client-integrations/{clientId}", module: "server/management/integration-routes", mutates: true, mechanism: "prefix-decode" },
322
+ { method: "GET", path: "/api/request-history/{id}", module: "server/management/request-history-routes", mutates: false, mechanism: "slice" },
323
+ { method: "GET", path: "/api/request-history/{id}/route-decision", module: "server/management/request-history-routes", mutates: false, mechanism: "ends-with" },
324
+ { method: "PUT", path: "/api/providers/{provider}/alias", module: "server/management/model-routes", mutates: true, mechanism: "regex" },
325
+ { method: "PUT", path: "/api/providers/{provider}/model-aliases", module: "server/management/model-routes", mutates: true, mechanism: "regex" },
326
+ { method: "PUT", path: "/api/custom-models/{id}", module: "server/management/model-routes", mutates: true, mechanism: "regex" },
327
+ { method: "DELETE", path: "/api/custom-models/{id}", module: "server/management/model-routes", mutates: true, mechanism: "regex" },
328
+ { method: "GET", path: "/api/lab/subjects/{id}", module: "server/management/lab-routes", mutates: false, mechanism: "regex", exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
329
+ { method: "GET", path: "/api/lab/events/{id}", module: "server/management/lab-routes", mutates: false, mechanism: "regex", exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
330
+ { method: "GET", path: "/api/lab/artifacts/{digest}", module: "server/management/lab-routes", mutates: false, mechanism: "regex", exempt: { reason: "local-transport", why: "ocx lab reads the same rows from the local SQLite projection; src/cli/lab.ts imports ../lab/query directly and never fetches /api/lab." } },
331
+ { method: "POST", path: "/api/lab/automation/runs/{id}/cancel", module: "server/management/lab-automation-routes", mutates: true, mechanism: "regex", exempt: { reason: "deferred-verb", why: "Lab automation run cancellation has no CLI verb yet. A local SQLite read cannot drive it, so local-transport does not apply.", owner: "wp7", ownerDoc: "devlog/_plan/260828_ocx_agentic_control/060_phase_gui_parity.md" } },
332
+ ];