@bitkyc08/opencodex 2.7.43 → 2.8.2-preview.20260731
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 +34 -8
- package/gui/dist/assets/index-BHsKRFh9.css +1 -0
- package/gui/dist/assets/index-GC0Vlu1Z.js +67 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +42 -7
- package/src/adapters/cursor/discovery.ts +4 -1
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/kiro.ts +15 -1
- package/src/adapters/openai-chat.ts +55 -4
- package/src/claude/alias.ts +94 -14
- package/src/claude/outbound.ts +6 -3
- package/src/cli/catalog-prewarm.ts +24 -0
- package/src/cli/claude-desktop.ts +2 -2
- package/src/cli/claude.ts +32 -7
- package/src/cli/doctor.ts +48 -1
- package/src/cli/index.ts +5 -0
- package/src/cli/init.ts +129 -102
- package/src/cli/interactive-confirm.ts +5 -1
- package/src/cli/star-prompt.ts +26 -4
- package/src/cli/v2.ts +10 -1
- package/src/codex/account-store.ts +2 -0
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/metadata.ts +6 -0
- package/src/codex/catalog/parsing.ts +26 -1
- package/src/codex/catalog/provider-fetch.ts +240 -82
- package/src/codex/catalog/sync.ts +27 -5
- package/src/codex/catalog.ts +3 -3
- package/src/codex/features.ts +524 -5
- package/src/codex/quota.ts +77 -2
- package/src/codex/runtime.ts +10 -1
- package/src/config.ts +8 -0
- package/src/generated/jawcode-model-metadata.ts +12 -12
- package/src/github/star-state.ts +191 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +6 -20
- package/src/lib/destination-policy.ts +21 -3
- package/src/lib/provider-outbound.ts +8 -2
- package/src/lib/shadow-call.ts +30 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/win-exec.ts +12 -2
- package/src/lib/winsw.ts +6 -0
- package/src/oauth/index.ts +29 -5
- package/src/oauth/key-providers.ts +21 -2
- package/src/oauth/kiro-credentials.ts +129 -9
- package/src/oauth/kiro.ts +15 -3
- package/src/oauth/login-cli.ts +1 -1
- package/src/oauth/store.ts +2 -0
- package/src/providers/derive.ts +2 -2
- package/src/providers/free-directory.ts +4 -1
- package/src/providers/model-discovery.ts +356 -0
- package/src/providers/registry.ts +114 -0
- package/src/router.ts +5 -3
- package/src/server/auth-cors.ts +4 -2
- package/src/server/index.ts +3 -3
- package/src/server/live.ts +75 -25
- package/src/server/management/agent-settings-routes.ts +82 -8
- package/src/server/management/config-routes.ts +24 -7
- package/src/server/management/context.ts +11 -1
- package/src/server/management/model-routes.ts +61 -14
- package/src/server/management/provider-routes.ts +44 -9
- package/src/server/management/shared.ts +18 -5
- package/src/server/management/sidebar-routes.ts +39 -0
- package/src/server/management-api.ts +3 -1
- package/src/server/proxy-liveness.ts +9 -2
- package/src/server/responses/core.ts +31 -20
- package/src/server/responses/upstream-error.ts +48 -0
- package/src/server/startup-action-control.ts +30 -14
- package/src/service.ts +395 -31
- package/src/storage/policy-job.ts +26 -5
- package/src/storage/restore-job.ts +16 -5
- package/src/storage/worker-lifecycle.ts +81 -0
- package/src/tray/windows.ts +86 -13
- package/src/types.ts +16 -0
- package/src/update/badge.ts +72 -0
- package/src/update/job.ts +8 -4
- package/src/usage/expected-prices.ts +6 -5
- package/src/usage/log.ts +8 -0
- package/src/web-search/loop.ts +57 -16
- package/gui/dist/assets/index-Czw-jpTU.css +0 -1
- package/gui/dist/assets/index-cmds12BG.js +0 -67
|
@@ -37,7 +37,7 @@ import { readUsageEntries } from "../../usage/log";
|
|
|
37
37
|
import { getUsageDebugLogEntries } from "../../usage/debug";
|
|
38
38
|
import { parseRange, parseUsageSurface, summarizeUsage } from "../../usage/summary";
|
|
39
39
|
import { stripCodexRuntimeProviderFields } from "../../codex/auth-context";
|
|
40
|
-
import { getProviderRegistryEntry } from "../../providers/registry";
|
|
40
|
+
import { getProviderRegistryEntry, providerMatchesRegistryTransport } from "../../providers/registry";
|
|
41
41
|
import { getDebugLogEntries } from "../../lib/debug-log-buffer";
|
|
42
42
|
import { getInjectionDebugLogEntries } from "../../lib/injection-debug-log";
|
|
43
43
|
import {
|
|
@@ -201,7 +201,7 @@ export async function fetchGrokCandidateModels(config: OcxConfig): Promise<GrokC
|
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
export function stripRegistryOnlyStaticHeaders(name: string, provider: OcxProviderConfig): OcxProviderConfig {
|
|
204
|
-
const entry = getProviderRegistryEntry(name);
|
|
204
|
+
const entry = providerMatchesRegistryTransport(name, provider) ? getProviderRegistryEntry(name) : undefined;
|
|
205
205
|
if (!entry?.staticHeaders || !provider.headers) return provider;
|
|
206
206
|
const headerEntries = Object.entries(provider.headers);
|
|
207
207
|
const staticEntries = Object.entries(entry.staticHeaders);
|
|
@@ -214,14 +214,14 @@ export function stripRegistryOnlyStaticHeaders(name: string, provider: OcxProvid
|
|
|
214
214
|
|
|
215
215
|
/** Shared Desktop profile DTO builder for the management API and CLI. */
|
|
216
216
|
export async function buildClaudeDesktopState(config: OcxConfig, stored?: OcxClaudeDesktopProfile) {
|
|
217
|
-
const { filterCatalogVisibleModels, nativeOpenAiContextWindow,
|
|
217
|
+
const { filterCatalogVisibleModels, nativeOpenAiContextWindow, desktopVisibleNativeSlugs } = await import("../../codex/catalog");
|
|
218
218
|
const { DESKTOP_SUPPORTS_1M_THRESHOLD } = await import("../../claude/desktop-3p");
|
|
219
219
|
const { reconcileDesktopProfile, renderDesktopProfile } = await import("../../claude/desktop-profile");
|
|
220
220
|
const routed = filterCatalogVisibleModels(await fetchAllModels(config), config);
|
|
221
221
|
const profileModels: DesktopProfileModel[] = [
|
|
222
222
|
// Native rows carry their real context window from the same accessor the Grok sync
|
|
223
223
|
// uses — otherwise Sol's 372k and gpt-5.5's 272k render as blank on Desktop.
|
|
224
|
-
...
|
|
224
|
+
...desktopVisibleNativeSlugs(config).map(id => {
|
|
225
225
|
const contextWindow = nativeOpenAiContextWindow(id);
|
|
226
226
|
return { route: `native/${id}`, label: `${id} (native)`,
|
|
227
227
|
...(contextWindow !== undefined ? { contextWindow } : {}) };
|
|
@@ -233,6 +233,19 @@ export async function buildClaudeDesktopState(config: OcxConfig, stored?: OcxCla
|
|
|
233
233
|
})),
|
|
234
234
|
];
|
|
235
235
|
const profile = reconcileDesktopProfile(stored ?? config.claudeCode?.desktopProfile, profileModels);
|
|
236
|
+
if (config.claudeCode?.desktopNativeModels === false) {
|
|
237
|
+
for (const route of Object.keys(profile.assignments)) {
|
|
238
|
+
if (route.startsWith("native/")) delete profile.assignments[route];
|
|
239
|
+
}
|
|
240
|
+
for (const family of ["opus", "fable", "sonnet", "haiku"] as const) {
|
|
241
|
+
const current = profile.defaults[family];
|
|
242
|
+
if (current?.startsWith("native/")) {
|
|
243
|
+
profile.defaults[family] = Object.keys(profile.assignments)
|
|
244
|
+
.filter(route => profile.assignments[route]?.family === family)
|
|
245
|
+
.sort()[0] ?? null;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
236
249
|
const available = new Set(profileModels.map(model => model.route));
|
|
237
250
|
const modelByRoute = new Map(profileModels.map(model => [model.route, model]));
|
|
238
251
|
// Effort support: routed models with a non-empty reasoningEfforts ladder support effort;
|
|
@@ -241,7 +254,7 @@ export async function buildClaudeDesktopState(config: OcxConfig, stored?: OcxCla
|
|
|
241
254
|
for (const m of routed) {
|
|
242
255
|
effortByRoute.set(`${m.provider}/${m.id}`, Array.isArray(m.reasoningEfforts) && m.reasoningEfforts.length > 0);
|
|
243
256
|
}
|
|
244
|
-
for (const id of
|
|
257
|
+
for (const id of desktopVisibleNativeSlugs(config)) {
|
|
245
258
|
effortByRoute.set(`native/${id}`, true);
|
|
246
259
|
}
|
|
247
260
|
const models = Object.keys(profile.assignments).sort().map(route => ({
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* /api/github/star and /api/update/badge — the two cheap polls behind the
|
|
3
|
+
* sidebar's GitHub star and update controls.
|
|
4
|
+
*
|
|
5
|
+
* Both ride the standard management gate (auth + origin check happen before
|
|
6
|
+
* dispatch), and both are scalar-only: a star state enum, a repo slug, version
|
|
7
|
+
* strings, and a fixed error code. No GitHub token, account login, or raw `gh`/npm
|
|
8
|
+
* output is ever serialized here — starring runs through the user's own `gh` CLI and
|
|
9
|
+
* this surface only learns the yes/no answer. `gh` writes the authenticated account
|
|
10
|
+
* name to stderr, so that output is discarded at the source rather than forwarded.
|
|
11
|
+
*/
|
|
12
|
+
import { jsonResponse } from "../auth-cors";
|
|
13
|
+
import type { ManagementContext } from "./context";
|
|
14
|
+
|
|
15
|
+
export async function handleSidebarRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
16
|
+
const { req, url } = ctx;
|
|
17
|
+
|
|
18
|
+
if (url.pathname === "/api/github/star" && req.method === "GET") {
|
|
19
|
+
const { getStarStatus } = await import("../../github/star-state");
|
|
20
|
+
return jsonResponse(await getStarStatus());
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (url.pathname === "/api/github/star" && req.method === "POST") {
|
|
24
|
+
const { starRepository } = await import("../../github/star-state");
|
|
25
|
+
const result = await starRepository();
|
|
26
|
+
return jsonResponse({
|
|
27
|
+
...result.status,
|
|
28
|
+
ok: result.ok,
|
|
29
|
+
...(result.code ? { code: result.code } : {}),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (url.pathname === "/api/update/badge" && req.method === "GET") {
|
|
34
|
+
const { readUpdateBadge } = await import("../../update/badge");
|
|
35
|
+
return jsonResponse(readUpdateBadge());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
@@ -65,6 +65,7 @@ import { handleAgentSettingsRoutes } from "./management/agent-settings-routes";
|
|
|
65
65
|
import { handleOauthAccountRoutes } from "./management/oauth-account-routes";
|
|
66
66
|
import { handleComboRoutes } from "./management/combo-routes";
|
|
67
67
|
import { handleSystemRoutes } from "./management/system-routes";
|
|
68
|
+
import { handleSidebarRoutes } from "./management/sidebar-routes";
|
|
68
69
|
import type { ManagementContext } from "./management/context";
|
|
69
70
|
export type { ManagementApiDeps } from "./management/context";
|
|
70
71
|
import { fetchAllModels } from "./management/shared";
|
|
@@ -130,7 +131,8 @@ export async function handleManagementAPI(req: Request, url: URL, config: OcxCon
|
|
|
130
131
|
?? (await handleAgentSettingsRoutes(ctx))
|
|
131
132
|
?? (await handleOauthAccountRoutes(ctx))
|
|
132
133
|
?? (await handleComboRoutes(ctx))
|
|
133
|
-
?? (await handleSystemRoutes(ctx))
|
|
134
|
+
?? (await handleSystemRoutes(ctx))
|
|
135
|
+
?? (await handleSidebarRoutes(ctx));
|
|
134
136
|
if (routed) return routed;
|
|
135
137
|
|
|
136
138
|
if (url.pathname === "/api/stop" && req.method === "POST") {
|
|
@@ -109,6 +109,13 @@ export async function findLiveProxy(io: LivenessIo = {}): Promise<LiveProxy | nu
|
|
|
109
109
|
return verified === candidate ? verified : null;
|
|
110
110
|
};
|
|
111
111
|
|
|
112
|
+
const verifiedReportedPid = (reported: number | null): number | null => {
|
|
113
|
+
if (reported === null) return null;
|
|
114
|
+
if (!Number.isSafeInteger(reported) || reported <= 0) return null;
|
|
115
|
+
const verified = verifyPidFn(reported);
|
|
116
|
+
return verified === reported ? verified : null;
|
|
117
|
+
};
|
|
118
|
+
|
|
112
119
|
const pid = readPidFn();
|
|
113
120
|
let probedPort: number | null = null;
|
|
114
121
|
if (pid) {
|
|
@@ -136,7 +143,7 @@ export async function findLiveProxy(io: LivenessIo = {}): Promise<LiveProxy | nu
|
|
|
136
143
|
// (its process dead, the port reused by a pidless legacy proxy) — synthesizing it
|
|
137
144
|
// would hand destructive callers (stopProxy → kill fallback) a reusable pid.
|
|
138
145
|
if (identity) {
|
|
139
|
-
return { pid: identity.pid
|
|
146
|
+
return { pid: verifiedReportedPid(identity.pid), port: record.port, hostname: record.hostname, source: "runtime" };
|
|
140
147
|
}
|
|
141
148
|
}
|
|
142
149
|
|
|
@@ -145,7 +152,7 @@ export async function findLiveProxy(io: LivenessIo = {}): Promise<LiveProxy | nu
|
|
|
145
152
|
const identity = await proxyIdentityAt(port, { hostname: config.hostname }, io);
|
|
146
153
|
if (identity) {
|
|
147
154
|
return {
|
|
148
|
-
pid: identity.pid ?? killablePid(pid),
|
|
155
|
+
pid: verifiedReportedPid(identity.pid) ?? killablePid(pid),
|
|
149
156
|
port,
|
|
150
157
|
hostname: config.hostname,
|
|
151
158
|
source: "config",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Server } from "bun";
|
|
2
2
|
import { bridgeToResponsesSSE, buildResponseJSON, formatErrorResponse, type ResponsesTerminalStatus } from "../../bridge";
|
|
3
3
|
import { formatPassthroughUpstreamError } from "./passthrough-error";
|
|
4
|
+
import { describeUpstreamConnectFailure } from "./upstream-error";
|
|
4
5
|
import {
|
|
5
6
|
getConfigPath,
|
|
6
7
|
multiAgentGuidanceEnabled,
|
|
@@ -173,16 +174,9 @@ export function sidecarOutcomeRecorder(
|
|
|
173
174
|
|
|
174
175
|
|
|
175
176
|
|
|
176
|
-
|
|
177
|
+
import { isShadowSourceModel } from "../../lib/shadow-call";
|
|
177
178
|
|
|
178
|
-
export
|
|
179
|
-
if (modelId.includes("/")) return false;
|
|
180
|
-
const configuredStrings = Array.isArray(configured)
|
|
181
|
-
? configured.filter((v): v is string => typeof v === "string" && v.trim() !== "")
|
|
182
|
-
: [];
|
|
183
|
-
const prefixes = configuredStrings.length > 0 ? configuredStrings : DEFAULT_SHADOW_SOURCE_MODELS;
|
|
184
|
-
return prefixes.some(prefix => modelId.startsWith(prefix.trim()));
|
|
185
|
-
}
|
|
179
|
+
export { DEFAULT_SHADOW_SOURCE_MODELS, isShadowSourceModel, shadowSourceModels } from "../../lib/shadow-call";
|
|
186
180
|
|
|
187
181
|
|
|
188
182
|
|
|
@@ -1143,10 +1137,6 @@ export async function handleResponses(
|
|
|
1143
1137
|
}
|
|
1144
1138
|
if (parsed._compactionRequest === true) parsed._cursorIsolateConversation = true;
|
|
1145
1139
|
|
|
1146
|
-
if (isThreadSpawnRequest(req.headers)) {
|
|
1147
|
-
await maybePrimeSubagentQuota(config);
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
1140
|
let route: RouteResult;
|
|
1151
1141
|
try {
|
|
1152
1142
|
route = routeModel(config, parsed.modelId);
|
|
@@ -1157,6 +1147,17 @@ export async function handleResponses(
|
|
|
1157
1147
|
return formatErrorResponse(404, "invalid_request_error", err instanceof Error ? err.message : String(err));
|
|
1158
1148
|
}
|
|
1159
1149
|
|
|
1150
|
+
const hasUnexpandedPreviousResponse = !!parsed.previousResponseId
|
|
1151
|
+
&& parsed._previousResponseInputExpanded !== true;
|
|
1152
|
+
// A canonical replay miss must not poll quota upstream before the final fail-closed decision.
|
|
1153
|
+
// Cached fallback state can still select a provider with native continuation support below.
|
|
1154
|
+
if (
|
|
1155
|
+
isThreadSpawnRequest(req.headers)
|
|
1156
|
+
&& !(hasUnexpandedPreviousResponse && isCanonicalOpenAiForwardProvider(route.provider))
|
|
1157
|
+
) {
|
|
1158
|
+
await maybePrimeSubagentQuota(config);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1160
1161
|
let authCtx: CodexAuthContext = { kind: "main", accountId: null };
|
|
1161
1162
|
let selectedForwardHeaders = req.headers;
|
|
1162
1163
|
let subagentFallbackAccountId = config.activeCodexAccountId ?? null;
|
|
@@ -1205,6 +1206,20 @@ export async function handleResponses(
|
|
|
1205
1206
|
return unreadableEncryptedAgentTaskResponse();
|
|
1206
1207
|
}
|
|
1207
1208
|
|
|
1209
|
+
// The canonical ChatGPT backend rejects previous_response_id, so a local replay miss leaves no
|
|
1210
|
+
// safe way to recover the omitted history. Fail before auth, adapter construction, or upstream
|
|
1211
|
+
// I/O instead of stripping the id and silently forwarding a context-free delta (#702).
|
|
1212
|
+
if (
|
|
1213
|
+
hasUnexpandedPreviousResponse
|
|
1214
|
+
&& isCanonicalOpenAiForwardProvider(route.provider)
|
|
1215
|
+
) {
|
|
1216
|
+
return formatErrorResponse(
|
|
1217
|
+
400,
|
|
1218
|
+
"invalid_request_error",
|
|
1219
|
+
"OpenAI forward continuation state is unavailable or expired; start a new session instead of reusing this previous_response_id.",
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1208
1223
|
await applyFinalRouteRequestNormalization({ parsed, route, config, req, logCtx });
|
|
1209
1224
|
|
|
1210
1225
|
{
|
|
@@ -1443,7 +1458,7 @@ export async function handleResponses(
|
|
|
1443
1458
|
}
|
|
1444
1459
|
const msg = outcome === "timeout"
|
|
1445
1460
|
? `Provider connect timeout after ${connectMs}ms`
|
|
1446
|
-
:
|
|
1461
|
+
: describeUpstreamConnectFailure(err, connectMs);
|
|
1447
1462
|
return formatErrorResponse(502, "upstream_error", msg);
|
|
1448
1463
|
};
|
|
1449
1464
|
try {
|
|
@@ -2101,9 +2116,7 @@ export async function handleResponses(
|
|
|
2101
2116
|
cleanupUpstreamAbort();
|
|
2102
2117
|
upstream.abort();
|
|
2103
2118
|
if (options.abortSignal?.aborted) return clientCancelledResponse();
|
|
2104
|
-
const msg = err
|
|
2105
|
-
? `Provider connect timeout after ${connectMs}ms`
|
|
2106
|
-
: `Provider unreachable: ${err instanceof Error ? err.message : String(err)}`;
|
|
2119
|
+
const msg = describeUpstreamConnectFailure(err, connectMs);
|
|
2107
2120
|
return formatErrorResponse(502, "upstream_error", msg);
|
|
2108
2121
|
}
|
|
2109
2122
|
|
|
@@ -2143,9 +2156,7 @@ export async function handleResponses(
|
|
|
2143
2156
|
if (options.abortSignal?.aborted) {
|
|
2144
2157
|
return { failed: clientCancelledResponse() };
|
|
2145
2158
|
}
|
|
2146
|
-
const msg = err
|
|
2147
|
-
? `Provider connect timeout after ${connectMs}ms`
|
|
2148
|
-
: `Provider unreachable: ${err instanceof Error ? err.message : String(err)}`;
|
|
2159
|
+
const msg = describeUpstreamConnectFailure(err, connectMs);
|
|
2149
2160
|
return { failed: formatErrorResponse(502, "upstream_error", msg) };
|
|
2150
2161
|
}
|
|
2151
2162
|
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upstream connection failures share one message shape across the three catch sites in
|
|
3
|
+
* core.ts. A TLS certificate/hostname mismatch deserves its own wording: the generic
|
|
4
|
+
* "Provider unreachable" reads as if opencodex built a wrong endpoint, which sent issue
|
|
5
|
+
* #553 looking for an adapter URL bug that does not exist. Name the likely cause and the
|
|
6
|
+
* command that settles it.
|
|
7
|
+
*/
|
|
8
|
+
export function describeUpstreamConnectFailure(err: unknown, connectMs: number): string {
|
|
9
|
+
if (err instanceof Error && err.name === "TimeoutError") {
|
|
10
|
+
return `Provider connect timeout after ${connectMs}ms`;
|
|
11
|
+
}
|
|
12
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
13
|
+
const code = err instanceof Error ? (err as { code?: unknown }).code : undefined;
|
|
14
|
+
// `code` is the reliable signal. The message fallback is anchored to the head because Bun
|
|
15
|
+
// renders this rejection as `ERR_TLS_CERT_ALTNAME_INVALID fetching "<url>"`; matching the
|
|
16
|
+
// bare substring anywhere would also fire on text that merely quotes the code back at us.
|
|
17
|
+
// Only transport failures reach these call sites, so that is defensive rather than load-bearing.
|
|
18
|
+
if (code === "ERR_TLS_CERT_ALTNAME_INVALID" || detail.startsWith("ERR_TLS_CERT_ALTNAME_INVALID")) {
|
|
19
|
+
const host = extractHostname(detail);
|
|
20
|
+
const target = host ?? "the provider host";
|
|
21
|
+
const probe = host ?? "<host>";
|
|
22
|
+
return `Provider TLS certificate does not match ${target}: ${redactUrlUserinfo(detail)}. `
|
|
23
|
+
+ "opencodex did not rewrite this hostname — a certificate that does not cover it normally "
|
|
24
|
+
+ "means TLS interception (corporate proxy, VPN, or local MITM tooling) or a poisoned DNS "
|
|
25
|
+
+ `answer. Check with: openssl s_client -connect ${probe}:443 -servername ${probe} `
|
|
26
|
+
+ "</dev/null | openssl x509 -noout -subject -ext subjectAltName";
|
|
27
|
+
}
|
|
28
|
+
return `Provider unreachable: ${redactUrlUserinfo(detail)}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A provider base URL may carry credentials as userinfo (`https://user:token@host/`), and the
|
|
33
|
+
* runtime error echoes the URL it was fetching. Strip it before the message reaches a client
|
|
34
|
+
* or a log line.
|
|
35
|
+
*/
|
|
36
|
+
function redactUrlUserinfo(detail: string): string {
|
|
37
|
+
return detail.replace(/(https?:\/\/)[^/\s"'@]*@/g, "$1<redacted>@");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function extractHostname(detail: string): string | null {
|
|
41
|
+
const match = detail.match(/https?:\/\/([^/\s"']+)/);
|
|
42
|
+
if (!match?.[1]) return null;
|
|
43
|
+
try {
|
|
44
|
+
return new URL(`https://${match[1]}`).hostname || null;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -106,10 +106,14 @@ export function resetStartupInstallStateForTests(): void {
|
|
|
106
106
|
installState = { status: "idle" };
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
export function startupInstallArgv(
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
export function startupInstallArgv(
|
|
110
|
+
action: StartupInstallAction,
|
|
111
|
+
options?: { repair?: boolean },
|
|
112
|
+
): string[] {
|
|
113
|
+
if (action === "install-service") {
|
|
114
|
+
return options?.repair ? ["service", "repair"] : ["service", "install"];
|
|
115
|
+
}
|
|
116
|
+
return ["codex-shim", "install"];
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
export interface CliInstallFailure {
|
|
@@ -152,10 +156,13 @@ export function installFailureDetail(stdout: string, stderr: string, error: Erro
|
|
|
152
156
|
return classifyCliInstallFailure(stdout, stderr, error).detail;
|
|
153
157
|
}
|
|
154
158
|
|
|
155
|
-
function runCliInstall(
|
|
159
|
+
function runCliInstall(
|
|
160
|
+
action: StartupInstallAction,
|
|
161
|
+
options?: { repair?: boolean },
|
|
162
|
+
): Promise<{ stdout: string; stderr: string }> {
|
|
156
163
|
const bun = durableBunPath();
|
|
157
164
|
const cli = join(import.meta.dir, "..", "cli", "index.ts");
|
|
158
|
-
const argv = [cli, ...startupInstallArgv(action)];
|
|
165
|
+
const argv = [cli, ...startupInstallArgv(action, options)];
|
|
159
166
|
return new Promise((resolve, reject) => {
|
|
160
167
|
execFile(bun, argv, {
|
|
161
168
|
encoding: "utf8",
|
|
@@ -219,29 +226,37 @@ function applyReconciliationOutcome(
|
|
|
219
226
|
/**
|
|
220
227
|
* Execute the existing fixed CLI installer outside the proxy event loop.
|
|
221
228
|
*
|
|
229
|
+
* Repair mode (`options.repair`) runs `ocx service repair` — asset rewrite + restart
|
|
230
|
+
* without Task Scheduler re-registration, so it must not enter the UAC elevation path.
|
|
231
|
+
*
|
|
222
232
|
* After an elevation request timeout the lock becomes `indeterminate` until the
|
|
223
233
|
* original elevated transaction completes and is reconciled. A process restart
|
|
224
234
|
* clears this in-memory lock — callers must then inspect Task Scheduler reality
|
|
225
235
|
* (see evaluateSchedulerInstallRestartReconciliation) before installing again.
|
|
226
236
|
*/
|
|
227
|
-
export function runStartupInstallAction(
|
|
237
|
+
export function runStartupInstallAction(
|
|
238
|
+
action: StartupInstallAction,
|
|
239
|
+
options?: { repair?: boolean },
|
|
240
|
+
): Promise<{ message: string }> {
|
|
228
241
|
const busy = rejectIfBusy(action);
|
|
229
242
|
if (busy) return Promise.reject(busy);
|
|
230
243
|
|
|
244
|
+
const repair = options?.repair === true;
|
|
231
245
|
const attemptId = randomUUID();
|
|
232
246
|
const startedAt = Date.now();
|
|
233
247
|
installState = { status: "running", action, attemptId, startedAt };
|
|
234
248
|
|
|
235
249
|
const operation = (async () => {
|
|
236
250
|
try {
|
|
237
|
-
await runCliInstall(action);
|
|
251
|
+
await runCliInstall(action, { repair });
|
|
238
252
|
} catch (error) {
|
|
239
253
|
const code = installFailureCode(error);
|
|
240
254
|
const detail = error instanceof Error ? error.message : String(error);
|
|
241
|
-
// Elevate only for
|
|
242
|
-
// WinSW removal, asset writes, or generic permission errors.
|
|
255
|
+
// Elevate only for fresh install + structured Task Scheduler /create access denial —
|
|
256
|
+
// never for repair, WinSW removal, asset writes, or generic permission errors.
|
|
243
257
|
if (
|
|
244
|
-
|
|
258
|
+
!repair
|
|
259
|
+
&& action === "install-service"
|
|
245
260
|
&& process.platform === "win32"
|
|
246
261
|
&& (code === WINDOWS_SCHTASKS_CREATE_ACCESS_DENIED_MARKER
|
|
247
262
|
|| isWindowsSchtasksCreateAccessDenied(detail))
|
|
@@ -276,10 +291,11 @@ export function runStartupInstallAction(action: StartupInstallAction): Promise<{
|
|
|
276
291
|
throw error;
|
|
277
292
|
}
|
|
278
293
|
}
|
|
294
|
+
if (action === "install-service") {
|
|
295
|
+
return { message: repair ? "Background service repaired." : "Background service installed." };
|
|
296
|
+
}
|
|
279
297
|
return {
|
|
280
|
-
message:
|
|
281
|
-
? "Background service installed."
|
|
282
|
-
: "Codex launcher shim installed.",
|
|
298
|
+
message: repair ? "Codex launcher shim repaired." : "Codex launcher shim installed.",
|
|
283
299
|
};
|
|
284
300
|
})();
|
|
285
301
|
|