@bitkyc08/opencodex 2.12.0 → 2.13.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 (173) hide show
  1. package/README.md +1 -1
  2. package/gui/dist/assets/index-BHldBl6_.js +76 -0
  3. package/gui/dist/index.html +1 -1
  4. package/package.json +1 -1
  5. package/src/adapters/anthropic-output-schema.ts +137 -0
  6. package/src/adapters/anthropic.ts +15 -0
  7. package/src/adapters/google-antigravity-replay.ts +269 -5
  8. package/src/adapters/openai-chat.ts +25 -18
  9. package/src/adapters/openai-responses.ts +63 -17
  10. package/src/bridge.ts +11 -5
  11. package/src/claude/inbound.ts +24 -3
  12. package/src/cli/account-api.ts +2 -0
  13. package/src/cli/account-extended.ts +262 -1
  14. package/src/cli/account.ts +3 -1
  15. package/src/cli/dispatch.ts +532 -0
  16. package/src/cli/doctor.ts +26 -21
  17. package/src/cli/help.ts +9 -278
  18. package/src/cli/index.ts +45 -506
  19. package/src/cli/lab.ts +171 -65
  20. package/src/cli/registry.ts +384 -0
  21. package/src/cli/root.ts +86 -0
  22. package/src/cli/status.ts +9 -2
  23. package/src/cli/v2.ts +37 -2
  24. package/src/codex/account-label.ts +14 -1
  25. package/src/codex/account-store.ts +12 -1
  26. package/src/codex/admission.ts +2 -1
  27. package/src/codex/auth-api.ts +3 -2
  28. package/src/codex/catalog/metadata.ts +6 -4
  29. package/src/codex/catalog/parsing.ts +31 -7
  30. package/src/codex/catalog/provider-fetch.ts +2 -1
  31. package/src/codex/catalog/sync.ts +65 -11
  32. package/src/codex/features.ts +437 -31
  33. package/src/codex/history-job.ts +6 -11
  34. package/src/codex/history-provider.ts +10 -9
  35. package/src/codex/inject.ts +14 -1
  36. package/src/codex/native-profile-startup.ts +49 -3
  37. package/src/codex/native-residue.ts +14 -2
  38. package/src/codex/paths.ts +75 -1
  39. package/src/codex/reset-credit-recovery.ts +1044 -0
  40. package/src/codex/shim.ts +876 -16
  41. package/src/codex/sync.ts +27 -2
  42. package/src/config.ts +148 -10
  43. package/src/generated/compatibility-version.json +2572 -0
  44. package/src/images/loop.ts +1 -1
  45. package/src/lab/artifacts/sanitize.ts +3 -3
  46. package/src/lab/automation/budgets.ts +78 -0
  47. package/src/lab/automation/config-persistence.ts +256 -0
  48. package/src/lab/automation/constants.ts +39 -0
  49. package/src/lab/automation/cooldown.ts +103 -0
  50. package/src/lab/automation/dispatch.ts +211 -0
  51. package/src/lab/automation/index.ts +13 -0
  52. package/src/lab/automation/orchestrator.ts +480 -0
  53. package/src/lab/automation/persistence.ts +512 -0
  54. package/src/lab/automation/planner.ts +371 -0
  55. package/src/lab/automation/policy.ts +136 -0
  56. package/src/lab/automation/queue.ts +191 -0
  57. package/src/lab/automation/recovery.ts +24 -0
  58. package/src/lab/automation/route-context.ts +21 -0
  59. package/src/lab/automation/run-key.ts +44 -0
  60. package/src/lab/automation/runs-query.ts +34 -0
  61. package/src/lab/automation/types.ts +160 -0
  62. package/src/lab/conformance/types.ts +23 -0
  63. package/src/lab/events/types.ts +2 -1
  64. package/src/lab/events/validate.ts +2 -1
  65. package/src/lab/fabric/constants.ts +40 -0
  66. package/src/lab/fabric/executor.ts +492 -0
  67. package/src/lab/fabric/index.ts +80 -0
  68. package/src/lab/fabric/manifest.ts +222 -0
  69. package/src/lab/fabric/observe.ts +454 -0
  70. package/src/lab/fabric/patch.ts +79 -0
  71. package/src/lab/fabric/producer-child.ts +139 -0
  72. package/src/lab/fabric/producer-isolate.ts +276 -0
  73. package/src/lab/fabric/producer-protocol.ts +61 -0
  74. package/src/lab/fabric/scratch.ts +439 -0
  75. package/src/lab/fabric/subject.ts +106 -0
  76. package/src/lab/fabric/types.ts +134 -0
  77. package/src/lab/fabric/verifier.ts +98 -0
  78. package/src/lab/index.ts +2 -0
  79. package/src/lab/ledger/store.ts +173 -0
  80. package/src/lab/live/executor.ts +29 -4
  81. package/src/lab/live/transport.ts +3 -1
  82. package/src/lab/observe/from-conformance.ts +12 -57
  83. package/src/lab/observe/from-live.ts +9 -1
  84. package/src/lab/paths.ts +74 -9
  85. package/src/lab/projection/schema.ts +14 -1
  86. package/src/lab/projection/verdicts.ts +89 -53
  87. package/src/lab/projection/verification.ts +83 -3
  88. package/src/lab/query/catalog.ts +34 -5
  89. package/src/lab/query/freshness.ts +53 -0
  90. package/src/lab/query/index.ts +11 -0
  91. package/src/lab/query/latest-observation.ts +59 -0
  92. package/src/lab/query/passive-production.ts +159 -0
  93. package/src/lab/subject/installation-salt.ts +24 -4
  94. package/src/lab/subject/protocol-subject.ts +80 -0
  95. package/src/lab/subject/route-subject.ts +2 -1
  96. package/src/lib/fabric-task-execution-authority.ts +7 -0
  97. package/src/lib/fabric-task-host.ts +29 -0
  98. package/src/lib/lab-live-route-production.ts +130 -0
  99. package/src/lib/local-management-capability.ts +100 -0
  100. package/src/lib/server-resource-ownership.ts +71 -0
  101. package/src/lib/windows-elevation.ts +8 -1
  102. package/src/lib/windows-user-principal.ts +62 -2
  103. package/src/lib/winsw.ts +8 -2
  104. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  105. package/src/oauth/account-import/index.ts +15 -0
  106. package/src/oauth/account-import/parser.ts +83 -0
  107. package/src/oauth/account-import/registry.ts +18 -0
  108. package/src/oauth/account-import/service.ts +75 -0
  109. package/src/oauth/account-import/types.ts +91 -0
  110. package/src/oauth/anthropic.ts +12 -1
  111. package/src/oauth/callback-server.ts +8 -2
  112. package/src/oauth/chatgpt.ts +12 -1
  113. package/src/oauth/google-antigravity.ts +27 -0
  114. package/src/oauth/health.ts +12 -48
  115. package/src/oauth/index.ts +122 -0
  116. package/src/oauth/kimi.ts +16 -2
  117. package/src/oauth/nous.ts +749 -0
  118. package/src/oauth/store.ts +56 -3
  119. package/src/providers/codex-capacity.ts +1 -0
  120. package/src/providers/registry.ts +161 -16
  121. package/src/responses/custom-tool-compat.ts +182 -0
  122. package/src/responses/parser.ts +22 -0
  123. package/src/responses/reasoning-replay-cache.ts +168 -11
  124. package/src/responses/schema.ts +1 -1
  125. package/src/router.ts +5 -20
  126. package/src/routing/capability.ts +9 -3
  127. package/src/routing/compatibility/assemble.ts +159 -0
  128. package/src/routing/compatibility/behavior.ts +224 -0
  129. package/src/routing/compatibility/catalog.ts +99 -0
  130. package/src/routing/compatibility/endpoint.ts +52 -0
  131. package/src/routing/compatibility/policy.ts +181 -0
  132. package/src/routing/compatibility/reader.ts +110 -0
  133. package/src/routing/compatibility/subject.ts +191 -0
  134. package/src/routing/compatibility/types.ts +64 -0
  135. package/src/routing/compatibility/version.ts +104 -0
  136. package/src/routing/evaluator.ts +27 -32
  137. package/src/routing/profile.ts +118 -4
  138. package/src/routing/trace.ts +82 -28
  139. package/src/server/auth-cors.ts +7 -0
  140. package/src/server/background-lifecycle.ts +20 -7
  141. package/src/server/chat-completions.ts +2 -2
  142. package/src/server/claude-messages.ts +6 -3
  143. package/src/server/direct-local-http.ts +343 -0
  144. package/src/server/index.ts +80 -18
  145. package/src/server/lifecycle.ts +12 -4
  146. package/src/server/local-management-read-client.ts +90 -0
  147. package/src/server/management/agent-settings-routes.ts +27 -3
  148. package/src/server/management/lab-automation-routes.ts +206 -0
  149. package/src/server/management/lab-routes.ts +13 -0
  150. package/src/server/management/logs-usage-routes.ts +1 -0
  151. package/src/server/management/oauth-account-routes.ts +57 -1
  152. package/src/server/management/provider-routes.ts +16 -0
  153. package/src/server/management/routing-profile-routes.ts +12 -24
  154. package/src/server/management-api.ts +2 -0
  155. package/src/server/management-auth.ts +68 -3
  156. package/src/server/proxy-liveness.ts +3 -2
  157. package/src/server/request-decompress.ts +9 -3
  158. package/src/server/request-log.ts +53 -4
  159. package/src/server/responses/collaboration.ts +93 -17
  160. package/src/server/responses/compact.ts +3 -0
  161. package/src/server/responses/core.ts +202 -15
  162. package/src/server/responses/policy-fallback.ts +1 -0
  163. package/src/server/responses-custom-tool-repair.ts +280 -0
  164. package/src/server/responses-terminal-repair.ts +342 -0
  165. package/src/server/startup-action-control.ts +8 -1
  166. package/src/service.ts +278 -12
  167. package/src/types.ts +49 -3
  168. package/src/usage/expected-prices.ts +30 -0
  169. package/src/usage/log.ts +30 -4
  170. package/src/usage/summary.ts +153 -2
  171. package/src/web-search/loop.ts +1 -1
  172. package/gui/dist/assets/index-Cw1_mxO8.js +0 -70
  173. package/src/cli/internal-dispatch.ts +0 -20
@@ -224,6 +224,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
224
224
  const {
225
225
  isMultiAgentV2Enabled, hasAgentsMaxThreads, getLogicalMaxThreads,
226
226
  getAgentsEnabled, getAgentsMaxDepth, getSubagentDeveloperInstructions,
227
+ getMultiAgentModeHintText,
227
228
  } = await import("../../codex/features");
228
229
  const enabled = isMultiAgentV2Enabled();
229
230
  return jsonResponse({
@@ -234,6 +235,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
234
235
  agentsEnabled: getAgentsEnabled(),
235
236
  agentsMaxDepth: getAgentsMaxDepth(),
236
237
  subagentDeveloperInstructions: getSubagentDeveloperInstructions(),
238
+ multiAgentModeHintText: getMultiAgentModeHintText(),
237
239
  // max_depth is V1-only upstream; this is the global-flag statement, derived
238
240
  // server-side so no client can present it as an effective V2 limit.
239
241
  agentsMaxDepthAppliesWhenV2Disabled: !enabled,
@@ -247,6 +249,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
247
249
  agentsEnabled?: unknown;
248
250
  agentsMaxDepth?: unknown;
249
251
  subagentDeveloperInstructions?: unknown;
252
+ multiAgentModeHintText?: unknown;
250
253
  };
251
254
  try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
252
255
  const wantsFlag = body.enabled !== undefined;
@@ -255,8 +258,9 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
255
258
  const wantsAgentsEnabled = body.agentsEnabled !== undefined;
256
259
  const wantsMaxDepth = body.agentsMaxDepth !== undefined;
257
260
  const wantsSubagentInstructions = body.subagentDeveloperInstructions !== undefined;
258
- if (!wantsFlag && !wantsThreads && !wantsMode && !wantsAgentsEnabled && !wantsMaxDepth && !wantsSubagentInstructions) {
259
- return jsonResponse({ error: "body must set enabled, multiAgentMode, maxConcurrentThreadsPerSession, agentsEnabled, agentsMaxDepth, and/or subagentDeveloperInstructions" }, 400);
261
+ const wantsModeHintText = body.multiAgentModeHintText !== undefined;
262
+ if (!wantsFlag && !wantsThreads && !wantsMode && !wantsAgentsEnabled && !wantsMaxDepth && !wantsSubagentInstructions && !wantsModeHintText) {
263
+ return jsonResponse({ error: "body must set enabled, multiAgentMode, maxConcurrentThreadsPerSession, agentsEnabled, agentsMaxDepth, subagentDeveloperInstructions, and/or multiAgentModeHintText" }, 400);
260
264
  }
261
265
  if (wantsFlag && typeof body.enabled !== "boolean") return jsonResponse({ error: "body.enabled must be a boolean" }, 400);
262
266
  if (wantsMode && body.multiAgentMode !== "v1" && body.multiAgentMode !== "default" && body.multiAgentMode !== "v2") {
@@ -280,6 +284,14 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
280
284
  if (wantsSubagentInstructions && body.subagentDeveloperInstructions !== null && typeof body.subagentDeveloperInstructions !== "string") {
281
285
  return jsonResponse({ error: "body.subagentDeveloperInstructions must be a string or null" }, 400);
282
286
  }
287
+ // null unsets the upstream key (effort-derived policy resumes); an empty/whitespace
288
+ // string is rejected because codex-rs treats any present hint as an override that
289
+ // suppresses even the Ultra-derived Proactive message (Option<String>, no blank
290
+ // special-case in effective_multi_agent_mode).
291
+ if (wantsModeHintText && body.multiAgentModeHintText !== null
292
+ && (typeof body.multiAgentModeHintText !== "string" || body.multiAgentModeHintText.trim().length === 0)) {
293
+ return jsonResponse({ error: "body.multiAgentModeHintText must be a non-empty string or null" }, 400);
294
+ }
283
295
  const mode = wantsMode ? body.multiAgentMode as "v1" | "default" | "v2" : undefined;
284
296
  const modeFlag = mode === "v2" ? true : mode === "v1" ? false : undefined;
285
297
  if (wantsFlag && modeFlag !== undefined && body.enabled !== modeFlag) {
@@ -288,8 +300,18 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
288
300
  const {
289
301
  isMultiAgentV2Enabled, hasAgentsMaxThreads, getLogicalMaxThreads, transitionMultiAgentV2,
290
302
  getAgentsEnabled, getAgentsMaxDepth, getSubagentDeveloperInstructions,
291
- setAgentsEnabled, setAgentsMaxDepth, setSubagentDeveloperInstructions,
303
+ getMultiAgentModeHintText, probeCodexSupportsModeHint, setAgentsEnabled, setAgentsMaxDepth,
304
+ setSubagentDeveloperInstructions, setMultiAgentModeHintText, MODE_HINT_UNSUPPORTED_ERROR,
292
305
  } = await import("../../codex/features");
306
+ // Probe the capability before any combined-request mutation. The scalar writer
307
+ // repeats this check, but doing it here prevents an earlier flag/mode/agents
308
+ // write from landing before an unsupported runtime returns 502.
309
+ if (wantsModeHintText && body.multiAgentModeHintText !== null
310
+ && probeCodexSupportsModeHint() === false) {
311
+ return jsonResponse({
312
+ error: `writing multiAgentModeHintText failed: ${MODE_HINT_UNSUPPORTED_ERROR}`,
313
+ }, 502);
314
+ }
293
315
  const warnings: string[] = [];
294
316
  const requestedFlag = wantsFlag ? body.enabled as boolean : modeFlag;
295
317
  if (requestedFlag !== undefined || wantsThreads) {
@@ -322,6 +344,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
322
344
  if (wantsAgentsEnabled) scalarWrites.push({ field: "agentsEnabled", run: () => setAgentsEnabled(body.agentsEnabled as boolean | null) });
323
345
  if (wantsMaxDepth) scalarWrites.push({ field: "agentsMaxDepth", run: () => setAgentsMaxDepth(body.agentsMaxDepth as number | null) });
324
346
  if (wantsSubagentInstructions) scalarWrites.push({ field: "subagentDeveloperInstructions", run: () => setSubagentDeveloperInstructions(body.subagentDeveloperInstructions as string | null) });
347
+ if (wantsModeHintText) scalarWrites.push({ field: "multiAgentModeHintText", run: () => setMultiAgentModeHintText(body.multiAgentModeHintText as string | null) });
325
348
  const landed: string[] = [];
326
349
  for (const write of scalarWrites) {
327
350
  try {
@@ -353,6 +376,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
353
376
  agentsEnabled: getAgentsEnabled(),
354
377
  agentsMaxDepth: getAgentsMaxDepth(),
355
378
  subagentDeveloperInstructions: getSubagentDeveloperInstructions(),
379
+ multiAgentModeHintText: getMultiAgentModeHintText(),
356
380
  agentsMaxDepthAppliesWhenV2Disabled: !enabled,
357
381
  warnings,
358
382
  catalogRefresh,
@@ -0,0 +1,206 @@
1
+ /**
2
+ * CL-08 Compatibility Lab automation management API.
3
+ *
4
+ * - GET /api/lab/automation
5
+ * - PUT /api/lab/automation
6
+ * - GET /api/lab/automation/runs
7
+ * - POST /api/lab/automation/run
8
+ * - POST /api/lab/automation/runs/:id/cancel
9
+ *
10
+ * Read endpoints never trigger scheduler ticks or evidence collection.
11
+ */
12
+
13
+ import { readConfigDiagnostics, getConfigDir } from "../../config";
14
+ import {
15
+ buildLabAutomationStatus,
16
+ cancelLabAutomationRun,
17
+ enqueueManualLabRun,
18
+ reconcileLabAutomationQueue,
19
+ startLabAutomationScheduler,
20
+ stopLabAutomationScheduler,
21
+ } from "../../lab/automation/orchestrator";
22
+ import { planManualLabRun } from "../../lab/automation/planner";
23
+ import {
24
+ loadLabAutomationState,
25
+ normalizeLabAutomationRoutesV1,
26
+ } from "../../lab/automation/persistence";
27
+ import {
28
+ loadLabAutomationConfig,
29
+ saveLabAutomationConfig,
30
+ } from "../../lab/automation/config-persistence";
31
+ import { normalizeLabAutomationPolicyV1 } from "../../lab/automation/policy";
32
+ import { listLabAutomationRuns } from "../../lab/automation/runs-query";
33
+ import type { LabAutomationLayer, LabAutomationPolicyV1 } from "../../lab/automation/types";
34
+ import { LabAutomationError } from "../../lab/automation/types";
35
+ import { jsonResponse } from "../auth-cors";
36
+ import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
37
+ import type { ManagementContext } from "./context";
38
+ import { isPlainRecord } from "./shared";
39
+
40
+ const AUTOMATION_LAYERS: readonly LabAutomationLayer[] = [
41
+ "protocol_conformance",
42
+ "live_route_compatibility",
43
+ "task_effectiveness",
44
+ ];
45
+ const MANUAL_RUN_KEYS = new Set(["evidenceLayer", "scenarioId", "providerName", "modelId"]);
46
+
47
+ function automationErrorResponse(code: string, message: string, status: number, ctx: ManagementContext): Response {
48
+ return jsonResponse({ error: { code, message } }, status, ctx.req, ctx.config);
49
+ }
50
+
51
+ function hasUnknownKeys(value: Record<string, unknown>, allowed: ReadonlySet<string>): boolean {
52
+ return Object.keys(value).some((key) => !allowed.has(key));
53
+ }
54
+
55
+ function parseLimit(raw: string | null, ctx: ManagementContext): number | Response {
56
+ if (raw === null) return 50;
57
+ const trimmed = raw.trim();
58
+ if (trimmed.length === 0) return 50;
59
+ const value = Number(trimmed);
60
+ if (!Number.isInteger(value) || value < 1 || value > 100) {
61
+ return automationErrorResponse("invalid_limit", "limit must be an integer from 1 to 100", 400, ctx);
62
+ }
63
+ return value;
64
+ }
65
+
66
+ function applySchedulerPolicy(policy: LabAutomationPolicyV1, configDir?: string): void {
67
+ reconcileLabAutomationQueue(configDir);
68
+ if (policy.enabled) {
69
+ startLabAutomationScheduler(configDir);
70
+ } else {
71
+ stopLabAutomationScheduler(configDir);
72
+ }
73
+ }
74
+
75
+ export async function handleLabAutomationRoutes(ctx: ManagementContext): Promise<Response | null> {
76
+ const { url, req, config } = ctx;
77
+ if (!url.pathname.startsWith("/api/lab/automation")) return null;
78
+
79
+ const configDir = getConfigDir();
80
+
81
+ if (url.pathname === "/api/lab/automation" && req.method === "GET") {
82
+ return jsonResponse(buildLabAutomationStatus(configDir), 200, req, config);
83
+ }
84
+
85
+ if (url.pathname === "/api/lab/automation/runs" && req.method === "GET") {
86
+ const limit = parseLimit(url.searchParams.get("limit"), ctx);
87
+ if (limit instanceof Response) return limit;
88
+ const cursor = url.searchParams.get("cursor")?.trim() || undefined;
89
+ try {
90
+ const status = buildLabAutomationStatus(configDir);
91
+ const runsPage = listLabAutomationRuns(loadLabAutomationState(configDir), limit, cursor);
92
+ return jsonResponse({
93
+ runs: runsPage.items,
94
+ hasMore: runsPage.hasMore,
95
+ ...(runsPage.nextCursor ? { nextCursor: runsPage.nextCursor } : {}),
96
+ counters: status.counters,
97
+ }, 200, req, config);
98
+ } catch (error) {
99
+ if (error instanceof LabAutomationError && error.code === "invalid_cursor") {
100
+ return automationErrorResponse(error.code, error.message, 400, ctx);
101
+ }
102
+ throw error;
103
+ }
104
+ }
105
+
106
+ const cancelMatch = url.pathname.match(/^\/api\/lab\/automation\/runs\/([^/]+)\/cancel$/);
107
+ if (cancelMatch && req.method === "POST") {
108
+ let runId: string;
109
+ try {
110
+ runId = decodeURIComponent(cancelMatch[1]!);
111
+ } catch {
112
+ return automationErrorResponse("invalid_run_id", "run id is not valid percent-encoding", 400, ctx);
113
+ }
114
+ const cancelled = cancelLabAutomationRun(runId, configDir);
115
+ if (!cancelled) return automationErrorResponse("not_found", "unknown or non-cancellable run", 404, ctx);
116
+ return jsonResponse({ cancelled: true, runId }, 200, req, config);
117
+ }
118
+
119
+ if (url.pathname === "/api/lab/automation/run" && req.method === "POST") {
120
+ try {
121
+ const body = await readManagementJsonBody(req);
122
+ if (!isPlainRecord(body)) return automationErrorResponse("invalid_body", "body must be an object", 400, ctx);
123
+ if (hasUnknownKeys(body, MANUAL_RUN_KEYS)) {
124
+ return automationErrorResponse("invalid_body", "manual run body contains unknown fields", 400, ctx);
125
+ }
126
+ const evidenceLayer = body.evidenceLayer;
127
+ const scenarioId = body.scenarioId;
128
+ if (typeof evidenceLayer !== "string" || !AUTOMATION_LAYERS.includes(evidenceLayer as LabAutomationLayer)) {
129
+ return automationErrorResponse("invalid_layer", "evidenceLayer must be a supported automation layer", 400, ctx);
130
+ }
131
+ if (typeof scenarioId !== "string" || scenarioId.length === 0) {
132
+ return automationErrorResponse("invalid_scenario", "scenarioId is required", 400, ctx);
133
+ }
134
+ let ocxConfig: import("../../types").OcxConfig | undefined;
135
+ try {
136
+ ocxConfig = readConfigDiagnostics().config;
137
+ } catch {
138
+ ocxConfig = config;
139
+ }
140
+ const planned = planManualLabRun({
141
+ evidenceLayer: evidenceLayer as LabAutomationLayer,
142
+ scenarioId,
143
+ providerName: typeof body.providerName === "string" ? body.providerName : undefined,
144
+ modelId: typeof body.modelId === "string" ? body.modelId : undefined,
145
+ config: ocxConfig,
146
+ configDir,
147
+ });
148
+ // This endpoint is intentionally synchronous: completion is the acknowledgement boundary.
149
+ // Existing harness execution limits provide the route-level deadline, and disconnects cancel
150
+ // through Request.signal instead of leaving orphaned provider work running.
151
+ const record = await enqueueManualLabRun(planned, configDir, req.signal);
152
+ if (!record) return automationErrorResponse("enqueue_failed", "manual run could not be enqueued", 500, ctx);
153
+ return jsonResponse({ run: record, trigger: "manual" }, 200, req, config);
154
+ } catch (error) {
155
+ rethrowManagementBodyTooLarge(error);
156
+ if (error instanceof LabAutomationError) {
157
+ return automationErrorResponse(error.code, error.message, 400, ctx);
158
+ }
159
+ return automationErrorResponse("server_error", "manual run failed", 500, ctx);
160
+ }
161
+ }
162
+
163
+ if (url.pathname === "/api/lab/automation" && req.method === "PUT") {
164
+ try {
165
+ const body = await readManagementJsonBody(req);
166
+ if (!isPlainRecord(body)) return automationErrorResponse("invalid_body", "body must be an object", 400, ctx);
167
+ // Only policy/routes are authoritative. Other top-level properties are ignored rather than
168
+ // treated as injectable process-local capabilities (for example a fake routeExecutor).
169
+ let { policy, routes } = loadLabAutomationConfig(configDir);
170
+ if (body.policy !== undefined) {
171
+ if (!isPlainRecord(body.policy)) return automationErrorResponse("invalid_policy", "policy must be an object", 400, ctx);
172
+ const incoming = body.policy as Record<string, unknown>;
173
+ const mergedLayers = incoming.layers === undefined
174
+ ? policy.layers
175
+ : isPlainRecord(incoming.layers)
176
+ ? { ...policy.layers, ...incoming.layers }
177
+ : incoming.layers;
178
+ policy = normalizeLabAutomationPolicyV1({
179
+ ...policy,
180
+ ...incoming,
181
+ layers: mergedLayers,
182
+ });
183
+ }
184
+ if (body.routes !== undefined) {
185
+ if (!isPlainRecord(body.routes)) return automationErrorResponse("invalid_routes", "routes must be an object", 400, ctx);
186
+ routes = normalizeLabAutomationRoutesV1(body.routes);
187
+ }
188
+
189
+ // One atomic rename publishes policy and routes as a coherent generation. A failed write
190
+ // leaves the previous generation authoritative and scheduler reconciliation is not applied.
191
+ if (body.policy !== undefined || body.routes !== undefined) {
192
+ saveLabAutomationConfig(policy, routes, configDir);
193
+ }
194
+ applySchedulerPolicy(policy, configDir);
195
+ return jsonResponse(buildLabAutomationStatus(configDir), 200, req, config);
196
+ } catch (error) {
197
+ rethrowManagementBodyTooLarge(error);
198
+ if (error instanceof LabAutomationError) {
199
+ return automationErrorResponse(error.code, error.message, 400, ctx);
200
+ }
201
+ return automationErrorResponse("server_error", "automation policy update failed", 500, ctx);
202
+ }
203
+ }
204
+
205
+ return automationErrorResponse("not_found", "unknown resource", 404, ctx);
206
+ }
@@ -40,6 +40,7 @@ import {
40
40
  queryLabSubjectById,
41
41
  queryLabSubjects,
42
42
  queryLabVerdicts,
43
+ queryPassiveProductionSignals,
43
44
  } from "../../lab/query";
44
45
  import { jsonResponse } from "../auth-cors";
45
46
  import type { ManagementContext } from "./context";
@@ -194,6 +195,18 @@ export async function handleLabRoutes(ctx: ManagementContext): Promise<Response
194
195
  return jsonResponse(queryLabStatus(), 200, req, config);
195
196
  }
196
197
 
198
+ if (url.pathname === "/api/lab/production-signals") {
199
+ const subjectId = url.searchParams.get("subjectId")?.trim();
200
+ if (!subjectId) return errorResponse("invalid_subject", "subjectId is required", 400, ctx);
201
+ const limit = parseLimit(url.searchParams.get("limit"), ctx);
202
+ if (limit instanceof Response) return limit;
203
+ try {
204
+ return jsonResponse(queryPassiveProductionSignals(subjectId, limit), 200, req, config);
205
+ } catch {
206
+ return errorResponse("invalid_subject", "subjectId must be an exact Lab route subject id", 400, ctx);
207
+ }
208
+ }
209
+
197
210
  if (url.pathname === "/api/lab/catalog") {
198
211
  const layerRaw = url.searchParams.get("layer");
199
212
  const layerParsed = layerRaw ? parseLayer(layerRaw, ctx) : undefined;
@@ -261,6 +261,7 @@ export async function handleLogsUsageRoutes(ctx: ManagementContext): Promise<Res
261
261
  days: [],
262
262
  models: [],
263
263
  providers: [],
264
+ accounts: [],
264
265
  historyTruncated: false,
265
266
  truncatedPrefixBytes: 0,
266
267
  entriesTruncated: false,
@@ -29,7 +29,7 @@ import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/ke
29
29
  import { deriveProviderPresets } from "../../providers/derive";
30
30
  import { providerCodexAccountMode } from "../../providers/registry";
31
31
  import { routedSlug, slugEquals } from "../../providers/slug-codec";
32
- import { clearProviderQuotaCache, fetchProviderAccountQuotas, fetchProviderQuotaReports, supportsPerAccountQuota } from "../../providers/quota";
32
+ import { clearAccountQuotaCache, clearProviderQuotaCache, fetchProviderAccountQuotas, fetchProviderQuotaReports, supportsPerAccountQuota } from "../../providers/quota";
33
33
  import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
34
34
  import { clearThreadAccountMap } from "../../codex/routing";
35
35
  import {
@@ -69,6 +69,13 @@ import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, C
69
69
  import type { ManagementContext } from "./context";
70
70
  import { readManagementJsonBody, readManagementJsonBodyOr, rethrowManagementBodyTooLarge } from "./body";
71
71
  import { codexAccountNamespaceProviderCollisionError } from "../../codex/account-namespace-match";
72
+ import { ACCOUNT_IMPORT_DEADLINE_MS, ACCOUNT_IMPORT_MAX_REQUEST_BYTES } from "../../oauth/account-import";
73
+ import { readBoundedJsonRequestBody } from "../request-decompress";
74
+
75
+ // ACCOUNT_IMPORT_DEADLINE_MS is the shared CLI/server import window. Individual
76
+ // provider requests keep their own shorter timeouts; this is only the server-side
77
+ // backstop for the admitted batch. A disconnected request aborts immediately
78
+ // through req.signal below.
72
79
 
73
80
  /**
74
81
  * Parses a bounded JSON object body, or null. Malformed JSON is swallowed; an
@@ -384,6 +391,55 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
384
391
  return jsonResponse({ ok: true, cleared });
385
392
  }
386
393
 
394
+ if (url.pathname === "/api/oauth/accounts/import" && req.method === "POST") {
395
+ const controller = new AbortController();
396
+ const abortRequest = () => controller.abort();
397
+ if (req.signal.aborted) abortRequest();
398
+ else req.signal.addEventListener("abort", abortRequest, { once: true });
399
+ const deadline = setTimeout(abortRequest, ACCOUNT_IMPORT_DEADLINE_MS);
400
+ try {
401
+ let rawBody: unknown;
402
+ try {
403
+ rawBody = await readBoundedJsonRequestBody(
404
+ req,
405
+ ACCOUNT_IMPORT_MAX_REQUEST_BYTES,
406
+ undefined,
407
+ { signal: controller.signal },
408
+ );
409
+ } catch {
410
+ if (controller.signal.aborted) return jsonResponse({ code: "import_cancelled" }, 408);
411
+ return jsonResponse({ code: "invalid_document" }, 400);
412
+ }
413
+ if (!isPlainRecord(rawBody)) return jsonResponse({ code: "invalid_document" }, 400);
414
+ const provider = typeof rawBody.provider === "string" ? rawBody.provider : "";
415
+ const format = typeof rawBody.format === "string" ? rawBody.format : "";
416
+ const { importAccounts } = await import("../../oauth/account-import");
417
+ const imported = await importAccounts({
418
+ provider,
419
+ format,
420
+ document: rawBody.document,
421
+ signal: controller.signal,
422
+ });
423
+ const changed = imported.ok
424
+ ? imported.result.importedCount > 0 || imported.result.updatedCount > 0
425
+ : imported.changed === true;
426
+ if (changed) {
427
+ reconcileLiveStateStores();
428
+ const { clearModelCache } = await import("../../codex/model-cache");
429
+ const { clearGatherRoutedModelsInflight } = await import("../../codex/catalog");
430
+ clearModelCache(provider);
431
+ clearGatherRoutedModelsInflight();
432
+ clearProviderQuotaCache();
433
+ clearAccountQuotaCache(provider);
434
+ }
435
+ if (!imported.ok) return jsonResponse({ code: imported.code }, imported.status);
436
+ return jsonResponse(imported.result);
437
+ } finally {
438
+ clearTimeout(deadline);
439
+ req.signal.removeEventListener("abort", abortRequest);
440
+ }
441
+ }
442
+
387
443
  if (url.pathname === "/api/oauth/accounts/alias" && req.method === "PUT") {
388
444
  const body = await readManagementJsonBodyOr(req, {}) as { provider?: unknown; accountId?: unknown; alias?: unknown };
389
445
  const provider = typeof body.provider === "string" ? body.provider.trim().toLowerCase() : "";
@@ -8,6 +8,8 @@ import {
8
8
  hasOwnProvider,
9
9
  isValidProviderName,
10
10
  multiAgentGuidanceEnabled,
11
+ nonBlankStringArrayConfigError,
12
+ normalizeNonBlankStringArray,
11
13
  providerBaseUrlConfigError,
12
14
  providerHeadersConfigError,
13
15
  saveConfigPreservingClaudeCode,
@@ -205,6 +207,19 @@ function applyProviderPatchFields(
205
207
  }
206
208
  touched = true;
207
209
  }
210
+ if (Object.hasOwn(rawBody, "noStructuredOutputModels")) {
211
+ const value = rawBody.noStructuredOutputModels;
212
+ if (value === null) {
213
+ delete next.noStructuredOutputModels;
214
+ } else {
215
+ const error = nonBlankStringArrayConfigError(value, "noStructuredOutputModels");
216
+ if (error) return { error };
217
+ const models = normalizeNonBlankStringArray(value as string[]);
218
+ if (models.length > 0) next.noStructuredOutputModels = models;
219
+ else delete next.noStructuredOutputModels;
220
+ }
221
+ touched = true;
222
+ }
208
223
 
209
224
  // headers is the one object-valued field in the mask. PATCH semantics merge it
210
225
  // shallowly into the existing block so a single fingerprint header can be added
@@ -291,6 +306,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
291
306
  models: p.models ?? [],
292
307
  contextWindow: p.contextWindow,
293
308
  modelContextWindows: p.modelContextWindows,
309
+ noStructuredOutputModels: p.noStructuredOutputModels,
294
310
  authMode: p.authMode,
295
311
  apiKeyTransport: p.apiKeyTransport,
296
312
  disabled: p.disabled === true,
@@ -16,10 +16,9 @@ import {
16
16
  routingProfileIssues,
17
17
  } from "../../routing/profile";
18
18
  import { evaluatePolicyProfile, type PolicyCandidateEvidence, type PolicyRequestEvidence } from "../../routing/evaluator";
19
- import { candidateCapabilityEvidence } from "../../routing/capability";
20
- import { policyCandidateHealthEvidence } from "../../routing/health";
19
+ import { assemblePolicyCandidateEvidence } from "../../routing/compatibility/assemble";
21
20
  import { quotaEvidenceForCandidate } from "../../routing/quota";
22
- import { costEvidenceForCandidate } from "../../routing/cost";
21
+ import { routedProviderConfig } from "../../router";
23
22
  import { saveConfigPreservingClaudeCode } from "../../config";
24
23
  import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
25
24
  import { isPlainRecord } from "./shared";
@@ -41,6 +40,7 @@ function profileDto(config: Parameters<typeof getRoutingProfile>[0], id: string)
41
40
  optimize: profile.optimize,
42
41
  limits: profile.limits,
43
42
  unknownEvidence: profile.unknownEvidence,
43
+ ...(profile.compatibility ? { compatibility: profile.compatibility } : {}),
44
44
  };
45
45
  }
46
46
 
@@ -98,26 +98,11 @@ function parseCandidateEvidence(raw: unknown): PolicyCandidateEvidence[] | null
98
98
  function assembleCandidateEvidence(
99
99
  config: OcxConfig,
100
100
  profile: NonNullable<ReturnType<typeof getRoutingProfile>>,
101
+ now: number,
101
102
  ): PolicyCandidateEvidence[] {
102
- // Match execution: fill the same candidate evidence the router would
103
- // assemble, so dry-run/evaluate reports the same eligibility as real routing
104
- // instead of treating every capability as unknown. Cost is always present so
105
- // evaluate mode can surface the profile limit even without a usage estimate.
106
- return profile.candidates.map(candidate => ({
107
- provider: candidate.provider,
108
- model: candidate.model,
109
- capability: candidateCapabilityEvidence(config, candidate.provider, candidate.model),
110
- health: policyCandidateHealthEvidence(config, candidate),
111
- quota: quotaEvidenceForCandidate({
112
- provider: candidate.provider,
113
- model: candidate.model,
114
- }),
115
- cost: costEvidenceForCandidate({
116
- provider: candidate.provider,
117
- model: candidate.model,
118
- limitUsd: profile.limits.maxEstimatedCostUsd,
119
- }),
120
- }));
103
+ return assemblePolicyCandidateEvidence(config, profile, now, {
104
+ routedProviderConfig,
105
+ });
121
106
  }
122
107
 
123
108
  function storedProfile(
@@ -370,13 +355,16 @@ export async function handleRoutingProfileRoutes(ctx: ManagementContext): Promis
370
355
  if (!ok) {
371
356
  return jsonResponse({ error: { code: "invalid_evidence", message: "evidence must be an object" } }, 400, req, config);
372
357
  }
358
+ // One clock read for both assembly and evaluation keeps freshness, health,
359
+ // and trace timestamps mutually consistent with the production router.
360
+ const now = Date.now();
373
361
  const candidateEvidence = body.candidates === undefined
374
- ? assembleCandidateEvidence(config, resolvedProfile)
362
+ ? assembleCandidateEvidence(config, resolvedProfile, now)
375
363
  : parseCandidateEvidence(body.candidates);
376
364
  if (candidateEvidence === null) {
377
365
  return jsonResponse({ error: { code: "invalid_candidates", message: "candidates must be an array of evidence objects" } }, 400, req, config);
378
366
  }
379
- const result = evaluatePolicyProfile(config, profile, evidence, candidateEvidence);
367
+ const result = evaluatePolicyProfile(config, profile, evidence, candidateEvidence, now);
380
368
  return jsonResponse(result, 200, req, config);
381
369
  }
382
370
 
@@ -69,6 +69,7 @@ import { handleOauthAccountRoutes } from "./management/oauth-account-routes";
69
69
  import { handleComboRoutes } from "./management/combo-routes";
70
70
  import { handleSystemRoutes } from "./management/system-routes";
71
71
  import { handleLabRoutes } from "./management/lab-routes";
72
+ import { handleLabAutomationRoutes } from "./management/lab-automation-routes";
72
73
  import { handleSidebarRoutes } from "./management/sidebar-routes";
73
74
  import { handleIntegrationRoutes } from "./management/integration-routes";
74
75
  import { handleNativeIntegrationRoutes } from "./management/native-integration-routes";
@@ -188,6 +189,7 @@ export async function handleManagementAPI(
188
189
  ?? (await handleOauthAccountRoutes(ctx))
189
190
  ?? (await handleComboRoutes(ctx))
190
191
  ?? (await handleSystemRoutes(ctx))
192
+ ?? (await handleLabAutomationRoutes(ctx))
191
193
  ?? (await handleLabRoutes(ctx))
192
194
  ?? (await handleSidebarRoutes(ctx));
193
195
  } catch (error) {
@@ -13,6 +13,14 @@ import {
13
13
  } from "node:fs";
14
14
  import { dirname, join } from "node:path";
15
15
  import { adminApiTokenFilePath } from "../lib/admin-secrets";
16
+ import {
17
+ LOCAL_MANAGEMENT_CAPABILITY_HEADER,
18
+ LOCAL_MANAGEMENT_CAPABILITY_EXPIRES_AT_HEADER,
19
+ LOCAL_MANAGEMENT_EXPECTED_PID_HEADER,
20
+ LOCAL_MANAGEMENT_NONCE_HEADER,
21
+ parseExpectedLocalManagementPid,
22
+ verifyLocalManagementReadCapability,
23
+ } from "../lib/local-management-capability";
16
24
  import {
17
25
  SYSTEM_RESTART_CAPABILITY_HEADER,
18
26
  SYSTEM_RESTART_EXPECTED_PID_HEADER,
@@ -34,6 +42,9 @@ import {
34
42
 
35
43
  const GUI_SESSION_TTL_MS = 5 * 60_000;
36
44
  const GUI_SESSION_LIMIT = 128;
45
+ const LOCAL_READ_REPLAY_LIMIT = 256;
46
+ const consumedLocalReadCapabilities = new Map<string, number>();
47
+ const admittedLocalReadRequests = new WeakSet<Request>();
37
48
 
38
49
  interface GuiSessionRecord {
39
50
  csrfToken: string;
@@ -253,10 +264,15 @@ export function issueGuiSession(
253
264
  * minted for a browser, and it only authorizes a mutation after the origin and the
254
265
  * per-session CSRF token match. Consent-bearing routes must key off this value
255
266
  * rather than off request headers, which the token holder can forge freely.
256
- * `system-restart-capability` is a process-scoped HMAC accepted only for the exact
257
- * restart route and bound to the current process PID and listening port.
267
+ * The capability principals are process-scoped HMACs bound to the current process
268
+ * PID and listening port. Local reads are accepted only for two exact GET paths;
269
+ * restart remains a separate wire contract for its exact POST.
258
270
  */
259
- export type ManagementPrincipal = "admin-token" | "gui-session" | "system-restart-capability";
271
+ export type ManagementPrincipal =
272
+ | "admin-token"
273
+ | "gui-session"
274
+ | "local-read-capability"
275
+ | "system-restart-capability";
260
276
 
261
277
  export interface LocalManagementAuthContext {
262
278
  attestationSecret: string;
@@ -291,6 +307,53 @@ function hasSystemRestartCapability(
291
307
  );
292
308
  }
293
309
 
310
+ function hasLocalReadCapability(
311
+ req: Request,
312
+ local: LocalManagementAuthContext | undefined,
313
+ ): boolean {
314
+ // requireManagementAuth and managementPrincipal inspect the same Request in
315
+ // sequence. Preserve that one admission without accepting a replayed request.
316
+ if (admittedLocalReadRequests.has(req)) return true;
317
+ if (!local || req.method !== "GET") return false;
318
+ let url: URL;
319
+ try {
320
+ url = new URL(req.url);
321
+ } catch {
322
+ return false;
323
+ }
324
+ // Do not let a future query-bearing variant silently inherit this narrow grant.
325
+ if (url.search !== "") return false;
326
+ const expectedPid = parseExpectedLocalManagementPid(
327
+ req.headers.get(LOCAL_MANAGEMENT_EXPECTED_PID_HEADER),
328
+ );
329
+ if (expectedPid.kind !== "present" || expectedPid.pid !== local.pid) return false;
330
+ const expiresAtRaw = req.headers.get(LOCAL_MANAGEMENT_CAPABILITY_EXPIRES_AT_HEADER);
331
+ if (!expiresAtRaw || !/^[1-9]\d*$/.test(expiresAtRaw)) return false;
332
+ const expiresAt = Number(expiresAtRaw);
333
+ if (!Number.isSafeInteger(expiresAt)) return false;
334
+ const capability = req.headers.get(LOCAL_MANAGEMENT_CAPABILITY_HEADER);
335
+ const now = Date.now();
336
+ if (!verifyLocalManagementReadCapability(
337
+ local.attestationSecret,
338
+ req.headers.get(LOCAL_MANAGEMENT_NONCE_HEADER),
339
+ req.method,
340
+ url.pathname,
341
+ local.pid,
342
+ local.port,
343
+ expiresAt,
344
+ capability,
345
+ now,
346
+ )) return false;
347
+ for (const [consumed, retainedUntil] of consumedLocalReadCapabilities) {
348
+ if (retainedUntil <= now) consumedLocalReadCapabilities.delete(consumed);
349
+ }
350
+ if (!capability || consumedLocalReadCapabilities.has(capability)) return false;
351
+ if (consumedLocalReadCapabilities.size >= LOCAL_READ_REPLAY_LIMIT) return false;
352
+ consumedLocalReadCapabilities.set(capability, expiresAt);
353
+ admittedLocalReadRequests.add(req);
354
+ return true;
355
+ }
356
+
294
357
  /**
295
358
  * The principal for a request that already passed `requireManagementAuth`. Kept as a
296
359
  * separate resolution (rather than a changed return type) so every existing caller
@@ -305,6 +368,7 @@ export function managementPrincipal(
305
368
  local?: LocalManagementAuthContext,
306
369
  ): ManagementPrincipal | null {
307
370
  if (hasSystemRestartCapability(req, local)) return "system-restart-capability";
371
+ if (hasLocalReadCapability(req, local)) return "local-read-capability";
308
372
  if (!state.available) return null;
309
373
  const actual = req.headers.get("x-opencodex-api-key")?.trim()
310
374
  || req.headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
@@ -322,6 +386,7 @@ export function requireManagementAuth(
322
386
  local?: LocalManagementAuthContext,
323
387
  ): Response | null {
324
388
  if (hasSystemRestartCapability(req, local)) return null;
389
+ if (hasLocalReadCapability(req, local)) return null;
325
390
  if (!state.available) {
326
391
  return Response.json({
327
392
  error: "management API unavailable",