@bitkyc08/opencodex 2.78.0 → 2.80.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/gui/dist/assets/App-CSJvmjNs.css +1 -0
- package/gui/dist/assets/App-MbDVfUeC.js +54 -0
- package/gui/dist/assets/{Tray-DiJpSub0.js → Tray-CzkxK82A.js} +1 -1
- package/gui/dist/assets/{index-DmQB8D_R.css → index-Brk6F3fn.css} +1 -1
- package/gui/dist/assets/index-UJt6npXk.js +94 -0
- package/gui/dist/assets/{quota-summary-B_a96nXj.js → quota-summary-DU3sD6Z-.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +3 -2
- package/src/adapters/cursor/live-transport.ts +5 -3
- package/src/adapters/cursor/native-exec-fs.ts +12 -9
- package/src/adapters/cursor/native-exec.ts +13 -5
- package/src/adapters/cursor/protobuf-request.ts +13 -7
- package/src/adapters/cursor/request-builder.ts +23 -10
- package/src/adapters/cursor/tool-definitions.ts +9 -6
- package/src/adapters/cursor/tool-guidance.ts +29 -8
- package/src/adapters/cursor/tool-naming.ts +40 -7
- package/src/adapters/cursor/tool-wording.ts +46 -0
- package/src/adapters/cursor.ts +3 -0
- package/src/adapters/devin.ts +37 -2
- package/src/adapters/openai-chat/messages.ts +3 -1
- package/src/claude/desktop-picker.ts +11 -9
- package/src/claude/inbound-content-options.ts +29 -2
- package/src/claude/inbound.ts +9 -0
- package/src/claude/intercept/local-ca-files.ts +265 -0
- package/src/claude/intercept/local-ca-windows.ts +73 -0
- package/src/claude/intercept/local-ca.ts +12 -24
- package/src/claude/intercept/picker-ca-persistence.ts +106 -0
- package/src/claude/intercept/picker-ca-startup.ts +16 -0
- package/src/claude/intercept/picker-ca-store.ts +70 -0
- package/src/claude/intercept/picker-ca.ts +43 -9
- package/src/claude/intercept/picker-runtime.ts +14 -4
- package/src/claude/intercept/runtime.ts +32 -23
- package/src/cli/ensure-readiness.ts +48 -0
- package/src/cli/gui-pair-client.ts +15 -7
- package/src/cli/gui.ts +4 -1
- package/src/cli/hub.ts +1 -0
- package/src/cli/index.ts +10 -11
- package/src/cli/status.ts +2 -2
- package/src/cli/update-restart-child.ts +4 -1
- package/src/codex/account-credit-use.ts +8 -2
- package/src/codex/auth-api/account-pause-group.ts +113 -0
- package/src/codex/auth-api/routes.ts +18 -12
- package/src/codex/auth-context.ts +45 -3
- package/src/codex/autostart-health.ts +13 -3
- package/src/codex/journal.ts +36 -5
- package/src/codex/pool-credit-policy.ts +67 -0
- package/src/codex/shim-overlay.ts +9 -1
- package/src/combos/jev.ts +3 -1
- package/src/generated/compatibility-version.json +135 -63
- package/src/lib/gui-pair-capability.ts +1 -1
- package/src/lib/gui-pair-intent.ts +170 -0
- package/src/lib/windows-owner-acl.ts +143 -0
- package/src/providers/openai-sidecar-credit.ts +26 -0
- package/src/providers/openai-sidecar.ts +35 -0
- package/src/server/claude-messages.ts +1 -0
- package/src/server/gui-pair-delivery.ts +23 -0
- package/src/server/index/serve-options.ts +5 -8
- package/src/server/index.ts +12 -14
- package/src/server/live-native-calls.ts +94 -0
- package/src/server/live.ts +28 -5
- package/src/server/local-management-read-client.ts +15 -11
- package/src/server/management/oauth-account-routes.ts +15 -0
- package/src/server/management/route-registry.ts +1 -0
- package/src/server/management/shared.ts +9 -3
- package/src/server/messages-native-scope.ts +60 -0
- package/src/server/messages-native.ts +9 -0
- package/src/server/responses/adapter-dispatch.ts +31 -2
- package/src/server/responses/combo-requested-effort.ts +14 -0
- package/src/server/responses/compact.ts +17 -2
- package/src/server/responses/core-auth.ts +18 -1
- package/src/server/responses/core-codex-account.ts +18 -8
- package/src/server/responses/core-combo.ts +5 -7
- package/src/server/responses/non-replayable-error.ts +48 -0
- package/src/server/responses/passthrough-delivery.ts +5 -1
- package/src/server/responses/passthrough-dispatch.ts +21 -19
- package/src/server/responses/request-prepare.ts +11 -1
- package/src/server/responses/sidecar-execution.ts +1 -0
- package/src/server/search.ts +4 -0
- package/src/server/startup-health-cache.ts +2 -6
- package/src/service/desktop-startup.ts +100 -3
- package/src/vision/describe.ts +17 -10
- package/src/vision/index.ts +1 -0
- package/src/web-search/executor.ts +17 -10
- package/src/web-search/format-result.ts +304 -20
- package/src/web-search/header-deadline.ts +67 -0
- package/src/web-search/loop.ts +7 -6
- package/src/web-search/passthrough-bridge.ts +1 -0
- package/src/web-search/run-turn-loop.ts +1 -1
- package/gui/dist/assets/App-BtfG5id-.js +0 -54
- package/gui/dist/assets/App-lCIslZ1L.css +0 -1
- package/gui/dist/assets/index-Btc3Ogw4.js +0 -94
|
@@ -18,7 +18,8 @@ import {
|
|
|
18
18
|
relayWithAbort,
|
|
19
19
|
} from "../relay";
|
|
20
20
|
import { isUsageDebugEnabled } from "../../usage/debug";
|
|
21
|
-
import { isReplayRefusalResponse } from "../../lib/upstream-retry";
|
|
21
|
+
import { isNonReplayableResponse, isReplayRefusalResponse } from "../../lib/upstream-retry";
|
|
22
|
+
import { sanitizeNonReplayableUpstreamError } from "./non-replayable-error";
|
|
22
23
|
import { teeWithBoundedInspection } from "../inspection-tee";
|
|
23
24
|
import {
|
|
24
25
|
codexForwardTerminalOutcomeRecorder,
|
|
@@ -543,6 +544,9 @@ export async function deliverPassthroughResponse(
|
|
|
543
544
|
// through sanitizePassthroughHeaders) so a redirect to a dead host can never
|
|
544
545
|
// masquerade as a pre-connection failure after the credential was seen.
|
|
545
546
|
// The numeric outcome above already classified it neutral — no streak.
|
|
547
|
+
if (isNonReplayableResponse(upstreamResponse) && !isReplayRefusalResponse(upstreamResponse)) {
|
|
548
|
+
return await sanitizeNonReplayableUpstreamError(upstreamResponse, upstream.signal);
|
|
549
|
+
}
|
|
546
550
|
if (upstreamResponse.status >= 300 && upstreamResponse.status < 400) {
|
|
547
551
|
return new Response(upstreamResponse.body, {
|
|
548
552
|
status: upstreamResponse.status,
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
codexProbeLeaseId,
|
|
38
38
|
codexProbeQuotaScope,
|
|
39
39
|
codexTransientProbeGrant,
|
|
40
|
-
|
|
40
|
+
createCodexAuthDispatchGuard,
|
|
41
41
|
} from "../../codex/auth-context";
|
|
42
42
|
import {
|
|
43
43
|
NamespaceToolCollisionError,
|
|
@@ -87,7 +87,6 @@ import {
|
|
|
87
87
|
fetchWithHeaderTimeout,
|
|
88
88
|
providerFetch,
|
|
89
89
|
safeHostLabel,
|
|
90
|
-
storedPoolReplayDispatchNotifier,
|
|
91
90
|
} from "./fetch-helpers";
|
|
92
91
|
import { classifyPoolRecoveryDispatch } from "../../routing/probe-lease";
|
|
93
92
|
import { clientCancelledResponse } from "./core-errors";
|
|
@@ -816,7 +815,7 @@ export async function preparePassthroughExchange(
|
|
|
816
815
|
throw new Error("Credential selection changed before pre-output stream recovery");
|
|
817
816
|
}
|
|
818
817
|
if (isCanonicalOpenAiForwardProvider(route.provider)) {
|
|
819
|
-
|
|
818
|
+
createCodexAuthDispatchGuard(
|
|
820
819
|
admissionState.authCtx,
|
|
821
820
|
options.codexAuthPolicy ?? config,
|
|
822
821
|
route.modelId,
|
|
@@ -984,7 +983,7 @@ export async function preparePassthroughExchange(
|
|
|
984
983
|
modelId: route.modelId,
|
|
985
984
|
onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
|
|
986
985
|
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
987
|
-
?
|
|
986
|
+
? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
988
987
|
}),
|
|
989
988
|
route.provider.authMode === "forward")
|
|
990
989
|
// Every real attempt response — including an intermediate 5xx the
|
|
@@ -1089,7 +1088,7 @@ export async function preparePassthroughExchange(
|
|
|
1089
1088
|
modelId: route.modelId,
|
|
1090
1089
|
onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
|
|
1091
1090
|
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
1092
|
-
?
|
|
1091
|
+
? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
1093
1092
|
}),
|
|
1094
1093
|
route.provider.authMode === "forward")
|
|
1095
1094
|
.then(adoptObservedResponse);
|
|
@@ -1180,13 +1179,13 @@ export async function preparePassthroughExchange(
|
|
|
1180
1179
|
// The replay-dispatched signal is what bounds the rest of this logical request, so it
|
|
1181
1180
|
// has to describe a send that actually happened. fetchWithHeaderTimeout awaits pacing
|
|
1182
1181
|
// admission BEFORE calling the executor, so signalling at the call site would spend the
|
|
1183
|
-
// budget even when a rejected pacing wait means nothing reaches the network.
|
|
1184
|
-
//
|
|
1185
|
-
//
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1182
|
+
// budget even when a rejected pacing wait means nothing reaches the network. Signal
|
|
1183
|
+
// only after the physical-dispatch policy check succeeds; a local refusal is no send.
|
|
1184
|
+
// A replacement is another send of the same replay, rather than a second announcement.
|
|
1185
|
+
const replayGuard = isCanonicalOpenAiForwardProvider(route.provider)
|
|
1186
|
+
? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined;
|
|
1187
|
+
let replayNotified = false;
|
|
1188
|
+
const oauthReplayExecutor = providerFetch(route.provider, options.codexWsRuntimeIdentity, {
|
|
1190
1189
|
httpOnly: canonicalBufferedJson,
|
|
1191
1190
|
nativeControl: nativeResponseControlEligible(route.provider, options.nativeControl) && options.inboundTransport === "websocket" && !options.comboAttempt
|
|
1192
1191
|
&& responseEffects.plaintextV2AgentMessageToolNames.size === 0
|
|
@@ -1195,11 +1194,14 @@ export async function preparePassthroughExchange(
|
|
|
1195
1194
|
providerName: route.providerName,
|
|
1196
1195
|
modelId: route.modelId,
|
|
1197
1196
|
onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
|
|
1198
|
-
beforeDispatch:
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1197
|
+
beforeDispatch: headers => {
|
|
1198
|
+
replayGuard?.(headers);
|
|
1199
|
+
if (codex401ReplayKind === "stored" && !replayNotified) {
|
|
1200
|
+
replayNotified = true;
|
|
1201
|
+
options.onStoredPool401ReplayDispatched?.();
|
|
1202
|
+
}
|
|
1203
|
+
},
|
|
1204
|
+
});
|
|
1203
1205
|
// Routed through the shared helper so an ambiguous reset on THIS leg answers with the
|
|
1204
1206
|
// same refusal every other leg gives. A bare fetch here rejected instead, and the
|
|
1205
1207
|
// caller's transport-failure path turns a rejection into a client-retryable 502 --
|
|
@@ -1346,7 +1348,7 @@ export async function preparePassthroughExchange(
|
|
|
1346
1348
|
modelId: route.modelId,
|
|
1347
1349
|
onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
|
|
1348
1350
|
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
1349
|
-
?
|
|
1351
|
+
? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
1350
1352
|
}),
|
|
1351
1353
|
route.provider.authMode === "forward")
|
|
1352
1354
|
.then(adoptObservedResponse);
|
|
@@ -1481,7 +1483,7 @@ export async function preparePassthroughExchange(
|
|
|
1481
1483
|
modelId: route.modelId,
|
|
1482
1484
|
onCodexWsQuota: codexWsQuotaObserver(admissionState.authCtx, route.provider, route.modelId),
|
|
1483
1485
|
beforeDispatch: isCanonicalOpenAiForwardProvider(route.provider)
|
|
1484
|
-
?
|
|
1486
|
+
? createCodexAuthDispatchGuard(admissionState.authCtx, options.codexAuthPolicy ?? config, route.modelId, options.admission, options.visionDescribeTerminal === true) : undefined,
|
|
1485
1487
|
}),
|
|
1486
1488
|
route.provider.authMode === "forward")
|
|
1487
1489
|
.then(adoptObservedResponse);
|
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
codexPoolAffinityKey,
|
|
47
47
|
previewCodexPoolLineage,
|
|
48
48
|
applyCodexAuthContextToProvider,
|
|
49
|
+
releaseCodexAuthContextProbeLease,
|
|
49
50
|
hasCallerCodexBearer,
|
|
50
51
|
requestOwnedMainPinState,
|
|
51
52
|
requestOwnedMainCredentialIsLive,
|
|
@@ -58,6 +59,7 @@ import {
|
|
|
58
59
|
previousResponseProviderState,
|
|
59
60
|
} from "../../responses/state";
|
|
60
61
|
import { formatErrorResponse } from "../../bridge";
|
|
62
|
+
import { mapCodexAuthContextErrorToResponse } from "./codex-auth-error";
|
|
61
63
|
import type { OcxParsedRequest } from "../../types";
|
|
62
64
|
import { buildToolBridgeMaps } from "./collaboration";
|
|
63
65
|
import { parseRequest } from "../../responses/parser";
|
|
@@ -1352,7 +1354,15 @@ export async function prepareResponsesRequest(
|
|
|
1352
1354
|
substituteMainCredential = finalAuth.substituteMainCredential;
|
|
1353
1355
|
}
|
|
1354
1356
|
|
|
1355
|
-
|
|
1357
|
+
try {
|
|
1358
|
+
route.provider = applyCodexAuthContextToProvider(route.provider, admissionState.authCtx, route.codexAccountMode);
|
|
1359
|
+
} catch (error) {
|
|
1360
|
+
releaseCodexAuthContextProbeLease(admissionState.authCtx);
|
|
1361
|
+
if (options.abortSignal?.aborted || req.signal.aborted) return clientCancelledResponse();
|
|
1362
|
+
const mapped = mapCodexAuthContextErrorToResponse(error, { now: Date.now(), accountSelector: route.codexAccountNamespace });
|
|
1363
|
+
if (mapped) return mapped;
|
|
1364
|
+
throw error;
|
|
1365
|
+
}
|
|
1356
1366
|
applyCodexAccountGatedWireNormalization(parsed, route, logCtx);
|
|
1357
1367
|
try {
|
|
1358
1368
|
recordPolicyPreparedDestination(policyScope, route.providerName, parsed._wireModelOverride ?? parsed.modelId);
|
|
@@ -510,6 +510,7 @@ export async function executeResponsesSidecars(
|
|
|
510
510
|
transportState.bindKeyUsageFromBridge(usage);
|
|
511
511
|
},
|
|
512
512
|
recordSidecarOutcome: wsPlan.forwardSidecar?.recordOutcome,
|
|
513
|
+
beforeSidecarDispatch: wsPlan.forwardSidecar?.beforeDispatch,
|
|
513
514
|
connectTimeoutMs: config.connectTimeoutMs ?? 200_000,
|
|
514
515
|
routedModelStallTimeoutMs: wsPlan.routedModelStallTimeoutMs,
|
|
515
516
|
stallTimeoutSec: wsPlan.stallTimeoutSec,
|
package/src/server/search.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* paid backend than the one the operator named.
|
|
12
12
|
*/
|
|
13
13
|
import { formatErrorResponse } from "../bridge";
|
|
14
|
+
import { openAiSidecarCreditRefusal } from "../providers/openai-sidecar-credit";
|
|
14
15
|
import {
|
|
15
16
|
CodexAccountCooldownError,
|
|
16
17
|
codexMainProfileDrainingResponse,
|
|
@@ -202,6 +203,7 @@ export async function handleSearch(
|
|
|
202
203
|
const sidecarExit = sidecarEnter("search");
|
|
203
204
|
let upstreamResponse: Response | undefined;
|
|
204
205
|
try {
|
|
206
|
+
upstream.beforeDispatch?.();
|
|
205
207
|
upstreamResponse = await fetch(url, {
|
|
206
208
|
method: "POST",
|
|
207
209
|
headers,
|
|
@@ -233,6 +235,8 @@ export async function handleSearch(
|
|
|
233
235
|
if (req.signal.aborted) {
|
|
234
236
|
return formatErrorResponse(499, "client_closed_request", "search request canceled by client");
|
|
235
237
|
}
|
|
238
|
+
const policyRefusal = openAiSidecarCreditRefusal(err);
|
|
239
|
+
if (policyRefusal) return cooldownErrorResponse(policyRefusal, Date.now(), accountNamespace);
|
|
236
240
|
if (linkedSignal.signal.aborted || (err instanceof Error && err.name === "TimeoutError")) {
|
|
237
241
|
upstream.recordOutcome?.("timeout");
|
|
238
242
|
return formatErrorResponse(504, "upstream_error", "search upstream timed out");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { codexAutoStartEnabled } from "../config";
|
|
4
|
-
import { deriveStartupHealth, type StartupHealth } from "../codex/autostart-health";
|
|
4
|
+
import { deriveStartupHealth, startupHealthProbeBudgetMs, type StartupHealth } from "../codex/autostart-health";
|
|
5
5
|
import { getCodexRoutingKind } from "../codex/inject";
|
|
6
6
|
import { diagnoseCodexShim } from "../codex/shim";
|
|
7
7
|
import { durableBunPath } from "../lib/bun-runtime";
|
|
@@ -11,7 +11,7 @@ import type { OcxConfig } from "../types";
|
|
|
11
11
|
import { truncateRetainedUtf8 } from "../lib/admission";
|
|
12
12
|
|
|
13
13
|
const CACHE_TTL_MS = 30_000;
|
|
14
|
-
const PROBE_TIMEOUT_MS =
|
|
14
|
+
const PROBE_TIMEOUT_MS = startupHealthProbeBudgetMs();
|
|
15
15
|
const INITIAL_PROBE_WAIT_MS = PROBE_TIMEOUT_MS + 500;
|
|
16
16
|
const MAX_DIAGNOSTIC_VALUE_BYTES = 8 * 1024;
|
|
17
17
|
|
|
@@ -31,10 +31,6 @@ const MAX_DIAGNOSTIC_VALUE_BYTES = 8 * 1024;
|
|
|
31
31
|
* bound in both cases: a wedged probe is still abandoned, and the caller still
|
|
32
32
|
* receives the previous reading rather than waiting on it.
|
|
33
33
|
*/
|
|
34
|
-
function probeTimeoutMs(): number {
|
|
35
|
-
return process.platform === "win32" ? 15_000 : 5_000;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
34
|
/** The probe bound, so a test's own budget cannot fall below what it must wait for. */
|
|
39
35
|
export function startupHealthProbeTimeoutMs(): number {
|
|
40
36
|
return PROBE_TIMEOUT_MS;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { accessSync, constants, readFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import { accessSync, constants, readFileSync, readlinkSync, realpathSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { dirname, join } from "node:path";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
5
|
import { readPid } from "../config/process-state";
|
|
6
6
|
import { resolveServiceOwnership, sameServiceOwnershipSubject, type ServiceOwnershipResolution } from "./state";
|
|
7
7
|
|
|
@@ -29,11 +29,28 @@ interface DesktopStartupDeps {
|
|
|
29
29
|
ownership?: () => ServiceOwnershipResolution;
|
|
30
30
|
readPid?: () => number | null;
|
|
31
31
|
run?: typeof run;
|
|
32
|
+
env?: NodeJS.ProcessEnv;
|
|
33
|
+
proc?: ProcReader;
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
/** Linux process identity from procfs: executable target and parent pid. */
|
|
37
|
+
interface ProcReader {
|
|
38
|
+
exe(pid: number): string;
|
|
39
|
+
parent(pid: number): number;
|
|
40
|
+
}
|
|
41
|
+
const procfs: ProcReader = {
|
|
42
|
+
exe: pid => readlinkSync(`/proc/${pid}/exe`),
|
|
43
|
+
// comm may contain spaces or parentheses, so fields are counted after the last ')'.
|
|
44
|
+
parent: pid => {
|
|
45
|
+
const stat = readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
46
|
+
return Number(stat.slice(stat.lastIndexOf(")") + 2).split(" ")[1]);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
34
50
|
/** Cheap ownership-only snapshot: no launchd/process probes on the server request path. */
|
|
35
51
|
export function desktopStartupOwnership(deps: DesktopStartupDeps = {}): DesktopStartupDiagnostic | undefined {
|
|
36
|
-
|
|
52
|
+
const platform = deps.platform ?? process.platform;
|
|
53
|
+
if (platform !== "darwin" && platform !== "linux") return undefined;
|
|
37
54
|
const owner = (deps.ownership ?? resolveServiceOwnership)();
|
|
38
55
|
return owner.kind === "owned" && owner.ownership.owner === "desktop"
|
|
39
56
|
? deriveDesktopStartup({ owned: true, loginEnabled: false, running: false }) : undefined;
|
|
@@ -91,3 +108,83 @@ export function diagnoseMacDesktopStartup(deps: DesktopStartupDeps = {}): Deskto
|
|
|
91
108
|
return deriveDesktopStartup({ ...facts, running: false });
|
|
92
109
|
}
|
|
93
110
|
}
|
|
111
|
+
|
|
112
|
+
/** HOME-based autostart entry written by the desktop app's login item (`tauri-plugin-autostart`). */
|
|
113
|
+
function linuxLoginApp(entry: string): string | null {
|
|
114
|
+
const fields = new Map<string, string>();
|
|
115
|
+
let section = "";
|
|
116
|
+
for (const raw of entry.split(/\r?\n/)) {
|
|
117
|
+
const line = raw.trim();
|
|
118
|
+
if (!line || line.startsWith("#")) continue;
|
|
119
|
+
if (line.startsWith("[")) { section = line; continue; }
|
|
120
|
+
const at = line.indexOf("=");
|
|
121
|
+
if (section === "[Desktop Entry]" && at > 0 && !fields.has(line.slice(0, at).trim())) {
|
|
122
|
+
fields.set(line.slice(0, at).trim(), line.slice(at + 1).trim());
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (fields.get("Type") !== "Application" || fields.get("Hidden") === "true"
|
|
126
|
+
|| fields.get("X-GNOME-Autostart-enabled") === "false"
|
|
127
|
+
|| ["OnlyShowIn", "NotShowIn", "TryExec"].some(key => fields.has(key))) return null;
|
|
128
|
+
// Credit only the backend's simple Exec form; desktop quoting/escaping/field codes are unsupported.
|
|
129
|
+
const app = /^([^\s"'\\%`><~|&;$*?#()]+) --autostart$/.exec(fields.get("Exec") ?? "")?.[1];
|
|
130
|
+
return app && isAbsolute(app) && basename(app) === "opencodex-desktop" ? app : null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Read-only Linux desktop ownership, HOME login entry and live parent/child checks via procfs. */
|
|
134
|
+
export function diagnoseLinuxDesktopStartup(deps: DesktopStartupDeps = {}): DesktopStartupDiagnostic | undefined {
|
|
135
|
+
if ((deps.platform ?? process.platform) !== "linux") return undefined;
|
|
136
|
+
const ownership = deps.ownership ?? resolveServiceOwnership;
|
|
137
|
+
const owner = ownership();
|
|
138
|
+
if (owner.kind !== "owned" || owner.ownership.owner !== "desktop") return undefined;
|
|
139
|
+
const facts = { owned: true, loginEnabled: false, running: false };
|
|
140
|
+
const proc = deps.proc ?? procfs;
|
|
141
|
+
const pidReader = deps.readPid ?? readPid;
|
|
142
|
+
try {
|
|
143
|
+
const env = deps.env ?? process.env;
|
|
144
|
+
const home = deps.home ?? homedir();
|
|
145
|
+
const configured = env.XDG_CONFIG_HOME;
|
|
146
|
+
const config = configured && isAbsolute(configured) ? configured : join(home, ".config");
|
|
147
|
+
// auto-launch 0.5.0 writes autostart under HOME; Tauri app_config_dir uses XDG for install-id.
|
|
148
|
+
// A relocated XDG_CONFIG_HOME puts that entry outside the session's autostart search path.
|
|
149
|
+
if (resolve(config) !== resolve(home, ".config")) return deriveDesktopStartup(facts);
|
|
150
|
+
const capture = () => {
|
|
151
|
+
const id = readFileSync(join(config, "com.opencodex.desktop", "install-id"), "utf8");
|
|
152
|
+
if (id.trim() !== owner.ownership.installId) return null;
|
|
153
|
+
const entry = readFileSync(join(home, ".config", "autostart", "OpenCodex.desktop"), "utf8");
|
|
154
|
+
const login = linuxLoginApp(entry);
|
|
155
|
+
if (!login) return null;
|
|
156
|
+
const app = realpathSync(login);
|
|
157
|
+
if (basename(app) !== "opencodex-desktop") return null;
|
|
158
|
+
const proxy = realpathSync(join(dirname(app), "ocx"));
|
|
159
|
+
if (dirname(proxy) !== dirname(app) || basename(proxy) !== "ocx") return null;
|
|
160
|
+
accessSync(app, constants.X_OK);
|
|
161
|
+
accessSync(proxy, constants.X_OK);
|
|
162
|
+
const pid = pidReader();
|
|
163
|
+
const parent = pid !== null ? proc.parent(pid) : null;
|
|
164
|
+
const child = pid !== null ? realpathSync(proc.exe(pid)) : null;
|
|
165
|
+
const parentApp = parent !== null && parent > 1 ? realpathSync(proc.exe(parent)) : null;
|
|
166
|
+
const running = pid !== null && child === proxy && parentApp === app && pidReader() === pid;
|
|
167
|
+
const currentOwner = ownership();
|
|
168
|
+
if (currentOwner.kind === "unknown" || !sameServiceOwnershipSubject(owner, currentOwner)) return null;
|
|
169
|
+
return { id, entry, login, app, proxy, pid, parent, child, parentApp, running };
|
|
170
|
+
};
|
|
171
|
+
const first = capture();
|
|
172
|
+
if (!first) return deriveDesktopStartup(facts);
|
|
173
|
+
const final = capture();
|
|
174
|
+
// Re-read the entire evidence chain, including files, targets and the exact parent PID.
|
|
175
|
+
// A changed or unreadable snapshot cannot retain protection from the first read.
|
|
176
|
+
if (final && JSON.stringify(first) === JSON.stringify(final)) {
|
|
177
|
+
facts.loginEnabled = true;
|
|
178
|
+
facts.running = final.running;
|
|
179
|
+
}
|
|
180
|
+
return deriveDesktopStartup(facts);
|
|
181
|
+
} catch {
|
|
182
|
+
// Unreadable login or procfs evidence never grants protection or releases the claim.
|
|
183
|
+
return deriveDesktopStartup({ ...facts, running: false });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Platform dispatch for the full (probing) desktop startup diagnostic. */
|
|
188
|
+
export function diagnoseDesktopStartup(deps: DesktopStartupDeps = {}): DesktopStartupDiagnostic | undefined {
|
|
189
|
+
return (deps.platform ?? process.platform) === "linux" ? diagnoseLinuxDesktopStartup(deps) : diagnoseMacDesktopStartup(deps);
|
|
190
|
+
}
|
package/src/vision/describe.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { applyUpstreamRecoveryInit, fetchWithResetRetry } from "../lib/upstream-
|
|
|
8
8
|
import { parseSidecarSSE } from "../web-search/parse";
|
|
9
9
|
import type { SidecarOutcomeRecorder } from "../web-search/executor";
|
|
10
10
|
import { NATIVE_RESERVE_MODEL } from "../codex/catalog/native-models";
|
|
11
|
+
import { openAiSidecarCreditRefusal } from "../providers/openai-sidecar-credit";
|
|
11
12
|
|
|
12
13
|
export interface VisionSettings {
|
|
13
14
|
model: string;
|
|
@@ -60,6 +61,7 @@ export async function describeImage(
|
|
|
60
61
|
settings: VisionSettings,
|
|
61
62
|
abortSignal?: AbortSignal,
|
|
62
63
|
recordOutcome?: SidecarOutcomeRecorder,
|
|
64
|
+
beforeDispatch?: () => void,
|
|
63
65
|
): Promise<DescribeOutcome> {
|
|
64
66
|
if (settings.reserveCompatibility && settings.model === NATIVE_RESERVE_MODEL) {
|
|
65
67
|
return { text: "", error: "Luna Reserve compatibility is only available as a conversation model, not a vision helper. Choose another vision helper model." };
|
|
@@ -98,16 +100,19 @@ export async function describeImage(
|
|
|
98
100
|
const res = await fetchWithResetRetry(
|
|
99
101
|
// The replay needs `keepalive: false` to abandon the half-closed pooled socket; Bun has
|
|
100
102
|
// ignored a bare `Connection: close` (oven-sh/bun#20492).
|
|
101
|
-
recovery =>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
103
|
+
recovery => {
|
|
104
|
+
beforeDispatch?.();
|
|
105
|
+
return fetch(`${forwardProvider.baseUrl}/responses`, applyUpstreamRecoveryInit({
|
|
106
|
+
method: "POST",
|
|
107
|
+
headers,
|
|
108
|
+
body: JSON.stringify(body),
|
|
109
|
+
signal: linkedSignal.signal,
|
|
110
|
+
// Credential-bearing: do not follow a cross-origin 3xx. Bun strips `Authorization`
|
|
111
|
+
// across origins but forwards nonstandard headers such as `chatgpt-account-id`,
|
|
112
|
+
// `session_id`, and `x-codex-turn-metadata` to the redirect target.
|
|
113
|
+
redirect: "manual",
|
|
114
|
+
}, recovery));
|
|
115
|
+
},
|
|
111
116
|
{ replaySafe: true, abortSignal: linkedSignal.signal, label: "vision-sidecar" },
|
|
112
117
|
);
|
|
113
118
|
const detachBodyGuard = cancelBodyOnAbort(res.body, linkedSignal.signal);
|
|
@@ -129,6 +134,8 @@ export async function describeImage(
|
|
|
129
134
|
detachBodyGuard();
|
|
130
135
|
}
|
|
131
136
|
} catch (e) {
|
|
137
|
+
const policyRefusal = openAiSidecarCreditRefusal(e);
|
|
138
|
+
if (policyRefusal) return { text: "", error: policyRefusal.message };
|
|
132
139
|
const kind = e instanceof Error && e.name === "TimeoutError" ? "timeout" : "connect_error";
|
|
133
140
|
const callerAborted = abortSignal?.aborted === true
|
|
134
141
|
&& linkedSignal.signal.aborted
|
package/src/vision/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { withUpstreamHttpVersion } from "../lib/upstream-http-version";
|
|
|
15
15
|
import { parseSidecarSSE, type WebSearchResult } from "./parse";
|
|
16
16
|
import type { CodexUpstreamOutcome } from "../codex/routing";
|
|
17
17
|
import { NATIVE_RESERVE_MODEL } from "../codex/catalog/native-models";
|
|
18
|
+
import { openAiSidecarCreditRefusal } from "../providers/openai-sidecar-credit";
|
|
18
19
|
|
|
19
20
|
export interface SidecarSettings {
|
|
20
21
|
model: string;
|
|
@@ -78,6 +79,7 @@ export async function runWebSearch(
|
|
|
78
79
|
settings: SidecarSettings,
|
|
79
80
|
abortSignal?: AbortSignal,
|
|
80
81
|
recordOutcome?: SidecarOutcomeRecorder,
|
|
82
|
+
beforeDispatch?: () => void,
|
|
81
83
|
): Promise<SidecarOutcome> {
|
|
82
84
|
if (settings.reserveCompatibility && settings.model === NATIVE_RESERVE_MODEL) {
|
|
83
85
|
return { text: "", sources: [], error: "Luna Reserve compatibility is only available as a conversation model, not a search helper. Choose another search helper model." };
|
|
@@ -115,16 +117,19 @@ export async function runWebSearch(
|
|
|
115
117
|
// defined init, and withUpstreamHttpVersion spreads the result, so `protocol` and the
|
|
116
118
|
// recovery fields (`connection: close` + Bun's transport-level `keepalive: false`) survive
|
|
117
119
|
// together. The reverse order needs a `?? init` fallback to type-check at all.
|
|
118
|
-
recovery =>
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
120
|
+
recovery => {
|
|
121
|
+
beforeDispatch?.();
|
|
122
|
+
return fetch(url, withUpstreamHttpVersion(url, applyUpstreamRecoveryInit({
|
|
123
|
+
method: "POST",
|
|
124
|
+
headers,
|
|
125
|
+
body: JSON.stringify(body),
|
|
126
|
+
signal: linkedSignal.signal,
|
|
127
|
+
// Credential-bearing: do not follow a cross-origin 3xx. Bun strips `Authorization`
|
|
128
|
+
// across origins but forwards nonstandard headers such as `chatgpt-account-id`,
|
|
129
|
+
// `session_id`, and `x-codex-turn-metadata` to the redirect target.
|
|
130
|
+
redirect: "manual",
|
|
131
|
+
}, recovery), forwardProvider));
|
|
132
|
+
},
|
|
128
133
|
{
|
|
129
134
|
replaySafe: true,
|
|
130
135
|
abortSignal: linkedSignal.signal,
|
|
@@ -180,6 +185,8 @@ export async function runWebSearch(
|
|
|
180
185
|
detachBodyGuard();
|
|
181
186
|
}
|
|
182
187
|
} catch (e) {
|
|
188
|
+
const policyRefusal = openAiSidecarCreditRefusal(e);
|
|
189
|
+
if (policyRefusal) return { text: "", sources: [], error: policyRefusal.message };
|
|
183
190
|
const kind = e instanceof Error && e.name === "TimeoutError" ? "timeout" : "connect_error";
|
|
184
191
|
const callerAborted = abortSignal?.aborted === true
|
|
185
192
|
&& linkedSignal.signal.aborted
|