@bitkyc08/opencodex 2.77.0 → 2.78.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 (222) hide show
  1. package/bin/ocx.mjs +11 -10
  2. package/gui/dist/assets/App-BtfG5id-.js +54 -0
  3. package/gui/dist/assets/App-lCIslZ1L.css +1 -0
  4. package/gui/dist/assets/{Tray-gloIYNaW.js → Tray-DiJpSub0.js} +1 -1
  5. package/gui/dist/assets/index-Btc3Ogw4.js +94 -0
  6. package/gui/dist/assets/index-DmQB8D_R.css +1 -0
  7. package/gui/dist/assets/quota-summary-B_a96nXj.js +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic/beta-allowlist.ts +19 -2
  11. package/src/adapters/anthropic/client-identity.ts +13 -4
  12. package/src/adapters/anthropic/native-client-preamble.ts +31 -0
  13. package/src/adapters/anthropic/passthrough.ts +103 -25
  14. package/src/adapters/devin/cloud-direct/chat.ts +7 -2
  15. package/src/adapters/devin/trajectory.ts +49 -0
  16. package/src/adapters/devin.ts +13 -0
  17. package/src/adapters/ollama-native.ts +35 -19
  18. package/src/claude/intercept/client-hello.ts +103 -0
  19. package/src/claude/intercept/model-bindings.ts +3 -2
  20. package/src/claude/intercept/picker-listener.ts +279 -66
  21. package/src/cli/access-audio-input.ts +84 -0
  22. package/src/cli/access-audio-live.ts +99 -0
  23. package/src/cli/access-audio.ts +123 -0
  24. package/src/cli/access-data-client.ts +127 -0
  25. package/src/cli/access-data-plane.ts +104 -0
  26. package/src/cli/access-data-response.ts +76 -0
  27. package/src/cli/access.ts +65 -2
  28. package/src/cli/account-api.ts +30 -6
  29. package/src/cli/account-auth.ts +111 -42
  30. package/src/cli/account-key-quota.ts +118 -0
  31. package/src/cli/account-main.ts +11 -4
  32. package/src/cli/account-next-actions.ts +63 -0
  33. package/src/cli/account-policy-dto.ts +68 -0
  34. package/src/cli/account-policy.ts +167 -0
  35. package/src/cli/account-target.ts +8 -0
  36. package/src/cli/account.ts +34 -8
  37. package/src/cli/agent-runtime-settings.ts +130 -0
  38. package/src/cli/agent-settings.ts +101 -0
  39. package/src/cli/agent.ts +19 -2
  40. package/src/cli/alias.ts +7 -2
  41. package/src/cli/aside-profiles.ts +2 -2
  42. package/src/cli/capabilities-access-remote.ts +147 -0
  43. package/src/cli/capabilities-accounts.ts +195 -0
  44. package/src/cli/capabilities-agents-routing.ts +414 -0
  45. package/src/cli/capabilities-base.ts +783 -0
  46. package/src/cli/capabilities-command.ts +1 -0
  47. package/src/cli/capabilities-integrations.ts +264 -0
  48. package/src/cli/capabilities-lab.ts +214 -0
  49. package/src/cli/capabilities-observe-system.ts +662 -0
  50. package/src/cli/capabilities-provider-models.ts +302 -0
  51. package/src/cli/capabilities.ts +30 -1199
  52. package/src/cli/capability-types.ts +54 -0
  53. package/src/cli/catalog-command-result.ts +57 -0
  54. package/src/cli/chatgpt-command.ts +25 -2
  55. package/src/cli/claude-agent-startup-sync.ts +17 -0
  56. package/src/cli/claude-desktop-profile.ts +100 -0
  57. package/src/cli/claude-desktop.ts +39 -6
  58. package/src/cli/codex-shim-autorestore.ts +2 -1
  59. package/src/cli/codex-shim-readiness.ts +23 -3
  60. package/src/cli/combo-input.ts +88 -0
  61. package/src/cli/combo-result.ts +52 -0
  62. package/src/cli/combo-stats.ts +123 -0
  63. package/src/cli/combo.ts +37 -14
  64. package/src/cli/companion-timeline.ts +142 -0
  65. package/src/cli/companion-usage.ts +146 -0
  66. package/src/cli/companion.ts +12 -3
  67. package/src/cli/config-command.ts +24 -8
  68. package/src/cli/dispatch.ts +52 -79
  69. package/src/cli/doctor.ts +22 -4
  70. package/src/cli/help-catalog.ts +31 -14
  71. package/src/cli/help-recovery.ts +2 -2
  72. package/src/cli/help.ts +25 -11
  73. package/src/cli/index.ts +38 -27
  74. package/src/cli/injection-follow.ts +35 -0
  75. package/src/cli/inspect.ts +5 -0
  76. package/src/cli/integration-aside-sync.ts +69 -0
  77. package/src/cli/integration-cursor.ts +106 -0
  78. package/src/cli/integration-input.ts +60 -0
  79. package/src/cli/integration-journal.ts +49 -0
  80. package/src/cli/integration-plan-dto.ts +156 -0
  81. package/src/cli/integration-preview.ts +215 -0
  82. package/src/cli/integrations.ts +24 -18
  83. package/src/cli/json-input.ts +134 -0
  84. package/src/cli/link.ts +32 -4
  85. package/src/cli/local-sync-result.ts +41 -0
  86. package/src/cli/log-follow.ts +84 -0
  87. package/src/cli/log-view-filter.ts +146 -0
  88. package/src/cli/logout-command.ts +44 -0
  89. package/src/cli/model-picker-ordering.ts +137 -0
  90. package/src/cli/models-custom-input.ts +107 -0
  91. package/src/cli/models-custom-runtime.ts +112 -0
  92. package/src/cli/models-order.ts +194 -0
  93. package/src/cli/models-runtime-subcommands.ts +2 -0
  94. package/src/cli/models-runtime.ts +7 -0
  95. package/src/cli/models.ts +66 -81
  96. package/src/cli/observe-stream.ts +142 -0
  97. package/src/cli/observe.ts +75 -34
  98. package/src/cli/provider-batch.ts +77 -0
  99. package/src/cli/provider-lifecycle-runtime.ts +170 -0
  100. package/src/cli/provider-result.ts +124 -0
  101. package/src/cli/provider-runtime.ts +15 -12
  102. package/src/cli/provider-settings.ts +143 -0
  103. package/src/cli/provider.ts +112 -34
  104. package/src/cli/registry.ts +62 -20
  105. package/src/cli/remote-workspace-hub.ts +83 -0
  106. package/src/cli/remote-workspace.ts +6 -1
  107. package/src/cli/restart-failure.ts +27 -0
  108. package/src/cli/root.ts +12 -0
  109. package/src/cli/route-policy-write.ts +137 -0
  110. package/src/cli/route-policy.ts +8 -0
  111. package/src/cli/runtime-api.ts +56 -79
  112. package/src/cli/secret-args.ts +149 -0
  113. package/src/cli/settings-result.ts +30 -0
  114. package/src/cli/storage.ts +77 -7
  115. package/src/cli/system-command.ts +60 -2
  116. package/src/cli/system-restart-client.ts +20 -3
  117. package/src/cli/system-settings-parity.ts +83 -0
  118. package/src/cli/update-restart-candidate.ts +21 -0
  119. package/src/cli/update-restart-child.ts +81 -0
  120. package/src/cli/update-restart-home.ts +38 -0
  121. package/src/cli/update-restart-transport.ts +193 -0
  122. package/src/cli/update-restart.ts +260 -0
  123. package/src/cli/usage-model-search.ts +58 -0
  124. package/src/cli/usage-report.ts +13 -5
  125. package/src/cli/v2-input.ts +66 -0
  126. package/src/cli/v2-local-output.ts +37 -0
  127. package/src/cli/v2-runtime.ts +90 -0
  128. package/src/cli/v2.ts +102 -144
  129. package/src/clients/config-export/dsh.ts +21 -0
  130. package/src/clients/config-export.ts +7 -1
  131. package/src/codex/catalog/build-entries.ts +1 -1
  132. package/src/codex/catalog/bundled.ts +9 -2
  133. package/src/codex/catalog/heal-observation.ts +66 -0
  134. package/src/codex/catalog/publication-observer.ts +19 -0
  135. package/src/codex/catalog/remote.ts +13 -15
  136. package/src/codex/catalog/restore.ts +19 -6
  137. package/src/codex/catalog/retained-sync.ts +58 -10
  138. package/src/codex/catalog/routed-removal.ts +116 -0
  139. package/src/codex/catalog/write-audit-contract.ts +21 -0
  140. package/src/codex/catalog/write-audit.ts +171 -0
  141. package/src/codex/catalog-self-heal.ts +284 -0
  142. package/src/codex/catalog-write-serialization.ts +71 -7
  143. package/src/codex/codex-home-owner.ts +124 -0
  144. package/src/codex/convergence.ts +55 -15
  145. package/src/codex/features.ts +8 -1
  146. package/src/codex/inject/restore.ts +46 -4
  147. package/src/codex/inject.ts +11 -1
  148. package/src/codex/internal/catalog-writer.ts +120 -13
  149. package/src/codex/journal.ts +49 -13
  150. package/src/codex/management-convergence.ts +3 -0
  151. package/src/codex/quota-types.ts +3 -0
  152. package/src/codex/quota.ts +13 -3
  153. package/src/codex/refresh.ts +20 -9
  154. package/src/codex/runtime.ts +7 -1
  155. package/src/codex/shim-diagnostics.ts +80 -0
  156. package/src/codex/shim-fingerprint.ts +7 -2
  157. package/src/codex/shim-migration.ts +153 -0
  158. package/src/codex/shim-overlay.ts +509 -0
  159. package/src/codex/shim-path-resolution.ts +14 -2
  160. package/src/codex/shim-probe.ts +4 -3
  161. package/src/codex/shim-state-file.ts +93 -7
  162. package/src/codex/shim.ts +15 -68
  163. package/src/codex/sync.ts +24 -4
  164. package/src/combos/failover.ts +19 -0
  165. package/src/combos/index.ts +1 -0
  166. package/src/combos/resolve.ts +12 -0
  167. package/src/config/atomic-write.ts +13 -8
  168. package/src/config/diagnostics.ts +19 -1
  169. package/src/config/load-degrade.ts +3 -0
  170. package/src/config/schema/chatgpt-desktop.ts +22 -0
  171. package/src/config/schema/config-schema.ts +32 -14
  172. package/src/config/schema/leaf-validators.ts +3 -2
  173. package/src/generated/compatibility-version.json +445 -129
  174. package/src/integrations/merge.ts +13 -5
  175. package/src/integrations/mutation-plan.ts +46 -3
  176. package/src/integrations/omp-yaml-source.ts +181 -1
  177. package/src/integrations/ownership-policy.ts +66 -0
  178. package/src/integrations/registry.ts +50 -1
  179. package/src/integrations/state.ts +46 -21
  180. package/src/integrations/target.ts +25 -2
  181. package/src/integrations/writer.ts +89 -31
  182. package/src/lib/package-tree-integrity.ts +42 -1
  183. package/src/lib/proxy-env.ts +3 -0
  184. package/src/lib/test-home-guard.ts +2 -1
  185. package/src/oauth/anthropic-account-refusal.ts +19 -8
  186. package/src/oauth/anthropic-routing.ts +17 -12
  187. package/src/oauth/anthropic.ts +27 -2
  188. package/src/oauth/index.ts +6 -8
  189. package/src/oauth/pool-settings-capability.ts +9 -2
  190. package/src/protocols/plan-snapshot.ts +1 -1
  191. package/src/protocols/settings.ts +28 -4
  192. package/src/server/caller-session-identity.ts +32 -0
  193. package/src/server/claude-messages.ts +11 -4
  194. package/src/server/index/serve-options.ts +12 -7
  195. package/src/server/index.ts +1 -1
  196. package/src/server/management/agent-settings-routes.ts +6 -5
  197. package/src/server/management/context.ts +8 -1
  198. package/src/server/management/cursor-integration-routes.ts +3 -3
  199. package/src/server/management/integration-routes.ts +5 -5
  200. package/src/server/management/model-routes.ts +3 -3
  201. package/src/server/management/native-integration-routes.ts +2 -2
  202. package/src/server/management/oauth-account-routes.ts +96 -30
  203. package/src/server/management/route-registry.ts +25 -24
  204. package/src/server/messages-native-eligibility.ts +3 -6
  205. package/src/server/messages-native-oauth.ts +82 -35
  206. package/src/server/messages-native.ts +78 -16
  207. package/src/server/responses/adapter-dispatch.ts +53 -28
  208. package/src/server/responses/core-auth.ts +10 -4
  209. package/src/server/responses/core-combo.ts +46 -6
  210. package/src/service/cli.ts +6 -1
  211. package/src/service/ownership-mutation-lease.d.mts +26 -0
  212. package/src/service/ownership-mutation-lease.mjs +76 -1
  213. package/src/types/config.ts +2 -0
  214. package/src/types/provider.ts +10 -10
  215. package/src/update/index.ts +2 -1
  216. package/src/update/update-failure-guidance.d.mts +6 -0
  217. package/src/update/update-failure-guidance.mjs +30 -5
  218. package/gui/dist/assets/App-COdEeqSj.css +0 -1
  219. package/gui/dist/assets/App-DEpCJEP_.js +0 -54
  220. package/gui/dist/assets/index-Cd7ittMv.css +0 -1
  221. package/gui/dist/assets/index-DwJKFJm5.js +0 -94
  222. package/gui/dist/assets/quota-summary-DGcEJU3h.js +0 -1
@@ -1032,18 +1032,16 @@ export async function refreshAnthropicAccountWithLock(
1032
1032
  // OAuthLoginRequiredError. One 503 locked the account out of refresh until manual
1033
1033
  // re-auth even after upstream recovered.
1034
1034
  //
1035
- // Only clear the intent when the server DEFINITIVELY answered and rejected the
1036
- // request. The adapter attaches an HTTP status only to that explicit non-success
1037
- // response. A timeout, a dropped connection, or an unreadable/unparseable body
1038
- // carries no status: the server may already have
1039
- // rotated the token, and replaying it could trip refresh-token-reuse revocation.
1040
- // Those outcomes keep the intent so the guard still refuses a blind replay.
1041
- if ((!refreshMayHaveReachedProvider || definitivelyAnswered(error)) && attemptIntent) {
1035
+ // Clear only for proven pre-dispatch failure or an explicit HTTP rejection.
1036
+ // Other transport/body failures may follow rotation and keep the replay guard.
1037
+ const requestNotSent = !refreshMayHaveReachedProvider
1038
+ || (error instanceof AnthropicTokenError && error.requestNotSent);
1039
+ if ((requestNotSent || definitivelyAnswered(error)) && attemptIntent) {
1042
1040
  await clearAnthropicRefreshIntentForKnownFailure(
1043
1041
  provider,
1044
1042
  accountId,
1045
1043
  attemptIntent,
1046
- refreshMayHaveReachedProvider ? "definitive-rejection" : "pre-dispatch",
1044
+ requestNotSent ? "pre-dispatch" : "definitive-rejection",
1047
1045
  error,
1048
1046
  );
1049
1047
  }
@@ -50,7 +50,7 @@ export function parseGenericStickyLimit(value: unknown): number | null {
50
50
 
51
51
  /** Fields the unified pool-settings contract can carry, per kind. */
52
52
  export const POOL_SETTINGS_FIELDS = [
53
- "enabled", "strategy", "stickyLimit", "autoSwitchThreshold", "quotaWindow", "maxConcurrentPerAccount", "routes",
53
+ "enabled", "strategy", "stickyLimit", "autoSwitchThreshold", "quotaWindow", "maxConcurrentPerAccount", "routes", "nativeMessages",
54
54
  ] as const;
55
55
  export type PoolSettingsField = typeof POOL_SETTINGS_FIELDS[number];
56
56
 
@@ -84,8 +84,12 @@ export interface PoolSettingsDto {
84
84
  quotaWindow: string | null;
85
85
  maxConcurrentPerAccount: number | null;
86
86
  routes: AnthropicModelRoute[] | null;
87
+ /** Anthropic preference, default true; null for unsupported pool kinds. */
88
+ nativeMessages: boolean | null;
87
89
  /** Present only when stored Anthropic routes fail validation on read. */
88
90
  routesError?: string;
91
+ /** Saved state confirmed despite a post-publication failure. */
92
+ warning?: "config_bookkeeping_failed";
89
93
  }
90
94
 
91
95
 
@@ -134,7 +138,7 @@ export function genericPoolSettingsDto(
134
138
  /** Which fields each kind actually honours. Declared, never silently omitted. */
135
139
  const SUPPORTED_BY_KIND: Record<PoolSettingsKind, PoolSettingsField[]> = {
136
140
  codex: ["strategy", "stickyLimit", "autoSwitchThreshold"],
137
- anthropic: ["enabled", "strategy", "stickyLimit", "autoSwitchThreshold", "quotaWindow", "routes"],
141
+ anthropic: ["enabled", "strategy", "stickyLimit", "autoSwitchThreshold", "quotaWindow", "routes", "nativeMessages"],
138
142
  generic: ["enabled", "strategy", "stickyLimit", "autoSwitchThreshold"],
139
143
  };
140
144
 
@@ -165,6 +169,7 @@ export function unifiedPoolSettingsDto(
165
169
  quotaWindow: null,
166
170
  maxConcurrentPerAccount: null,
167
171
  routes: null,
172
+ nativeMessages: null,
168
173
  };
169
174
  }
170
175
  if (kind === "anthropic") {
@@ -174,6 +179,7 @@ export function unifiedPoolSettingsDto(
174
179
  ...base,
175
180
  enabled,
176
181
  enabledEffective: enabled === true,
182
+ nativeMessages: !Object.hasOwn(pool, "nativeMessages") || pool.nativeMessages === true,
177
183
  strategy: parseGenericPoolStrategy(pool.strategy) ?? "quota",
178
184
  stickyLimit: parseGenericStickyLimit(pool.stickyLimit) ?? 1,
179
185
  autoSwitchThreshold: parseGenericAutoSwitchThreshold(pool.autoSwitchThreshold) ?? 80,
@@ -198,5 +204,6 @@ export function unifiedPoolSettingsDto(
198
204
  quotaWindow: null,
199
205
  maxConcurrentPerAccount: provider === "kiro" ? parseKiroAccountCap(failover.maxConcurrentPerAccount) : null,
200
206
  routes: null,
207
+ nativeMessages: null,
201
208
  };
202
209
  }
@@ -138,7 +138,7 @@ function candidateFor(
138
138
  const reason = rows.effortRow ? "effort-row" : nativeChatDeclineReason(settled, chatBodyForFeatures(features), config);
139
139
  nativeEligible = reason === undefined;
140
140
  if (reason) declineReasons = [reason];
141
- } else if (inbound === "messages" && resolveProtocolSettings(config).rollout.managedMessagesNative) {
141
+ } else if (inbound === "messages" && resolveProtocolSettings(config, settled.providerName).rollout.managedMessagesNative) {
142
142
  // The runtime rule itself. With the switch off nothing is judged, so the default preview
143
143
  // is exactly what it was before the managed native lane existed.
144
144
  // Pinned effort is judged from config and the route; blocked-skill elision and the web-search
@@ -71,19 +71,29 @@ export interface ProtocolSettings {
71
71
  }
72
72
 
73
73
  /** Resolve protocol policy with conservative defaults for every absent or malformed field. */
74
- export function resolveProtocolSettings(config: Pick<OcxConfig, "protocols">): Readonly<ProtocolSettings> {
74
+ export function resolveProtocolSettings(config: Pick<OcxConfig, "protocols" | "anthropicAccountPool">, providerName?: string): Readonly<ProtocolSettings> {
75
75
  const raw: unknown = config.protocols;
76
76
  const protocols = isRec(raw) ? raw : {};
77
77
  const rollout = isRec(protocols.rollout) ? protocols.rollout : {};
78
78
  const on = (key: keyof ProtocolRolloutSettings): boolean => rollout[key] === true;
79
- const managedMessagesNative = on("managedMessagesNative");
79
+ // A present malformed container cannot become an absent/default-on policy.
80
+ const validContainers = (raw === undefined || isRec(raw))
81
+ && (!Object.hasOwn(protocols, "rollout") || isRec(protocols.rollout));
82
+ const rawPool: unknown = config.anthropicAccountPool;
83
+ const pool = isRec(rawPool) ? rawPool : {};
84
+ const pooled = providerName === "anthropic" && pool.enabled === true;
85
+ const poolPreference = !Object.hasOwn(pool, "nativeMessages") || pool.nativeMessages === true;
86
+ const nativeOn = (key: "managedMessagesNative" | "managedMessagesNativeOAuth") => validContainers
87
+ && (!pooled || poolPreference)
88
+ && (Object.hasOwn(rollout, key) ? rollout[key] === true : pooled);
89
+ const managedMessagesNative = nativeOn("managedMessagesNative");
80
90
  return Object.freeze({
81
91
  unrepresentable: protocols.unrepresentable === "reject" ? "reject" : "legacy",
82
92
  rollout: Object.freeze({
83
93
  nativeChatCombos: on("nativeChatCombos"),
84
94
  managedMessagesNative,
85
95
  // OAuth extension is meaningless without the key-auth path it extends.
86
- managedMessagesNativeOAuth: managedMessagesNative && on("managedMessagesNativeOAuth"),
96
+ managedMessagesNativeOAuth: managedMessagesNative && nativeOn("managedMessagesNativeOAuth"),
87
97
  directEncoders: on("directEncoders"),
88
98
  shadowPlan: on("shadowPlan"),
89
99
  }),
@@ -95,16 +105,30 @@ export function resolveProtocolSettings(config: Pick<OcxConfig, "protocols">): R
95
105
  * dashboard can tell a preview computed under an older policy from a current one. Not a
96
106
  * security boundary; FNV-1a over a canonical JSON projection.
97
107
  */
98
- export function protocolPolicyRevision(config: Pick<OcxConfig, "apiSurfaces" | "claudeCode" | "protocols">): string {
108
+ export function protocolPolicyRevision(config: Pick<OcxConfig, "apiSurfaces" | "claudeCode" | "protocols" | "anthropicAccountPool">): string {
99
109
  const surfaces = resolveApiSurfaceSettings(config);
100
110
  const settings = resolveProtocolSettings(config);
111
+ const anthropicSettings = resolveProtocolSettings(config, "anthropic");
112
+ const state = (record: unknown, key: string) => !isRec(record) || !Object.hasOwn(record, key)
113
+ ? "absent" : record[key] === true ? "true" : record[key] === false ? "false" : "invalid";
114
+ const container = (value: unknown) => value === undefined ? "absent" : isRec(value) ? "object" : "invalid";
115
+ const rawProtocols: unknown = config.protocols;
116
+ const rawRollout = isRec(rawProtocols) ? rawProtocols.rollout : undefined;
101
117
  const canonical = JSON.stringify({
102
118
  messages: [surfaces.messages.enabled, surfaces.messages.source],
103
119
  unrepresentable: settings.unrepresentable,
120
+ nativeInputs: [
121
+ container(rawProtocols), container(rawRollout),
122
+ state(rawRollout, "managedMessagesNative"), state(rawRollout, "managedMessagesNativeOAuth"),
123
+ container(config.anthropicAccountPool), state(config.anthropicAccountPool, "enabled"),
124
+ state(config.anthropicAccountPool, "nativeMessages"),
125
+ ],
104
126
  rollout: [
105
127
  settings.rollout.nativeChatCombos,
106
128
  settings.rollout.managedMessagesNative,
107
129
  settings.rollout.managedMessagesNativeOAuth,
130
+ anthropicSettings.rollout.managedMessagesNative,
131
+ anthropicSettings.rollout.managedMessagesNativeOAuth,
108
132
  settings.rollout.directEncoders,
109
133
  settings.rollout.shadowPlan,
110
134
  ],
@@ -0,0 +1,32 @@
1
+ /** Promote third-party conversation markers after ingress authentication and origin checks. */
2
+ import { contextPrincipalIdOf, type DataPlaneAdmission } from "./auth-cors";
3
+
4
+ const SESSION_HEADERS = ["session_id", "session-id", "thread-id"] as const;
5
+ const SAFE_CALLER_SESSION_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
6
+
7
+ export function safeCallerSessionId(raw: string | null): string | undefined {
8
+ const value = raw?.trim();
9
+ return value && SAFE_CALLER_SESSION_ID.test(value) ? value : undefined;
10
+ }
11
+
12
+ export function callerSessionId(headers: Headers): string | undefined {
13
+ if (SESSION_HEADERS.some(name => headers.has(name))) return undefined;
14
+ return safeCallerSessionId(headers.get("x-session-id"));
15
+ }
16
+
17
+ /** Authenticated callers share continuity only within their trusted credential principal. */
18
+ export function withCallerSessionIdentity(req: Request, admission: DataPlaneAdmission): Request {
19
+ const conversation = callerSessionId(req.headers);
20
+ if (!conversation) return req;
21
+ let sessionId = conversation;
22
+ if (admission.kind !== "loopback") {
23
+ const principal = contextPrincipalIdOf(admission);
24
+ if (!principal) return req;
25
+ sessionId = new Bun.CryptoHasher("sha256")
26
+ .update(JSON.stringify(["opencodex-caller-session-v1", principal, conversation])).digest("hex");
27
+ }
28
+ const headers = new Headers(req.headers);
29
+ headers.set("session_id", sessionId);
30
+ // Preserve the unread body stream and propagate the original request's abort signal.
31
+ return new Request(req, { headers });
32
+ }
@@ -80,7 +80,7 @@ import { upstreamWireForAdapter } from "../protocols/contract";
80
80
  import { createProtocolEnvelope, type ProtocolEnvelope } from "../protocols/envelope";
81
81
  import { featuresFromMessagesBody, type ProtocolFeature } from "../protocols/features";
82
82
  import { credentialDomainFor, messagesBodyHasOpaqueState } from "../protocols/opaque-state";
83
- import { hasAnthropicFailoverQuorum } from "../oauth/anthropic-routing";
83
+ import { hasAnthropicFailoverQuorum, anthropicSessionKeyFromParts } from "../oauth/anthropic-routing";
84
84
  import { checkRepresentable, unrepresentableMessage } from "../protocols/guard";
85
85
  import { requestPathForLane } from "../protocols/path";
86
86
  import { resolveApiSurfaceSettings, resolveProtocolSettings } from "../protocols/settings";
@@ -1208,6 +1208,13 @@ async function handleClaudeMessagesWithBudget(
1208
1208
  // Compatibility identity is an opaque request-local handle, separate from credentials.
1209
1209
  clientIdentity: captureAnthropicClientIdentity(req.headers),
1210
1210
  callerAnthropicBeta: req.headers.get("anthropic-beta"),
1211
+ sessionKey: anthropicSessionKeyFromParts({
1212
+ sessionIdHeader: req.headers.get("session_id")?.trim() || req.headers.get("x-claude-code-session-id"),
1213
+ threadIdHeader: req.headers.get("thread_id"),
1214
+ clientThreadId: conversationIdFromClaudeMetadata(isRec(nativeBody.metadata) ? nativeBody.metadata : undefined),
1215
+ promptCacheKey: typeof internalBody.prompt_cache_key === "string" ? internalBody.prompt_cache_key : null,
1216
+ promptCacheKeyIsSharedCohort: cacheKeySource === "system",
1217
+ }),
1211
1218
  });
1212
1219
  }
1213
1220
 
@@ -1685,9 +1692,9 @@ export async function handleClaudeCountTokens(
1685
1692
  // A thread delta would undercount; refuse it exactly as the translated Messages path does.
1686
1693
  if (carriesMessageThread(raw)) return messageThreadUnsupportedResponse();
1687
1694
  // PF-08: an eligible managed-key route counts the body the native lane would send.
1688
- const nativeCountBody = resolveProtocolSettings(config).rollout.managedMessagesNative
1689
- ? (await import("./messages-native")).nativeMessagesCountBody(config, cc, raw, { fastRow: countFastRow !== null })
1690
- : undefined;
1695
+ const nativeCountBody = (await import("./messages-native")).nativeMessagesCountBody(
1696
+ config, cc, raw, { fastRow: countFastRow !== null }, captureAnthropicClientIdentity(req.headers),
1697
+ );
1691
1698
  // A count answers for the prompt a real turn from this model would forward, so it projects
1692
1699
  // the same unserialized content that turn's `message_start` floor does. Counting the raw
1693
1700
  // caller body instead reported replayed thinking this route never sends (#4857 family).
@@ -203,6 +203,7 @@ import { readyProtocolMetadata } from "../../remote/protocol";
203
203
  import { modelCapabilityFields } from "../models-capabilities";
204
204
  import { createWebsocketHandler } from "./websocket-handler";
205
205
  import { withGrokSessionIdentity } from "../../grok/session-identity";
206
+ import { withCallerSessionIdentity } from "../caller-session-identity";
206
207
 
207
208
  export type ServerIngress = "public" | "unauthenticated-loopback" | "hub-management" | "claude-intercept" | "hub-link";
208
209
 
@@ -298,9 +299,11 @@ export function createServeOptions(ctx: ServeOptionsContext) {
298
299
  const requestMetrics = metricsExportEnabled(config)
299
300
  ? createRequestMetricsOwner(Date.now() / 1000, cachedKiroQuotaMetricRows) : undefined;
300
301
  const requestMetricsLogContext = requestMetrics ? { requestMetricsRecorder: requestMetrics } : {};
301
- const requestManagementApiDeps: ManagementApiDeps = requestMetrics
302
- ? { ...managementApiDeps, requestMetrics: { snapshot: () => requestMetrics.snapshot() } }
303
- : managementApiDeps;
302
+ const requestManagementApiDeps: ManagementApiDeps = {
303
+ ...managementApiDeps,
304
+ liveListenPort: () => ctx.boundPort ?? listenPort,
305
+ ...(requestMetrics ? { requestMetrics: { snapshot: () => requestMetrics.snapshot() } } : {}),
306
+ };
304
307
  const serveOptions = {
305
308
  idleTimeout: 255,
306
309
  // Bun rejects an oversized body before `fetch` runs, so the listener has to be raised
@@ -1474,10 +1477,11 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1474
1477
  logged = true;
1475
1478
  addFinalRequestLog(requestId, start, logCtx, status, meta);
1476
1479
  };
1477
- return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
1480
+ const sessionReq = withCallerSessionIdentity(withGrokSessionIdentity(req), admission);
1481
+ return runAdmittedHttpTurn(sessionReq, policy, async turnAdmissionLease => {
1478
1482
  let response: Response;
1479
1483
  try {
1480
- response = await handleResponses(withGrokSessionIdentity(req), config, logCtx, {
1484
+ response = await handleResponses(sessionReq, config, logCtx, {
1481
1485
  turnAdmissionLease,
1482
1486
  admission,
1483
1487
  onRequestBodyRead: () => disableResponsesRequestTimeout(req, requestServer),
@@ -1554,8 +1558,9 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1554
1558
  // Logging is finalized inside handleClaudeMessages (Responses-vocab tap on the
1555
1559
  // pre-translation stream + native passthrough callbacks) — do not re-wrap the
1556
1560
  // translated Anthropic stream here.
1557
- return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => withCors(
1558
- await handleClaudeMessages(req, config, logCtx, { requestId, start, turnAdmissionLease, admission }, policy, { claudeIntercept: ingress === "claude-intercept" }),
1561
+ const sessionReq = withCallerSessionIdentity(req, admission);
1562
+ return runAdmittedHttpTurn(sessionReq, policy, async turnAdmissionLease => withCors(
1563
+ await handleClaudeMessages(sessionReq, config, logCtx, { requestId, start, turnAdmissionLease, admission }, policy, { claudeIntercept: ingress === "claude-intercept" }),
1559
1564
  req,
1560
1565
  policy,
1561
1566
  ), { requestId, start, logCtx });
@@ -281,7 +281,7 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
281
281
  // with the later startup sync and warns ONCE about stale app-servers; warning
282
282
  // here instead would read a catalog mtime the sync is about to move.
283
283
  const outcome = withCatalogWriteSerialization(startupCodexHome, permit =>
284
- invalidateCodexModelsCacheWithPermit(permit, startupCodexHome));
284
+ invalidateCodexModelsCacheWithPermit(permit, startupCodexHome), { intent: "cache", writer: "startup-cache" });
285
285
  // A refused permit is not a write; only a completed run that returned true is.
286
286
  setStartupCacheInvalidationWrite(outcome.kind === "completed" && outcome.value === true);
287
287
  } catch { /* no readable Codex home: nothing to invalidate */ }
@@ -205,7 +205,7 @@ export function setGrokApplyFlightTestHooks(
205
205
  grokApplyFlight = null;
206
206
  grokApplyHighWaterBytes = 0;
207
207
  }
208
- import type { ManagementContext } from "./context";
208
+ import { managementInferencePort, type ManagementContext } from "./context";
209
209
 
210
210
  async function injectionModelOptions(config: OcxConfig, models: readonly CatalogModel[]) {
211
211
  const disabled = new Set(config.disabledModels ?? []);
@@ -262,7 +262,8 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
262
262
  const writtenProfile = current.claudeCode.desktopProfile;
263
263
  const markerBaseline = captureDesktopAppliedMarker(writtenProfile);
264
264
  const result = (deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(
265
- current.port ?? 10100,
265
+ // Live bound port, not config: a CLI override or ephemeral bind must not strand Desktop (#6598).
266
+ managementInferencePort({ config: current, deps }) ?? 10100,
266
267
  [...desktopVisibleNativeSlugs(current)],
267
268
  routed,
268
269
  current.apiKeys?.[0]?.key,
@@ -898,7 +899,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
898
899
  if (url.pathname === "/api/claude-desktop" && req.method === "GET") {
899
900
  try {
900
901
  const state = await buildClaudeDesktopState(config);
901
- const runtimePort = Number(url.port) || config.port;
902
+ const runtimePort = managementInferencePort(ctx);
902
903
  return jsonResponse({ ...state, port: runtimePort });
903
904
  } catch (error) {
904
905
  return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 400);
@@ -966,7 +967,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
966
967
  };
967
968
  }
968
969
  const saved = await buildClaudeDesktopState(config);
969
- const runtimePort = Number(url.port) || config.port;
970
+ const runtimePort = managementInferencePort(ctx);
970
971
  return jsonResponse({ ok: true, ...saved, port: runtimePort });
971
972
  } catch (error) {
972
973
  return jsonResponse({ error: error instanceof Error ? error.message : String(error) }, 400);
@@ -1131,7 +1132,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1131
1132
  // Picker mode belongs to first-party: stop terminating claude.ai before the gateway lands.
1132
1133
  const pickerOff = await ops.disableLocked({ persist: false });
1133
1134
  const result = (deps.writeDesktop3pConfig ?? writeDesktop3pConfig)(
1134
- Number(url.port) || latest.port,
1135
+ managementInferencePort({ config: latest, deps }),
1135
1136
  [...desktopVisibleNativeSlugs(latest)],
1136
1137
  routed,
1137
1138
  latest.apiKeys?.[0]?.key,
@@ -160,7 +160,7 @@ export interface ManagementApiDeps {
160
160
  issueApiKey?: (config: OcxConfig, name: string) => IssuedApiKey;
161
161
  revokeApiKey?: (config: OcxConfig, id: string) => boolean;
162
162
  loadLinkCandidates?: () => Array<{ alias: string; source: "ssh_config" | "tailscale" }>;
163
- /** The port this runtime listens on; a join is refused unless it is the configured port. */
163
+ /** Bound public inference port, not the ingress receiving this management request. */
164
164
  liveListenPort?: () => number | undefined;
165
165
  now?: () => number;
166
166
  }
@@ -191,3 +191,10 @@ export interface ManagementContext {
191
191
  convergeCodexCatalog: () => Promise<CatalogDisposition>;
192
192
  syncClaudeAgentDefsBestEffort: () => Promise<void>;
193
193
  }
194
+
195
+ /** A management-only ingress cannot supply the port used by generated inference clients. */
196
+ export function managementInferencePort(ctx: Pick<ManagementContext, "config" | "deps">): number {
197
+ // Lifecycle owns the actual port (including CLI overrides/ephemeral binds); direct route
198
+ // fixtures fall back to config without consulting another runtime's state file.
199
+ return ctx.deps.liveListenPort?.() ?? ctx.config.port;
200
+ }
@@ -53,9 +53,9 @@ export async function buildCursorIntegrationStatus(
53
53
  const privateInference = pick(installs, "private-inference");
54
54
  const regular = pick(installs, "regular");
55
55
  const runtime = (deps.readRuntimePort ?? readRuntimePort)(process.pid);
56
- // The port the browser reached is the one Cursor on the same machine will reach too; the
57
- // runtime record and config.port are fallbacks for a request that carries no port.
58
- const port = runtime?.port ?? (Number(ctx.url?.port) || config.port);
56
+ // The lifecycle-bound public port wins, then the PID-matched runtime record, then config.
57
+ // A management-only ingress port must never become the Cursor gateway (#6598).
58
+ const port = deps.liveListenPort?.() ?? runtime?.port ?? config.port;
59
59
  // Cursor runs on this machine, so the gateway URL it is told to paste is the LOCAL one: the
60
60
  // unauthenticated loopback listener when one is enabled, and otherwise the bind address on the
61
61
  // public port — 127.0.0.1 for a loopback or wildcard bind exactly as before, and the tailnet
@@ -46,7 +46,7 @@ import {
46
46
  } from "../../integrations/mutation-flight";
47
47
  import { jsonResponse } from "../auth-cors";
48
48
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
49
- import type { ManagementContext } from "./context";
49
+ import { managementInferencePort, type ManagementContext } from "./context";
50
50
  import { exportSnapshotIdentity, loadExportModels, previewExportSnapshot } from "./model-rows";
51
51
  import {
52
52
  previewIntegration,
@@ -230,7 +230,7 @@ function asideOptions(ctx: ManagementContext): AsideProfileRouteOptions {
230
230
  return {
231
231
  input: () => input ??= {
232
232
  config: ctx.config,
233
- port: Number(ctx.url.port) || ctx.config.port,
233
+ port: managementInferencePort(ctx),
234
234
  models: () => loadExportModels(ctx.config),
235
235
  store: integrationStore(),
236
236
  ...pathOverrides(),
@@ -252,7 +252,7 @@ async function buildIntegrationWriteInput(
252
252
  clientId,
253
253
  models: await loadExportModels(ctx.config),
254
254
  config: ctx.config,
255
- port: Number(ctx.url.port) || ctx.config.port,
255
+ port: managementInferencePort(ctx),
256
256
  ...(droidReasoningDefaults === undefined ? {} : { droidReasoningDefaults: { ...droidReasoningDefaults } }),
257
257
  store,
258
258
  io: integrationMutationTestHooks?.io,
@@ -284,7 +284,7 @@ async function buildIntegrationPreviewInput(
284
284
  clientId,
285
285
  models: snapshot.models,
286
286
  config: ctx.config,
287
- port: Number(ctx.url.port) || ctx.config.port,
287
+ port: managementInferencePort(ctx),
288
288
  ...(droidReasoningDefaults === undefined ? {} : { droidReasoningDefaults: { ...droidReasoningDefaults } }),
289
289
  store,
290
290
  io: integrationMutationTestHooks?.io,
@@ -619,7 +619,7 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
619
619
  if (url.pathname === "/api/client-integrations" && req.method === "GET") {
620
620
  try {
621
621
  const models = await loadExportModels(ctx.config);
622
- const port = Number(url.port) || ctx.config.port;
622
+ const port = managementInferencePort(ctx);
623
623
  // One store for the whole collection read: without it this route retried
624
624
  // maintenance and counted snapshots in the default store even when the
625
625
  // caller had bound everything else to a temp root (A-gate round 13).
@@ -167,7 +167,7 @@ import type {
167
167
 
168
168
  import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
169
169
  import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
170
- import type { ManagementContext } from "./context";
170
+ import { managementInferencePort, type ManagementContext } from "./context";
171
171
  import { effectiveModelReasoningEfforts, inheritedModelReasoningEfforts, listManagementModelRows, loadExportModels } from "./model-rows";
172
172
  import { initialModelSelectionPending } from "../../providers/initial-model-selection";
173
173
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
@@ -204,7 +204,7 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
204
204
  ?? (await import("../../integrations/catalog-refresh")).refreshOwnedCatalogIntegrations;
205
205
  const clientIntegrations = await refresh({
206
206
  config,
207
- port: Number(url.port) || config.port,
207
+ port: managementInferencePort(ctx),
208
208
  models: () => loadExportModels(config),
209
209
  });
210
210
  return { catalogRefresh, clientIntegrations };
@@ -562,7 +562,7 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
562
562
  reason: "non_loopback",
563
563
  }, 400, req, config);
564
564
  }
565
- const baseUrl = opencodeProxyBaseUrl(Number(url.port) || config.port, config.hostname, config);
565
+ const baseUrl = opencodeProxyBaseUrl(managementInferencePort(ctx), config.hostname, config);
566
566
  let models: ExportModel[];
567
567
  try {
568
568
  // The ONE loader every export surface uses. It carries the visibility
@@ -51,7 +51,7 @@ import type { CodexNativeRestoreResult } from "../../codex/inject";
51
51
  import type { OcxConfig } from "../../types";
52
52
  import { jsonResponse } from "../auth-cors";
53
53
  import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
54
- import type { ManagementContext } from "./context";
54
+ import { managementInferencePort, type ManagementContext } from "./context";
55
55
 
56
56
  export type NativeIntegrationClientId = "claude" | "grok" | "codex" | "claude-desktop";
57
57
 
@@ -540,7 +540,7 @@ async function handleGrokToggle(ctx: ManagementContext): Promise<Response> {
540
540
  * a stale config.hostname picks the wrong loopback policy branch entirely.
541
541
  */
542
542
  const runtime = (deps.readRuntimePort ?? readRuntimePort)(process.pid);
543
- const port = runtime?.port ?? (Number(ctx.url.port) || config.port);
543
+ const port = deps.liveListenPort?.() ?? runtime?.port ?? managementInferencePort(ctx);
544
544
  const hostname = runtime?.hostname ?? config.hostname;
545
545
 
546
546
  /*