@bitkyc08/opencodex 2.10.0 → 2.10.1

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 (274) hide show
  1. package/AGENTS_INSTALL.md +77 -0
  2. package/README.md +4 -10
  3. package/bin/ocx.mjs +71 -18
  4. package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
  5. package/gui/dist/assets/index-ChZQsmBY.js +70 -0
  6. package/gui/dist/index.html +2 -2
  7. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  8. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  9. package/gui/dist/provider-icons/claude-color.svg +1 -1
  10. package/gui/dist/provider-icons/cline-color.svg +16 -0
  11. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  12. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  13. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  14. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  15. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  16. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  17. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  18. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  19. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  20. package/gui/dist/provider-icons/grok.svg +1 -1
  21. package/gui/dist/provider-icons/groq-color.svg +1 -1
  22. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  23. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  24. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  25. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  26. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  27. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  28. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  29. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  30. package/gui/dist/provider-icons/openai.svg +1 -1
  31. package/gui/dist/provider-icons/opencode.svg +2 -1
  32. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  33. package/gui/dist/provider-icons/pi.svg +2 -2
  34. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  35. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  36. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  37. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  38. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  39. package/package.json +8 -4
  40. package/src/adapters/anthropic.ts +208 -14
  41. package/src/adapters/base.ts +16 -5
  42. package/src/adapters/cursor/effort-map.ts +3 -2
  43. package/src/adapters/cursor/framing.ts +39 -0
  44. package/src/adapters/cursor/live-transport.ts +105 -95
  45. package/src/adapters/cursor/native-exec.ts +32 -6
  46. package/src/adapters/cursor/protobuf-request.ts +20 -15
  47. package/src/adapters/cursor/request-builder.ts +21 -7
  48. package/src/adapters/cursor/types.ts +7 -0
  49. package/src/adapters/google-antigravity-replay.ts +237 -21
  50. package/src/adapters/google-truncation.ts +11 -0
  51. package/src/adapters/google.ts +50 -9
  52. package/src/adapters/identity.ts +39 -6
  53. package/src/adapters/kiro-errors.ts +11 -0
  54. package/src/adapters/kiro-events.ts +19 -1
  55. package/src/adapters/kiro-thinking.ts +10 -2
  56. package/src/adapters/kiro-tools.ts +10 -1
  57. package/src/adapters/kiro.ts +37 -11
  58. package/src/adapters/openai-chat.ts +284 -83
  59. package/src/adapters/openai-responses.ts +182 -24
  60. package/src/bridge.ts +177 -7
  61. package/src/chat/outbound.ts +78 -23
  62. package/src/claude/agents-inject.ts +27 -5
  63. package/src/claude/inbound.ts +11 -1
  64. package/src/claude/model-info.ts +13 -10
  65. package/src/claude/outbound.ts +17 -0
  66. package/src/cli/account-api.ts +24 -0
  67. package/src/cli/account-auth.ts +31 -6
  68. package/src/cli/account-main.ts +317 -0
  69. package/src/cli/account.ts +5 -0
  70. package/src/cli/claude.ts +2 -1
  71. package/src/cli/doctor.ts +93 -22
  72. package/src/cli/export-command.ts +26 -12
  73. package/src/cli/help.ts +8 -6
  74. package/src/cli/index.ts +56 -22
  75. package/src/cli/integrations.ts +84 -1
  76. package/src/cli/observe.ts +54 -1
  77. package/src/cli/opencode.ts +2 -1
  78. package/src/cli/provider-runtime.ts +18 -1
  79. package/src/cli/route-policy.ts +92 -0
  80. package/src/cli/runtime-api.ts +6 -3
  81. package/src/cli/star-prompt.ts +71 -15
  82. package/src/cli/status.ts +1 -1
  83. package/src/cli/v2.ts +36 -9
  84. package/src/clients/config-export.ts +687 -10
  85. package/src/codex/account-lifecycle.ts +30 -5
  86. package/src/codex/account-usability.ts +22 -2
  87. package/src/codex/admission.ts +255 -0
  88. package/src/codex/auth-api.ts +427 -140
  89. package/src/codex/auth-context.ts +155 -30
  90. package/src/codex/autostart-health.ts +8 -1
  91. package/src/codex/catalog/account-models.ts +62 -0
  92. package/src/codex/catalog/aggregation.ts +14 -1
  93. package/src/codex/catalog/bundled.ts +282 -32
  94. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  95. package/src/codex/catalog/metadata.ts +51 -6
  96. package/src/codex/catalog/parsing.ts +6 -3
  97. package/src/codex/catalog/provider-fetch.ts +576 -41
  98. package/src/codex/catalog/sync.ts +505 -66
  99. package/src/codex/catalog-admission.ts +197 -0
  100. package/src/codex/catalog-write-serialization.ts +241 -0
  101. package/src/codex/catalog.ts +2 -1
  102. package/src/codex/codex-write-lock.ts +372 -0
  103. package/src/codex/convergence-types.ts +593 -0
  104. package/src/codex/convergence.ts +441 -0
  105. package/src/codex/desired-state.ts +177 -0
  106. package/src/codex/features.ts +52 -8
  107. package/src/codex/generation.ts +202 -0
  108. package/src/codex/history-job.ts +257 -0
  109. package/src/codex/history-lock.ts +241 -0
  110. package/src/codex/history-migration-guardian.ts +18 -5
  111. package/src/codex/history-provider.ts +9 -2
  112. package/src/codex/history-transition.ts +105 -0
  113. package/src/codex/history-worker.ts +176 -0
  114. package/src/codex/inject-coordination.ts +245 -0
  115. package/src/codex/inject.ts +605 -124
  116. package/src/codex/integration-record.ts +266 -0
  117. package/src/codex/internal/catalog-writer.ts +203 -0
  118. package/src/codex/internal/history-writer.ts +80 -0
  119. package/src/codex/journal.ts +10 -1
  120. package/src/codex/main-account-cache.ts +24 -0
  121. package/src/codex/management-convergence.ts +114 -0
  122. package/src/codex/native-main-admission.ts +47 -0
  123. package/src/codex/native-main-auth-temp.ts +187 -0
  124. package/src/codex/native-main-claim.ts +167 -0
  125. package/src/codex/native-main-lock-file.ts +158 -0
  126. package/src/codex/native-main-owner.ts +315 -0
  127. package/src/codex/native-profile-api.ts +247 -0
  128. package/src/codex/native-profile-manager.ts +1512 -0
  129. package/src/codex/native-profile-processes.ts +121 -0
  130. package/src/codex/native-profile-recovery.ts +99 -0
  131. package/src/codex/native-profile-stage-store.ts +387 -0
  132. package/src/codex/native-profile-startup.ts +340 -0
  133. package/src/codex/native-profile-store.ts +855 -0
  134. package/src/codex/native-profile-types.ts +120 -0
  135. package/src/codex/native-residue.ts +557 -0
  136. package/src/codex/project-config-warnings.ts +18 -4
  137. package/src/codex/prompt-journal.ts +311 -0
  138. package/src/codex/prompt-layers.ts +967 -0
  139. package/src/codex/prompt-lock.ts +143 -0
  140. package/src/codex/quota-rejection.ts +224 -0
  141. package/src/codex/quota.ts +86 -3
  142. package/src/codex/routing.ts +299 -62
  143. package/src/codex/runtime.ts +159 -38
  144. package/src/codex/shim.ts +39 -13
  145. package/src/codex/subagent-model-fallback.ts +73 -12
  146. package/src/codex/transition-state.ts +604 -0
  147. package/src/codex/upstream-host-health.ts +70 -0
  148. package/src/codex/user-identity.ts +266 -0
  149. package/src/codex/write-coordination.ts +114 -0
  150. package/src/config.ts +562 -26
  151. package/src/generated/jawcode-model-metadata.ts +2 -2
  152. package/src/grok/inject.ts +15 -4
  153. package/src/grok/inspect.ts +45 -0
  154. package/src/images/loop.ts +113 -20
  155. package/src/integrations/config-io.ts +151 -0
  156. package/src/integrations/journal.ts +315 -0
  157. package/src/integrations/merge.ts +135 -0
  158. package/src/integrations/native/ownership-preflight.ts +165 -0
  159. package/src/integrations/ownership.ts +111 -0
  160. package/src/integrations/registry.ts +101 -0
  161. package/src/integrations/serialize.ts +235 -0
  162. package/src/integrations/state.ts +290 -0
  163. package/src/integrations/store.ts +103 -0
  164. package/src/integrations/writer.ts +492 -0
  165. package/src/lib/bounded-body.ts +46 -8
  166. package/src/lib/bun-runtime.ts +110 -1
  167. package/src/lib/bun-stream-caps.ts +2 -1
  168. package/src/lib/redact.ts +407 -2
  169. package/src/lib/shadow-call.ts +24 -0
  170. package/src/lib/translator-budget.ts +10 -0
  171. package/src/lib/upstream-reachability.ts +91 -0
  172. package/src/lib/upstream-retry.ts +154 -2
  173. package/src/lib/windows-secret-acl.ts +212 -11
  174. package/src/lib/winsw.ts +9 -3
  175. package/src/oauth/index.ts +61 -3
  176. package/src/oauth/key-providers.ts +4 -0
  177. package/src/oauth/kiro.ts +50 -6
  178. package/src/oauth/store.ts +31 -6
  179. package/src/oauth/token-guardian.ts +9 -3
  180. package/src/providers/codex-capacity.ts +288 -0
  181. package/src/providers/derive.ts +33 -1
  182. package/src/providers/free-directory.ts +3 -1
  183. package/src/providers/key-failover.ts +71 -3
  184. package/src/providers/openai-sidecar.ts +64 -4
  185. package/src/providers/openai-virtual-models.ts +1 -0
  186. package/src/providers/quota.ts +334 -26
  187. package/src/providers/registry.ts +284 -16
  188. package/src/providers/xai-transport.ts +11 -4
  189. package/src/responses/compaction.ts +8 -1
  190. package/src/responses/hosted-tool-policy.ts +9 -0
  191. package/src/responses/parser.ts +44 -2
  192. package/src/responses/reasoning-envelope.ts +9 -1
  193. package/src/responses/reasoning-replay-cache.ts +105 -0
  194. package/src/responses/spill-store.ts +45 -8
  195. package/src/responses/state.ts +161 -17
  196. package/src/router.ts +302 -16
  197. package/src/routing/analytics.ts +377 -0
  198. package/src/routing/capability.ts +204 -0
  199. package/src/routing/cost.ts +77 -0
  200. package/src/routing/evaluator.ts +444 -0
  201. package/src/routing/health.ts +401 -0
  202. package/src/routing/history/cursor.ts +43 -0
  203. package/src/routing/history/indexer.ts +590 -0
  204. package/src/routing/history/schema.ts +72 -0
  205. package/src/routing/profile.ts +423 -0
  206. package/src/routing/quota.ts +124 -0
  207. package/src/routing/request-evidence.ts +45 -0
  208. package/src/routing/trace.ts +686 -0
  209. package/src/server/auth-cors.ts +46 -6
  210. package/src/server/chat-completions.ts +28 -13
  211. package/src/server/claude-messages.ts +23 -15
  212. package/src/server/gui-static.ts +39 -10
  213. package/src/server/images.ts +10 -1
  214. package/src/server/index.ts +238 -52
  215. package/src/server/lifecycle.ts +228 -9
  216. package/src/server/live.ts +14 -3
  217. package/src/server/management/agent-settings-routes.ts +64 -14
  218. package/src/server/management/combo-routes.ts +5 -5
  219. package/src/server/management/config-routes.ts +1 -1
  220. package/src/server/management/context.ts +42 -2
  221. package/src/server/management/integration-routes.ts +538 -0
  222. package/src/server/management/logs-usage-routes.ts +1 -1
  223. package/src/server/management/model-routes.ts +32 -113
  224. package/src/server/management/model-rows.ts +117 -0
  225. package/src/server/management/native-integration-routes.ts +587 -0
  226. package/src/server/management/oauth-account-routes.ts +1 -1
  227. package/src/server/management/provider-routes.ts +218 -117
  228. package/src/server/management/request-history-routes.ts +191 -0
  229. package/src/server/management/routing-analytics-routes.ts +74 -0
  230. package/src/server/management/routing-profile-routes.ts +177 -0
  231. package/src/server/management/shared.ts +2 -2
  232. package/src/server/management/sidebar-routes.ts +47 -31
  233. package/src/server/management/sync-response.ts +69 -0
  234. package/src/server/management/system-restart.ts +276 -24
  235. package/src/server/management/system-routes.ts +4 -0
  236. package/src/server/management-api.ts +84 -9
  237. package/src/server/management-auth.ts +43 -5
  238. package/src/server/relay-eager.ts +82 -42
  239. package/src/server/relay.ts +120 -6
  240. package/src/server/request-log.ts +26 -6
  241. package/src/server/responses/collaboration.ts +63 -8
  242. package/src/server/responses/compact.ts +272 -41
  243. package/src/server/responses/core.ts +730 -132
  244. package/src/server/responses/fetch-helpers.ts +15 -1
  245. package/src/server/responses-item-id-repair.ts +32 -3
  246. package/src/server/responses-json-events.ts +52 -0
  247. package/src/server/responses-snapshot-repair.ts +621 -0
  248. package/src/server/search.ts +51 -6
  249. package/src/server/sse-payload-rewrite.ts +89 -12
  250. package/src/server/startup-health-cache.ts +7 -1
  251. package/src/server/ws-bridge.ts +11 -17
  252. package/src/service-manager-probe.ts +297 -0
  253. package/src/service.ts +222 -32
  254. package/src/tray/windows-tray.ps1 +9 -0
  255. package/src/tray/windows.ts +15 -7
  256. package/src/types.ts +194 -14
  257. package/src/update/index.ts +13 -13
  258. package/src/update/job.ts +24 -21
  259. package/src/update/notify.ts +7 -3
  260. package/src/usage/cost.ts +0 -0
  261. package/src/usage/expected-prices.ts +129 -10
  262. package/src/usage/log.ts +50 -15
  263. package/src/usage/summary.ts +4 -4
  264. package/src/vision/index.ts +6 -1
  265. package/src/web-search/loop.ts +161 -34
  266. package/gui/dist/assets/index-OY43ubAq.css +0 -1
  267. package/gui/dist/assets/index-YwNnKZcL.js +0 -67
  268. package/gui/dist/provider-icons/antigravity.svg +0 -1
  269. package/gui/dist/provider-icons/claude.svg +0 -1
  270. package/gui/dist/provider-icons/copilot.svg +0 -1
  271. package/gui/dist/provider-icons/cursor.svg +0 -2
  272. package/gui/dist/provider-icons/gemini.svg +0 -1
  273. package/gui/dist/provider-icons/grok-color.svg +0 -1
  274. package/gui/dist/provider-icons/kiro.svg +0 -14
@@ -20,6 +20,8 @@ import {
20
20
  beginBackgroundShellShutdown,
21
21
  terminateAllBackgroundShells,
22
22
  } from "../adapters/cursor/native-exec-shell";
23
+ import type { CodexAccountSelectionAdmission } from "../codex/auth-context";
24
+ import { releaseNativeMainStartupLifecycle } from "../codex/native-profile-startup";
23
25
 
24
26
  // ---------------------------------------------------------------------------
25
27
  // Active turn tracking + graceful shutdown drain
@@ -29,24 +31,138 @@ export const MAX_ACTIVE_TURNS = 256;
29
31
  const turnGate = createAdmissionGate("active_turns", MAX_ACTIVE_TURNS);
30
32
  export interface ActiveTurnLease extends AdmissionLease {
31
33
  bindAbortController(ac: AbortController): void;
34
+ beginCodexAccountSelection(): CodexAccountSelectionAdmission;
32
35
  isTransferred(): boolean;
33
36
  }
34
37
  const activeTurns = new Map<AbortController, ActiveTurnLease>();
35
38
  const admittedTurns = new Set<ActiveTurnLease>();
36
39
  const knownTurnControllers = new WeakSet<AbortController>();
37
40
  let turnReleaseMisses = 0;
38
- let draining = false;
41
+ let shutdownDraining = false;
42
+ const temporaryDrainOwners = new Set<symbol>();
43
+ const nativeMainDrainOwners = new Set<symbol>();
44
+ const temporaryDrainWaiters = new Set<() => void>();
45
+ const nativeMainTurns = new Set<ActiveTurnLease>();
46
+ let nativeMainSelections = 0;
47
+ let legacyDrainLease: AdmissionLease | null = null;
39
48
  let recyclingForExit = false;
40
49
  let _serverRef: ReturnType<typeof Bun.serve> | undefined;
50
+ let serverStopFlights = new WeakMap<ReturnType<typeof Bun.serve>, Promise<void>>();
51
+ let serverStartupReleaseFlights = new WeakMap<ReturnType<typeof Bun.serve>, Promise<void>>();
52
+ let releaseServerStartupLifecycleImpl: typeof releaseNativeMainStartupLifecycle = releaseNativeMainStartupLifecycle;
41
53
 
42
54
  export function setServerRef(server: ReturnType<typeof Bun.serve> | undefined): void { _serverRef = server; }
43
- export function setDraining(value: boolean): void { draining = value; }
55
+ /**
56
+ * Legacy/test-only drain control. Production scoped operations must use a lease,
57
+ * while process shutdown uses the irreversible shutdown latch.
58
+ */
59
+ export function setDraining(value: boolean): void {
60
+ if (value) {
61
+ legacyDrainLease ??= acquireTemporaryDrain("legacy");
62
+ } else {
63
+ legacyDrainLease?.release();
64
+ legacyDrainLease = null;
65
+ }
66
+ }
67
+
68
+ function temporaryDrainCount(): number {
69
+ return temporaryDrainOwners.size + nativeMainDrainOwners.size;
70
+ }
71
+
72
+ function notifyTemporaryDrainsSettled(): void {
73
+ if (temporaryDrainCount() !== 0) return;
74
+ for (const resolve of temporaryDrainWaiters) resolve();
75
+ temporaryDrainWaiters.clear();
76
+ }
77
+
78
+ /** Acquire the single owner-scoped global data-plane fence. */
79
+ export function acquireTemporaryDrain(owner: string): AdmissionLease | null {
80
+ if (shutdownDraining || temporaryDrainCount() > 0) return null;
81
+ const token = Symbol(owner);
82
+ temporaryDrainOwners.add(token);
83
+ let active = true;
84
+ return {
85
+ release() {
86
+ if (!active) return;
87
+ active = false;
88
+ temporaryDrainOwners.delete(token);
89
+ notifyTemporaryDrainsSettled();
90
+ },
91
+ };
92
+ }
93
+
94
+ /** Fence only turns that select the native Codex `__main__` account. */
95
+ export function acquireNativeMainProfileDrain(owner: string): AdmissionLease | null {
96
+ if (shutdownDraining || temporaryDrainCount() > 0) return null;
97
+ const token = Symbol(owner);
98
+ nativeMainDrainOwners.add(token);
99
+ let active = true;
100
+ return {
101
+ release() {
102
+ if (!active) return;
103
+ active = false;
104
+ nativeMainDrainOwners.delete(token);
105
+ notifyTemporaryDrainsSettled();
106
+ },
107
+ };
108
+ }
109
+
110
+ /** Permanently fence this process for shutdown; scoped lease release cannot clear it. */
111
+ export function beginShutdownDrain(): boolean {
112
+ if (shutdownDraining) return false;
113
+ shutdownDraining = true;
114
+ return true;
115
+ }
116
+
117
+ export function isShutdownDraining(): boolean { return shutdownDraining; }
118
+
119
+ export function waitForTemporaryDrains(): Promise<void> {
120
+ if (temporaryDrainCount() === 0) return Promise.resolve();
121
+ return new Promise(resolve => temporaryDrainWaiters.add(resolve));
122
+ }
123
+
124
+ /** Wait for scoped drains without allowing them to outlive the shutdown deadline. */
125
+ async function waitForTemporaryDrainsUntil(deadlineMs: number): Promise<boolean> {
126
+ if (temporaryDrainCount() === 0) return true;
127
+ const remainingMs = Math.max(0, deadlineMs - Date.now());
128
+ if (remainingMs === 0) return false;
129
+ return new Promise<boolean>((resolve) => {
130
+ let settled = false;
131
+ let timer: ReturnType<typeof setTimeout> | undefined;
132
+ const finish = (drained: boolean) => {
133
+ if (settled) return;
134
+ settled = true;
135
+ if (timer) clearTimeout(timer);
136
+ resolve(drained);
137
+ };
138
+ timer = setTimeout(() => finish(false), remainingMs);
139
+ void waitForTemporaryDrains().then(() => finish(true));
140
+ });
141
+ }
142
+
143
+ /** Test-only process-lifetime reset. Never call from production recovery paths. */
144
+ export function resetLifecycleDrainStateForTests(): void {
145
+ legacyDrainLease?.release();
146
+ legacyDrainLease = null;
147
+ temporaryDrainOwners.clear();
148
+ nativeMainDrainOwners.clear();
149
+ nativeMainTurns.clear();
150
+ nativeMainSelections = 0;
151
+ for (const resolve of temporaryDrainWaiters) resolve();
152
+ temporaryDrainWaiters.clear();
153
+ shutdownDraining = false;
154
+ serverStopFlights = new WeakMap<ReturnType<typeof Bun.serve>, Promise<void>>();
155
+ serverStartupReleaseFlights = new WeakMap<ReturnType<typeof Bun.serve>, Promise<void>>();
156
+ releaseServerStartupLifecycleImpl = releaseNativeMainStartupLifecycle;
157
+ }
44
158
  export function tryAdmitTurn(): ActiveTurnLease | null {
159
+ if (isDraining()) return null;
45
160
  const gateLease = turnGate.tryAcquire();
46
161
  if (!gateLease) return null;
47
162
  const controllers = new Set<AbortController>();
48
163
  let active = true;
49
164
  let transferred = false;
165
+ let nativeMainClaimed = false;
50
166
  const lease: ActiveTurnLease = {
51
167
  bindAbortController(ac) {
52
168
  knownTurnControllers.add(ac);
@@ -58,6 +174,31 @@ export function tryAdmitTurn(): ActiveTurnLease | null {
58
174
  controllers.add(ac);
59
175
  activeTurns.set(ac, lease);
60
176
  },
177
+ beginCodexAccountSelection() {
178
+ const mainProfileDraining = nativeMainDrainOwners.size > 0;
179
+ let selectionActive = !mainProfileDraining;
180
+ let released = false;
181
+ if (selectionActive) nativeMainSelections += 1;
182
+ return {
183
+ mainProfileDraining,
184
+ claimMainProfile() {
185
+ if (released || mainProfileDraining || !active) return false;
186
+ if (!nativeMainClaimed) {
187
+ nativeMainClaimed = true;
188
+ nativeMainTurns.add(lease);
189
+ }
190
+ return true;
191
+ },
192
+ release() {
193
+ if (released) return;
194
+ released = true;
195
+ if (selectionActive) {
196
+ selectionActive = false;
197
+ nativeMainSelections = Math.max(0, nativeMainSelections - 1);
198
+ }
199
+ },
200
+ };
201
+ },
61
202
  isTransferred() { return transferred; },
62
203
  release() {
63
204
  if (!active) return;
@@ -67,12 +208,43 @@ export function tryAdmitTurn(): ActiveTurnLease | null {
67
208
  if (activeTurns.get(controller) === lease) activeTurns.delete(controller);
68
209
  }
69
210
  controllers.clear();
211
+ nativeMainTurns.delete(lease);
70
212
  gateLease.release();
71
213
  },
72
214
  };
73
215
  admittedTurns.add(lease);
74
216
  return lease;
75
217
  }
218
+ export function codexAccountSelectionForTurn(
219
+ lease?: AdmissionLease,
220
+ ): (() => CodexAccountSelectionAdmission | undefined) | undefined {
221
+ if (!lease || !("beginCodexAccountSelection" in lease)) return undefined;
222
+ const activeLease = lease as ActiveTurnLease;
223
+ return () => activeLease.beginCodexAccountSelection();
224
+ }
225
+
226
+ /** Promote a physical native-main credential read onto an already admitted turn. */
227
+ export function tryClaimNativeMainProfileForTurn(lease?: AdmissionLease): boolean {
228
+ const beginSelection = codexAccountSelectionForTurn(lease);
229
+ if (!beginSelection) return false;
230
+ const selection = beginSelection();
231
+ if (!selection) return false;
232
+ try {
233
+ return !selection.mainProfileDraining && selection.claimMainProfile();
234
+ } finally {
235
+ selection.release();
236
+ }
237
+ }
238
+
239
+ /** Acquire standalone native-main ownership for management/background work. */
240
+ export function tryAcquireNativeMainProfileClaim(): AdmissionLease | null {
241
+ const turn = tryAdmitTurn();
242
+ if (!turn) return null;
243
+ if (tryClaimNativeMainProfileForTurn(turn)) return turn;
244
+ turn.release();
245
+ return null;
246
+ }
247
+
76
248
  export function registerTurn(ac: AbortController, lease?: AdmissionLease): void {
77
249
  if (lease && "bindAbortController" in lease) (lease as ActiveTurnLease).bindAbortController(ac);
78
250
  }
@@ -85,8 +257,11 @@ export function unregisterTurn(ac: AbortController): void {
85
257
  }
86
258
  lease.release();
87
259
  }
88
- export function isDraining(): boolean { return draining; }
260
+ export function isDraining(): boolean { return shutdownDraining || temporaryDrainOwners.size > 0; }
89
261
  export function getActiveTurnCount(): number { return turnGate.metrics().active; }
262
+ export function getNativeMainProfileRequestCount(): number {
263
+ return nativeMainSelections + nativeMainTurns.size;
264
+ }
90
265
  export function activeRegistryMetrics(): Record<string, AdmissionMetrics> {
91
266
  const turns = turnGate.metrics();
92
267
  return {
@@ -111,6 +286,44 @@ export function getServerListenPort(): number | undefined {
111
286
  const port = _serverRef?.port;
112
287
  return typeof port === "number" && port > 0 ? port : undefined;
113
288
  }
289
+
290
+ /**
291
+ * Stop one concrete listener exactly once. Deadline restart handoff can race the
292
+ * ordinary drain's finally block; both callers must observe the same stop result
293
+ * before any replacement process is allowed to bind the port.
294
+ */
295
+ export function stopServerListener(
296
+ server: ReturnType<typeof Bun.serve> | undefined = _serverRef,
297
+ ): Promise<void> {
298
+ if (!server) return Promise.resolve();
299
+ const existing = serverStopFlights.get(server);
300
+ if (existing) return existing;
301
+ // Bun's Server.stop returns Promise<void>; fire-and-forget races a
302
+ // follow-on listen and can leave the replacement seeing the old proxy.
303
+ const flight = Promise.resolve().then(() => server.stop(true));
304
+ serverStopFlights.set(server, flight);
305
+ return flight;
306
+ }
307
+
308
+ /** Native-main startup ownership cleanup, separate from the listen socket flight. */
309
+ export function releaseServerStartupLifecycle(
310
+ server: ReturnType<typeof Bun.serve> | undefined = _serverRef,
311
+ ): Promise<void> {
312
+ if (!server) return Promise.resolve();
313
+ const existing = serverStartupReleaseFlights.get(server);
314
+ if (existing) return existing;
315
+ const flight = Promise.resolve().then(() => releaseServerStartupLifecycleImpl(server));
316
+ serverStartupReleaseFlights.set(server, flight);
317
+ return flight;
318
+ }
319
+
320
+ /** Test seam for a held/rejected startup lifecycle release. */
321
+ export function setServerStartupLifecycleReleaseForTests(
322
+ release: typeof releaseNativeMainStartupLifecycle | undefined,
323
+ ): void {
324
+ releaseServerStartupLifecycleImpl = release ?? releaseNativeMainStartupLifecycle;
325
+ serverStartupReleaseFlights = new WeakMap<ReturnType<typeof Bun.serve>, Promise<void>>();
326
+ }
114
327
  /**
115
328
  * Mark this process as a recycle (dashboard drain-and-restart). Exit cleanup
116
329
  * must keep Codex/Grok/system-env injection so the replacement process inherits
@@ -158,10 +371,16 @@ export async function drainAndShutdown(
158
371
  timeoutMs: number,
159
372
  ): Promise<void> {
160
373
  const s = server ?? _serverRef;
161
- draining = true;
374
+ // One absolute budget covers both a pre-existing scoped profile drain and
375
+ // ordinary in-flight turns. A stuck scoped owner must not pin shutdown forever.
376
+ const deadline = Date.now() + Math.max(0, timeoutMs);
377
+ beginShutdownDrain();
378
+ const temporaryDrainsSettled = await waitForTemporaryDrainsUntil(deadline);
379
+ if (!temporaryDrainsSettled) {
380
+ console.warn("Temporary drain lease did not settle before the shutdown deadline; forcing shutdown");
381
+ }
162
382
  beginBackgroundShellShutdown();
163
383
  try {
164
- const deadline = Date.now() + timeoutMs;
165
384
  while (admittedTurns.size > 0 && Date.now() < deadline) {
166
385
  await Bun.sleep(100);
167
386
  }
@@ -217,11 +436,11 @@ export async function drainAndShutdown(
217
436
  setStorageCleanupPolicyJobLiveApply(null);
218
437
  } finally {
219
438
  try {
220
- // Bun's Server.stop returns Promise<void>; fire-and-forget races the next
221
- // isolate reclaim / follow-on listen the same way unterminated Workers did.
222
- if (s) await s.stop(true);
439
+ await stopServerListener(s);
223
440
  } finally {
224
- draining = false;
441
+ await releaseServerStartupLifecycle(s);
442
+ // shutdownDraining is a process-lifetime latch. A stopped server must
443
+ // never resume admission merely because shutdown cleanup returned.
225
444
  }
226
445
  }
227
446
  }
@@ -24,8 +24,10 @@ import { appendFileSync } from "node:fs";
24
24
  import { formatErrorResponse } from "../bridge";
25
25
  import {
26
26
  CodexAccountCooldownError,
27
+ codexMainProfileDrainingResponse,
27
28
  cooldownErrorResponse,
28
29
  CodexAuthContextError,
30
+ CodexMainProfileDrainingError,
29
31
  CodexPoolAuthenticationError,
30
32
  CodexThreadAffinityExpiredError,
31
33
  } from "../codex/auth-context";
@@ -37,6 +39,8 @@ import { resolveFirstUsableOpenAiSidecar, selectOpenAiImagesProvider } from "../
37
39
  import { ForwardAdmissionCredentialError, validateForwardAdmissionCredential } from "./auth-cors";
38
40
  import type { RequestLogContext } from "./request-log";
39
41
  import { codexLogAccountId } from "./responses";
42
+ import type { AdmissionLease } from "../lib/admission";
43
+ import { codexAccountSelectionForTurn } from "./lifecycle";
40
44
 
41
45
  /** Voice call create can wait on SDP negotiation; bound a hung upstream. */
42
46
  const LIVE_UPSTREAM_TIMEOUT_MS = 120_000;
@@ -409,6 +413,7 @@ export async function resolveLiveRelay(
409
413
  req: Request,
410
414
  config: OcxConfig,
411
415
  logCtx: RequestLogContext,
416
+ turnAdmissionLease?: AdmissionLease,
412
417
  ): Promise<LiveRelayTarget | Response> {
413
418
  try {
414
419
  validateForwardAdmissionCredential(req.headers, config);
@@ -433,7 +438,9 @@ export async function resolveLiveRelay(
433
438
  let forwardAuthError: Response | undefined;
434
439
  if (candidates.forwardCandidates.length > 0) {
435
440
  try {
436
- forward = await resolveFirstUsableOpenAiSidecar(candidates.forwardCandidates, req.headers, config);
441
+ forward = await resolveFirstUsableOpenAiSidecar(candidates.forwardCandidates, req.headers, config, {
442
+ beginCodexAccountSelection: codexAccountSelectionForTurn(turnAdmissionLease),
443
+ });
437
444
  if (forward) {
438
445
  logCtx.provider = formatCodexProviderForLog(
439
446
  forward.providerName,
@@ -444,6 +451,8 @@ export async function resolveLiveRelay(
444
451
  } catch (err) {
445
452
  if (err instanceof CodexAccountCooldownError) {
446
453
  forwardAuthError = cooldownErrorResponse(err);
454
+ } else if (err instanceof CodexMainProfileDrainingError) {
455
+ forwardAuthError = codexMainProfileDrainingResponse();
447
456
  } else if (err instanceof CodexThreadAffinityExpiredError) {
448
457
  forwardAuthError = formatErrorResponse(
449
458
  409,
@@ -506,13 +515,14 @@ export async function handleLive(
506
515
  req: Request,
507
516
  config: OcxConfig,
508
517
  logCtx: RequestLogContext,
518
+ turnAdmissionLease?: AdmissionLease,
509
519
  ): Promise<Response> {
510
520
  const inboundContentType = req.headers.get("content-type") ?? "application/octet-stream";
511
521
  const inboundBodyOrError = await readRequestBodyCapped(req, LIVE_REQUEST_MAX_BYTES);
512
522
  if (inboundBodyOrError instanceof Response) return inboundBodyOrError;
513
523
  const inboundBody = inboundBodyOrError;
514
524
 
515
- const relay = await resolveLiveRelay(req, config, logCtx);
525
+ const relay = await resolveLiveRelay(req, config, logCtx, turnAdmissionLease);
516
526
  if (relay instanceof Response) return relay;
517
527
 
518
528
  const headers: Record<string, string> = { ...relay.headers };
@@ -587,8 +597,9 @@ export async function resolveLiveSidebandUpgrade(
587
597
  config: OcxConfig,
588
598
  logCtx: RequestLogContext,
589
599
  target: LiveSidebandTarget,
600
+ turnAdmissionLease?: AdmissionLease,
590
601
  ): Promise<{ headers: Record<string, string>; upstreamWsUrl: string; recordOutcome?: LiveRelayTarget["recordOutcome"] } | Response> {
591
- const relay = await resolveLiveRelay(req, config, logCtx);
602
+ const relay = await resolveLiveRelay(req, config, logCtx, turnAdmissionLease);
592
603
  if (relay instanceof Response) return relay;
593
604
  return {
594
605
  headers: relay.headers,
@@ -125,7 +125,7 @@ export function setGrokApplyFlightTestHooks(
125
125
  import type { ManagementContext } from "./context";
126
126
 
127
127
  export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise<Response | null> {
128
- const { req, url, config, deps, refreshCodexCatalogBestEffort, syncClaudeAgentDefsBestEffort } = ctx;
128
+ const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
129
129
 
130
130
  /** Best-effort Desktop 3P config auto-reconcile when providers change. */
131
131
  async function autoApplyDesktopBestEffort(): Promise<void> {
@@ -229,16 +229,11 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
229
229
  const warnings: string[] = [];
230
230
  const requestedFlag = wantsFlag ? body.enabled as boolean : modeFlag;
231
231
  if (requestedFlag !== undefined || wantsThreads) {
232
- const targetFlag = requestedFlag ?? isMultiAgentV2Enabled();
233
- let toggle = deps.toggleCodexMultiAgentV2;
234
- if (!toggle) {
235
- const { execFileSync } = await import("node:child_process");
236
- const { codexFeaturesInvocation } = await import("../../cli/v2");
237
- toggle = (enabled: boolean) => {
238
- const inv = codexFeaturesInvocation(enabled ? "enable" : "disable");
239
- execFileSync(inv.file, inv.args,
240
- { stdio: ["ignore", "pipe", "pipe"], timeout: 15_000, windowsHide: true, ...inv.options });
241
- };
232
+ const targetFlag = requestedFlag ?? isMultiAgentV2Enabled();
233
+ let toggle = deps.toggleCodexMultiAgentV2;
234
+ if (!toggle) {
235
+ const { runCodexFeaturesCommand } = await import("../../cli/v2");
236
+ toggle = (enabled: boolean) => runCodexFeaturesCommand(enabled ? "enable" : "disable");
242
237
  }
243
238
  const result = transitionMultiAgentV2(targetFlag, toggle, {
244
239
  ...(wantsThreads ? { threadLimit: body.maxConcurrentThreadsPerSession as number } : {}),
@@ -282,7 +277,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
282
277
  if (getAgentsEnabled() === false && isMultiAgentV2Enabled()) {
283
278
  warnings.push("agents.enabled = false has no effect while features.multi_agent_v2 is enabled; upstream keeps V2 active.");
284
279
  }
285
- await refreshCodexCatalogBestEffort();
280
+ const catalogRefresh = await convergeCodexCatalog();
286
281
  if (requestedFlag !== undefined) warnings.push("Applies to new sessions; restart the Codex app or wait out its picker cache to see the ladder change.");
287
282
  const enabled = isMultiAgentV2Enabled();
288
283
  return jsonResponse({
@@ -296,9 +291,64 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
296
291
  subagentDeveloperInstructions: getSubagentDeveloperInstructions(),
297
292
  agentsMaxDepthAppliesWhenV2Disabled: !enabled,
298
293
  warnings,
294
+ catalogRefresh,
299
295
  });
300
296
  }
301
297
 
298
+ // default_mode_request_user_input feature toggle (Codex Auth page). GET reads the
299
+ // flag from $CODEX_HOME/config.toml; PUT flips it via the official `codex features`
300
+ // CLI so the TOML edit stays upstream-owned and format-preserving.
301
+ if (url.pathname === "/api/codex-auth/features/default-mode-request-user-input" && req.method === "GET") {
302
+ const { isDefaultModeRequestUserInputEnabled, DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY } = await import("../../codex/features");
303
+ return jsonResponse({
304
+ enabled: isDefaultModeRequestUserInputEnabled(),
305
+ key: DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY,
306
+ });
307
+ }
308
+ if (url.pathname === "/api/codex-auth/features/default-mode-request-user-input" && req.method === "PUT") {
309
+ let parsedBody: unknown;
310
+ try {
311
+ parsedBody = await readManagementJsonBody(req);
312
+ } catch (error) {
313
+ rethrowManagementBodyTooLarge(error);
314
+ return jsonResponse({ error: "invalid JSON body" }, 400);
315
+ }
316
+ if (!parsedBody || typeof parsedBody !== "object" || Array.isArray(parsedBody)) {
317
+ return jsonResponse({ error: "body must be a JSON object" }, 400);
318
+ }
319
+ const body = parsedBody as { enabled?: unknown };
320
+ if (typeof body.enabled !== "boolean") return jsonResponse({ error: "body.enabled must be a boolean" }, 400);
321
+ const { isDefaultModeRequestUserInputEnabled, DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY } = await import("../../codex/features");
322
+ const before = isDefaultModeRequestUserInputEnabled();
323
+ let toggle = deps.toggleDefaultModeRequestUserInput;
324
+ if (!toggle) {
325
+ const { runCodexFeaturesCommand } = await import("../../cli/v2");
326
+ toggle = (enabled: boolean) => runCodexFeaturesCommand(enabled ? "enable" : "disable", DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY);
327
+ }
328
+ let toggleError: string | null = null;
329
+ try {
330
+ toggle(body.enabled);
331
+ } catch (error) {
332
+ const err = error as { stderr?: unknown; message?: string };
333
+ const raw = err.stderr;
334
+ const stderrText = typeof raw === "string"
335
+ ? raw.trim()
336
+ : raw instanceof Uint8Array ? new TextDecoder().decode(raw).trim() : "";
337
+ toggleError = stderrText || (err.message ?? String(error));
338
+ }
339
+ const enabled = isDefaultModeRequestUserInputEnabled();
340
+ if (toggleError !== null || enabled !== body.enabled) {
341
+ const reason = toggleError
342
+ ?? `postcondition failed - the installed Codex build may not know the ${DEFAULT_MODE_REQUEST_USER_INPUT_FEATURE_KEY} flag yet`;
343
+ return jsonResponse({ error: `default_mode_request_user_input toggle failed: ${reason}` }, 502);
344
+ }
345
+ const warnings: string[] = [];
346
+ if (enabled !== before) {
347
+ warnings.push("Applies to new sessions; restart the Codex app or wait out its picker cache to see the change.");
348
+ }
349
+ return jsonResponse({ ok: true, enabled, changed: enabled !== before, warnings });
350
+ }
351
+
302
352
  // Subagent prompt injection model: single native or routed model whose info is
303
353
  // dynamically injected into the v1 proactive prompt, plus an optional reasoning
304
354
  // effort the prompt tells the agent to pass to spawn_agent. GET returns the current
@@ -473,10 +523,10 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
473
523
  config.subagentModels = chosen;
474
524
  const { saveConfigPreservingClaudeCode: save } = await import("../../config");
475
525
  save(config);
476
- await refreshCodexCatalogBestEffort();
526
+ const catalogRefresh = await convergeCodexCatalog();
477
527
  await syncClaudeAgentDefsBestEffort();
478
528
  await autoApplyDesktopBestEffort();
479
- return jsonResponse({ ok: true, applied: chosen });
529
+ return jsonResponse({ ok: true, applied: chosen, catalogRefresh });
480
530
  }
481
531
 
482
532
  // Priority-ordered subagent model fallback chain for quota-aware spawn routing.
@@ -66,7 +66,7 @@ import type { ManagementContext } from "./context";
66
66
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
67
67
 
68
68
  export async function handleComboRoutes(ctx: ManagementContext): Promise<Response | null> {
69
- const { req, url, config, deps, refreshCodexCatalogBestEffort, syncClaudeAgentDefsBestEffort } = ctx;
69
+ const { req, url, config, deps, convergeCodexCatalog, syncClaudeAgentDefsBestEffort } = ctx;
70
70
 
71
71
  if (url.pathname === "/api/combos" && req.method === "GET") {
72
72
  const { comboPublicModelId, getCombo, listComboIds } = await import("../../combos");
@@ -195,9 +195,9 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
195
195
  clearComboSelectionState(renameFrom);
196
196
  clearComboTargetCooldowns(renameFrom);
197
197
  }
198
- await refreshCodexCatalogBestEffort();
198
+ const catalogRefresh = await convergeCodexCatalog();
199
199
  if (shouldSyncClaudeAgentDefs) await syncClaudeAgentDefsBestEffort();
200
- return jsonResponse({ success: true, id, model: newPublicModel, combo: normalized });
200
+ return jsonResponse({ success: true, id, model: newPublicModel, combo: normalized, catalogRefresh });
201
201
  }
202
202
 
203
203
  if (url.pathname === "/api/combos" && req.method === "DELETE") {
@@ -213,8 +213,8 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
213
213
  reconcileLiveStateStores();
214
214
  clearComboSelectionState(id);
215
215
  clearComboTargetCooldowns(id);
216
- await refreshCodexCatalogBestEffort();
217
- return jsonResponse({ success: true, id });
216
+ const catalogRefresh = await convergeCodexCatalog();
217
+ return jsonResponse({ success: true, id, catalogRefresh });
218
218
  }
219
219
  return null;
220
220
  }
@@ -74,7 +74,7 @@ import type { ManagementContext } from "./context";
74
74
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
75
75
 
76
76
  export async function handleConfigRoutes(ctx: ManagementContext): Promise<Response | null> {
77
- const { req, url, config, deps, refreshCodexCatalogBestEffort, syncClaudeAgentDefsBestEffort } = ctx;
77
+ const { req, url, config, deps, syncClaudeAgentDefsBestEffort } = ctx;
78
78
  if (url.pathname === "/api/config" && req.method === "GET") {
79
79
  return jsonResponse(safeConfigDTO(config));
80
80
  }
@@ -1,9 +1,16 @@
1
1
  import type { OcxConfig } from "../../types";
2
+ import type { NativeProfileApiDeps } from "../../codex/native-profile-api";
2
3
  import type { StartupInstallAction } from "../startup-action-control";
4
+ import type { ManagementPrincipal } from "../management-auth";
5
+ import type { CatalogModel } from "../../codex/catalog";
6
+ import type { injectGrokConfig } from "../../grok/inject";
7
+ import type { RuntimePortState } from "../../config";
8
+ import type { CatalogDisposition, ConvergeCodex } from "../../codex/convergence-types";
3
9
 
4
10
  export interface ManagementApiDeps {
5
11
  toggleCodexMultiAgentV2?: (enabled: boolean) => void;
6
- refreshCodexCatalog?: () => Promise<void>;
12
+ toggleDefaultModeRequestUserInput?: (enabled: boolean) => void;
13
+ createManagementConvergeCodex?: (config: Readonly<OcxConfig>) => ConvergeCodex;
7
14
  /**
8
15
  * Persistence seam for route-level tests. Production leaves this unset and uses
9
16
  * `saveConfigPreservingClaudeCode`; tests that pass an in-memory fixture config
@@ -11,6 +18,25 @@ export interface ManagementApiDeps {
11
18
  * OPENCODEX_HOME (incident: devlog 260730.../070).
12
19
  */
13
20
  saveConfigPreservingClaudeCode?: (config: OcxConfig) => void;
21
+ /**
22
+ * Catalog seam for the Grok toggle (WP2, devlog 260803_integrations_toggle_all
23
+ * Rev 3 N2). Production leaves this unset and the route dynamic-imports the
24
+ * real one — a static import would close a cycle with management-api.ts.
25
+ * Tests stub it to orphan the fixture file mid-fetch (the r7 recheck test).
26
+ */
27
+ fetchAllModels?: (config: OcxConfig) => Promise<CatalogModel[]>;
28
+ /**
29
+ * Writer seam for the Grok toggle: lets a test place the file in any state
30
+ * between the pre-write recheck and the write itself (the r8 post-inspection
31
+ * tests). Production leaves this unset and uses the real writer.
32
+ */
33
+ injectGrokConfig?: typeof injectGrokConfig;
34
+ /**
35
+ * Runtime-state seam: the fence must name the host/port the RUNNING process
36
+ * bound (agent-settings-routes.ts:99-103 pattern), and a test must not depend
37
+ * on the developer's real runtime state file.
38
+ */
39
+ readRuntimePort?: (pid: number) => RuntimePortState | null;
14
40
  clearThreadAccountMap?: () => void;
15
41
  clearProviderQuotaCache?: () => void;
16
42
  primeCodexPoolQuotas?: (config: OcxConfig, reason: string) => Promise<void> | void;
@@ -18,6 +44,11 @@ export interface ManagementApiDeps {
18
44
  action: StartupInstallAction,
19
45
  options?: { repair?: boolean },
20
46
  ) => Promise<{ message: string }>;
47
+ /**
48
+ * Native-main profile persistence seam for server-boundary tests. Production
49
+ * leaves this unset, so the route creates its normal NativeProfileManager.
50
+ */
51
+ nativeProfileApi?: NativeProfileApiDeps;
21
52
  }
22
53
 
23
54
 
@@ -26,6 +57,15 @@ export interface ManagementContext {
26
57
  url: URL;
27
58
  config: OcxConfig;
28
59
  deps: ManagementApiDeps;
29
- refreshCodexCatalogBestEffort: () => Promise<void>;
60
+ /**
61
+ * Which credential authorized this request, resolved by the auth gate before
62
+ * dispatch. Routes that spend the USER's identity (not just the proxy's) must
63
+ * branch on this instead of on request headers: the admin token is readable by
64
+ * anything running as the user, so a token holder can forge any header a route
65
+ * might otherwise treat as browser evidence. Undefined only in direct-dispatch
66
+ * tests, which are treated as the untrusted `admin-token` case.
67
+ */
68
+ principal?: ManagementPrincipal;
69
+ convergeCodexCatalog: () => Promise<CatalogDisposition>;
30
70
  syncClaudeAgentDefsBestEffort: () => Promise<void>;
31
71
  }