@bitkyc08/opencodex 2.90.0 → 2.92.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.
- package/bin/ocx.mjs +37 -3
- package/gui/dist/assets/App-DMps4OLk.css +1 -0
- package/gui/dist/assets/{App-DgxkE6Md.js → App-sUf_8afJ.js} +18 -18
- package/gui/dist/assets/{Tray-DbTNc_88.js → Tray-BSsxznDm.js} +1 -1
- package/gui/dist/assets/index-BlFE_VB_.js +94 -0
- package/gui/dist/assets/quota-summary-BRlbXgw0.js +1 -0
- package/gui/dist/index.html +1 -1
- package/gui/dist/provider-icons/workbuddy-color.svg +29 -0
- package/package.json +4 -3
- package/src/adapters/anthropic.ts +2 -5
- package/src/adapters/claude-cli/usage-admission.ts +14 -0
- package/src/adapters/google-vertex-endpoint.ts +9 -1
- package/src/adapters/google-vertex-key-project.ts +223 -0
- package/src/adapters/google.ts +13 -3
- package/src/adapters/openai-chat/tool-name-registry.ts +3 -1
- package/src/adapters/openai-chat.ts +2 -2
- package/src/adapters/openai-responses/passthrough.ts +2 -1
- package/src/adapters/openai-responses/request-strips.ts +92 -0
- package/src/adapters/openai-responses/transport.ts +6 -0
- package/src/adapters/registry.ts +5 -0
- package/src/adapters/workbuddy-schema.ts +61 -0
- package/src/adapters/workbuddy.ts +68 -0
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +1 -1
- package/src/chat/inbound.ts +20 -2
- package/src/chat/outbound.ts +47 -8
- package/src/chat/reasoning-details.ts +137 -0
- package/src/cli/capabilities-agents-routing.ts +53 -3
- package/src/cli/capabilities-base.ts +15 -2
- package/src/cli/capabilities-observe-system.ts +4 -4
- package/src/cli/codex-cli-update.ts +8 -0
- package/src/cli/codex-shim-autorestore.ts +5 -0
- package/src/cli/combo-result.ts +6 -0
- package/src/cli/combo-stats.ts +3 -0
- package/src/cli/combo.ts +66 -42
- package/src/cli/dispatch.ts +30 -0
- package/src/cli/fallback.ts +127 -0
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +1 -2
- package/src/cli/launcher-context.ts +49 -2
- package/src/cli/link-apply-restart.ts +72 -0
- package/src/cli/link-child.ts +6 -2
- package/src/cli/link-cleanup.ts +95 -0
- package/src/cli/link-enrollment.ts +4 -0
- package/src/cli/link.ts +123 -35
- package/src/cli/provider.ts +46 -3
- package/src/cli/registry.ts +13 -1
- package/src/cli/restart-failure.ts +4 -0
- package/src/cli/runtime-api.ts +1 -1
- package/src/cli/status-codex-settings.ts +56 -0
- package/src/cli/status-fallback.ts +29 -0
- package/src/cli/status-install.ts +52 -0
- package/src/cli/status.ts +20 -1
- package/src/cli/system-restart-client.ts +32 -1
- package/src/cli/update-restart-child.ts +4 -3
- package/src/cli/update-restart-home.ts +20 -8
- package/src/cli/usage-compare.ts +68 -12
- package/src/client/connect.ts +82 -6
- package/src/client/home-unreachable.ts +16 -0
- package/src/client/hub-relay.ts +8 -2
- package/src/client/link-home.ts +119 -6
- package/src/client/link-ingress.ts +1 -1
- package/src/client/link-parity-reporter.ts +107 -0
- package/src/client/link-readiness.ts +97 -0
- package/src/client/link-relay-transport.ts +84 -9
- package/src/client/link-relay.ts +98 -15
- package/src/client/link-tunnel.ts +45 -18
- package/src/client/machine-listener.ts +103 -3
- package/src/client/runtime-restart.ts +127 -0
- package/src/client/runtime.ts +155 -25
- package/src/codex/account-store.ts +17 -3
- package/src/codex/app-server-restart-service.ts +6 -5
- package/src/codex/auth-context.ts +10 -6
- package/src/codex/catalog/effort.ts +9 -2
- package/src/codex/catalog/gather-capture.ts +4 -2
- package/src/codex/catalog/parsing.ts +4 -0
- package/src/codex/catalog/provider-models.ts +14 -8
- package/src/codex/chatgpt-refresh-failure.ts +6 -2
- package/src/codex/cli-install-provenance.ts +418 -0
- package/src/codex/cli-installation-targets.ts +5 -1
- package/src/codex/cli-update-plan.ts +43 -16
- package/src/codex/desktop-app/picker-freshness.ts +10 -8
- package/src/codex/desktop-compatibility/usage-activation.ts +2 -1
- package/src/codex/desktop-compatibility/usage-controller.ts +7 -1
- package/src/codex/desktop-compatibility/windows-key-protection.ts +18 -6
- package/src/codex/pool-refresh-backoff.ts +3 -1
- package/src/codex/quota-history.ts +7 -0
- package/src/codex/quota.ts +10 -0
- package/src/codex/subagent-model-fallback.ts +24 -1
- package/src/codex/user-identity.ts +35 -6
- package/src/codex/windows-current-sid.ts +162 -0
- package/src/combos/index.ts +1 -0
- package/src/combos/jev-quota-activation.ts +5 -1
- package/src/combos/jev-quota-route.ts +1 -1
- package/src/combos/jev-quota.ts +9 -1
- package/src/combos/jev-session-pin-config.ts +51 -0
- package/src/combos/jev.ts +6 -0
- package/src/combos/resolve.ts +12 -0
- package/src/combos/types.ts +19 -0
- package/src/config/diagnostics.ts +5 -0
- package/src/config/load-degrade.ts +2 -0
- package/src/config/provider-validation.ts +7 -0
- package/src/config/schema/config-schema.ts +3 -0
- package/src/config/schema/fallback-policy.ts +40 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +375 -195
- package/src/lib/authorization-scheme.ts +95 -0
- package/src/lib/decision-quota-sink.ts +10 -0
- package/src/lib/provider-client-headers.ts +1 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/system-restart-contract.ts +24 -0
- package/src/lib/test-home-guard.ts +78 -11
- package/src/lib/translator-budget.ts +4 -4
- package/src/lib/upstream-retry.ts +4 -1
- package/src/link/child-parity.ts +124 -0
- package/src/link/codex-settings.ts +37 -0
- package/src/link/compatibility.ts +119 -0
- package/src/link/diagnostics.ts +193 -0
- package/src/link/management-routes.ts +2 -0
- package/src/link/peer-report.ts +100 -0
- package/src/link/relay-auth.ts +6 -0
- package/src/link/remote-platform.ts +78 -0
- package/src/link/ssh-argv.ts +19 -0
- package/src/link/status-projection.ts +63 -2
- package/src/link/supervisor.ts +45 -12
- package/src/link/tunnel-history.ts +109 -0
- package/src/link/tunnel-state.ts +80 -17
- package/src/messaging/remote-files.ts +24 -1
- package/src/oauth/generic-account-failover.ts +91 -7
- package/src/oauth/index.ts +16 -1
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/model-discovery-auth.ts +5 -0
- package/src/oauth/nous.ts +4 -1
- package/src/oauth/store.ts +6 -2
- package/src/oauth/token-guardian.ts +7 -0
- package/src/oauth/types.ts +9 -0
- package/src/oauth/workbuddy.ts +139 -0
- package/src/protocols/codecs/chat.ts +3 -3
- package/src/protocols/encoders/adapter-events.ts +26 -2
- package/src/protocols/encoders/chat.ts +55 -3
- package/src/providers/antigravity-quota-identity.ts +37 -0
- package/src/providers/base-url-choices.ts +8 -5
- package/src/providers/derive.ts +23 -3
- package/src/providers/mistral-transport.ts +37 -4
- package/src/providers/model-discovery.ts +2 -0
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/quota/account-cache.ts +3 -2
- package/src/providers/quota/antigravity.ts +27 -22
- package/src/providers/quota/report-cache.ts +13 -4
- package/src/providers/quota/vendor-probes-key.ts +60 -0
- package/src/providers/quota/workbuddy.ts +83 -0
- package/src/providers/quota-decision-antigravity.ts +64 -0
- package/src/providers/quota-decision-publication.ts +11 -0
- package/src/providers/quota-decision-snapshot.ts +21 -4
- package/src/providers/quota-types.ts +2 -0
- package/src/providers/quota.ts +3 -1
- package/src/providers/registry/entries-core.ts +9 -0
- package/src/providers/registry/entries-extended.ts +20 -7
- package/src/providers/registry/model-ids.ts +3 -0
- package/src/providers/registry/model-seeds.ts +27 -28
- package/src/providers/registry/types.ts +19 -1
- package/src/providers/resolved-model-policy.ts +4 -4
- package/src/providers/routed-config.ts +15 -0
- package/src/providers/stale-model-roster-migration.ts +83 -15
- package/src/providers/workbuddy-credit-balance.ts +81 -0
- package/src/providers/workbuddy-models.ts +37 -0
- package/src/providers/workbuddy.ts +60 -0
- package/src/quota-pool/assemble.ts +326 -0
- package/src/quota-pool/contract.ts +58 -0
- package/src/quota-pool/identity.ts +203 -0
- package/src/remote/features.ts +74 -0
- package/src/remote/protocol.ts +2 -0
- package/src/remote/served-by.ts +57 -0
- package/src/responses/parser.ts +7 -0
- package/src/responses/reasoning-envelope.ts +12 -0
- package/src/responses/reasoning-replay-cache.ts +18 -14
- package/src/router.ts +2 -0
- package/src/routing/analytics.ts +2 -3
- package/src/routing/fallback-policy.ts +290 -0
- package/src/routing/fallback-state.ts +179 -0
- package/src/server/audio-client.ts +2 -1
- package/src/server/audio-dictation.ts +2 -1
- package/src/server/audio-upstream.ts +2 -1
- package/src/server/auth-cors.ts +7 -5
- package/src/server/background-lifecycle.ts +4 -0
- package/src/server/chat-completions.ts +22 -2
- package/src/server/chat-native-reasoning.ts +10 -3
- package/src/server/chat-native.ts +13 -2
- package/src/server/claude-messages.ts +3 -0
- package/src/server/gui-session.ts +2 -1
- package/src/server/index/link-listener.ts +11 -1
- package/src/server/index/link-management.ts +47 -0
- package/src/server/index/link-relay-sessions.ts +20 -7
- package/src/server/index/optional-listeners.ts +11 -2
- package/src/server/index/serve-options.ts +19 -2
- package/src/server/live.ts +5 -0
- package/src/server/local-management-read-client.ts +3 -1
- package/src/server/management/combo-routes.ts +10 -4
- package/src/server/management/context.ts +1 -1
- package/src/server/management/fallback-routes.ts +75 -0
- package/src/server/management/link-enrollment.ts +21 -3
- package/src/server/management/link-route-state.ts +2 -0
- package/src/server/management/link-routes.ts +16 -2
- package/src/server/management/provider-routes.ts +1 -1
- package/src/server/management/quota-pool-routes.ts +24 -0
- package/src/server/management/route-registry.ts +3 -0
- package/src/server/management-api.ts +16 -0
- package/src/server/management-auth.ts +2 -1
- package/src/server/port-reclaim.ts +51 -9
- package/src/server/request-log-conversation.ts +49 -0
- package/src/server/request-log.ts +2 -0
- package/src/server/responses/agent-task-recovery.ts +1 -1
- package/src/server/responses/anthropic-thinking-recovery.ts +1 -0
- package/src/server/responses/compact.ts +12 -4
- package/src/server/responses/core-auth.ts +11 -1
- package/src/server/responses/core-combo.ts +125 -32
- package/src/server/responses/core-lifetime.ts +2 -2
- package/src/server/responses/core-normalize.ts +6 -3
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +5 -0
- package/src/server/responses/core-replay.ts +2 -1
- package/src/server/responses/encrypted-payload.ts +3 -3
- package/src/server/responses/jev-combo-choice.ts +39 -0
- package/src/server/responses/jev-session-pin.ts +203 -0
- package/src/server/responses/main-hard-lock-routing.ts +54 -2
- package/src/server/responses/passthrough-delivery.ts +4 -0
- package/src/server/responses/policy-fallback.ts +44 -1
- package/src/server/responses/request-prepare.ts +19 -6
- package/src/server/responses/request-transport.ts +25 -6
- package/src/server/responses/run-turn-execution.ts +2 -4
- package/src/server/responses/sidecar-execution.ts +2 -4
- package/src/server/responses-phase-inference.ts +267 -0
- package/src/server/restart-replacement.ts +50 -2
- package/src/server/sse-payload-rewrite.ts +14 -1
- package/src/server/ws-bridge.ts +5 -0
- package/src/service/cli.ts +25 -2
- package/src/service/definition.ts +20 -8
- package/src/service/diagnostics.ts +20 -6
- package/src/service/status-json.ts +116 -0
- package/src/types/config.ts +13 -0
- package/src/types/fallback.ts +38 -0
- package/src/types/provider.ts +9 -1
- package/src/types/request.ts +1 -0
- package/src/types/wire.ts +1 -0
- package/src/update/codex-cli-update-launch-policy.d.mts +9 -0
- package/src/update/codex-cli-update-launch-policy.mjs +30 -0
- package/src/usage/jev-stats.ts +54 -14
- package/gui/dist/assets/App-dnkJ85te.css +0 -1
- package/gui/dist/assets/index-TmaHa54u.js +0 -94
- package/gui/dist/assets/quota-summary-B16x5DZZ.js +0 -1
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The connected client runtime's answer to the bound `POST /api/system/restart` (OCX-6).
|
|
3
|
+
*
|
|
4
|
+
* It is the same restart `ocx restart` drives on a standalone proxy, with one difference the
|
|
5
|
+
* Remote Link parity work requires: a failed restart keeps the old runtime. That is only possible
|
|
6
|
+
* when this process starts its own replacement, so:
|
|
7
|
+
*
|
|
8
|
+
* - Under the desktop app or a viable service supervisor the restart is refused (409
|
|
9
|
+
* `restart_supervised`) and the runtime keeps serving. The supervisor owns this process and its
|
|
10
|
+
* port; handing off to it means exiting before anything proves the replacement can start.
|
|
11
|
+
* - Otherwise the listener drains, a replacement `ocx start` is spawned and must answer within the
|
|
12
|
+
* budget. Success exits this process. A replacement that exited, or never answered and was then
|
|
13
|
+
* stopped, leaves this process to bind the port again, start a fresh tunnel supervisor and
|
|
14
|
+
* republish its records: the old runtime keeps serving and the CLI, still seeing its pid,
|
|
15
|
+
* reports the restart as failed.
|
|
16
|
+
*
|
|
17
|
+
* Everything runs inside {@link CLIENT_RESTART_BUDGET_MS} from acceptance.
|
|
18
|
+
*/
|
|
19
|
+
import {
|
|
20
|
+
CLIENT_RESTART_DRAIN_MS,
|
|
21
|
+
CLIENT_RESTART_READY_BY_MS,
|
|
22
|
+
isDesktopSupervised,
|
|
23
|
+
} from "../lib/system-restart-contract";
|
|
24
|
+
import type { OcxClientConnectionConfig } from "../types";
|
|
25
|
+
import { sameClientConnectionOwner, type readClientConnectionState } from "./state";
|
|
26
|
+
|
|
27
|
+
type ClientConnectionState = ReturnType<typeof readClientConnectionState>;
|
|
28
|
+
|
|
29
|
+
export type ClientRestartSupervisor = "service" | "desktop";
|
|
30
|
+
|
|
31
|
+
export type ClientRestartAdmission =
|
|
32
|
+
| { kind: "accepted"; alreadyRestarting: boolean }
|
|
33
|
+
| { kind: "supervised"; supervisor: ClientRestartSupervisor };
|
|
34
|
+
|
|
35
|
+
export interface ClientRestartIo {
|
|
36
|
+
/** Which supervisor owns this process, or null when it starts its own replacement. */
|
|
37
|
+
supervisor(): Promise<ClientRestartSupervisor | null>;
|
|
38
|
+
/** Stop the tunnel supervisor and the listener, giving in-flight requests up to `drainMs`. */
|
|
39
|
+
stop(drainMs: number): Promise<void>;
|
|
40
|
+
/** Start the replacement; resolves once it answers, rejects when none survives. */
|
|
41
|
+
spawnReplacement(deadlineAt: number): Promise<void>;
|
|
42
|
+
/** Serve again on this process: listener, fresh tunnel supervisor, records. */
|
|
43
|
+
resume(): Promise<void>;
|
|
44
|
+
exitProcess(code: number): void;
|
|
45
|
+
now?: () => number;
|
|
46
|
+
sleep?: (ms: number) => Promise<void>;
|
|
47
|
+
warn?: (message: string) => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ClientRestartController {
|
|
51
|
+
request(): Promise<ClientRestartAdmission>;
|
|
52
|
+
/** Settles when the restart in flight (if any) has finished or rolled back. Tests only. */
|
|
53
|
+
settled(): Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Stable, path-free label for a failure (an error message can carry a path). */
|
|
57
|
+
function failureCode(error: unknown): string {
|
|
58
|
+
const code = error && typeof error === "object" && "code" in error ? (error as { code?: unknown }).code : undefined;
|
|
59
|
+
return typeof code === "string" && /^[A-Za-z0-9_]{1,64}$/.test(code) ? code : "failed";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createClientRestartController(io: ClientRestartIo): ClientRestartController {
|
|
63
|
+
const now = io.now ?? Date.now;
|
|
64
|
+
const sleep = io.sleep ?? Bun.sleep;
|
|
65
|
+
const warn = io.warn ?? ((message: string) => console.warn(message));
|
|
66
|
+
let flight: Promise<void> | null = null;
|
|
67
|
+
|
|
68
|
+
const run = async (acceptedAt: number): Promise<void> => {
|
|
69
|
+
// Let the 202 reach the CLI before the listener starts closing.
|
|
70
|
+
await sleep(200);
|
|
71
|
+
try {
|
|
72
|
+
await io.stop(CLIENT_RESTART_DRAIN_MS);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
warn(`[client] restart: stopping the runtime failed (${failureCode(error)}); continuing`);
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
await io.spawnReplacement(acceptedAt + CLIENT_RESTART_READY_BY_MS);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
warn(`[client] restart: the replacement did not start (${failureCode(error)}); this runtime keeps serving`);
|
|
80
|
+
try {
|
|
81
|
+
await io.resume();
|
|
82
|
+
} catch (resumeError) {
|
|
83
|
+
warn(`[client] restart: this runtime could not serve again (${failureCode(resumeError)}); run 'ocx start'`);
|
|
84
|
+
io.exitProcess(1);
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
io.exitProcess(0);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
async request() {
|
|
93
|
+
const supervisor = await io.supervisor();
|
|
94
|
+
if (supervisor) return { kind: "supervised", supervisor };
|
|
95
|
+
if (flight) return { kind: "accepted", alreadyRestarting: true };
|
|
96
|
+
flight = run(now()).finally(() => { flight = null; });
|
|
97
|
+
return { kind: "accepted", alreadyRestarting: false };
|
|
98
|
+
},
|
|
99
|
+
async settled() {
|
|
100
|
+
await flight;
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Production supervision check: the desktop app, then a service that will really relaunch us. */
|
|
106
|
+
export async function clientRestartSupervisor(): Promise<ClientRestartSupervisor | null> {
|
|
107
|
+
if (isDesktopSupervised()) return "desktop";
|
|
108
|
+
if (process.env.OCX_SERVICE !== "1") return null;
|
|
109
|
+
// Same viability test as the standalone restart: stale service assets would not relaunch.
|
|
110
|
+
const { isServiceViable } = await import("../service/orchestration");
|
|
111
|
+
return isServiceViable() ? "service" : null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* After a failed replacement: serve the same connection again only while it is still the durable
|
|
116
|
+
* one (same owner and link). Anything else, a disconnect or a different connection, goes to the
|
|
117
|
+
* recycle, which starts what the current state calls for instead of a stale client runtime.
|
|
118
|
+
*/
|
|
119
|
+
export function clientRestartRollbackAction(
|
|
120
|
+
served: OcxClientConnectionConfig,
|
|
121
|
+
current: ClientConnectionState,
|
|
122
|
+
): "resume" | "recycle" {
|
|
123
|
+
return current.kind === "connected" && sameClientConnectionOwner(current.value, served)
|
|
124
|
+
&& current.value.link?.linkId === served.link?.linkId
|
|
125
|
+
? "resume"
|
|
126
|
+
: "recycle";
|
|
127
|
+
}
|
package/src/client/runtime.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { flushLinkDiagnostics, recordLinkDiagnostic } from "../link/diagnostics";
|
|
1
2
|
import { existsSync } from "node:fs";
|
|
2
3
|
import type { Server } from "bun";
|
|
3
4
|
import { siblingRuntimeField, withSiblingMarker } from "../codex/sibling-start";
|
|
@@ -9,6 +10,7 @@ import { createLocalAttestationSecret } from "../lib/local-management-attestatio
|
|
|
9
10
|
import { loadServiceTokenFromFile, serviceApiTokenFingerprint } from "../lib/service-secrets";
|
|
10
11
|
import { handledSignalExitCode } from "../lib/handled-signal-exit";
|
|
11
12
|
import {
|
|
13
|
+
CLIENT_RESTART_REBIND_WAIT_MS,
|
|
12
14
|
DESKTOP_RESTART_EXIT_CODE,
|
|
13
15
|
DESKTOP_SUPERVISED_ENV,
|
|
14
16
|
DESKTOP_SUPERVISED_PORT_WAIT_MS,
|
|
@@ -19,16 +21,27 @@ import type { ReplacementStartRequest } from "../server/restart-replacement";
|
|
|
19
21
|
import type { OcxClientConnectionConfig } from "../types";
|
|
20
22
|
import { createLinkKeySource } from "./link-ingress";
|
|
21
23
|
import { HOME_INITIATED_LINK_TUNNEL } from "./link-relay";
|
|
24
|
+
import { clientInitiatedLinkReadiness, createHomeInitiatedLinkReadiness } from "./link-readiness";
|
|
22
25
|
import { createClientLinkSupervisor, type ClientLinkSupervisor } from "./link-tunnel";
|
|
23
26
|
import { clientLinkStatePath, isChildInitiatedLink, recordChildInitiatedLink } from "./link-state";
|
|
24
27
|
import { startMachineListener, type MachineListenerDeps } from "./machine-listener";
|
|
25
28
|
import { isLinkConnection, readClientConnectionState } from "./state";
|
|
29
|
+
import { withClientLifecycleSync } from "./lifecycle-lock";
|
|
30
|
+
import { clientRestartRollbackAction, clientRestartSupervisor, createClientRestartController, type ClientRestartController } from "./runtime-restart";
|
|
31
|
+
import { createLinkParityReporter, type LinkParityReporter } from "./link-parity-reporter";
|
|
26
32
|
|
|
27
33
|
let activeServer: Server<unknown> | null = null;
|
|
28
34
|
let activePort: number | null = null;
|
|
29
35
|
let activeSupervisor: ClientLinkSupervisor | null = null;
|
|
36
|
+
let activeReporter: LinkParityReporter | null = null;
|
|
30
37
|
let recycleScheduled = false;
|
|
31
38
|
|
|
39
|
+
/**
|
|
40
|
+
* When this runtime's process started (OCX-6, P5): what `/healthz` reports as `startedAt` and the
|
|
41
|
+
* Child reports to its Home. A Home settings apply recorded after it means this runtime predates it.
|
|
42
|
+
*/
|
|
43
|
+
export const CLIENT_RUNTIME_STARTED_AT = new Date(Date.now() - process.uptime() * 1000).toISOString();
|
|
44
|
+
|
|
32
45
|
/**
|
|
33
46
|
* How long link mode waits for its configured port: the budget a hard-pinned `ocx start --port`
|
|
34
47
|
* gives `reclaimListenPort` (`src/cli/index.ts`).
|
|
@@ -53,6 +66,8 @@ export interface ClientRuntimeIo {
|
|
|
53
66
|
/** Link-mode port budget; defaults to {@link LINK_PORT_WAIT_MS}. */
|
|
54
67
|
portWaitMs?: number;
|
|
55
68
|
startListener?: typeof startMachineListener;
|
|
69
|
+
/** Which supervisor owns this process for a bound restart; tests replace the production check. */
|
|
70
|
+
restartSupervisor?: () => Promise<"service" | "desktop" | null>;
|
|
56
71
|
}
|
|
57
72
|
|
|
58
73
|
export interface StandaloneRecycleIo {
|
|
@@ -151,6 +166,7 @@ export async function recycleStandalone(
|
|
|
151
166
|
console.warn(`[client] listener stop failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
152
167
|
}
|
|
153
168
|
cleanup();
|
|
169
|
+
await flushLinkDiagnostics();
|
|
154
170
|
// Recycling back to standalone after `ocx disconnect` must actually bring a standalone
|
|
155
171
|
// proxy back, under every launch shape.
|
|
156
172
|
//
|
|
@@ -214,7 +230,7 @@ export async function bindClientListener(
|
|
|
214
230
|
preferred: number;
|
|
215
231
|
explicitPort: boolean;
|
|
216
232
|
configuredPort: number;
|
|
217
|
-
} & Pick<MachineListenerDeps, "linkStatus" | "linkKeySource" | "linkTunnel">,
|
|
233
|
+
} & Pick<MachineListenerDeps, "linkStatus" | "linkKeySource" | "linkTunnel" | "linkReadiness" | "restart">,
|
|
218
234
|
io: ClientRuntimeIo = {},
|
|
219
235
|
): Promise<{ server: Server<unknown>; port: number }> {
|
|
220
236
|
const { linkMode } = request;
|
|
@@ -254,6 +270,8 @@ export async function bindClientListener(
|
|
|
254
270
|
...(request.linkStatus ? { linkStatus: request.linkStatus } : {}),
|
|
255
271
|
...(request.linkKeySource ? { linkKeySource: request.linkKeySource } : {}),
|
|
256
272
|
...(request.linkTunnel ? { linkTunnel: request.linkTunnel } : {}),
|
|
273
|
+
...(request.linkReadiness ? { linkReadiness: request.linkReadiness } : {}),
|
|
274
|
+
...(request.restart ? { restart: request.restart } : {}),
|
|
257
275
|
});
|
|
258
276
|
return { server, port: server.port ?? port };
|
|
259
277
|
} catch (error) {
|
|
@@ -286,33 +304,142 @@ export async function startClientRuntime(
|
|
|
286
304
|
if (linkMode && state.value.link) {
|
|
287
305
|
try { recordChildInitiatedLink(state.value.link.linkId); } catch { /* the sidecar check below still applies */ }
|
|
288
306
|
}
|
|
289
|
-
const
|
|
290
|
-
? createClientLinkSupervisor({
|
|
291
|
-
onLinkEnded: () => scheduleStandaloneRecycle(state.value.tokenFingerprint),
|
|
292
|
-
linkKey,
|
|
293
|
-
})
|
|
294
|
-
: null;
|
|
307
|
+
const supervised = linkMode && existsSync(clientLinkStatePath());
|
|
295
308
|
// A Child with no sidecar and no record of joining itself was connected by its Home over
|
|
296
309
|
// `ssh -R`; that link keeps 2.67.0's unproven forward. A Child-initiated link that lost its
|
|
297
310
|
// sidecar gets no gate at all, so the relay refuses it.
|
|
298
|
-
const homeInitiated = linkMode && !
|
|
311
|
+
const homeInitiated = linkMode && !supervised && !!state.value.link
|
|
299
312
|
&& !isChildInitiatedLink(state.value.link.linkId);
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
+
// One secret for the life of this process: a rollback republishes the same identity.
|
|
314
|
+
const attestationSecret = createLocalAttestationSecret();
|
|
315
|
+
|
|
316
|
+
// Bind, start a fresh tunnel supervisor and publish. Runs once at start and again when a failed
|
|
317
|
+
// restart rolls back (OCX-6); the stopped supervisor cannot be started twice, so each call builds one.
|
|
318
|
+
// The port this process actually serves on; a rollback binds exactly this one again.
|
|
319
|
+
let servedPort = preferred;
|
|
320
|
+
// Returns false only for a rollback whose connection changed while it was binding: the new
|
|
321
|
+
// listener is then closed unpublished and the caller recycles.
|
|
322
|
+
const serve = async (rollback?: { port: number; portWaitMs: number }): Promise<boolean> => {
|
|
323
|
+
const supervisor = supervised
|
|
324
|
+
? createClientLinkSupervisor({
|
|
325
|
+
diagnostic: recordLinkDiagnostic,
|
|
326
|
+
onLinkEnded: () => scheduleStandaloneRecycle(state.value.tokenFingerprint),
|
|
327
|
+
linkKey,
|
|
328
|
+
})
|
|
329
|
+
: null;
|
|
330
|
+
// What `/readyz` reports about the link: the supervisor's tunnel, or for a Home-initiated
|
|
331
|
+
// link the shared relay preflight. A link with neither refuses every relayed request.
|
|
332
|
+
const link = state.value.link;
|
|
333
|
+
const linkReadiness: MachineListenerDeps["linkReadiness"] = !linkMode ? undefined
|
|
334
|
+
: supervisor ? () => clientInitiatedLinkReadiness(supervisor.status())
|
|
335
|
+
: homeInitiated && link && linkKey
|
|
336
|
+
? createHomeInitiatedLinkReadiness({ target: { tunnelPort: link.tunnelPort, linkId: link.linkId, apiKeyId: state.value.apiKeyId }, linkKey })
|
|
337
|
+
: () => ({ ready: false, reason: "tunnel_unavailable" });
|
|
338
|
+
const { server, port: boundPort } = await bindClientListener({
|
|
339
|
+
state: state.value,
|
|
340
|
+
linkMode,
|
|
341
|
+
// A rollback takes back exactly the port it gave up: never the preferred one, never another.
|
|
342
|
+
preferred: rollback?.port ?? preferred,
|
|
343
|
+
explicitPort: rollback !== undefined || options.port !== undefined,
|
|
344
|
+
configuredPort: config.port,
|
|
345
|
+
restart: { attestationSecret, startedAt: CLIENT_RUNTIME_STARTED_AT, request: () => restartController.request() },
|
|
346
|
+
...(linkMode ? { linkStatus: () => activeSupervisor?.status() ?? { kind: "stopped" as const }, linkKeySource: linkKey } : {}),
|
|
347
|
+
...(supervisor ? { linkTunnel: supervisor } : homeInitiated ? { linkTunnel: HOME_INITIATED_LINK_TUNNEL } : {}),
|
|
348
|
+
...(linkReadiness ? { linkReadiness } : {}),
|
|
349
|
+
}, rollback === undefined ? io : { ...io, portWaitMs: rollback.portWaitMs });
|
|
350
|
+
const publish = (): void => {
|
|
351
|
+
activeServer = server;
|
|
352
|
+
activePort = boundPort;
|
|
353
|
+
servedPort = boundPort;
|
|
354
|
+
activeSupervisor = supervisor;
|
|
355
|
+
supervisor?.start();
|
|
356
|
+
writePid(process.pid);
|
|
357
|
+
writeRuntimePort(clientRuntimeRecord(process.pid, boundPort, attestationSecret));
|
|
358
|
+
if (linkMode && state.value.link && linkKey) {
|
|
359
|
+
activeReporter = createLinkParityReporter({ connection: state.value, linkKey, runtimeStartedAt: CLIENT_RUNTIME_STARTED_AT });
|
|
360
|
+
activeReporter.start();
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
if (!rollback) {
|
|
364
|
+
publish();
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
// A rollback publishes under the client lifecycle lock, after rechecking the connection there,
|
|
368
|
+
// so a disconnect or connect that committed during the bind can never be served over. A lock
|
|
369
|
+
// held by such an operation counts as a change.
|
|
370
|
+
let published = false;
|
|
371
|
+
try {
|
|
372
|
+
published = withClientLifecycleSync(() => {
|
|
373
|
+
if (clientRestartRollbackAction(state.value, readClientConnectionState()) === "recycle") return false;
|
|
374
|
+
publish();
|
|
375
|
+
return true;
|
|
376
|
+
});
|
|
377
|
+
} catch {
|
|
378
|
+
published = false;
|
|
379
|
+
}
|
|
380
|
+
if (!published) {
|
|
381
|
+
try { await server.stop(true); } catch { /* never published */ }
|
|
382
|
+
}
|
|
383
|
+
return published;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
// Stop everything this process serves, giving in-flight requests up to `drainMs`.
|
|
387
|
+
const stopServing = async (drainMs: number): Promise<void> => {
|
|
388
|
+
activeReporter?.stop();
|
|
389
|
+
activeReporter = null;
|
|
390
|
+
try {
|
|
391
|
+
await activeSupervisor?.stop();
|
|
392
|
+
} catch (error) {
|
|
393
|
+
console.warn(`[client] link supervisor stop failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
394
|
+
}
|
|
395
|
+
activeSupervisor = null;
|
|
396
|
+
const server = activeServer;
|
|
397
|
+
activeServer = null;
|
|
398
|
+
if (!server) return;
|
|
399
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
400
|
+
try {
|
|
401
|
+
await Promise.race([
|
|
402
|
+
Promise.resolve(server.stop(false)),
|
|
403
|
+
new Promise<void>(resolve => { timer = setTimeout(resolve, drainMs); }),
|
|
404
|
+
]);
|
|
405
|
+
} catch { /* closed below */ } finally {
|
|
406
|
+
if (timer) clearTimeout(timer);
|
|
407
|
+
}
|
|
408
|
+
try { await server.stop(true); } catch { /* already closed */ }
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const restartController: ClientRestartController = createClientRestartController({
|
|
412
|
+
supervisor: io.restartSupervisor ?? clientRestartSupervisor,
|
|
413
|
+
stop: stopServing,
|
|
414
|
+
spawnReplacement: async deadlineAt => {
|
|
415
|
+
const { replacementStartEnvironment } = await import("../server/management/system-restart");
|
|
416
|
+
const { spawnReplacementStart } = await import("../server/restart-replacement");
|
|
417
|
+
await spawnReplacementStart(
|
|
418
|
+
{ port: activePort ?? config.port, waitForHealth: true, env: replacementStartEnvironment(true), notReady: "terminate" },
|
|
419
|
+
{ deadlineAt },
|
|
420
|
+
);
|
|
421
|
+
},
|
|
422
|
+
resume: async () => {
|
|
423
|
+
// The connection may have changed while the replacement was starting (`ocx disconnect`, a
|
|
424
|
+
// new connect). Serving the old one again would be a stale runtime nothing recycles; hand
|
|
425
|
+
// off to the recycle, which starts whatever the durable state now calls for.
|
|
426
|
+
if (clientRestartRollbackAction(state.value, readClientConnectionState()) === "recycle") {
|
|
427
|
+
await recycleStandalone(state.value.tokenFingerprint);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (!(await serve({ port: servedPort, portWaitMs: CLIENT_RESTART_REBIND_WAIT_MS }))) {
|
|
431
|
+
await recycleStandalone(state.value.tokenFingerprint);
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
exitProcess: async code => {
|
|
435
|
+
await flushLinkDiagnostics();
|
|
436
|
+
process.exit(code);
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
|
|
313
440
|
installCrashGuards();
|
|
314
|
-
|
|
315
|
-
|
|
441
|
+
// serve() publishes the pid and runtime record; the ownership lease is released only after it.
|
|
442
|
+
await serve();
|
|
316
443
|
options.afterPublish?.();
|
|
317
444
|
|
|
318
445
|
let shuttingDown = false;
|
|
@@ -320,18 +447,21 @@ export async function startClientRuntime(
|
|
|
320
447
|
if (shuttingDown) return;
|
|
321
448
|
shuttingDown = true;
|
|
322
449
|
void (async () => {
|
|
450
|
+
activeReporter?.stop();
|
|
451
|
+
activeReporter = null;
|
|
323
452
|
try {
|
|
324
|
-
await
|
|
453
|
+
await activeSupervisor?.stop();
|
|
325
454
|
} catch (error) {
|
|
326
455
|
console.warn(`[client] link supervisor stop failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
327
456
|
}
|
|
328
457
|
activeSupervisor = null;
|
|
329
458
|
try {
|
|
330
|
-
|
|
459
|
+
activeServer?.stop(true);
|
|
331
460
|
} catch (error) {
|
|
332
461
|
console.warn(`[client] listener stop failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
333
462
|
} finally {
|
|
334
463
|
cleanup();
|
|
464
|
+
await flushLinkDiagnostics();
|
|
335
465
|
// launchd relaunches only unsuccessful exits; an external signal must still count.
|
|
336
466
|
process.exit(handledSignalExitCode(signal));
|
|
337
467
|
}
|
|
@@ -12,13 +12,15 @@ import {
|
|
|
12
12
|
backupInvalidConfig,
|
|
13
13
|
hardenConfigDir,
|
|
14
14
|
hardenExistingSecret,
|
|
15
|
+
loadConfig,
|
|
15
16
|
withConfigMutationLockSync,
|
|
16
17
|
} from "../config";
|
|
17
18
|
import { notifyCodexStoreLoaded } from "../lib/decision-quota-sink";
|
|
18
19
|
import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
|
|
19
|
-
import type { CodexAccountCredentialRecord, CodexAccountCredentials } from "../types";
|
|
20
|
+
import type { CodexAccount, CodexAccountCredentialRecord, CodexAccountCredentials } from "../types";
|
|
20
21
|
import { advanceCodexCredentialMutationEpoch } from "./credential-mutation-epoch";
|
|
21
22
|
import { isValidCodexAccountId } from "./account-id";
|
|
23
|
+
import { codexAccountLogLabel, fallbackCodexAccountLogLabel } from "./account-label";
|
|
22
24
|
import type { PoolQuotaWriter } from "./quota-types";
|
|
23
25
|
import { CODEX_REFRESH_FLIGHT_CEILING_MS } from "./quota-recovery-timing";
|
|
24
26
|
|
|
@@ -183,6 +185,18 @@ function persistCredentialMutation(store: CodexAccountStore): void {
|
|
|
183
185
|
advanceCodexCredentialMutationEpoch();
|
|
184
186
|
}
|
|
185
187
|
|
|
188
|
+
/**
|
|
189
|
+
* The durable `p<hex6>` label the dashboard and usage log show for this pool account, so a refresh
|
|
190
|
+
* failure line can be matched to the account row (#7592). The id hash is only the label of a
|
|
191
|
+
* legacy row that never received one. A config read failure must not replace the refresh error
|
|
192
|
+
* being reported, so it degrades to that same hash.
|
|
193
|
+
*/
|
|
194
|
+
function poolAccountLogLabel(id: string): string {
|
|
195
|
+
let account: CodexAccount | undefined;
|
|
196
|
+
try { account = loadConfig().codexAccounts?.find(row => row.id === id); } catch { account = undefined; }
|
|
197
|
+
return account ? codexAccountLogLabel(account) : fallbackCodexAccountLogLabel(id);
|
|
198
|
+
}
|
|
199
|
+
|
|
186
200
|
/**
|
|
187
201
|
* Validation metadata that survives a credential write.
|
|
188
202
|
*
|
|
@@ -1055,7 +1069,7 @@ export async function forceRefreshCodexPoolToken(
|
|
|
1055
1069
|
if (isTerminalCodexPoolRefreshFailure(error) || isOperationalCodexPoolRefreshFailure(error)) {
|
|
1056
1070
|
if (isTerminalCodexPoolRefreshFailure(error)) clearCodexPoolRefreshFailure(id);
|
|
1057
1071
|
} else {
|
|
1058
|
-
noteCodexPoolRefreshFailure(id, classifyCodexPoolRefreshFailureReason(error), undefined, refreshFence);
|
|
1072
|
+
noteCodexPoolRefreshFailure(id, classifyCodexPoolRefreshFailureReason(error), undefined, refreshFence, poolAccountLogLabel);
|
|
1059
1073
|
}
|
|
1060
1074
|
settle({ kind: "failed", error });
|
|
1061
1075
|
},
|
|
@@ -1376,7 +1390,7 @@ async function resolveCodexToken(
|
|
|
1376
1390
|
.catch(() => undefined);
|
|
1377
1391
|
const failure = classifyChatgptRefreshFailure(res.status, body?.displaySafe ? body.text : "");
|
|
1378
1392
|
const { reason } = failure;
|
|
1379
|
-
noteChatgptRefreshFailure("pool", res.status, failure);
|
|
1393
|
+
noteChatgptRefreshFailure("pool", res.status, failure, poolAccountLogLabel(id));
|
|
1380
1394
|
throw new TokenRefreshError(reason, `Codex token refresh failed (${reason}); reauthenticate the account.`, res.status, failure.code);
|
|
1381
1395
|
}
|
|
1382
1396
|
const data = (await res.json()) as { access_token: string; refresh_token?: string; expires_in: number };
|
|
@@ -182,11 +182,12 @@ async function runCodexRestart(io: CodexRestartServiceIo): Promise<CodexRestartR
|
|
|
182
182
|
// Decided here, before the early returns, because the model picker lives in the DESKTOP
|
|
183
183
|
// app: "no app-server is running" is not a reason to leave a stale roster on screen.
|
|
184
184
|
//
|
|
185
|
-
// The shell
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
185
|
+
// The shell gets a restart attempt whenever its picker cannot be proven current: at
|
|
186
|
+
// or before the catalog mtime it is stale, and newer is unknown because an earlier
|
|
187
|
+
// sync may have rebound an already-existing catalog file (#4761). Quitting it
|
|
188
|
+
// discards composer drafts, unsent picker selections and pending approvals. The
|
|
189
|
+
// attempt is explicit, not a guarantee: the restart helper can refuse or partially
|
|
190
|
+
// fail, and desktopApp reports that outcome.
|
|
190
191
|
//
|
|
191
192
|
// allowHandoff is false and that is deliberate. The handoff waits for the CALLING
|
|
192
193
|
// process to exit, and this runs inside a long-lived proxy that does not, so every
|
|
@@ -98,6 +98,7 @@ import { getMainReserveAuthorization, isMainReserveAuthorizationLive, nativeUser
|
|
|
98
98
|
import { UpstreamRetryEvidenceError } from "../lib/upstream-retry";
|
|
99
99
|
import { getEffectiveCodexAutoSwitchThreshold } from "./account-auto-switch";
|
|
100
100
|
import { CODEX_MAIN_DUPLICATE_ACTION, configuredCodexMainDuplicate } from "./collision-identity";
|
|
101
|
+
import { AgentAssertionUnsupportedError, authorizationCredential, hasAgentAssertionAuthorization } from "../lib/authorization-scheme";
|
|
101
102
|
|
|
102
103
|
/**
|
|
103
104
|
* A request-owned bearer cannot inspect the physical main credential for its plan, but cached
|
|
@@ -701,7 +702,7 @@ async function authorizeReserveCredential(
|
|
|
701
702
|
|
|
702
703
|
function selectedCodexToken(headers: Headers): { accessToken: string; chatgptAccountId: string } {
|
|
703
704
|
return {
|
|
704
|
-
accessToken: headers
|
|
705
|
+
accessToken: authorizationCredential(headers) ?? "",
|
|
705
706
|
chatgptAccountId: headers.get("chatgpt-account-id") ?? "",
|
|
706
707
|
};
|
|
707
708
|
}
|
|
@@ -716,7 +717,7 @@ function selectedCodexToken(headers: Headers): { accessToken: string; chatgptAcc
|
|
|
716
717
|
export function callerCodexWorkspaceAccountId(headers: Headers): string | undefined {
|
|
717
718
|
const explicit = headers.get("chatgpt-account-id");
|
|
718
719
|
if (explicit) return explicit;
|
|
719
|
-
const bearer = headers
|
|
720
|
+
const bearer = authorizationCredential(headers);
|
|
720
721
|
return bearer ? extractAccountId(undefined, bearer) : undefined;
|
|
721
722
|
}
|
|
722
723
|
|
|
@@ -807,7 +808,7 @@ function assertMainAccountPolicy(
|
|
|
807
808
|
|
|
808
809
|
/** No auth-file I/O: an unsigned claim alone never identifies a caller as stored main. */
|
|
809
810
|
function callerMatchesObservedMain(headers: Headers): boolean {
|
|
810
|
-
const bearer = headers
|
|
811
|
+
const bearer = authorizationCredential(headers);
|
|
811
812
|
if (!bearer) return false;
|
|
812
813
|
const effectiveAccountId = headers.get("chatgpt-account-id")
|
|
813
814
|
?? extractAccountId(undefined, bearer);
|
|
@@ -831,7 +832,7 @@ function sameCredentialMaterial(a: string, b: string): boolean {
|
|
|
831
832
|
* cooldown — the safe direction.
|
|
832
833
|
*/
|
|
833
834
|
function callerIsCooledPoolAccount(headers: Headers, config: OcxConfig, accountId: string): boolean {
|
|
834
|
-
const bearer = headers
|
|
835
|
+
const bearer = authorizationCredential(headers);
|
|
835
836
|
if (!bearer) return true;
|
|
836
837
|
const callerAccountId = headers.get("chatgpt-account-id") ?? extractAccountId(undefined, bearer);
|
|
837
838
|
if (callerAccountId === undefined) return true;
|
|
@@ -1767,7 +1768,7 @@ export class CodexMainSubstitutionUnavailableError extends Error {
|
|
|
1767
1768
|
|
|
1768
1769
|
/** Dispatch identity comes only from an owned observation of the selected credential. */
|
|
1769
1770
|
function selectedMainQuotaDispatch(selected: Headers): MainQuotaDispatch | undefined {
|
|
1770
|
-
const bearer = selected
|
|
1771
|
+
const bearer = authorizationCredential(selected);
|
|
1771
1772
|
if (!bearer) return undefined;
|
|
1772
1773
|
const accountId = selected.get("chatgpt-account-id") ?? extractAccountId(undefined, bearer);
|
|
1773
1774
|
return captureMainQuotaDispatch(bearer, accountId, captureConfigGeneration());
|
|
@@ -1792,6 +1793,8 @@ export function materializeCodexUpstreamAuth(
|
|
|
1792
1793
|
ctx: CodexAuthContext,
|
|
1793
1794
|
options: CodexAuthMaterializationOptions = {},
|
|
1794
1795
|
): Headers {
|
|
1796
|
+
// assertion_only must never be answered with a pool, stored-main, or other substituted Bearer.
|
|
1797
|
+
if (hasAgentAssertionAuthorization(headers)) throw new AgentAssertionUnsupportedError();
|
|
1795
1798
|
if (ctx.kind === "main") ctx.mainQuotaDispatch = undefined;
|
|
1796
1799
|
const selected = new Headers();
|
|
1797
1800
|
for (const name of FORWARD_HEADERS) {
|
|
@@ -1814,7 +1817,7 @@ export function materializeCodexUpstreamAuth(
|
|
|
1814
1817
|
}
|
|
1815
1818
|
if (ctx.kind === "main" && options.substituteMainCredential !== true
|
|
1816
1819
|
&& !selected.has("chatgpt-account-id")) {
|
|
1817
|
-
const bearer = selected
|
|
1820
|
+
const bearer = authorizationCredential(selected);
|
|
1818
1821
|
const accountId = bearer ? extractAccountId(undefined, bearer) : undefined;
|
|
1819
1822
|
if (accountId) selected.set("chatgpt-account-id", accountId);
|
|
1820
1823
|
}
|
|
@@ -1892,6 +1895,7 @@ export async function materializeCodexUpstreamAuthAsync(
|
|
|
1892
1895
|
ctx: CodexAuthContext,
|
|
1893
1896
|
options: CodexAuthMaterializationOptions = {},
|
|
1894
1897
|
): Promise<Headers> {
|
|
1898
|
+
if (hasAgentAssertionAuthorization(headers)) throw new AgentAssertionUnsupportedError();
|
|
1895
1899
|
if (ctx.kind === "main") ctx.mainQuotaDispatch = undefined;
|
|
1896
1900
|
if (requiresReserveAuthorization(options.config, options.modelId, options.admission)) {
|
|
1897
1901
|
return materializeReserveUpstreamAuth(headers, ctx, options);
|
|
@@ -55,6 +55,12 @@ import {
|
|
|
55
55
|
export function nativeEffortClamp(slug: string, effort: string | undefined): string | null {
|
|
56
56
|
if (!effort || (effort !== "max" && effort !== "ultra")) return null;
|
|
57
57
|
if (slug.includes("/")) return null; // routed models map efforts in their adapters
|
|
58
|
+
// `ultra` is a client-facing catalog choice (max reasoning plus proactive delegation) that
|
|
59
|
+
// codex-rs converts to `max` before any request (core/src/client.rs
|
|
60
|
+
// `reasoning_effort_for_request`). The ChatGPT backend rejects it with HTTP 400 even for a
|
|
61
|
+
// model whose pinned ladder lists it (#7590), so it is never a native wire value: fold it to
|
|
62
|
+
// `max`, then clamp `max` like a direct selection.
|
|
63
|
+
const wire = "max";
|
|
58
64
|
const entry = UPSTREAM_NATIVE_ENTRIES.get(slug);
|
|
59
65
|
const levels = Array.isArray(entry?.supported_reasoning_levels)
|
|
60
66
|
? entry.supported_reasoning_levels as Array<{ effort?: string }>
|
|
@@ -65,10 +71,11 @@ export function nativeEffortClamp(slug: string, effort: string | undefined): str
|
|
|
65
71
|
// a retired slug a client still asks for, and future old-ladder slugs)
|
|
66
72
|
// really stops at xhigh — the ChatGPT backend error names exactly none..xhigh — so clamp
|
|
67
73
|
// the synthetic top tier.
|
|
68
|
-
|
|
74
|
+
if (!isGpt56NativeSlug(slug)) return "xhigh";
|
|
75
|
+
return effort === wire ? null : wire;
|
|
69
76
|
}
|
|
70
77
|
const supported = levels.flatMap(l => typeof l.effort === "string" ? [l.effort] : []);
|
|
71
|
-
if (supported.includes(
|
|
78
|
+
if (supported.includes(wire)) return effort === wire ? null : wire;
|
|
72
79
|
const rank = ["minimal", "low", "medium", "high", "xhigh", "max"];
|
|
73
80
|
const highest = supported
|
|
74
81
|
.filter(e => rank.includes(e))
|
|
@@ -119,6 +119,7 @@ export interface CatalogGatherProviderModelOutcome {
|
|
|
119
119
|
readonly state: "authoritative" | "degraded";
|
|
120
120
|
}
|
|
121
121
|
export interface ModelsAuthResolution {
|
|
122
|
+
readonly workbuddy?: import("../../oauth/types").WorkBuddyOAuthMetadata;
|
|
122
123
|
readonly apiKey: string | undefined;
|
|
123
124
|
readonly observed: boolean;
|
|
124
125
|
readonly oauthAccountId?: string;
|
|
@@ -334,8 +335,9 @@ export function captureModelsRequest(
|
|
|
334
335
|
name: string,
|
|
335
336
|
provider: OcxProviderConfig,
|
|
336
337
|
oauthApiBaseUrl: string | undefined,
|
|
338
|
+
workbuddy?: import("../../oauth/types").WorkBuddyOAuthMetadata,
|
|
337
339
|
): CapturedModelsRequest {
|
|
338
|
-
const observed = { oauthApiBaseUrl };
|
|
340
|
+
const observed = { oauthApiBaseUrl, workbuddy };
|
|
339
341
|
const withoutCredential = buildModelsRequest(provider, undefined, name, observed);
|
|
340
342
|
const withCredential = buildModelsRequest(provider, REQUEST_CREDENTIAL_SENTINEL, name, observed);
|
|
341
343
|
const method = withoutCredential.method ?? "GET";
|
|
@@ -400,7 +402,7 @@ export function captureProviderGather(
|
|
|
400
402
|
: authResolver.kind === "refreshing" && provider.authMode === "oauth" && mayResolveModelsOAuth(name, provider)
|
|
401
403
|
? getOAuthCredentialApiBaseUrl(name)
|
|
402
404
|
: undefined;
|
|
403
|
-
const request = captureModelsRequest(name, provider, oauthApiBaseUrl);
|
|
405
|
+
const request = captureModelsRequest(name, provider, oauthApiBaseUrl, observedAuth?.workbuddy);
|
|
404
406
|
const refreshingOAuthAccountId = !observedAuth && authResolver.kind === "refreshing" && provider.authMode === "oauth" && mayResolveModelsOAuth(name, provider)
|
|
405
407
|
? getAccountSet(name)?.activeAccountId
|
|
406
408
|
: undefined;
|
|
@@ -809,6 +809,10 @@ export function normalizeRoutedCatalogEntry(
|
|
|
809
809
|
delete entry.multi_agent_version;
|
|
810
810
|
delete entry.multi_agent_reasoning_effort;
|
|
811
811
|
delete entry.use_responses_lite;
|
|
812
|
+
// Codex 0.164 sends positional `configuration_update` effort items only for rows declaring this
|
|
813
|
+
// and freezes the request-level effort for the context window. A routed provider never
|
|
814
|
+
// negotiated that contract, so a row cloned from a GPT-6 template must not inherit it.
|
|
815
|
+
delete entry.supports_reasoning_effort_updates;
|
|
812
816
|
delete entry.supports_websockets;
|
|
813
817
|
// Routed rows cloned from native templates must not inherit OpenAI-only experimental context
|
|
814
818
|
// delivery. Codex reads the flag as "this model accepts experimental context history" and drives
|