@bitkyc08/opencodex 2.7.43-preview.20260728 → 2.8.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 +8 -1
  2. package/bin/ocx.mjs +47 -22
  3. package/gui/dist/assets/index-BDjpkcRN.js +67 -0
  4. package/gui/dist/assets/index-BHsKRFh9.css +1 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +1 -1
  7. package/src/AGENTS.md +28 -0
  8. package/src/adapters/anthropic.ts +15 -6
  9. package/src/adapters/cursor/discovery.ts +4 -1
  10. package/src/adapters/cursor/effort-map.ts +3 -0
  11. package/src/adapters/cursor/native-exec-shell.ts +18 -6
  12. package/src/adapters/cursor/protobuf-events.ts +24 -2
  13. package/src/adapters/cursor/protobuf-request.ts +1 -2
  14. package/src/adapters/cursor/tool-definitions.ts +68 -29
  15. package/src/adapters/google-wire-compiler.ts +4 -0
  16. package/src/adapters/google.ts +128 -2
  17. package/src/adapters/identity.ts +12 -2
  18. package/src/adapters/kiro.ts +64 -7
  19. package/src/adapters/mimo-free.ts +2 -0
  20. package/src/adapters/openai-responses.ts +246 -59
  21. package/src/claude/agents-inject.ts +5 -0
  22. package/src/claude/alias.ts +94 -14
  23. package/src/claude/inbound.ts +26 -9
  24. package/src/claude/outbound.ts +6 -3
  25. package/src/cli/account-auth.ts +1 -1
  26. package/src/cli/agent-driven.ts +37 -0
  27. package/src/cli/catalog-prewarm.ts +24 -0
  28. package/src/cli/claude.ts +35 -10
  29. package/src/cli/doctor.ts +71 -19
  30. package/src/cli/help.ts +42 -6
  31. package/src/cli/index.ts +93 -19
  32. package/src/cli/interactive-confirm.ts +133 -0
  33. package/src/cli/opencode.ts +701 -0
  34. package/src/cli/provider-runtime.ts +3 -0
  35. package/src/cli/provider.ts +31 -10
  36. package/src/cli/star-prompt.ts +79 -18
  37. package/src/cli/status.ts +47 -13
  38. package/src/cli/v2.ts +10 -1
  39. package/src/codex/account-id.ts +34 -0
  40. package/src/codex/account-lifecycle.ts +4 -1
  41. package/src/codex/account-namespace-match.ts +63 -0
  42. package/src/codex/account-namespaces.ts +149 -0
  43. package/src/codex/account-pause.ts +20 -0
  44. package/src/codex/account-store.ts +2 -0
  45. package/src/codex/account-usability.ts +6 -1
  46. package/src/codex/app-server-processes.ts +511 -0
  47. package/src/codex/auth-api.ts +293 -34
  48. package/src/codex/auth-collision.ts +2 -1
  49. package/src/codex/auth-context.ts +60 -17
  50. package/src/codex/catalog/bundled.ts +9 -2
  51. package/src/codex/catalog/parsing.ts +42 -2
  52. package/src/codex/catalog/provider-fetch.ts +264 -70
  53. package/src/codex/catalog/sync.ts +45 -8
  54. package/src/codex/catalog.ts +2 -2
  55. package/src/codex/features.ts +524 -5
  56. package/src/codex/history-provider.ts +145 -1
  57. package/src/codex/inject.ts +114 -14
  58. package/src/codex/main-account.ts +2 -8
  59. package/src/codex/pool-rotation.ts +186 -0
  60. package/src/codex/quota.ts +92 -2
  61. package/src/codex/routing.ts +695 -106
  62. package/src/codex/runtime.ts +10 -1
  63. package/src/codex/shim.ts +4 -1
  64. package/src/codex/subagent-defaults.ts +550 -0
  65. package/src/codex/subagent-model-fallback.ts +2 -0
  66. package/src/codex/sync.ts +3 -0
  67. package/src/config.ts +574 -25
  68. package/src/generated/jawcode-model-metadata.ts +12 -12
  69. package/src/github/star-state.ts +191 -0
  70. package/src/images/artifacts.ts +516 -0
  71. package/src/images/fulfill-video.ts +163 -0
  72. package/src/images/fulfill.ts +111 -0
  73. package/src/images/index.ts +4 -0
  74. package/src/images/loop.ts +789 -0
  75. package/src/images/plan.ts +133 -0
  76. package/src/images/synthetic-tool.ts +133 -0
  77. package/src/images/types.ts +41 -0
  78. package/src/images/xai-client.ts +141 -0
  79. package/src/images/xai-video-client.ts +163 -0
  80. package/src/lib/admin-secrets.ts +25 -0
  81. package/src/lib/bun-binary-validator.d.mts +3 -0
  82. package/src/lib/bun-binary-validator.mjs +18 -0
  83. package/src/lib/bun-runtime.ts +6 -20
  84. package/src/lib/config-ownership.ts +327 -0
  85. package/src/lib/crash-guard.ts +2 -0
  86. package/src/lib/destination-policy.ts +132 -7
  87. package/src/lib/pinned-http.ts +151 -0
  88. package/src/lib/process-control.ts +2 -2
  89. package/src/lib/provider-outbound.ts +167 -0
  90. package/src/lib/provider-url.ts +14 -0
  91. package/src/lib/proxy-env.ts +18 -0
  92. package/src/lib/shadow-call.ts +30 -0
  93. package/src/lib/test-home-guard.ts +90 -0
  94. package/src/lib/win-exec.ts +12 -2
  95. package/src/lib/windows-elevation.ts +81 -3
  96. package/src/lib/windows-secret-acl.ts +189 -12
  97. package/src/lib/winsw.ts +2 -0
  98. package/src/oauth/anthropic-routing.ts +570 -0
  99. package/src/oauth/health.ts +6 -0
  100. package/src/oauth/index.ts +310 -75
  101. package/src/oauth/key-providers.ts +38 -8
  102. package/src/oauth/kimi.ts +2 -0
  103. package/src/oauth/kiro-credentials.ts +373 -12
  104. package/src/oauth/kiro.ts +424 -43
  105. package/src/oauth/login-cli.ts +33 -6
  106. package/src/oauth/store.ts +56 -4
  107. package/src/oauth/types.ts +11 -0
  108. package/src/providers/alibaba-region-migration.ts +16 -3
  109. package/src/providers/antigravity-models.ts +3 -0
  110. package/src/providers/api-keys.ts +13 -6
  111. package/src/providers/derive.ts +8 -2
  112. package/src/providers/key-failover.ts +24 -4
  113. package/src/providers/model-discovery.ts +356 -0
  114. package/src/providers/quota.ts +233 -29
  115. package/src/providers/registry.ts +125 -3
  116. package/src/responses/parser.ts +11 -0
  117. package/src/responses/state.ts +22 -8
  118. package/src/responses/tool-groups.ts +19 -0
  119. package/src/router.ts +19 -7
  120. package/src/server/auth-cors.ts +114 -24
  121. package/src/server/claude-messages.ts +8 -1
  122. package/src/server/gui-static.ts +30 -6
  123. package/src/server/images.ts +303 -9
  124. package/src/server/index.ts +77 -9
  125. package/src/server/lifecycle.ts +25 -1
  126. package/src/server/live.ts +75 -25
  127. package/src/server/management/agent-settings-routes.ts +106 -8
  128. package/src/server/management/combo-routes.ts +7 -0
  129. package/src/server/management/config-routes.ts +22 -7
  130. package/src/server/management/context.ts +11 -1
  131. package/src/server/management/logs-usage-routes.ts +167 -3
  132. package/src/server/management/model-routes.ts +46 -13
  133. package/src/server/management/oauth-account-routes.ts +163 -17
  134. package/src/server/management/provider-routes.ts +73 -10
  135. package/src/server/management/shared.ts +2 -2
  136. package/src/server/management/sidebar-routes.ts +39 -0
  137. package/src/server/management/system-restart.ts +172 -0
  138. package/src/server/management/system-routes.ts +33 -10
  139. package/src/server/management-api.ts +5 -3
  140. package/src/server/management-auth.ts +216 -0
  141. package/src/server/proxy-liveness.ts +14 -3
  142. package/src/server/responses/compact.ts +21 -13
  143. package/src/server/responses/core.ts +614 -172
  144. package/src/server/responses/upstream-error.ts +48 -0
  145. package/src/server/responses-image-gen-repair.ts +118 -0
  146. package/src/server/responses-item-id-repair.ts +10 -85
  147. package/src/server/sse-payload-rewrite.ts +116 -0
  148. package/src/server/startup-action-control.ts +30 -14
  149. package/src/server/system-env.ts +28 -10
  150. package/src/service.ts +284 -19
  151. package/src/storage/cleanup-job.ts +57 -0
  152. package/src/storage/cleanup.ts +1504 -28
  153. package/src/storage/policy-job.ts +387 -0
  154. package/src/storage/policy-scheduler.ts +40 -0
  155. package/src/storage/policy-worker.ts +53 -0
  156. package/src/storage/policy.ts +522 -0
  157. package/src/storage/restore-job.ts +253 -0
  158. package/src/storage/restore-worker.ts +52 -0
  159. package/src/storage/storage-mutation-coordinator.ts +109 -0
  160. package/src/storage/worker-lifecycle.ts +81 -0
  161. package/src/tray/windows.ts +34 -4
  162. package/src/types.ts +107 -1
  163. package/src/update/badge.ts +72 -0
  164. package/src/update/index.ts +36 -18
  165. package/src/update/job.ts +111 -16
  166. package/src/update/npm-invocation.d.mts +23 -0
  167. package/src/update/npm-invocation.mjs +94 -0
  168. package/src/usage/debug.ts +2 -0
  169. package/src/usage/expected-prices.ts +6 -5
  170. package/src/usage/log.ts +12 -0
  171. package/src/web-search/loop.ts +57 -16
  172. package/gui/dist/assets/index-CjKFJHSC.js +0 -65
  173. package/gui/dist/assets/index-DfVGuN88.css +0 -1
@@ -1,25 +1,31 @@
1
1
  /**
2
- * /api/system/* — service-process runtime/memory introspection (#314 WP3).
2
+ * /api/system/* — service-process runtime/memory introspection (#314 WP3)
3
+ * and the memory-card drain-and-restart action (#563).
3
4
  *
4
5
  * Rides the standard management gate: every /api/* request already passed
5
- * requireApiAuth("management") + the origin check before dispatch, so these
6
+ * the independent management-auth gate + the origin check before dispatch, so these
6
7
  * routes add no auth of their own. NEVER expose this data on the
7
8
  * unauthenticated /healthz surface.
8
9
  *
9
- * The payload is scalar-only (numbers, enum strings): no paths, no tokens, no
10
- * account identifiers. `external` and `arrayBuffers` keep Windows diagnostics
11
- * honest when RSS/working-set counters under-report committed retention.
12
- * `jscHeap` (bun:jsc heapStats) is useful context, but on Bun 1.3.14 it is not a
13
- * standalone leak discriminator. `responseState` attributes growth further: it
14
- * is the proxy's previous_response_id continuation store, so a growing
15
- * responseState.totalBytes under rising observed memory points at conversation
16
- * retention rather than the runtime allocator.
10
+ * The payload is scalar-only (numbers, enum strings, booleans): no paths, no
11
+ * tokens, no account identifiers. `external` and `arrayBuffers` keep Windows
12
+ * diagnostics honest when RSS/working-set counters under-report committed
13
+ * retention. `jscHeap` (bun:jsc heapStats) is useful context, but on Bun 1.3.14
14
+ * it is not a standalone leak discriminator. `responseState` attributes growth
15
+ * further: it is the proxy's previous_response_id continuation store, so a
16
+ * growing responseState.totalBytes under rising observed memory points at
17
+ * conversation retention rather than the runtime allocator.
18
+ *
19
+ * `activeTurnCount` / `isDraining` are scalar lifecycle counters for the
20
+ * dashboard drain-and-restart confirm UX — never request bodies or IDs.
17
21
  */
18
22
  import { decideEagerRelay } from "../../lib/bun-stream-caps";
23
+ import { getActiveTurnCount, isDraining } from "../lifecycle";
19
24
  import { getActiveMemoryWatchdog, observedMemoryCounter } from "../memory-watchdog";
20
25
  import { responseStateMetrics } from "../../responses/state";
21
26
  import { jsonResponse } from "../auth-cors";
22
27
  import type { ManagementContext } from "./context";
28
+ import { acceptSystemRestart } from "./system-restart";
23
29
 
24
30
  const ENDPOINT_SAMPLE_LIMIT = 60;
25
31
 
@@ -76,7 +82,24 @@ export async function handleSystemRoutes(ctx: ManagementContext): Promise<Respon
76
82
  streamMode,
77
83
  eagerRelay: process.platform === "win32" ? decideEagerRelay(streamMode) : null,
78
84
  watchdog,
85
+ activeTurnCount: getActiveTurnCount(),
86
+ isDraining: isDraining(),
79
87
  });
80
88
  }
89
+
90
+ if (url.pathname === "/api/system/restart" && req.method === "POST") {
91
+ // Longer informed drain than /api/stop; does not tear down Codex/Grok injection.
92
+ const result = acceptSystemRestart();
93
+ return jsonResponse({
94
+ success: true,
95
+ message: result.alreadyDraining
96
+ ? "Drain already in progress."
97
+ : "Draining in-flight requests, then restarting.",
98
+ activeTurnCount: result.activeTurnCount,
99
+ drainTimeoutMs: result.drainTimeoutMs,
100
+ alreadyDraining: result.alreadyDraining,
101
+ }, 202, req, config);
102
+ }
103
+
81
104
  return null;
82
105
  }
@@ -53,7 +53,7 @@ import { drainAndShutdown } from "./lifecycle";
53
53
  import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "./request-log";
54
54
  import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerSecond } from "../usage/cost";
55
55
  import type { PersistedUsageAttempt } from "../usage/log";
56
- import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "./auth-cors";
56
+ import { isAllowedManagementOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "./auth-cors";
57
57
  import { applySystemEnvToggle } from "./system-env";
58
58
 
59
59
  import type { ManagementApiDeps } from "./management/context";
@@ -65,6 +65,7 @@ import { handleAgentSettingsRoutes } from "./management/agent-settings-routes";
65
65
  import { handleOauthAccountRoutes } from "./management/oauth-account-routes";
66
66
  import { handleComboRoutes } from "./management/combo-routes";
67
67
  import { handleSystemRoutes } from "./management/system-routes";
68
+ import { handleSidebarRoutes } from "./management/sidebar-routes";
68
69
  import type { ManagementContext } from "./management/context";
69
70
  export type { ManagementApiDeps } from "./management/context";
70
71
  import { fetchAllModels } from "./management/shared";
@@ -79,7 +80,7 @@ export const VERSION = (() => {
79
80
  })();
80
81
 
81
82
  export async function handleManagementAPI(req: Request, url: URL, config: OcxConfig, deps: ManagementApiDeps = {}): Promise<Response | null> {
82
- if (!isAllowedRequestOrigin(req, config)) {
83
+ if (!isAllowedManagementOrigin(req, config)) {
83
84
  return jsonResponse({ error: "cross-origin request blocked" }, 403, req, config);
84
85
  }
85
86
  // Management bodies are small JSON (provider names, key ids, settings). Reject oversized
@@ -130,7 +131,8 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
130
131
  ?? (await handleAgentSettingsRoutes(ctx))
131
132
  ?? (await handleOauthAccountRoutes(ctx))
132
133
  ?? (await handleComboRoutes(ctx))
133
- ?? (await handleSystemRoutes(ctx));
134
+ ?? (await handleSystemRoutes(ctx))
135
+ ?? (await handleSidebarRoutes(ctx));
134
136
  if (routed) return routed;
135
137
 
136
138
  if (url.pathname === "/api/stop" && req.method === "POST") {
@@ -0,0 +1,216 @@
1
+ import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
2
+ import {
3
+ chmodSync,
4
+ closeSync,
5
+ fsyncSync,
6
+ linkSync,
7
+ lstatSync,
8
+ mkdirSync,
9
+ openSync,
10
+ readFileSync,
11
+ unlinkSync,
12
+ writeFileSync,
13
+ } from "node:fs";
14
+ import { dirname, join } from "node:path";
15
+ import { adminApiTokenFilePath } from "../lib/admin-secrets";
16
+ import { hardenSecretDir, hardenSecretPath } from "../lib/windows-secret-acl";
17
+ import type { OcxConfig } from "../types";
18
+ import {
19
+ isAllowedManagementOrigin,
20
+ isApiAuthRequired,
21
+ isDataPlaneAdmissionSecret,
22
+ isLoopbackHostname,
23
+ managementRequestOrigin,
24
+ parseHttpHost,
25
+ } from "./auth-cors";
26
+
27
+ const GUI_SESSION_TTL_MS = 5 * 60_000;
28
+ const GUI_SESSION_LIMIT = 128;
29
+
30
+ interface GuiSessionRecord {
31
+ csrfToken: string;
32
+ origin: string;
33
+ expiresAt: number;
34
+ }
35
+
36
+ export interface GuiSessionBootstrap extends GuiSessionRecord {
37
+ token: string;
38
+ }
39
+
40
+ export type ManagementAuthState =
41
+ | {
42
+ available: true;
43
+ token: string;
44
+ source: "environment" | "file";
45
+ sessions: Map<string, GuiSessionRecord>;
46
+ }
47
+ | { available: false; reason: string };
48
+
49
+ function fail(reason: string): ManagementAuthState {
50
+ return { available: false, reason };
51
+ }
52
+
53
+ function assertSafeDirectory(path: string): void {
54
+ mkdirSync(path, { recursive: true, mode: 0o700 });
55
+ const stat = lstatSync(path);
56
+ if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("management token directory is not a regular directory");
57
+ chmodSync(path, 0o700);
58
+ const hardened = hardenSecretDir(path, { required: true });
59
+ if (!hardened.ok) throw new Error("management token directory ACL hardening did not complete");
60
+ }
61
+
62
+ function readExistingToken(path: string): string {
63
+ const stat = lstatSync(path);
64
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 512) {
65
+ throw new Error("management token path is not a regular secret file");
66
+ }
67
+ chmodSync(path, 0o600);
68
+ const hardened = hardenSecretPath(path, { required: true });
69
+ if (!hardened.ok) throw new Error("management token file ACL hardening did not complete");
70
+ const token = readFileSync(path, "utf8").trim();
71
+ if (!/^ocx_admin_[A-Za-z0-9_-]{43}$/.test(token)) throw new Error("management token file is invalid");
72
+ return token;
73
+ }
74
+
75
+ function removeBestEffort(path: string): void {
76
+ try { unlinkSync(path); } catch { /* fail-closed state is preserved by the caller */ }
77
+ }
78
+
79
+ function createTokenFile(path: string): string {
80
+ const directory = dirname(path);
81
+ const token = `ocx_admin_${randomBytes(32).toString("base64url")}`;
82
+ const temporary = join(directory, `.${randomUUID()}.admin-token.tmp`);
83
+ let linked = false;
84
+ let fd: number | null = null;
85
+ try {
86
+ fd = openSync(temporary, "wx", 0o600);
87
+ writeFileSync(fd, `${token}\n`, "utf8");
88
+ fsyncSync(fd);
89
+ closeSync(fd);
90
+ fd = null;
91
+ chmodSync(temporary, 0o600);
92
+ const temporaryHardened = hardenSecretPath(temporary, { required: true });
93
+ if (!temporaryHardened.ok) throw new Error("management token temporary ACL hardening did not complete");
94
+ try {
95
+ linkSync(temporary, path);
96
+ linked = true;
97
+ } catch (error) {
98
+ if ((error as NodeJS.ErrnoException).code === "EEXIST") return readExistingToken(path);
99
+ throw error;
100
+ }
101
+ const finalHardened = hardenSecretPath(path, { required: true });
102
+ if (!finalHardened.ok) throw new Error("management token file ACL hardening did not complete");
103
+ return token;
104
+ } catch (error) {
105
+ if (linked) removeBestEffort(path);
106
+ throw error;
107
+ } finally {
108
+ if (fd !== null) {
109
+ try { closeSync(fd); } catch { /* best effort */ }
110
+ }
111
+ removeBestEffort(temporary);
112
+ }
113
+ }
114
+
115
+ function ready(token: string, source: "environment" | "file", config: OcxConfig): ManagementAuthState {
116
+ if (isDataPlaneAdmissionSecret(token, config)) {
117
+ return fail("management credential conflicts with a data-plane credential");
118
+ }
119
+ return { available: true, token, source, sessions: new Map() };
120
+ }
121
+
122
+ export function initializeManagementAuthState(config: OcxConfig): ManagementAuthState {
123
+ const environmentToken = process.env.OPENCODEX_ADMIN_AUTH_TOKEN?.trim();
124
+ if (environmentToken) {
125
+ return ready(environmentToken, "environment", config);
126
+ }
127
+ try {
128
+ const path = adminApiTokenFilePath();
129
+ assertSafeDirectory(dirname(path));
130
+ let token: string;
131
+ try {
132
+ token = readExistingToken(path);
133
+ } catch (error) {
134
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
135
+ token = createTokenFile(path);
136
+ }
137
+ return ready(token, "file", config);
138
+ } catch (error) {
139
+ return fail(error instanceof Error ? error.message : "management token initialization failed");
140
+ }
141
+ }
142
+
143
+ function equalSecret(actual: string, expected: string): boolean {
144
+ const encoder = new TextEncoder();
145
+ const left = encoder.encode(actual);
146
+ const right = encoder.encode(expected);
147
+ return left.length === right.length && timingSafeEqual(left, right);
148
+ }
149
+
150
+ function removeExpiredSessions(state: Extract<ManagementAuthState, { available: true }>, now = Date.now()): void {
151
+ for (const [token, session] of state.sessions) {
152
+ if (session.expiresAt <= now) state.sessions.delete(token);
153
+ }
154
+ }
155
+
156
+ function randomSessionSecret(prefix: "ocx_session_"): string {
157
+ return `${prefix}${randomBytes(32).toString("base64url")}`;
158
+ }
159
+
160
+ export function issueGuiSession(
161
+ req: Request,
162
+ config: OcxConfig,
163
+ state: ManagementAuthState,
164
+ ): GuiSessionBootstrap | null {
165
+ if (isApiAuthRequired(config) || !state.available || req.method !== "GET" || !isAllowedManagementOrigin(req, config)) return null;
166
+ const host = parseHttpHost(req.headers.get("Host"));
167
+ if (!host || !isLoopbackHostname(host.hostname)) return null;
168
+ const origin = managementRequestOrigin(req, config);
169
+ if (!origin) return null;
170
+ const now = Date.now();
171
+ removeExpiredSessions(state, now);
172
+ while (state.sessions.size >= GUI_SESSION_LIMIT) {
173
+ const oldest = state.sessions.keys().next().value as string | undefined;
174
+ if (!oldest) break;
175
+ state.sessions.delete(oldest);
176
+ }
177
+ const token = randomSessionSecret("ocx_session_");
178
+ const session: GuiSessionRecord = {
179
+ csrfToken: randomBytes(32).toString("base64url"),
180
+ origin,
181
+ expiresAt: now + GUI_SESSION_TTL_MS,
182
+ };
183
+ state.sessions.set(token, session);
184
+ return { token, ...session };
185
+ }
186
+
187
+ export function requireManagementAuth(
188
+ req: Request,
189
+ state: ManagementAuthState,
190
+ config?: OcxConfig,
191
+ ): Response | null {
192
+ if (!state.available) {
193
+ return Response.json({ error: "management API unavailable" }, { status: 503 });
194
+ }
195
+ const actual = req.headers.get("x-opencodex-api-key")?.trim()
196
+ || req.headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
197
+ if (actual && equalSecret(actual, state.token)) return null;
198
+ if (actual && config) {
199
+ removeExpiredSessions(state);
200
+ const session = state.sessions.get(actual);
201
+ if (session) {
202
+ const requestOrigin = managementRequestOrigin(req, config);
203
+ const claimedOrigin = req.headers.get("x-opencodex-gui-origin");
204
+ const browserOrigin = req.headers.get("Origin");
205
+ const sameOrigin = requestOrigin === session.origin
206
+ && claimedOrigin === session.origin
207
+ && (!browserOrigin || browserOrigin === session.origin);
208
+ const safeMethod = req.method === "GET" || req.method === "HEAD";
209
+ const csrf = req.headers.get("x-opencodex-csrf-token")?.trim();
210
+ if (sameOrigin && (safeMethod || (browserOrigin === session.origin && !!csrf && equalSecret(csrf, session.csrfToken)))) {
211
+ return null;
212
+ }
213
+ }
214
+ }
215
+ return Response.json({ error: "opencodex admin token required" }, { status: 401 });
216
+ }
@@ -37,6 +37,8 @@ export interface LiveProxy {
37
37
  port: number;
38
38
  /** Raw bind hostname the probe succeeded against; compose URLs via `probeHostname`. */
39
39
  hostname?: string;
40
+ /** Whether the successful probe used runtime-port metadata or the configured listen port. */
41
+ source: "runtime" | "config";
40
42
  }
41
43
 
42
44
  /**
@@ -118,7 +120,7 @@ export async function findLiveProxy(io: LivenessIo = {}): Promise<LiveProxy | nu
118
120
  // healthz confirmed the pid itself → trusted; a pidless legacy body did not,
119
121
  // so the cheap pid must pass full identity verification before it is returned.
120
122
  const trusted = identity.pid === pid ? pid : killablePid(pid);
121
- return { pid: trusted, port: runtime.port, hostname: runtime.hostname };
123
+ return { pid: trusted, port: runtime.port, hostname: runtime.hostname, source: "runtime" };
122
124
  }
123
125
  }
124
126
  }
@@ -133,12 +135,21 @@ export async function findLiveProxy(io: LivenessIo = {}): Promise<LiveProxy | nu
133
135
  // Only the healthz-reported pid is authoritative here. The record's pid may be stale
134
136
  // (its process dead, the port reused by a pidless legacy proxy) — synthesizing it
135
137
  // would hand destructive callers (stopProxy → kill fallback) a reusable pid.
136
- if (identity) return { pid: identity.pid ?? null, port: record.port, hostname: record.hostname };
138
+ if (identity) {
139
+ return { pid: identity.pid ?? null, port: record.port, hostname: record.hostname, source: "runtime" };
140
+ }
137
141
  }
138
142
 
139
143
  const config = configFn();
140
144
  const port = config.port ?? 10100;
141
145
  const identity = await proxyIdentityAt(port, { hostname: config.hostname }, io);
142
- if (identity) return { pid: identity.pid ?? killablePid(pid), port, hostname: config.hostname };
146
+ if (identity) {
147
+ return {
148
+ pid: identity.pid ?? killablePid(pid),
149
+ port,
150
+ hostname: config.hostname,
151
+ source: "config",
152
+ };
153
+ }
143
154
  return null;
144
155
  }
@@ -51,6 +51,7 @@ import {
51
51
  isCodexAuthContextUsable,
52
52
  resolveCodexAuthContext,
53
53
  codexProbeLeaseId,
54
+ codexProbeQuotaScope,
54
55
  type CodexAuthContext,
55
56
  } from "../../codex/auth-context";
56
57
  import {
@@ -217,7 +218,7 @@ export async function handleResponsesCompact(
217
218
  const headers = new Headers({ "content-type": "application/json" });
218
219
  try {
219
220
  if (route.codexAccountMode) {
220
- authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode);
221
+ authCtx = await resolveCodexAuthContext(req.headers, config, route.codexAccountMode, { modelId: selectedModelId });
221
222
  const selected = headersForCodexAuthContext(req.headers, authCtx);
222
223
  compactProvider = applyCodexAuthContextToProvider(route.provider, authCtx, route.codexAccountMode);
223
224
  for (const name of FORWARD_HEADERS) {
@@ -255,12 +256,17 @@ export async function handleResponsesCompact(
255
256
  const compactUrl = `${base}/responses/compact`;
256
257
  const compactThreadId = req.headers.get("x-codex-parent-thread-id");
257
258
  const connectMs = config.connectTimeoutMs ?? 200_000;
258
- const recordCompactPoolOutcome = (outcome: CodexUpstreamOutcome, meta: { retryAfter?: string | null } = {}) => {
259
+ const recordCompactPoolOutcome = (
260
+ outcome: CodexUpstreamOutcome,
261
+ meta: { retryAfter?: string | null; resetAt?: unknown | unknown[] } = {},
262
+ ) => {
259
263
  if (!usesCodexForwardPoolAuth(authCtx, route.provider)) return;
260
264
  recordCodexUpstreamOutcome(config, authCtx.accountId, outcome, {
261
265
  ...meta,
262
266
  threadId: compactThreadId,
267
+ modelId: selectedModelId,
263
268
  probeLeaseId: codexProbeLeaseId(authCtx),
269
+ probeQuotaScope: codexProbeQuotaScope(authCtx),
264
270
  });
265
271
  };
266
272
  let upstream: Response;
@@ -283,28 +289,30 @@ export async function handleResponsesCompact(
283
289
  { abortSignal: req.signal, label: safeHostLabel(compactUrl) },
284
290
  );
285
291
  } catch (err) {
286
- if (req.signal.aborted) return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
292
+ if (req.signal.aborted) {
293
+ recordCompactPoolOutcome(499);
294
+ return formatErrorResponse(499, "client_cancelled", "Client cancelled compact request");
295
+ }
287
296
  const outcome = err instanceof Error && err.name === "TimeoutError" ? "timeout" : "connect_error";
288
297
  recordCompactPoolOutcome(outcome);
289
298
  return formatErrorResponse(502, "upstream_error", "Failed to connect to compact upstream");
290
299
  }
291
300
  const retryAfter = upstream.headers.get("retry-after");
301
+ const resetAt = [
302
+ upstream.headers.get("x-codex-primary-reset-at"),
303
+ upstream.headers.get("x-codex-secondary-reset-at"),
304
+ upstream.headers.get("x-codex-tertiary-reset-at"),
305
+ ].filter(Boolean);
292
306
  const buffered = await bufferCompactResponse(upstream, req.signal);
293
307
  // Record pool health only after the body is fully delivered (or definitively failed).
294
308
  // A premature 200 would clear soft-avoid while the client still sees a buffer 502.
295
309
  if (buffered.status === 499) {
310
+ recordCompactPoolOutcome(499);
296
311
  return buffered;
297
312
  }
298
- if (upstream.ok && buffered.status >= 500) {
299
- // The upstream account returned 200 it is healthy. The buffering failure
300
- // (oversized body exceeding COMPACT_RESPONSE_MAX_BYTES, or a rare mid-read
301
- // reset on a small JSON payload) is a local proxy issue, not account flakiness.
302
- // Record the upstream status so a deterministic payload-size limit does not
303
- // soft-avoid a healthy account and rotate a thread for 30s.
304
- recordCompactPoolOutcome(upstream.status, { retryAfter });
305
- } else {
306
- recordCompactPoolOutcome(upstream.status, { retryAfter });
307
- }
313
+ // Always record the real upstream status: a local buffering failure after a
314
+ // 200 upstream response must not soft-avoid a healthy account or rotate a thread.
315
+ recordCompactPoolOutcome(upstream.status, { retryAfter, resetAt });
308
316
  return buffered;
309
317
  }
310
318