@bitkyc08/opencodex 2.65.0 → 2.66.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 (213) hide show
  1. package/README.md +14 -0
  2. package/gui/dist/assets/App-DyHlFeNB.js +51 -0
  3. package/gui/dist/assets/App-gkoJIEOV.css +1 -0
  4. package/gui/dist/assets/{Tray-D4bA1vff.js → Tray-CI8cVKWu.js} +1 -1
  5. package/gui/dist/assets/index-BiwnVAUm.js +86 -0
  6. package/gui/dist/assets/index-ComVsdSk.css +1 -0
  7. package/gui/dist/assets/{tray-data-BZXhB3qz.js → tray-data-_WyYzbMt.js} +1 -1
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +1 -2
  10. package/src/adapters/anthropic/beta-allowlist.ts +80 -0
  11. package/src/adapters/anthropic/passthrough.ts +221 -0
  12. package/src/adapters/anthropic-image-codec.ts +56 -19
  13. package/src/adapters/anthropic-image-normalize.ts +20 -10
  14. package/src/adapters/anthropic.ts +55 -24
  15. package/src/adapters/coding-agent/protocol.ts +29 -19
  16. package/src/adapters/cursor/live-transport.ts +18 -3
  17. package/src/adapters/cursor/native-exec-shell.ts +18 -63
  18. package/src/adapters/cursor/native-exec.ts +5 -2
  19. package/src/adapters/cursor/native-foreground-shell.ts +44 -0
  20. package/src/adapters/devin/cloud-direct/chat.ts +7 -1
  21. package/src/adapters/exec-tool-result-normalize.ts +4 -1
  22. package/src/adapters/google.ts +12 -0
  23. package/src/adapters/ollama-native.ts +32 -1
  24. package/src/adapters/openai-chat/serialized-tool-call-content.ts +131 -34
  25. package/src/adapters/openai-chat.ts +6 -4
  26. package/src/adapters/openai-responses/passthrough.ts +1 -0
  27. package/src/adapters/openai-responses/reasoning.ts +45 -1
  28. package/src/bridge/response-json.ts +10 -2
  29. package/src/chat/outbound.ts +104 -52
  30. package/src/claude/claude-code-block.ts +16 -0
  31. package/src/claude/context-windows.ts +52 -4
  32. package/src/claude/desktop-first-party.ts +25 -8
  33. package/src/claude/inbound.ts +22 -0
  34. package/src/claude/intercept/connect-proxy.ts +37 -4
  35. package/src/claude/intercept/proxy-auth.ts +166 -0
  36. package/src/claude/intercept/runtime.ts +21 -0
  37. package/src/claude/intercept/settings.ts +33 -12
  38. package/src/claude/outbound.ts +45 -27
  39. package/src/cli/account-extended.ts +47 -15
  40. package/src/cli/account-target.ts +115 -0
  41. package/src/cli/account.ts +20 -28
  42. package/src/cli/agent.ts +10 -1
  43. package/src/cli/api-protocols.ts +206 -0
  44. package/src/cli/capabilities.ts +86 -0
  45. package/src/cli/combo.ts +2 -2
  46. package/src/cli/connect.ts +127 -3
  47. package/src/cli/dispatch.ts +8 -0
  48. package/src/cli/doctor.ts +33 -0
  49. package/src/cli/help.ts +2 -0
  50. package/src/cli/link.ts +288 -0
  51. package/src/cli/registry.ts +23 -0
  52. package/src/cli/runtime-api.ts +79 -0
  53. package/src/cli/system-command.ts +21 -2
  54. package/src/client/connect.ts +125 -23
  55. package/src/client/hub-relay.ts +12 -8
  56. package/src/client/link-join.ts +323 -0
  57. package/src/client/link-relay.ts +241 -0
  58. package/src/client/link-state.ts +110 -0
  59. package/src/client/link-teardown.ts +63 -0
  60. package/src/client/link-tunnel.ts +465 -0
  61. package/src/client/machine-listener.ts +14 -5
  62. package/src/client/runtime.ts +91 -42
  63. package/src/client/state.ts +4 -0
  64. package/src/codex/catalog/build-entries.ts +4 -0
  65. package/src/codex/catalog/derive-entry.ts +5 -0
  66. package/src/codex/catalog/parsing.ts +4 -0
  67. package/src/codex/catalog/retained-sync.ts +4 -1
  68. package/src/codex/desktop-switches.ts +88 -8
  69. package/src/codex/inject/plan.ts +16 -3
  70. package/src/codex/inject.ts +3 -0
  71. package/src/codex/internal/catalog-writer.ts +13 -2
  72. package/src/combos/failover.ts +3 -2
  73. package/src/combos/index.ts +12 -0
  74. package/src/combos/jev.ts +646 -0
  75. package/src/combos/request.ts +5 -0
  76. package/src/combos/resolve.ts +17 -15
  77. package/src/combos/types.ts +47 -3
  78. package/src/config/atomic-write.ts +3 -0
  79. package/src/config/live-reconcile.ts +20 -4
  80. package/src/config/persist-unlocked.ts +20 -7
  81. package/src/config/schema/config-schema.ts +16 -0
  82. package/src/config/schema/leaf-validators.ts +38 -1
  83. package/src/generated/compatibility-version.json +416 -128
  84. package/src/integrations/omp-yaml-source.ts +109 -4
  85. package/src/lab/automation/orchestrator.ts +60 -4
  86. package/src/lab/events/limits.ts +1 -1
  87. package/src/lib/bounded-body.ts +55 -38
  88. package/src/lib/config-ownership.ts +11 -22
  89. package/src/lib/local-desktop-snapshot-capability.ts +56 -0
  90. package/src/lib/local-management-capability.ts +25 -3
  91. package/src/lib/optional-shutdown-hooks.ts +17 -0
  92. package/src/lib/state-store-registrations.ts +2 -2
  93. package/src/lib/windows-elevation.ts +62 -25
  94. package/src/lib/windows-secret-acl.ts +114 -23
  95. package/src/link/admission-wait.ts +73 -0
  96. package/src/link/compensation.ts +100 -0
  97. package/src/link/fingerprint.ts +21 -0
  98. package/src/link/paths.ts +19 -0
  99. package/src/link/ports.ts +12 -0
  100. package/src/link/routes.ts +32 -0
  101. package/src/link/ssh-argv.ts +170 -0
  102. package/src/link/ssh-config.ts +152 -0
  103. package/src/link/ssh-runner.ts +161 -0
  104. package/src/link/status-projection.ts +96 -0
  105. package/src/link/store.ts +139 -0
  106. package/src/link/supervisor.ts +404 -0
  107. package/src/link/tunnel-state.ts +91 -0
  108. package/src/protocols/baseline.ts +88 -0
  109. package/src/protocols/codecs/chat.ts +17 -0
  110. package/src/protocols/codecs/messages.ts +18 -0
  111. package/src/protocols/codecs/responses.ts +17 -0
  112. package/src/protocols/contract.ts +185 -0
  113. package/src/protocols/dto.ts +267 -0
  114. package/src/protocols/encoders/adapter-events.ts +876 -0
  115. package/src/protocols/encoders/chat.ts +243 -0
  116. package/src/protocols/encoders/messages.ts +462 -0
  117. package/src/protocols/envelope.ts +43 -0
  118. package/src/protocols/features.ts +295 -0
  119. package/src/protocols/guard.ts +33 -0
  120. package/src/protocols/opaque-state.ts +132 -0
  121. package/src/protocols/path.ts +39 -0
  122. package/src/protocols/plan-snapshot.ts +226 -0
  123. package/src/protocols/plan.ts +192 -0
  124. package/src/protocols/provider-summary.ts +101 -0
  125. package/src/protocols/settings.ts +118 -0
  126. package/src/protocols/shadow-plan.ts +36 -0
  127. package/src/protocols/shadow.ts +62 -0
  128. package/src/protocols/trace.ts +335 -0
  129. package/src/providers/model-rename-migration.ts +3 -3
  130. package/src/providers/registry/entries-core.ts +6 -1
  131. package/src/providers/registry/entries-extended.ts +15 -0
  132. package/src/providers/registry/model-ids.ts +1 -0
  133. package/src/providers/registry/types.ts +6 -0
  134. package/src/providers/registry.ts +1 -1
  135. package/src/remote-control/workspace-hub.ts +5 -4
  136. package/src/responses/apply-patch-envelope.ts +6 -1
  137. package/src/responses/code-mode-shell-input.ts +4 -2
  138. package/src/responses/freeform-wrapper-scan.ts +51 -24
  139. package/src/responses/progressive-freeform-input.ts +14 -20
  140. package/src/responses/spill-store.ts +187 -33
  141. package/src/responses/state/snapshot-select.ts +49 -0
  142. package/src/responses/state/spill-inspect.ts +46 -0
  143. package/src/responses/state/spill-queue.ts +16 -0
  144. package/src/responses/state.ts +51 -27
  145. package/src/server/audio-client.ts +8 -3
  146. package/src/server/audio-upstream.ts +9 -4
  147. package/src/server/auth-cors.ts +25 -8
  148. package/src/server/chat-completions.ts +71 -11
  149. package/src/server/chat-native-eligibility.ts +74 -0
  150. package/src/server/chat-native.ts +139 -74
  151. package/src/server/claude-messages.ts +140 -22
  152. package/src/server/hub-usage.ts +4 -3
  153. package/src/server/index/link-listener.ts +195 -0
  154. package/src/server/index/optional-listeners.ts +115 -0
  155. package/src/server/index/serve-options.ts +44 -10
  156. package/src/server/index.ts +10 -9
  157. package/src/server/inference/attempt.ts +51 -0
  158. package/src/server/inference/client-encoder-delivery.ts +235 -0
  159. package/src/server/inference/client-wire-log.ts +49 -0
  160. package/src/server/inference/client-wire.ts +69 -0
  161. package/src/server/inference/context.ts +15 -0
  162. package/src/server/inference/final-log.ts +37 -0
  163. package/src/server/local-desktop-snapshot-auth.ts +57 -0
  164. package/src/server/management/agent-settings-routes.ts +15 -13
  165. package/src/server/management/api-access.ts +11 -1
  166. package/src/server/management/config-routes.ts +3 -6
  167. package/src/server/management/context.ts +25 -0
  168. package/src/server/management/link-routes.ts +535 -0
  169. package/src/server/management/logs-usage-routes.ts +31 -1
  170. package/src/server/management/native-integration-routes.ts +6 -11
  171. package/src/server/management/oauth-account-routes.ts +42 -16
  172. package/src/server/management/protocol-routes.ts +159 -0
  173. package/src/server/management/protocol-settings-patch.ts +145 -0
  174. package/src/server/management/provider-patch-transaction.ts +61 -0
  175. package/src/server/management/provider-routes.ts +39 -18
  176. package/src/server/management/route-registry.ts +12 -0
  177. package/src/server/management/sidebar-routes.ts +8 -3
  178. package/src/server/management/system-routes.ts +18 -1
  179. package/src/server/management/usage-aggregate-cache.ts +206 -6
  180. package/src/server/management-api.ts +26 -1
  181. package/src/server/management-auth.ts +19 -5
  182. package/src/server/messages-native-eligibility.ts +161 -0
  183. package/src/server/messages-native-oauth.ts +149 -0
  184. package/src/server/messages-native.ts +790 -0
  185. package/src/server/relay.ts +9 -0
  186. package/src/server/request-log-filter.ts +92 -0
  187. package/src/server/request-log.ts +31 -70
  188. package/src/server/responses/adapter-delivery.ts +43 -16
  189. package/src/server/responses/agent-task-recovery.ts +90 -22
  190. package/src/server/responses/codex-ws-exchange.ts +4 -0
  191. package/src/server/responses/codex-ws-wire.ts +10 -1
  192. package/src/server/responses/core-combo-native.ts +323 -0
  193. package/src/server/responses/core-combo.ts +224 -13
  194. package/src/server/responses/core-options.ts +22 -0
  195. package/src/server/responses/core.ts +14 -26
  196. package/src/server/responses/request-sidecar-auth.ts +8 -3
  197. package/src/server/responses/run-turn-execution.ts +257 -63
  198. package/src/server/responses/sidecar-execution.ts +7 -4
  199. package/src/server/system-env.ts +7 -0
  200. package/src/service/diagnostics.ts +7 -1
  201. package/src/service/launchd.ts +26 -37
  202. package/src/service/windows-ops.ts +24 -20
  203. package/src/types/config.ts +58 -1
  204. package/src/types.ts +2 -0
  205. package/src/update/transactional-install.d.mts +0 -1
  206. package/src/update/transactional-install.mjs +17 -17
  207. package/src/usage/jev-stats.ts +495 -0
  208. package/src/usage/log.ts +24 -3
  209. package/src/web-search/run-turn-loop.ts +568 -0
  210. package/gui/dist/assets/App-8NMiZxT0.css +0 -1
  211. package/gui/dist/assets/App-DGLte6IR.js +0 -50
  212. package/gui/dist/assets/index--EWgGQvZ.css +0 -1
  213. package/gui/dist/assets/index-Bi1K37Wl.js +0 -86
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Adapter delivery straight into a Chat or Messages client's wire (PF-09).
3
+ *
4
+ * The streaming adapter branch normally bridges the guarded event stream into Responses SSE and
5
+ * lets the ingress convert that into the client's wire. With a `clientEncoder` option the same
6
+ * events are encoded directly instead; everything the bridge did besides framing keeps happening:
7
+ *
8
+ * - the events are also collected, charged to the request's translator budget, and folded with
9
+ * `buildResponseJSON` at the terminal, so the replay caches, `rememberResponseState`,
10
+ * `notifyResponseComplete` and `commitReasoningReplayServingRoute` see a completed response
11
+ * exactly when the bridge's `onCompletedResponse` would have fired;
12
+ * - the key-usage binding receives the adapter usage under the bridge's rules;
13
+ * - the stream is tracked for turn lifetime and stops the upstream the same way;
14
+ * - the request log receives the facts its Responses SSE tap used to read (client-wire.ts).
15
+ */
16
+ import type { AdapterEvent, OcxConfig, OcxProviderContinuationState, OcxReasoningReplayScopeRef, OcxUsage } from "../../types";
17
+ import type { AdmissionLease } from "../../lib/admission";
18
+ import {
19
+ releaseTranslatedEvent,
20
+ retainTranslatedEvent,
21
+ type TranslatorBudget,
22
+ } from "../../lib/translator-budget";
23
+ import { buildResponseJSON } from "../../bridge";
24
+ import { responsesUsage, uuid } from "../../bridge/internal";
25
+ import { awaitThoughtSignatureDurability } from "../../responses/thought-signature-replay";
26
+ import { attemptDeliveryRecorder } from "../../usage/attempt-delivery";
27
+ import { encodeChatCompletionSse, collectChatCompletionResponse } from "../../protocols/encoders/chat";
28
+ import { encodeAnthropicMessageSse, collectAnthropicMessageResponse } from "../../protocols/encoders/messages";
29
+ import type { ClientEncodeHooks, EncodedTerminal } from "../../protocols/encoders/adapter-events";
30
+ import { upstreamWireForAdapter } from "../../protocols/contract";
31
+ import { markAttemptProtocolPath } from "../../protocols/trace";
32
+ import { deliveryModeForLane, requestPathForLane } from "../../protocols/path";
33
+ import { resolveProtocolSettings } from "../../protocols/settings";
34
+ import { trackStreamLifetime } from "../lifecycle";
35
+ import type { RequestLogContext } from "../request-log";
36
+ import type { ClientEncoderOption, HandleResponsesOptions } from "../responses/core-options";
37
+ import { attachClientWireLog, createClientWireLog, markClientWire } from "./client-wire";
38
+
39
+ const CLIENT_SSE_HEADERS = {
40
+ "Content-Type": "text/event-stream; charset=utf-8",
41
+ "Cache-Control": "no-cache",
42
+ Connection: "keep-alive",
43
+ } as const;
44
+
45
+ /**
46
+ * Whether a Chat or Messages ingress asks for direct encoding on the route it settled: the
47
+ * rollout switch is on and the route is one concrete non-Responses target. Combo and policy
48
+ * routes read or retry the Responses body after delivery, and a Responses-wire route is the
49
+ * passthrough case this work leaves alone.
50
+ */
51
+ export function directEncodersApply(
52
+ config: Pick<OcxConfig, "protocols">,
53
+ route: { combo?: unknown; routeKind?: string; provider: { adapter: string } } | null | undefined,
54
+ ): boolean {
55
+ if (!route || !resolveProtocolSettings(config).rollout.directEncoders) return false;
56
+ if (route.combo !== undefined || route.routeKind === "policy") return false;
57
+ return upstreamWireForAdapter(route.provider.adapter) !== "responses";
58
+ }
59
+
60
+ /**
61
+ * The encoder this delivery may use, or undefined to keep the Responses body. The final route
62
+ * is re-checked because core can still change it after the ingress decided: a combo child's
63
+ * body is read by the combo's commit logic, a policy route may hop on an error body, a
64
+ * compaction turn must emit its synthetic item, and a Responses-wire upstream stays as it is.
65
+ */
66
+ export function clientEncoderForDelivery(
67
+ options: Pick<HandleResponsesOptions, "clientEncoder" | "comboAttempt">,
68
+ logCtx: Pick<RequestLogContext, "routeDecision">,
69
+ routedCompaction: boolean,
70
+ adapterName: string,
71
+ ): ClientEncoderOption | undefined {
72
+ const encoder = options.clientEncoder;
73
+ if (!encoder || options.comboAttempt || routedCompaction) return undefined;
74
+ if (logCtx.routeDecision?.routeKind === "policy" || logCtx.routeDecision?.routeKind === "combo") return undefined;
75
+ if (upstreamWireForAdapter(adapterName) === "responses") return undefined;
76
+ return encoder;
77
+ }
78
+
79
+ export interface ClientEncodedDelivery {
80
+ encoder: ClientEncoderOption;
81
+ events: AsyncIterable<AdapterEvent>;
82
+ logCtx: RequestLogContext;
83
+ translatorBudget: TranslatorBudget;
84
+ /** Model on the folded response and the log payloads, as on the bridge's snapshots. */
85
+ responseModelId: string;
86
+ adapterName: string;
87
+ fold: {
88
+ replayCacheScope?: OcxReasoningReplayScopeRef;
89
+ hideThinkingSummary?: boolean;
90
+ toolNsMap?: Map<string, { namespace: string; name: string; freeform?: true }>;
91
+ declaredToolNames?: ReadonlySet<string>;
92
+ toolParameterSchemas?: ReadonlyMap<string, Record<string, unknown>>;
93
+ freeformToolNames?: Set<string>;
94
+ toolSearchToolNames?: Set<string>;
95
+ };
96
+ stallTimeoutSec?: number;
97
+ turnAdmissionLease?: AdmissionLease;
98
+ onFirstOutput?: () => void;
99
+ /** The bridge's `onCancel`: stop completion notification and abort the upstream. */
100
+ stopUpstream: () => void;
101
+ /** Turn-lifetime cleanup when the client body finishes or is cancelled. */
102
+ onStreamDone: () => void;
103
+ /** The bridge's `onCompletedResponse`, fed the folded response. */
104
+ onCompletedResponse: (response: Record<string, unknown>, providerState?: OcxProviderContinuationState) => void;
105
+ /** The bridge's `onUsage`. */
106
+ bindUsage: (usage: OcxUsage | undefined) => void;
107
+ }
108
+
109
+ /**
110
+ * Record the attempt's observed path: the request still goes through the internal Responses
111
+ * bridge until the codecs decode to IR directly, so the request path and mode stay the bridge's;
112
+ * the response now reaches the client from the IR without the internal Responses hop.
113
+ */
114
+ function markDirectEncoderPath(logCtx: RequestLogContext, encoder: ClientEncoderOption, adapter: string): void {
115
+ const attempt = logCtx.activeAttempt;
116
+ if (!attempt) return;
117
+ const upstream = upstreamWireForAdapter(attempt.adapter || adapter);
118
+ markAttemptProtocolPath(attempt, {
119
+ mode: deliveryModeForLane(encoder.protocol, "bridge", upstream),
120
+ requestPath: requestPathForLane(encoder.protocol, "bridge", upstream),
121
+ responsePath: [upstream, "ir", encoder.protocol],
122
+ });
123
+ }
124
+
125
+ export async function deliverClientEncodedResponse(input: ClientEncodedDelivery): Promise<Response> {
126
+ const { encoder, logCtx, translatorBudget } = input;
127
+ markDirectEncoderPath(logCtx, encoder, input.adapterName);
128
+ const log = createClientWireLog();
129
+ const responseId = `resp_${uuid()}`;
130
+ const createdAt = Math.floor(Date.now() / 1000);
131
+ const snapshot = (status: string): Record<string, unknown> => ({
132
+ id: responseId, object: "response", created_at: createdAt,
133
+ status, model: input.responseModelId, output: [], usage: null,
134
+ });
135
+ log.record({ kind: "observe", payload: { type: "response.created", response: snapshot("in_progress") } });
136
+
137
+ // Collected copies: the adapter's own objects may already carry a lease from a buffered batch.
138
+ const collected: AdapterEvent[] = [];
139
+ let sealed = false;
140
+ const releaseCollected = () => {
141
+ sealed = true;
142
+ for (const event of collected) releaseTranslatedEvent(event, translatorBudget);
143
+ collected.length = 0;
144
+ };
145
+ const events = (async function* (): AsyncGenerator<AdapterEvent> {
146
+ for await (const event of input.events) {
147
+ if (!sealed) {
148
+ const copy = { ...event } as AdapterEvent;
149
+ retainTranslatedEvent(copy, translatorBudget, collected.at(-1));
150
+ collected.push(copy);
151
+ }
152
+ yield event;
153
+ }
154
+ })();
155
+ const fold = (): Record<string, unknown> | undefined => {
156
+ if (sealed) return undefined;
157
+ sealed = true;
158
+ try {
159
+ return buildResponseJSON(collected, input.responseModelId, {
160
+ ...input.fold,
161
+ enforceDeclaredToolNames: false,
162
+ translatorBudget,
163
+ recordBufferedDelivery: false,
164
+ });
165
+ } catch {
166
+ return undefined;
167
+ } finally {
168
+ releaseCollected();
169
+ }
170
+ };
171
+
172
+ const recorder = attemptDeliveryRecorder(translatorBudget);
173
+ let completedResponseDelivered = false;
174
+ const terminalPayload = (terminal: EncodedTerminal): Record<string, unknown> => ({
175
+ type: terminal.status === "completed" ? "response.completed"
176
+ : terminal.status === "failed" ? "response.failed"
177
+ : "response.incomplete",
178
+ response: {
179
+ ...snapshot(terminal.status),
180
+ ...(terminal.endTurn !== undefined ? { end_turn: terminal.endTurn } : {}),
181
+ usage: terminal.usageOnWire ? responsesUsage(terminal.usage) : null,
182
+ ...(terminal.incomplete ? { incomplete_details: { ...terminal.incomplete } } : {}),
183
+ ...(terminal.error ? { error: terminal.error, last_error: terminal.error } : {}),
184
+ ...(terminal.retryable !== undefined ? { retryable: terminal.retryable } : {}),
185
+ },
186
+ });
187
+ const hooks: ClientEncodeHooks = {
188
+ ...(input.onFirstOutput ? { onFirstOutput: input.onFirstOutput } : {}),
189
+ async beforeTerminal(terminal) {
190
+ const response = terminal.overflow ? (releaseCollected(), undefined) : fold();
191
+ if (terminal.durable) await awaitThoughtSignatureDurability();
192
+ if (terminal.completedResponse && response && !completedResponseDelivered) {
193
+ completedResponseDelivered = true;
194
+ input.onCompletedResponse(response, terminal.providerState);
195
+ }
196
+ if (terminal.reportUsage) input.bindUsage(terminal.usage);
197
+ },
198
+ afterTerminal(terminal) {
199
+ log.record({ kind: "terminal", status: terminal.status, payload: terminalPayload(terminal) });
200
+ },
201
+ stopUpstream: input.stopUpstream,
202
+ onClientCancel() {
203
+ releaseCollected();
204
+ log.record({ kind: "cancel" });
205
+ },
206
+ onRelayed(observation) {
207
+ recorder?.noteRelayedEvent(observation);
208
+ },
209
+ };
210
+ const encodeOptions = {
211
+ model: encoder.model,
212
+ translatorBudget,
213
+ ...input.fold,
214
+ ...(input.stallTimeoutSec !== undefined ? { stallTimeoutSec: input.stallTimeoutSec } : {}),
215
+ hooks,
216
+ };
217
+ const encoded = encoder.protocol === "chat"
218
+ ? encodeChatCompletionSse(events, encodeOptions)
219
+ : encodeAnthropicMessageSse(events, {
220
+ ...encodeOptions,
221
+ ...(encoder.inputTokenFloor !== undefined ? { inputTokenFloor: encoder.inputTokenFloor } : {}),
222
+ });
223
+ const tracked = trackStreamLifetime(encoded, new AbortController(), input.onStreamDone, input.turnAdmissionLease);
224
+
225
+ let response: Response;
226
+ if (encoder.stream) {
227
+ response = new Response(tracked, { status: 200, headers: CLIENT_SSE_HEADERS });
228
+ } else {
229
+ // The routed turn always streams internally; a non-streaming client gets the fold.
230
+ response = encoder.protocol === "chat"
231
+ ? await collectChatCompletionResponse(tracked, encoder.model, translatorBudget)
232
+ : await collectAnthropicMessageResponse(tracked, encoder.model, translatorBudget);
233
+ }
234
+ return attachClientWireLog(markClientWire(response, encoder.protocol), log);
235
+ }
@@ -0,0 +1,49 @@
1
+ import {
2
+ addFinalRequestLog,
3
+ httpStatusForRequestLogTerminal,
4
+ inspectResponseLogSsePayloadParsed,
5
+ type RequestLogContext,
6
+ type RequestLogEntry,
7
+ } from "../request-log";
8
+ import type { ClientWireLog } from "./client-wire";
9
+
10
+ /**
11
+ * The deferred request log of a client-wire response. It records what the Responses SSE tap
12
+ * (`trackSseForRequestLog`) records for a bridged body, from the facts the producer reports
13
+ * instead of from the body: the same payload inspection until the terminal, the same terminal
14
+ * transport phase and status mapping, and 499 for a client cancel before any terminal. The row
15
+ * is written once.
16
+ */
17
+ export function recordClientWireRequestLog(
18
+ log: ClientWireLog,
19
+ requestId: string,
20
+ start: number,
21
+ logCtx: RequestLogContext,
22
+ addLog: (entry: RequestLogEntry) => void,
23
+ ): void {
24
+ let logged = false;
25
+ const inspect = (payload: Record<string, unknown>) => {
26
+ try {
27
+ inspectResponseLogSsePayloadParsed(logCtx, JSON.stringify(payload), payload);
28
+ } catch { /* request log metadata is best-effort */ }
29
+ };
30
+ log.subscribe(event => {
31
+ if (logged) return;
32
+ if (event.kind === "observe") {
33
+ inspect(event.payload);
34
+ return;
35
+ }
36
+ logged = true;
37
+ if (event.kind === "cancel") {
38
+ addFinalRequestLog(requestId, start, logCtx, 499, { closeReason: "client_cancel" }, addLog);
39
+ return;
40
+ }
41
+ inspect(event.payload);
42
+ logCtx.transportPhase = "terminal_sse";
43
+ logCtx.terminalSource = "upstream";
44
+ addFinalRequestLog(requestId, start, logCtx, httpStatusForRequestLogTerminal(event.status, logCtx), {
45
+ terminalStatus: event.status,
46
+ closeReason: "terminal",
47
+ }, addLog);
48
+ });
49
+ }
@@ -0,0 +1,69 @@
1
+ import type { Protocol } from "../../protocols/contract";
2
+
3
+ const clientWires = new WeakMap<Response, Protocol>();
4
+
5
+ /**
6
+ * Record that `response` is already in `protocol`'s client wire, so an ingress can tell a body
7
+ * it may pass through from a Responses body it still has to convert. Identity-keyed: a
8
+ * rebuilt or cloned Response carries no mark. Returns the same response.
9
+ */
10
+ export function markClientWire(response: Response, protocol: Protocol): Response {
11
+ clientWires.set(response, protocol);
12
+ return response;
13
+ }
14
+
15
+ /** The client wire `response` was marked with, or undefined for an unmarked response. */
16
+ export function clientWireOf(response: Response): Protocol | undefined {
17
+ return clientWires.get(response);
18
+ }
19
+
20
+ /** What the request log of a client-wire response learns, in the order the stream learned it. */
21
+ export type ClientWireLogEvent =
22
+ /** A Responses-vocabulary payload the legacy log tap would have inspected. */
23
+ | { kind: "observe"; payload: Record<string, unknown> }
24
+ | { kind: "terminal"; status: "completed" | "failed" | "incomplete"; payload: Record<string, unknown> }
25
+ | { kind: "cancel" };
26
+
27
+ /**
28
+ * A client-wire body cannot be tapped for the request log the way a Responses body is: its
29
+ * frames are not Responses events. The producer records the facts the tap would have read, and
30
+ * the deferred request log subscribes. Events recorded before the subscription are replayed to
31
+ * it; the producer records at most a start payload, one terminal and one cancel.
32
+ */
33
+ export interface ClientWireLog {
34
+ record(event: ClientWireLogEvent): void;
35
+ subscribe(listener: (event: ClientWireLogEvent) => void): void;
36
+ }
37
+
38
+ export function createClientWireLog(): ClientWireLog {
39
+ let pending: ClientWireLogEvent[] | undefined = [];
40
+ let listener: ((event: ClientWireLogEvent) => void) | undefined;
41
+ const deliver = (event: ClientWireLogEvent) => {
42
+ try { listener?.(event); } catch { /* logging never throws into the stream */ }
43
+ };
44
+ return {
45
+ record(event) {
46
+ if (pending) pending.push(event);
47
+ else deliver(event);
48
+ },
49
+ subscribe(next) {
50
+ if (listener) return;
51
+ listener = next;
52
+ const buffered = pending ?? [];
53
+ pending = undefined;
54
+ for (const event of buffered) deliver(event);
55
+ },
56
+ };
57
+ }
58
+
59
+ const clientWireLogs = new WeakMap<Response, ClientWireLog>();
60
+
61
+ /** Attach the log channel a client-wire response reports through. Returns the same response. */
62
+ export function attachClientWireLog(response: Response, log: ClientWireLog): Response {
63
+ clientWireLogs.set(response, log);
64
+ return response;
65
+ }
66
+
67
+ export function clientWireLogOf(response: Response): ClientWireLog | undefined {
68
+ return clientWireLogs.get(response);
69
+ }
@@ -0,0 +1,15 @@
1
+ import { createRequestExecutionBudget, type RequestExecutionBudget } from "../../lib/request-execution-budget";
2
+ import type { RequestLogContext } from "../request-log";
3
+ import { attachRequestSpendTracker } from "../responses/request-spend";
4
+
5
+ /**
6
+ * The one construction of an ingress-owned send budget: the default guarded policy, no logical
7
+ * request id, and this request's spend tracker as the send observer. Attaching the tracker
8
+ * parks it on `logCtx`, so callers that inherit a holder must not call this at all.
9
+ */
10
+ export function createInferenceSendBudget(
11
+ req: Pick<Request, "headers">,
12
+ logCtx: RequestLogContext,
13
+ ): RequestExecutionBudget {
14
+ return createRequestExecutionBudget(undefined, undefined, attachRequestSpendTracker(req, logCtx));
15
+ }
@@ -0,0 +1,37 @@
1
+ import { addFinalRequestLog, type RequestLogContext, type RequestLogEntry } from "../request-log";
2
+
3
+ /** The request-row identity an ingress hands down; absent when nothing records the request. */
4
+ export interface FinalRequestLogIds {
5
+ requestId: string;
6
+ start: number;
7
+ }
8
+
9
+ export type FinalRequestLogMeta = Pick<RequestLogEntry, "terminalStatus" | "closeReason">;
10
+
11
+ export interface FinalRequestLog {
12
+ /** Write the final request row once. Every later call, from any path, is a no-op. */
13
+ finish(status: number, meta?: FinalRequestLogMeta): void;
14
+ /** True once `finish` has run, whether or not a row was written. */
15
+ finished(): boolean;
16
+ }
17
+
18
+ /**
19
+ * Finish-once ownership of one request's final log row. Terminal callbacks, cancellation and
20
+ * the non-stream return race each other, and only the first of them may write the row.
21
+ * Without log ids the claim still settles, so a caller's `finished()` check behaves the same
22
+ * whether or not the request is recorded.
23
+ */
24
+ export function createFinalRequestLog(
25
+ logIds: FinalRequestLogIds | undefined,
26
+ logCtx: RequestLogContext,
27
+ ): FinalRequestLog {
28
+ let done = false;
29
+ return {
30
+ finish: (status, meta) => {
31
+ if (done) return;
32
+ done = true;
33
+ if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, meta);
34
+ },
35
+ finished: () => done,
36
+ };
37
+ }
@@ -0,0 +1,57 @@
1
+ import { timingSafeEqual } from "node:crypto";
2
+ import {
3
+ LOCAL_MANAGEMENT_CAPABILITY_EXPIRES_AT_HEADER,
4
+ LOCAL_MANAGEMENT_CAPABILITY_HEADER,
5
+ LOCAL_MANAGEMENT_EXPECTED_PID_HEADER,
6
+ LOCAL_MANAGEMENT_NONCE_HEADER,
7
+ parseExpectedLocalManagementPid,
8
+ } from "../lib/local-management-capability";
9
+ import {
10
+ desktopSnapshotBodyDigest,
11
+ LOCAL_DESKTOP_SNAPSHOT_BODY_HEADER,
12
+ LOCAL_DESKTOP_SNAPSHOT_PATH,
13
+ verifyLocalDesktopSnapshotCapability,
14
+ } from "../lib/local-desktop-snapshot-capability";
15
+ import type { LocalManagementAuthContext } from "./management-auth";
16
+
17
+ const REPLAY_LIMIT = 256;
18
+ const consumed = new Map<string, number>();
19
+ const admittedBodyDigests = new WeakMap<Request, string>();
20
+
21
+ /** Admit only the snapshot POST; a grant never becomes a general management credential. */
22
+ export function hasLocalDesktopSnapshotCapability(
23
+ req: Request,
24
+ local: LocalManagementAuthContext | undefined,
25
+ now = Date.now(),
26
+ ): boolean {
27
+ if (admittedBodyDigests.has(req)) return true;
28
+ if (!local || req.method !== "POST" || req.headers.has("origin")) return false;
29
+ let url: URL;
30
+ try { url = new URL(req.url); } catch { return false; }
31
+ if (url.pathname !== LOCAL_DESKTOP_SNAPSHOT_PATH || url.search !== "") return false;
32
+ const expectedPid = parseExpectedLocalManagementPid(req.headers.get(LOCAL_MANAGEMENT_EXPECTED_PID_HEADER));
33
+ if (expectedPid.kind !== "present" || expectedPid.pid !== local.pid) return false;
34
+ const expiry = req.headers.get(LOCAL_MANAGEMENT_CAPABILITY_EXPIRES_AT_HEADER);
35
+ if (!expiry || !/^[1-9]\d*$/.test(expiry)) return false;
36
+ const expiresAt = Number(expiry);
37
+ const capability = req.headers.get(LOCAL_MANAGEMENT_CAPABILITY_HEADER);
38
+ const bodyDigest = req.headers.get(LOCAL_DESKTOP_SNAPSHOT_BODY_HEADER);
39
+ if (!verifyLocalDesktopSnapshotCapability(
40
+ local.attestationSecret, req.headers.get(LOCAL_MANAGEMENT_NONCE_HEADER),
41
+ req.method, url.pathname, local.pid, local.port, expiresAt, bodyDigest, capability, now,
42
+ )) return false;
43
+ for (const [proof, until] of consumed) if (until <= now) consumed.delete(proof);
44
+ if (!capability || !bodyDigest || consumed.has(capability) || consumed.size >= REPLAY_LIMIT) return false;
45
+ consumed.set(capability, expiresAt);
46
+ // Cache the authenticated digest, not a mutable header. The route must verify
47
+ // its bounded raw body against this value before it changes display state.
48
+ admittedBodyDigests.set(req, bodyDigest);
49
+ return true;
50
+ }
51
+
52
+ /** Finish admission after the route has read at most 1 KiB, before parsing or storing JSON. */
53
+ export function verifyLocalDesktopSnapshotBody(req: Request, body: Uint8Array): boolean {
54
+ const expected = admittedBodyDigests.get(req);
55
+ if (!expected || body.byteLength > 1024) return false;
56
+ return timingSafeEqual(Buffer.from(expected), Buffer.from(desktopSnapshotBodyDigest(body)));
57
+ }
@@ -1405,7 +1405,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1405
1405
  const models = await fetchAllModels(config);
1406
1406
  const { listCatalogNativeSlugs } = await import("../../codex/catalog");
1407
1407
  const { claudeCodeAlias, claudeCodeNativeAlias } = await import("../../claude/alias");
1408
- const { buildClaudeContextWindows, effectiveModelEnv } = await import("../../claude/context-windows");
1408
+ const { buildClaudeContextWindows, effectiveModelEnv, nativeClaudePassthroughFor } = await import("../../claude/context-windows");
1409
1409
  const { visibleNativeSlugs } = await import("../../codex/catalog");
1410
1410
  const disabled = new Set(config.disabledModels ?? []);
1411
1411
  const isDisabled = (provider: string, id: string) =>
@@ -1432,9 +1432,6 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1432
1432
  const listedId = (m.provider === "cursor" ? cursorFastIdFor?.(m.id) : undefined) ?? m.id;
1433
1433
  aliases.push({ id: claudeCodeAlias(m.provider, listedId), display_name: `${listedId} (${m.provider})` });
1434
1434
  }
1435
- const contextWindows = buildClaudeContextWindows([...visibleNativeSlugs(config)], models, nativeContextLimits(config));
1436
- const webSearchOverride = config.claudeCode?.webSearchSidecar;
1437
- const visionOverride = config.claudeCode?.visionSidecar;
1438
1435
  // Auto is a RESOLUTION, recomputed per request — never stored state. Detection is
1439
1436
  // daemon-side, so it cannot see a key exported only in the user's terminal; the
1440
1437
  // GUI labels the badge with detectionScope for exactly that reason.
@@ -1442,6 +1439,14 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1442
1439
  const { authModeIntent, resolveClaudeAuthMode } = await import("../../claude/auth-mode");
1443
1440
  const authDetection = detectClaudeAuth(defaultAuthDetectDeps(process.env, ownAdmissionTokens(config)));
1444
1441
  const resolvedAuthMode = resolveClaudeAuthMode(config, authDetection);
1442
+ const contextWindows = buildClaudeContextWindows(
1443
+ [...visibleNativeSlugs(config)],
1444
+ models,
1445
+ nativeContextLimits(config),
1446
+ nativeClaudePassthroughFor(config.claudeCode, resolvedAuthMode.markerMode),
1447
+ );
1448
+ const webSearchOverride = config.claudeCode?.webSearchSidecar;
1449
+ const visionOverride = config.claudeCode?.visionSidecar;
1445
1450
  return jsonResponse({
1446
1451
  enabled: config.claudeCode?.enabled !== false,
1447
1452
  // Three-state intent (devlog 260726_claude_auth_auto): an absent key is AUTO, not
@@ -1740,15 +1745,12 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
1740
1745
  }
1741
1746
  }
1742
1747
  if (body.fastMode !== undefined) config.fastMode = nextFastMode;
1743
- config.claudeCode = next;
1744
- // Stamp the migration sentinel on EVERY persist of this block. The migration reads
1745
- // "a claudeCode block with no authMode" as a pre-upgrade subscriber and pins it to
1746
- // literal subscription — correct for a config written before `auto` existed, fatal
1747
- // for one written after. Without this, choosing Auto (which DELETES authMode) or
1748
- // merely toggling Claude on (App.tsx PUTs `{enabled}` alone and creates the block)
1749
- // would be converted into a sticky manual subscription by the next startServer, and
1750
- // auto would survive exactly one proxy lifetime with no way back.
1751
- if (!next.authModeMigratedAt) next.authModeMigratedAt = new Date().toISOString();
1748
+ // Stamps the auth-mode migration sentinel on EVERY persist of this block. Without it,
1749
+ // choosing Auto (which DELETES authMode) or merely toggling Claude on (App.tsx PUTs
1750
+ // `{enabled}` alone and creates the block) would be converted into a sticky manual
1751
+ // subscription by the next startServer, with no way back.
1752
+ const { commitClaudeCodeBlock } = await import("../../claude/claude-code-block");
1753
+ commitClaudeCodeBlock(config, next);
1752
1754
  const { saveConfigPreservingClaudeCode: save } = await import("../../config");
1753
1755
  save(config);
1754
1756
  const warnings: string[] = [];
@@ -3,6 +3,7 @@ import { isWildcardHostname } from "../../codex/loopback-target";
3
3
  import { localCredentialDestinationHostname, localInferenceDestination } from "../../lib/local-destinations";
4
4
  import { isCanonicalOpenAiForwardProvider, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers-destination";
5
5
  import { LIVE_AUDIO_MODEL, TRANSCRIPTION_MODEL } from "../audio-upstream";
6
+ import { resolveApiSurfaceSettings, type ApiSurfaceSettings } from "../../protocols/settings";
6
7
 
7
8
  export interface AudioApiAccess {
8
9
  transcriptionEndpoint: string;
@@ -23,6 +24,13 @@ export interface ApiAccessEndpoints {
23
24
  chatCompletionsEndpoint: string;
24
25
  messagesEndpoint: string;
25
26
  modelsEndpoint: string;
27
+ /** Which public APIs are served and who decided it, from `resolveApiSurfaceSettings`. */
28
+ surfaces: ApiSurfaceSettings;
29
+ /**
30
+ * Back-compat for dashboards that predate `surfaces`: they hide the Messages endpoint when
31
+ * this is false. It mirrors `surfaces.messages.enabled`, not `claudeCode.enabled`, so an
32
+ * older dashboard never advertises a closed endpoint or hides an open one.
33
+ */
26
34
  claudeCodeEnabled: boolean;
27
35
  audio: AudioApiAccess;
28
36
  /** Back-compat alias for older GUI clients. */
@@ -165,13 +173,15 @@ export function buildApiAccessEndpoints(
165
173
  const apiConfigured = !!keyed && keyed.disabled !== true && keyed.adapter === "openai-responses"
166
174
  && keyed.authMode !== "forward" && keyed.baseUrl.replace(/\/+$/, "") === "https://api.openai.com/v1"
167
175
  && typeof keyed.apiKey === "string" && !!keyed.apiKey.trim();
176
+ const surfaces = resolveApiSurfaceSettings(config);
168
177
  return {
169
178
  baseUrl,
170
179
  responsesEndpoint,
171
180
  chatCompletionsEndpoint: `${baseUrl}/chat/completions`,
172
181
  messagesEndpoint: `${baseUrl}/messages`,
173
182
  modelsEndpoint: `${baseUrl}/models`,
174
- claudeCodeEnabled: config.claudeCode?.enabled !== false,
183
+ surfaces,
184
+ claudeCodeEnabled: surfaces.messages.enabled,
175
185
  audio: {
176
186
  transcriptionEndpoint: `${baseUrl}/audio/transcriptions`,
177
187
  dictationStreamEndpoint: `${socketBase.href}/audio/transcriptions/stream`,
@@ -8,6 +8,7 @@ import { catalogModelSlug, invalidateCodexModelsCache, nativeContextLimits, nati
8
8
  import {
9
9
  applyCodexConfigInjection,
10
10
  describeCodexDesktopSwitches,
11
+ observedCodexDesktopSwitchApply,
11
12
  type CodexDesktopSwitchApply,
12
13
  } from "../../codex/desktop-switches";
13
14
  import {
@@ -360,11 +361,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
360
361
  codexDesktopAuthless: config.codexDesktopAuthless === true,
361
362
  // Absent keeps Design B remote compaction; true selects the dedicated provider identity.
362
363
  codexClientCompaction: config.codexClientCompaction === true,
363
- codexDesktopSwitches: describeCodexDesktopSwitches(config, {
364
- applied: false,
365
- reason: "not_requested",
366
- retryable: false,
367
- }),
364
+ codexDesktopSwitches: describeCodexDesktopSwitches(config, await observedCodexDesktopSwitchApply()),
368
365
  compactionRouting: config.compactionRouting ?? null,
369
366
  startupHealth: await readStartupHealth(config),
370
367
  codexRuntime: {
@@ -687,7 +684,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
687
684
  // lock C — awaiting N while still holding C would invert that order.
688
685
  const desktopSwitchApply: CodexDesktopSwitchApply = desktopSwitchesChanged
689
686
  ? await applyCodexConfigInjection(config)
690
- : { applied: false, reason: "not_requested", retryable: false };
687
+ : await observedCodexDesktopSwitchApply();
691
688
  const codexDesktopSwitches = describeCodexDesktopSwitches(config, desktopSwitchApply);
692
689
  const catalogRefreshPending = catalogRefresh
693
690
  ? catalogRefreshIsPending(catalogRefresh)
@@ -25,6 +25,11 @@ import type { RequestMetricsSnapshotter } from "../request-metrics";
25
25
 
26
26
  import type { RemoteWorkspaceHub } from "../../remote-control/workspace-hub";
27
27
  import type { RemoteWorkspaceSessionService } from "../../remote-control/workspace-sessions";
28
+ import type { LinkSupervisor } from "../../link/supervisor";
29
+ import type { LinkListenerLifecycle } from "../index/link-listener";
30
+ import type { SshRunner } from "../../link/ssh-runner";
31
+ import type { LinkStore } from "../../link/store";
32
+ import type { IssuedApiKey } from "./oauth-account-routes";
28
33
 
29
34
  export type RemoteWorkspaceHubApi = Pick<RemoteWorkspaceHub,
30
35
  "identity" | "createPairingGrant" | "assertPairingSourceAllowed" | "pairDevice"
@@ -33,6 +38,11 @@ export type RemoteWorkspaceHubApi = Pick<RemoteWorkspaceHub,
33
38
  export type RemoteWorkspaceSessionsApi = Pick<RemoteWorkspaceSessionService,
34
39
  "availability" | "list" | "create" | "prompt" | "submitPrompt" | "stop" | "shutdown">;
35
40
 
41
+ export interface ManagementRequestIngress {
42
+ trustedLoopback: boolean;
43
+ guiSessionIssuance?: import("../gui-session").GuiSessionIssuance | null;
44
+ }
45
+
36
46
  export interface ManagementApiDeps {
37
47
  /** Read-only process-local aggregate metrics; absent keeps the scrape route unavailable. */
38
48
  requestMetrics?: RequestMetricsSnapshotter;
@@ -130,6 +140,17 @@ export interface ManagementApiDeps {
130
140
  * `saveConfigPreservingClaudeCode` above exists to prevent.
131
141
  */
132
142
  codexPromptPaths?: CodexPromptPaths;
143
+ /** Link seams are getters so the optional listener and supervisor are singletons. */
144
+ linkSupervisor?: () => LinkSupervisor;
145
+ linkListener?: () => Pick<LinkListenerLifecycle<unknown>, "ensureStarted" | "status" | "close" | "onAuthenticatedCatalog">;
146
+ readLinkStore?: () => LinkStore;
147
+ writeLinkStore?: (store: LinkStore) => void;
148
+ linkKnownHostsPath?: () => string;
149
+ sshRunner?: SshRunner;
150
+ issueApiKey?: (config: OcxConfig, name: string) => IssuedApiKey;
151
+ revokeApiKey?: (config: OcxConfig, id: string) => boolean;
152
+ loadLinkCandidates?: () => Array<{ alias: string; source: "ssh_config" | "tailscale" }>;
153
+ now?: () => number;
133
154
  }
134
155
 
135
156
 
@@ -151,6 +172,10 @@ export interface ManagementContext {
151
172
  principal?: ManagementPrincipal;
152
173
  /** Narrow current-session revocation seam; contains neither the token nor session map. */
153
174
  sessionControl?: ManagementSessionControl;
175
+ /** Whether the request arrived through a trusted loopback ingress. */
176
+ trustedLoopbackIngress: boolean;
177
+ /** The issuance mode of the session, when the principal is a GUI session. */
178
+ guiSessionIssuance: import("../gui-session").GuiSessionIssuance | null;
154
179
  convergeCodexCatalog: () => Promise<CatalogDisposition>;
155
180
  syncClaudeAgentDefsBestEffort: () => Promise<void>;
156
181
  }