@bitkyc08/opencodex 2.65.0 → 2.66.0-preview.20260925
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.
- package/README.md +14 -0
- package/gui/dist/assets/App-BVjA0T6O.js +51 -0
- package/gui/dist/assets/App-gkoJIEOV.css +1 -0
- package/gui/dist/assets/{Tray-D4bA1vff.js → Tray-BCFtiWwD.js} +1 -1
- package/gui/dist/assets/index-ComVsdSk.css +1 -0
- package/gui/dist/assets/index-vO4dgfiU.js +86 -0
- package/gui/dist/assets/{tray-data-BZXhB3qz.js → tray-data-C4FiNaxk.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -2
- package/src/adapters/anthropic/beta-allowlist.ts +80 -0
- package/src/adapters/anthropic/passthrough.ts +221 -0
- package/src/adapters/anthropic-image-codec.ts +56 -19
- package/src/adapters/anthropic-image-normalize.ts +20 -10
- package/src/adapters/anthropic.ts +55 -24
- package/src/adapters/coding-agent/protocol.ts +29 -19
- package/src/adapters/cursor/live-transport.ts +18 -3
- package/src/adapters/cursor/native-exec-shell.ts +18 -63
- package/src/adapters/cursor/native-exec.ts +5 -2
- package/src/adapters/cursor/native-foreground-shell.ts +44 -0
- package/src/adapters/devin/cloud-direct/chat.ts +7 -1
- package/src/adapters/exec-tool-result-normalize.ts +4 -1
- package/src/adapters/google.ts +12 -0
- package/src/adapters/ollama-native.ts +32 -1
- package/src/adapters/openai-chat/serialized-tool-call-content.ts +131 -34
- package/src/adapters/openai-chat.ts +6 -4
- package/src/adapters/openai-responses/passthrough.ts +1 -0
- package/src/adapters/openai-responses/reasoning.ts +45 -1
- package/src/bridge/response-json.ts +10 -2
- package/src/chat/outbound.ts +104 -52
- package/src/claude/claude-code-block.ts +16 -0
- package/src/claude/context-windows.ts +52 -4
- package/src/claude/desktop-first-party.ts +25 -8
- package/src/claude/inbound.ts +22 -0
- package/src/claude/intercept/connect-proxy.ts +37 -4
- package/src/claude/intercept/proxy-auth.ts +166 -0
- package/src/claude/intercept/runtime.ts +21 -0
- package/src/claude/intercept/settings.ts +33 -12
- package/src/claude/outbound.ts +45 -27
- package/src/cli/account-extended.ts +47 -15
- package/src/cli/account-target.ts +115 -0
- package/src/cli/account.ts +20 -28
- package/src/cli/agent.ts +10 -1
- package/src/cli/api-protocols.ts +206 -0
- package/src/cli/capabilities.ts +86 -0
- package/src/cli/combo.ts +2 -2
- package/src/cli/connect.ts +127 -3
- package/src/cli/dispatch.ts +8 -0
- package/src/cli/doctor.ts +33 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/link.ts +288 -0
- package/src/cli/registry.ts +23 -0
- package/src/cli/runtime-api.ts +79 -0
- package/src/cli/system-command.ts +21 -2
- package/src/client/connect.ts +125 -23
- package/src/client/hub-relay.ts +12 -8
- package/src/client/link-join.ts +323 -0
- package/src/client/link-relay.ts +241 -0
- package/src/client/link-state.ts +110 -0
- package/src/client/link-teardown.ts +63 -0
- package/src/client/link-tunnel.ts +465 -0
- package/src/client/machine-listener.ts +14 -5
- package/src/client/runtime.ts +91 -42
- package/src/client/state.ts +4 -0
- package/src/codex/catalog/build-entries.ts +4 -0
- package/src/codex/catalog/derive-entry.ts +5 -0
- package/src/codex/catalog/parsing.ts +4 -0
- package/src/codex/catalog/retained-sync.ts +4 -1
- package/src/codex/desktop-switches.ts +88 -8
- package/src/codex/inject/plan.ts +16 -3
- package/src/codex/inject.ts +3 -0
- package/src/codex/internal/catalog-writer.ts +13 -2
- package/src/combos/failover.ts +3 -2
- package/src/combos/index.ts +12 -0
- package/src/combos/jev.ts +646 -0
- package/src/combos/request.ts +5 -0
- package/src/combos/resolve.ts +17 -15
- package/src/combos/types.ts +47 -3
- package/src/config/atomic-write.ts +3 -0
- package/src/config/live-reconcile.ts +20 -4
- package/src/config/persist-unlocked.ts +20 -7
- package/src/config/schema/config-schema.ts +16 -0
- package/src/config/schema/leaf-validators.ts +38 -1
- package/src/generated/compatibility-version.json +416 -128
- package/src/integrations/omp-yaml-source.ts +109 -4
- package/src/lab/automation/orchestrator.ts +60 -4
- package/src/lab/events/limits.ts +1 -1
- package/src/lib/bounded-body.ts +55 -38
- package/src/lib/config-ownership.ts +11 -22
- package/src/lib/local-desktop-snapshot-capability.ts +56 -0
- package/src/lib/local-management-capability.ts +25 -3
- package/src/lib/optional-shutdown-hooks.ts +17 -0
- package/src/lib/state-store-registrations.ts +2 -2
- package/src/lib/windows-elevation.ts +62 -25
- package/src/lib/windows-secret-acl.ts +114 -23
- package/src/link/admission-wait.ts +73 -0
- package/src/link/compensation.ts +100 -0
- package/src/link/fingerprint.ts +21 -0
- package/src/link/paths.ts +19 -0
- package/src/link/ports.ts +12 -0
- package/src/link/routes.ts +32 -0
- package/src/link/ssh-argv.ts +170 -0
- package/src/link/ssh-config.ts +152 -0
- package/src/link/ssh-runner.ts +161 -0
- package/src/link/status-projection.ts +96 -0
- package/src/link/store.ts +139 -0
- package/src/link/supervisor.ts +404 -0
- package/src/link/tunnel-state.ts +91 -0
- package/src/protocols/baseline.ts +88 -0
- package/src/protocols/codecs/chat.ts +17 -0
- package/src/protocols/codecs/messages.ts +18 -0
- package/src/protocols/codecs/responses.ts +17 -0
- package/src/protocols/contract.ts +185 -0
- package/src/protocols/dto.ts +267 -0
- package/src/protocols/encoders/adapter-events.ts +876 -0
- package/src/protocols/encoders/chat.ts +243 -0
- package/src/protocols/encoders/messages.ts +462 -0
- package/src/protocols/envelope.ts +43 -0
- package/src/protocols/features.ts +295 -0
- package/src/protocols/guard.ts +33 -0
- package/src/protocols/opaque-state.ts +132 -0
- package/src/protocols/path.ts +39 -0
- package/src/protocols/plan-snapshot.ts +226 -0
- package/src/protocols/plan.ts +192 -0
- package/src/protocols/provider-summary.ts +101 -0
- package/src/protocols/settings.ts +118 -0
- package/src/protocols/shadow-plan.ts +36 -0
- package/src/protocols/shadow.ts +62 -0
- package/src/protocols/trace.ts +335 -0
- package/src/providers/model-rename-migration.ts +3 -3
- package/src/providers/registry/entries-core.ts +6 -1
- package/src/providers/registry/entries-extended.ts +15 -0
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/types.ts +6 -0
- package/src/providers/registry.ts +1 -1
- package/src/remote-control/workspace-hub.ts +5 -4
- package/src/responses/apply-patch-envelope.ts +6 -1
- package/src/responses/code-mode-shell-input.ts +4 -2
- package/src/responses/freeform-wrapper-scan.ts +51 -24
- package/src/responses/progressive-freeform-input.ts +14 -20
- package/src/responses/spill-store.ts +187 -33
- package/src/responses/state/snapshot-select.ts +49 -0
- package/src/responses/state/spill-inspect.ts +46 -0
- package/src/responses/state/spill-queue.ts +16 -0
- package/src/responses/state.ts +51 -27
- package/src/server/audio-client.ts +8 -3
- package/src/server/audio-upstream.ts +9 -4
- package/src/server/auth-cors.ts +25 -8
- package/src/server/chat-completions.ts +71 -11
- package/src/server/chat-native-eligibility.ts +74 -0
- package/src/server/chat-native.ts +139 -74
- package/src/server/claude-messages.ts +140 -22
- package/src/server/hub-usage.ts +4 -3
- package/src/server/index/link-listener.ts +195 -0
- package/src/server/index/optional-listeners.ts +115 -0
- package/src/server/index/serve-options.ts +44 -10
- package/src/server/index.ts +10 -9
- package/src/server/inference/attempt.ts +51 -0
- package/src/server/inference/client-encoder-delivery.ts +235 -0
- package/src/server/inference/client-wire-log.ts +49 -0
- package/src/server/inference/client-wire.ts +69 -0
- package/src/server/inference/context.ts +15 -0
- package/src/server/inference/final-log.ts +37 -0
- package/src/server/local-desktop-snapshot-auth.ts +57 -0
- package/src/server/management/agent-settings-routes.ts +15 -13
- package/src/server/management/api-access.ts +11 -1
- package/src/server/management/config-routes.ts +3 -6
- package/src/server/management/context.ts +25 -0
- package/src/server/management/link-routes.ts +535 -0
- package/src/server/management/logs-usage-routes.ts +31 -1
- package/src/server/management/native-integration-routes.ts +6 -11
- package/src/server/management/oauth-account-routes.ts +42 -16
- package/src/server/management/protocol-routes.ts +159 -0
- package/src/server/management/protocol-settings-patch.ts +145 -0
- package/src/server/management/provider-patch-transaction.ts +61 -0
- package/src/server/management/provider-routes.ts +39 -18
- package/src/server/management/route-registry.ts +12 -0
- package/src/server/management/sidebar-routes.ts +8 -3
- package/src/server/management/system-routes.ts +18 -1
- package/src/server/management/usage-aggregate-cache.ts +206 -6
- package/src/server/management-api.ts +26 -1
- package/src/server/management-auth.ts +19 -5
- package/src/server/messages-native-eligibility.ts +161 -0
- package/src/server/messages-native-oauth.ts +149 -0
- package/src/server/messages-native.ts +790 -0
- package/src/server/relay.ts +9 -0
- package/src/server/request-log-filter.ts +92 -0
- package/src/server/request-log.ts +31 -70
- package/src/server/responses/adapter-delivery.ts +43 -16
- package/src/server/responses/agent-task-recovery.ts +90 -22
- package/src/server/responses/codex-ws-exchange.ts +4 -0
- package/src/server/responses/codex-ws-wire.ts +10 -1
- package/src/server/responses/core-combo-native.ts +323 -0
- package/src/server/responses/core-combo.ts +224 -13
- package/src/server/responses/core-options.ts +22 -0
- package/src/server/responses/core.ts +14 -26
- package/src/server/responses/request-sidecar-auth.ts +8 -3
- package/src/server/responses/run-turn-execution.ts +257 -63
- package/src/server/responses/sidecar-execution.ts +7 -4
- package/src/server/system-env.ts +7 -0
- package/src/service/diagnostics.ts +7 -1
- package/src/service/launchd.ts +26 -37
- package/src/service/windows-ops.ts +24 -20
- package/src/types/config.ts +58 -1
- package/src/types.ts +2 -0
- package/src/update/transactional-install.d.mts +0 -1
- package/src/update/transactional-install.mjs +17 -17
- package/src/usage/jev-stats.ts +495 -0
- package/src/usage/log.ts +24 -3
- package/src/web-search/run-turn-loop.ts +568 -0
- package/gui/dist/assets/App-8NMiZxT0.css +0 -1
- package/gui/dist/assets/App-DGLte6IR.js +0 -50
- package/gui/dist/assets/index--EWgGQvZ.css +0 -1
- 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
|
-
|
|
1744
|
-
//
|
|
1745
|
-
//
|
|
1746
|
-
//
|
|
1747
|
-
|
|
1748
|
-
|
|
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
|
-
|
|
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
|
-
:
|
|
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
|
}
|