@bitkyc08/opencodex 2.77.0 → 2.78.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 +11 -10
- package/gui/dist/assets/App-BtfG5id-.js +54 -0
- package/gui/dist/assets/App-lCIslZ1L.css +1 -0
- package/gui/dist/assets/{Tray-gloIYNaW.js → Tray-DiJpSub0.js} +1 -1
- package/gui/dist/assets/index-Btc3Ogw4.js +94 -0
- package/gui/dist/assets/index-DmQB8D_R.css +1 -0
- package/gui/dist/assets/quota-summary-B_a96nXj.js +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic/beta-allowlist.ts +19 -2
- package/src/adapters/anthropic/client-identity.ts +13 -4
- package/src/adapters/anthropic/native-client-preamble.ts +31 -0
- package/src/adapters/anthropic/passthrough.ts +103 -25
- package/src/adapters/devin/cloud-direct/chat.ts +7 -2
- package/src/adapters/devin/trajectory.ts +49 -0
- package/src/adapters/devin.ts +13 -0
- package/src/adapters/ollama-native.ts +35 -19
- package/src/claude/intercept/client-hello.ts +103 -0
- package/src/claude/intercept/model-bindings.ts +3 -2
- package/src/claude/intercept/picker-listener.ts +279 -66
- package/src/cli/access-audio-input.ts +84 -0
- package/src/cli/access-audio-live.ts +99 -0
- package/src/cli/access-audio.ts +123 -0
- package/src/cli/access-data-client.ts +127 -0
- package/src/cli/access-data-plane.ts +104 -0
- package/src/cli/access-data-response.ts +76 -0
- package/src/cli/access.ts +65 -2
- package/src/cli/account-api.ts +30 -6
- package/src/cli/account-auth.ts +111 -42
- package/src/cli/account-key-quota.ts +118 -0
- package/src/cli/account-main.ts +11 -4
- package/src/cli/account-next-actions.ts +63 -0
- package/src/cli/account-policy-dto.ts +68 -0
- package/src/cli/account-policy.ts +167 -0
- package/src/cli/account-target.ts +8 -0
- package/src/cli/account.ts +34 -8
- package/src/cli/agent-runtime-settings.ts +130 -0
- package/src/cli/agent-settings.ts +101 -0
- package/src/cli/agent.ts +19 -2
- package/src/cli/alias.ts +7 -2
- package/src/cli/aside-profiles.ts +2 -2
- package/src/cli/capabilities-access-remote.ts +147 -0
- package/src/cli/capabilities-accounts.ts +195 -0
- package/src/cli/capabilities-agents-routing.ts +414 -0
- package/src/cli/capabilities-base.ts +783 -0
- package/src/cli/capabilities-command.ts +1 -0
- package/src/cli/capabilities-integrations.ts +264 -0
- package/src/cli/capabilities-lab.ts +214 -0
- package/src/cli/capabilities-observe-system.ts +662 -0
- package/src/cli/capabilities-provider-models.ts +302 -0
- package/src/cli/capabilities.ts +30 -1199
- package/src/cli/capability-types.ts +54 -0
- package/src/cli/catalog-command-result.ts +57 -0
- package/src/cli/chatgpt-command.ts +25 -2
- package/src/cli/claude-agent-startup-sync.ts +17 -0
- package/src/cli/claude-desktop-profile.ts +100 -0
- package/src/cli/claude-desktop.ts +39 -6
- package/src/cli/codex-shim-autorestore.ts +2 -1
- package/src/cli/codex-shim-readiness.ts +23 -3
- package/src/cli/combo-input.ts +88 -0
- package/src/cli/combo-result.ts +52 -0
- package/src/cli/combo-stats.ts +123 -0
- package/src/cli/combo.ts +37 -14
- package/src/cli/companion-timeline.ts +142 -0
- package/src/cli/companion-usage.ts +146 -0
- package/src/cli/companion.ts +12 -3
- package/src/cli/config-command.ts +24 -8
- package/src/cli/dispatch.ts +52 -79
- package/src/cli/doctor.ts +22 -4
- package/src/cli/help-catalog.ts +31 -14
- package/src/cli/help-recovery.ts +2 -2
- package/src/cli/help.ts +25 -11
- package/src/cli/index.ts +38 -27
- package/src/cli/injection-follow.ts +35 -0
- package/src/cli/inspect.ts +5 -0
- package/src/cli/integration-aside-sync.ts +69 -0
- package/src/cli/integration-cursor.ts +106 -0
- package/src/cli/integration-input.ts +60 -0
- package/src/cli/integration-journal.ts +49 -0
- package/src/cli/integration-plan-dto.ts +156 -0
- package/src/cli/integration-preview.ts +215 -0
- package/src/cli/integrations.ts +24 -18
- package/src/cli/json-input.ts +134 -0
- package/src/cli/link.ts +32 -4
- package/src/cli/local-sync-result.ts +41 -0
- package/src/cli/log-follow.ts +84 -0
- package/src/cli/log-view-filter.ts +146 -0
- package/src/cli/logout-command.ts +44 -0
- package/src/cli/model-picker-ordering.ts +137 -0
- package/src/cli/models-custom-input.ts +107 -0
- package/src/cli/models-custom-runtime.ts +112 -0
- package/src/cli/models-order.ts +194 -0
- package/src/cli/models-runtime-subcommands.ts +2 -0
- package/src/cli/models-runtime.ts +7 -0
- package/src/cli/models.ts +66 -81
- package/src/cli/observe-stream.ts +142 -0
- package/src/cli/observe.ts +75 -34
- package/src/cli/provider-batch.ts +77 -0
- package/src/cli/provider-lifecycle-runtime.ts +170 -0
- package/src/cli/provider-result.ts +124 -0
- package/src/cli/provider-runtime.ts +15 -12
- package/src/cli/provider-settings.ts +143 -0
- package/src/cli/provider.ts +112 -34
- package/src/cli/registry.ts +62 -20
- package/src/cli/remote-workspace-hub.ts +83 -0
- package/src/cli/remote-workspace.ts +6 -1
- package/src/cli/restart-failure.ts +27 -0
- package/src/cli/root.ts +12 -0
- package/src/cli/route-policy-write.ts +137 -0
- package/src/cli/route-policy.ts +8 -0
- package/src/cli/runtime-api.ts +56 -79
- package/src/cli/secret-args.ts +149 -0
- package/src/cli/settings-result.ts +30 -0
- package/src/cli/storage.ts +77 -7
- package/src/cli/system-command.ts +60 -2
- package/src/cli/system-restart-client.ts +20 -3
- package/src/cli/system-settings-parity.ts +83 -0
- package/src/cli/update-restart-candidate.ts +21 -0
- package/src/cli/update-restart-child.ts +81 -0
- package/src/cli/update-restart-home.ts +38 -0
- package/src/cli/update-restart-transport.ts +193 -0
- package/src/cli/update-restart.ts +260 -0
- package/src/cli/usage-model-search.ts +58 -0
- package/src/cli/usage-report.ts +13 -5
- package/src/cli/v2-input.ts +66 -0
- package/src/cli/v2-local-output.ts +37 -0
- package/src/cli/v2-runtime.ts +90 -0
- package/src/cli/v2.ts +102 -144
- package/src/clients/config-export/dsh.ts +21 -0
- package/src/clients/config-export.ts +7 -1
- package/src/codex/catalog/build-entries.ts +1 -1
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/heal-observation.ts +66 -0
- package/src/codex/catalog/publication-observer.ts +19 -0
- package/src/codex/catalog/remote.ts +13 -15
- package/src/codex/catalog/restore.ts +19 -6
- package/src/codex/catalog/retained-sync.ts +58 -10
- package/src/codex/catalog/routed-removal.ts +116 -0
- package/src/codex/catalog/write-audit-contract.ts +21 -0
- package/src/codex/catalog/write-audit.ts +171 -0
- package/src/codex/catalog-self-heal.ts +284 -0
- package/src/codex/catalog-write-serialization.ts +71 -7
- package/src/codex/codex-home-owner.ts +124 -0
- package/src/codex/convergence.ts +55 -15
- package/src/codex/features.ts +8 -1
- package/src/codex/inject/restore.ts +46 -4
- package/src/codex/inject.ts +11 -1
- package/src/codex/internal/catalog-writer.ts +120 -13
- package/src/codex/journal.ts +49 -13
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/quota-types.ts +3 -0
- package/src/codex/quota.ts +13 -3
- package/src/codex/refresh.ts +20 -9
- package/src/codex/runtime.ts +7 -1
- package/src/codex/shim-diagnostics.ts +80 -0
- package/src/codex/shim-fingerprint.ts +7 -2
- package/src/codex/shim-migration.ts +153 -0
- package/src/codex/shim-overlay.ts +509 -0
- package/src/codex/shim-path-resolution.ts +14 -2
- package/src/codex/shim-probe.ts +4 -3
- package/src/codex/shim-state-file.ts +93 -7
- package/src/codex/shim.ts +15 -68
- package/src/codex/sync.ts +24 -4
- package/src/combos/failover.ts +19 -0
- package/src/combos/index.ts +1 -0
- package/src/combos/resolve.ts +12 -0
- package/src/config/atomic-write.ts +13 -8
- package/src/config/diagnostics.ts +19 -1
- package/src/config/load-degrade.ts +3 -0
- package/src/config/schema/chatgpt-desktop.ts +22 -0
- package/src/config/schema/config-schema.ts +32 -14
- package/src/config/schema/leaf-validators.ts +3 -2
- package/src/generated/compatibility-version.json +445 -129
- package/src/integrations/merge.ts +13 -5
- package/src/integrations/mutation-plan.ts +46 -3
- package/src/integrations/omp-yaml-source.ts +181 -1
- package/src/integrations/ownership-policy.ts +66 -0
- package/src/integrations/registry.ts +50 -1
- package/src/integrations/state.ts +46 -21
- package/src/integrations/target.ts +25 -2
- package/src/integrations/writer.ts +89 -31
- package/src/lib/package-tree-integrity.ts +42 -1
- package/src/lib/proxy-env.ts +3 -0
- package/src/lib/test-home-guard.ts +2 -1
- package/src/oauth/anthropic-account-refusal.ts +19 -8
- package/src/oauth/anthropic-routing.ts +17 -12
- package/src/oauth/anthropic.ts +27 -2
- package/src/oauth/index.ts +6 -8
- package/src/oauth/pool-settings-capability.ts +9 -2
- package/src/protocols/plan-snapshot.ts +1 -1
- package/src/protocols/settings.ts +28 -4
- package/src/server/caller-session-identity.ts +32 -0
- package/src/server/claude-messages.ts +11 -4
- package/src/server/index/serve-options.ts +12 -7
- package/src/server/index.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +6 -5
- package/src/server/management/context.ts +8 -1
- package/src/server/management/cursor-integration-routes.ts +3 -3
- package/src/server/management/integration-routes.ts +5 -5
- package/src/server/management/model-routes.ts +3 -3
- package/src/server/management/native-integration-routes.ts +2 -2
- package/src/server/management/oauth-account-routes.ts +96 -30
- package/src/server/management/route-registry.ts +25 -24
- package/src/server/messages-native-eligibility.ts +3 -6
- package/src/server/messages-native-oauth.ts +82 -35
- package/src/server/messages-native.ts +78 -16
- package/src/server/responses/adapter-dispatch.ts +53 -28
- package/src/server/responses/core-auth.ts +10 -4
- package/src/server/responses/core-combo.ts +46 -6
- package/src/service/cli.ts +6 -1
- package/src/service/ownership-mutation-lease.d.mts +26 -0
- package/src/service/ownership-mutation-lease.mjs +76 -1
- package/src/types/config.ts +2 -0
- package/src/types/provider.ts +10 -10
- package/src/update/index.ts +2 -1
- package/src/update/update-failure-guidance.d.mts +6 -0
- package/src/update/update-failure-guidance.mjs +30 -5
- package/gui/dist/assets/App-COdEeqSj.css +0 -1
- package/gui/dist/assets/App-DEpCJEP_.js +0 -54
- package/gui/dist/assets/index-Cd7ittMv.css +0 -1
- package/gui/dist/assets/index-DwJKFJm5.js +0 -94
- package/gui/dist/assets/quota-summary-DGcEJU3h.js +0 -1
|
@@ -1,21 +1,31 @@
|
|
|
1
1
|
import {
|
|
2
2
|
closeSync,
|
|
3
|
+
constants,
|
|
3
4
|
existsSync,
|
|
4
5
|
fstatSync,
|
|
5
6
|
lstatSync,
|
|
6
7
|
mkdirSync,
|
|
7
8
|
openSync,
|
|
8
9
|
readSync,
|
|
10
|
+
renameSync,
|
|
11
|
+
unlinkSync,
|
|
9
12
|
writeFileSync,
|
|
10
13
|
type Stats,
|
|
11
14
|
} from "node:fs";
|
|
12
|
-
import {
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
16
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
13
17
|
import { getConfigDir } from "../config";
|
|
14
18
|
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
15
19
|
|
|
16
20
|
export const CODEX_SHIM_STATE_MAX_BYTES = 1024 * 1024;
|
|
17
21
|
|
|
18
22
|
interface ShimState {
|
|
23
|
+
schema?: 2;
|
|
24
|
+
mode?: "path-overlay";
|
|
25
|
+
launcherPath?: string;
|
|
26
|
+
wrapperIdentity?: OverlayIdentity;
|
|
27
|
+
envIdentity?: OverlayIdentity;
|
|
28
|
+
transactionId?: string;
|
|
19
29
|
platform: NodeJS.Platform;
|
|
20
30
|
wrapperPath: string;
|
|
21
31
|
originalPath: string;
|
|
@@ -23,6 +33,51 @@ interface ShimState {
|
|
|
23
33
|
wrappers?: ShimFileState[];
|
|
24
34
|
}
|
|
25
35
|
|
|
36
|
+
export interface OverlayIdentity { dev: number; ino: number }
|
|
37
|
+
export interface OverlayState {
|
|
38
|
+
schema: 2;
|
|
39
|
+
mode: "path-overlay";
|
|
40
|
+
platform: NodeJS.Platform;
|
|
41
|
+
wrapperPath: string;
|
|
42
|
+
launcherPath: string;
|
|
43
|
+
// Older schema-2 records remain readable, but absent identities confer no mutation authority.
|
|
44
|
+
wrapperIdentity?: OverlayIdentity;
|
|
45
|
+
envIdentity?: OverlayIdentity;
|
|
46
|
+
transactionId?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function decodeOverlayState(value: unknown, configDir: string): OverlayState | null {
|
|
50
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
51
|
+
const state = value as Record<string, unknown>;
|
|
52
|
+
const platforms = ["aix", "darwin", "freebsd", "linux", "openbsd", "sunos", "android", "netbsd", "haiku", "cygwin"];
|
|
53
|
+
const validPath = (path: unknown): path is string => typeof path === "string" && isAbsolute(path) && !/[\x00-\x1f\x7f]/.test(path);
|
|
54
|
+
if (state.schema !== 2 || state.mode !== "path-overlay" || !platforms.includes(String(state.platform))
|
|
55
|
+
|| !validPath(configDir) || !validPath(state.wrapperPath) || !validPath(state.launcherPath)
|
|
56
|
+
|| state.wrapperPath !== join(resolve(configDir), "bin", "codex")
|
|
57
|
+
|| dirname(state.wrapperPath).includes(":") || resolve(state.launcherPath) === state.wrapperPath) return null;
|
|
58
|
+
for (const field of ["originalPath", "backupPath", "realPath"]) {
|
|
59
|
+
if (state[field] !== undefined && state[field] !== state.launcherPath) return null;
|
|
60
|
+
}
|
|
61
|
+
if (state.preserveOnly !== undefined && state.preserveOnly !== false) return null;
|
|
62
|
+
if (state.wrappers !== undefined) {
|
|
63
|
+
if (!Array.isArray(state.wrappers) || state.wrappers.length !== 1) return null;
|
|
64
|
+
const file = state.wrappers[0];
|
|
65
|
+
if (!file || typeof file !== "object" || file.wrapperPath !== state.wrapperPath
|
|
66
|
+
|| file.originalPath !== state.launcherPath || file.backupPath !== state.launcherPath
|
|
67
|
+
|| (file.realPath !== undefined && file.realPath !== state.launcherPath) || file.preserveOnly) return null;
|
|
68
|
+
}
|
|
69
|
+
for (const key of ["wrapperIdentity", "envIdentity"]) {
|
|
70
|
+
const id = state[key] as Partial<OverlayIdentity> | undefined;
|
|
71
|
+
if (id !== undefined && (!id || !Number.isSafeInteger(id.dev) || !Number.isSafeInteger(id.ino))) return null;
|
|
72
|
+
}
|
|
73
|
+
if (state.transactionId !== undefined && (typeof state.transactionId !== "string" || !/^[a-f0-9-]{36}$/.test(state.transactionId))) return null;
|
|
74
|
+
return state as unknown as OverlayState;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function readOverlayState(configDir = getConfigDir()): OverlayState | null {
|
|
78
|
+
return decodeOverlayState(readStateResult(join(configDir, "codex-shim.json")).state, configDir);
|
|
79
|
+
}
|
|
80
|
+
|
|
26
81
|
interface ShimFileState {
|
|
27
82
|
wrapperPath: string;
|
|
28
83
|
originalPath: string;
|
|
@@ -43,7 +98,7 @@ function fileErrorCode(error: unknown): string | undefined {
|
|
|
43
98
|
: undefined;
|
|
44
99
|
}
|
|
45
100
|
|
|
46
|
-
function readBoundedRegularFile(path: string, maxBytes: number): { bytes: Buffer; content: string } | { warning: string } | null {
|
|
101
|
+
function readBoundedRegularFile(path: string, maxBytes: number): { bytes: Buffer; content: string; stat: Stats } | { warning: string } | null {
|
|
47
102
|
let lexicalBefore: Stats;
|
|
48
103
|
try {
|
|
49
104
|
lexicalBefore = lstatSync(path);
|
|
@@ -56,7 +111,7 @@ function readBoundedRegularFile(path: string, maxBytes: number): { bytes: Buffer
|
|
|
56
111
|
}
|
|
57
112
|
let fd: number;
|
|
58
113
|
try {
|
|
59
|
-
fd = openSync(path, "r");
|
|
114
|
+
fd = openSync(path, process.platform === "win32" ? "r" : constants.O_RDONLY | constants.O_NONBLOCK);
|
|
60
115
|
} catch (error) {
|
|
61
116
|
if (fileErrorCode(error) === "ENOENT") return null;
|
|
62
117
|
return { warning: `Codex shim state could not be opened as a regular file at ${path}.` };
|
|
@@ -87,11 +142,16 @@ function readBoundedRegularFile(path: string, maxBytes: number): { bytes: Buffer
|
|
|
87
142
|
}
|
|
88
143
|
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size
|
|
89
144
|
|| before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs
|
|
145
|
+
|| before.mode !== after.mode || before.uid !== after.uid
|
|
90
146
|
|| lexicalBefore.dev !== before.dev || lexicalBefore.ino !== before.ino
|
|
91
|
-
||
|
|
147
|
+
|| lexicalBefore.mode !== before.mode || lexicalBefore.uid !== before.uid
|
|
148
|
+
|| lexicalBefore.size !== before.size || lexicalBefore.mtimeMs !== before.mtimeMs || lexicalBefore.ctimeMs !== before.ctimeMs
|
|
149
|
+
|| !lexicalAfter.isFile() || lexicalAfter.isSymbolicLink() || lexicalAfter.dev !== after.dev || lexicalAfter.ino !== after.ino
|
|
150
|
+
|| lexicalAfter.mode !== after.mode || lexicalAfter.uid !== after.uid
|
|
151
|
+
|| lexicalAfter.size !== after.size || lexicalAfter.mtimeMs !== after.mtimeMs || lexicalAfter.ctimeMs !== after.ctimeMs) {
|
|
92
152
|
return { warning: `Codex shim state changed while being read at ${path}; auto-restore skipped.` };
|
|
93
153
|
}
|
|
94
|
-
return { bytes: buffer, content: buffer.toString("utf8") };
|
|
154
|
+
return { bytes: buffer, content: buffer.toString("utf8"), stat: after };
|
|
95
155
|
} finally {
|
|
96
156
|
closeSync(fd);
|
|
97
157
|
}
|
|
@@ -105,6 +165,15 @@ function readStateResult(path = statePath()): ShimStateReadResult {
|
|
|
105
165
|
const value = JSON.parse(bounded.content) as unknown;
|
|
106
166
|
if (!value || typeof value !== "object") return { state: null, present: true };
|
|
107
167
|
const state = value as Record<string, unknown>;
|
|
168
|
+
if (state.mode !== undefined || state.schema !== undefined) {
|
|
169
|
+
// Legacy records stay readable; private overlay authority requires safe ownership.
|
|
170
|
+
// Non-writable mode drift is readable for opt-in repair, never a healthy installation.
|
|
171
|
+
if (process.platform !== "win32" && (bounded.stat.uid !== process.getuid?.() || (bounded.stat.mode & 0o022))) {
|
|
172
|
+
return { state: null, present: true, warning: "Private Codex shim state is not owned by the current user or is group/world writable; preserving artifacts. Remove the private files manually before reinstalling." };
|
|
173
|
+
}
|
|
174
|
+
const overlay = decodeOverlayState(value, dirname(path));
|
|
175
|
+
return { state: overlay ? { ...overlay, originalPath: overlay.launcherPath, backupPath: overlay.launcherPath } : null, present: true };
|
|
176
|
+
}
|
|
108
177
|
if (typeof state.platform !== "string") return { state: null, present: true };
|
|
109
178
|
const validFile = (item: unknown): item is ShimFileState => {
|
|
110
179
|
if (!item || typeof item !== "object") return false;
|
|
@@ -134,18 +203,35 @@ function statePath(): string {
|
|
|
134
203
|
return join(getConfigDir(), "codex-shim.json");
|
|
135
204
|
}
|
|
136
205
|
|
|
137
|
-
function writeState(state: ShimState): void {
|
|
206
|
+
function writeState(state: ShimState | OverlayState): void {
|
|
138
207
|
const path = statePath();
|
|
139
208
|
recordOwnedConfigPath(getConfigDir(), path);
|
|
140
209
|
if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
|
|
210
|
+
if (state.mode === "path-overlay") {
|
|
211
|
+
if (!decodeOverlayState(state, getConfigDir()) || !state.wrapperIdentity || !state.envIdentity) {
|
|
212
|
+
throw new Error("Invalid Codex overlay state: recorded ownership identities are required");
|
|
213
|
+
}
|
|
214
|
+
const staged = `${path}.${randomUUID()}.tmp`;
|
|
215
|
+
try {
|
|
216
|
+
writeFileSync(staged, JSON.stringify(state, null, 2) + "\n", { flag: "wx", mode: 0o600 });
|
|
217
|
+
renameSync(staged, path);
|
|
218
|
+
} finally {
|
|
219
|
+
try { unlinkSync(staged); } catch { /* committed or absent */ }
|
|
220
|
+
}
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
141
223
|
writeFileSync(path, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
142
224
|
}
|
|
143
225
|
|
|
144
226
|
function stateFiles(state: ShimState): ShimFileState[] {
|
|
227
|
+
if (state.mode === "path-overlay" && state.launcherPath) return [{
|
|
228
|
+
wrapperPath: state.wrapperPath, originalPath: state.launcherPath,
|
|
229
|
+
backupPath: state.launcherPath, realPath: state.launcherPath,
|
|
230
|
+
}];
|
|
145
231
|
return state.wrappers?.length
|
|
146
232
|
? state.wrappers
|
|
147
233
|
: [{ wrapperPath: state.wrapperPath, originalPath: state.originalPath, backupPath: state.backupPath }];
|
|
148
234
|
}
|
|
149
235
|
|
|
150
236
|
export type { ShimState, ShimFileState };
|
|
151
|
-
export { fileErrorCode, readStateResult, readState, statePath, writeState, stateFiles };
|
|
237
|
+
export { readBoundedRegularFile, fileErrorCode, readStateResult, readState, statePath, writeState, stateFiles };
|
package/src/codex/shim.ts
CHANGED
|
@@ -55,6 +55,8 @@ import {
|
|
|
55
55
|
import { tryAcquireShimRestoreLock } from "./shim-restore-lock";
|
|
56
56
|
import {
|
|
57
57
|
isWindowsInteropDir,
|
|
58
|
+
isExecutableCodexCandidate,
|
|
59
|
+
inspectShimFile,
|
|
58
60
|
realIsDirectory,
|
|
59
61
|
resolveStableFnmCodexPath,
|
|
60
62
|
type CodexPathScanDeps,
|
|
@@ -69,6 +71,10 @@ export type { CodexPathScanDeps } from "./shim-path-resolution";
|
|
|
69
71
|
export { isWindowsInteropDir } from "./shim-path-resolution";
|
|
70
72
|
export { isLocalAbsoluteInspectionPath, inspectCodexShimBackingForCommand } from "./shim-inspect";
|
|
71
73
|
|
|
74
|
+
import { isShim, diagnoseCodexShim } from "./shim-diagnostics";
|
|
75
|
+
import { installUnixOverlay, autoRestoreUnixOverlay, uninstallUnixCodexShim, overlayPaths } from "./shim-overlay";
|
|
76
|
+
export { diagnoseCodexShim, codexShimStatus, type CodexShimDiagnostic } from "./shim-diagnostics";
|
|
77
|
+
|
|
72
78
|
export const CODEX_SHIM_REPLACEMENT_STABLE_MS = 100;
|
|
73
79
|
|
|
74
80
|
let lastShimDiscoveryError: string | null = null;
|
|
@@ -114,31 +120,12 @@ function commandNames(name: string): string[] {
|
|
|
114
120
|
return [name, ...exts.flatMap(ext => [`${name}${ext.toLowerCase()}`, `${name}${ext.toUpperCase()}`])];
|
|
115
121
|
}
|
|
116
122
|
|
|
117
|
-
function isShim(path: string): boolean {
|
|
118
|
-
try {
|
|
119
|
-
return readFileSync(path, "utf8").includes(SHIM_MARKER);
|
|
120
|
-
} catch {
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function isHealthyShim(path: string, platform: NodeJS.Platform): boolean {
|
|
126
|
-
try {
|
|
127
|
-
const content = readFileSync(path, "utf8");
|
|
128
|
-
if (content.length < 180 || !content.includes(SHIM_MARKER) || !content.includes("ensure")) return false;
|
|
129
|
-
if (platform !== "win32" && !content.includes(UNIX_SHIM_REVISION_MARKER)) return false;
|
|
130
|
-
if (platform !== "win32" && (lstatSync(path).mode & 0o111) === 0) return false;
|
|
131
|
-
return true;
|
|
132
|
-
} catch {
|
|
133
|
-
return false;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
123
|
export function findCodexOnPath(deps: CodexPathScanDeps = {}): string | null {
|
|
138
124
|
lastShimDiscoveryError = null;
|
|
139
125
|
const exists = deps.exists ?? existsSync;
|
|
140
|
-
const shimFile = deps.isShimFile ?? isShim;
|
|
126
|
+
const shimFile = deps.isShimFile ?? (process.platform === "win32" ? isShim : (path: string) => inspectShimFile(path) !== false);
|
|
141
127
|
const isDir = deps.isDirectory ?? realIsDirectory;
|
|
128
|
+
const eligible = deps.executableEligible ?? (deps.exists ? () => true : isExecutableCodexCandidate);
|
|
142
129
|
const wsl = deps.wsl ?? (process.platform === "linux" && isWslRuntime());
|
|
143
130
|
const usePosix = deps.posixPaths ?? (wsl || process.platform !== "win32");
|
|
144
131
|
// The injected path flavour governs join and separator alike; host-native `join` and
|
|
@@ -166,7 +153,9 @@ export function findCodexOnPath(deps: CodexPathScanDeps = {}): string | null {
|
|
|
166
153
|
const names = isWindowsInteropDir(dir, automountRoot) ? interopNames : commandNames("codex");
|
|
167
154
|
for (const name of names) {
|
|
168
155
|
const path = joinPath(dir, name);
|
|
169
|
-
if (!exists(path)
|
|
156
|
+
if (!exists(path)) continue;
|
|
157
|
+
if (usePosix && (path === overlayPaths().wrapper || !eligible(path))) continue;
|
|
158
|
+
if (shimFile(path)) continue;
|
|
170
159
|
if (!isDir(path)) {
|
|
171
160
|
const resolved = discoveredCodexPath(path, usePosix, deps.realpath);
|
|
172
161
|
if (resolved !== null) return resolved;
|
|
@@ -1072,7 +1061,8 @@ function installCodexShimInternal(options: InstallCodexShimInternalOptions): { i
|
|
|
1072
1061
|
};
|
|
1073
1062
|
}
|
|
1074
1063
|
|
|
1075
|
-
export function installCodexShim(): { installed: boolean; message: string } {
|
|
1064
|
+
export function installCodexShim(): { installed: boolean; message: string; refused?: boolean; runnable?: boolean } {
|
|
1065
|
+
if (process.platform !== "win32") return installUnixOverlay(findCodexOnPath);
|
|
1076
1066
|
return installCodexShimInternal({ allowFreshInstall: true });
|
|
1077
1067
|
}
|
|
1078
1068
|
|
|
@@ -1086,6 +1076,7 @@ export function autoRestoreCodexShim(options: {
|
|
|
1086
1076
|
/** Narrow deterministic race seam for the guarded transaction tests. */
|
|
1087
1077
|
beforeGuardedRefresh?: (wrapperPath: string, index: number) => void;
|
|
1088
1078
|
}): CodexShimAutoRestoreResult {
|
|
1079
|
+
if (process.platform !== "win32") return autoRestoreUnixOverlay(options);
|
|
1089
1080
|
const stateRead = readStateResult();
|
|
1090
1081
|
const state = stateRead.state;
|
|
1091
1082
|
if (!state) {
|
|
@@ -1181,6 +1172,7 @@ export function autoRestoreCodexShim(options: {
|
|
|
1181
1172
|
}
|
|
1182
1173
|
|
|
1183
1174
|
export function uninstallCodexShim(): { removed: boolean; message: string } {
|
|
1175
|
+
if (process.platform !== "win32") return uninstallUnixCodexShim();
|
|
1184
1176
|
const state = readState();
|
|
1185
1177
|
if (!state) return { removed: false, message: "Codex autostart shim is not installed." };
|
|
1186
1178
|
const files = stateFiles(state);
|
|
@@ -1199,48 +1191,3 @@ export function uninstallCodexShim(): { removed: boolean; message: string } {
|
|
|
1199
1191
|
export function isCodexShimInstalled(): boolean {
|
|
1200
1192
|
return diagnoseCodexShim().installed;
|
|
1201
1193
|
}
|
|
1202
|
-
|
|
1203
|
-
export interface CodexShimDiagnostic {
|
|
1204
|
-
installed: boolean;
|
|
1205
|
-
healthy: boolean;
|
|
1206
|
-
summary: string;
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
/** Structured, secret-free shim state for CLI/GUI lifecycle diagnostics. */
|
|
1210
|
-
export function diagnoseCodexShim(): CodexShimDiagnostic {
|
|
1211
|
-
const state = readState();
|
|
1212
|
-
if (!state) {
|
|
1213
|
-
if (existsSync(statePath())) {
|
|
1214
|
-
return {
|
|
1215
|
-
installed: true,
|
|
1216
|
-
healthy: false,
|
|
1217
|
-
summary: `Codex autostart shim state is invalid or corrupt at ${statePath()}. Reinstall or remove the shim.`,
|
|
1218
|
-
};
|
|
1219
|
-
}
|
|
1220
|
-
return {
|
|
1221
|
-
installed: false,
|
|
1222
|
-
healthy: false,
|
|
1223
|
-
summary: "Codex autostart shim is not installed.",
|
|
1224
|
-
};
|
|
1225
|
-
}
|
|
1226
|
-
const files = stateFiles(state);
|
|
1227
|
-
const healthy = files.length > 0 && files.every(file => file.preserveOnly
|
|
1228
|
-
? existsSync(file.backupPath) && !existsSync(file.originalPath)
|
|
1229
|
-
: existsSync(file.wrapperPath)
|
|
1230
|
-
&& (existsSync(file.backupPath) || (file.realPath ? existsSync(file.realPath) : false))
|
|
1231
|
-
&& isHealthyShim(file.wrapperPath, state.platform));
|
|
1232
|
-
const summary = files.map(file => {
|
|
1233
|
-
const wrapper = existsSync(file.wrapperPath)
|
|
1234
|
-
? isShim(file.wrapperPath)
|
|
1235
|
-
? "shim present"
|
|
1236
|
-
: "present but not an opencodex shim"
|
|
1237
|
-
: "missing";
|
|
1238
|
-
const backup = existsSync(file.backupPath) ? "present" : "missing";
|
|
1239
|
-
return `Codex autostart shim: wrapper ${wrapper} at ${file.wrapperPath}; original backup ${backup} at ${file.backupPath}.`;
|
|
1240
|
-
}).join("\n");
|
|
1241
|
-
return { installed: true, healthy, summary };
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
export function codexShimStatus(): string {
|
|
1245
|
-
return diagnoseCodexShim().summary;
|
|
1246
|
-
}
|
package/src/codex/sync.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { currentExternalCodexModelProvider, injectCodexConfig } from "./inject";
|
|
2
|
+
import { FOREIGN_CODEX_HOME_OWNER_MESSAGE, UNKNOWN_CODEX_HOME_OWNER_MESSAGE, unbackedRoutedRemovalMessage } from "./catalog/routed-removal";
|
|
2
3
|
import { printProjectCodexConfigWarnings, groupProjectCodexConfigWarningsByPath, type ProjectCodexConfigWarning } from "./project-config-warnings";
|
|
3
4
|
import { refreshCodexModelCatalog } from "./refresh";
|
|
4
5
|
import { applyProxyEnv, loadConfig } from "../config";
|
|
@@ -35,8 +36,8 @@ export interface CodexSyncResult {
|
|
|
35
36
|
cacheSynced: boolean;
|
|
36
37
|
/**
|
|
37
38
|
* Whether the catalog owner committed a validated catalog or refused the
|
|
38
|
-
* refresh.
|
|
39
|
-
*
|
|
39
|
+
* refresh. Available on applied and catalog-only results; `ok` on applied
|
|
40
|
+
* describes config injection, so callers must also inspect catalog evidence.
|
|
40
41
|
*/
|
|
41
42
|
refreshOutcome?: "committed" | "refused";
|
|
42
43
|
message: string;
|
|
@@ -262,17 +263,27 @@ export async function syncModelsToCodex(
|
|
|
262
263
|
let cacheSynced = false;
|
|
263
264
|
let warning: string | undefined;
|
|
264
265
|
let comboOmissions: ComboCatalogOmission[] = [];
|
|
266
|
+
let refreshOutcome: "committed" | "refused" | undefined;
|
|
265
267
|
|
|
266
268
|
try {
|
|
267
269
|
const cat = await deps.refreshCodexModelCatalog(config);
|
|
268
270
|
added = cat.added;
|
|
271
|
+
refreshOutcome = cat.refreshOutcome;
|
|
269
272
|
catalogExists = cat.catalogExists;
|
|
270
273
|
catalogWritten = cat.catalogWritten;
|
|
271
274
|
cacheSynced = cat.cacheSynced;
|
|
272
275
|
catalogPathForInjection = cat.catalogExists ? cat.path : null;
|
|
273
276
|
catalogPath = catalogPathForInjection;
|
|
274
277
|
comboOmissions = cat.comboOmissions ?? [];
|
|
275
|
-
if (cat.
|
|
278
|
+
if (cat.skippedReason === "unbacked_routed_removal" || cat.skippedReason === "foreign_owner" || cat.skippedReason === "owner_unknown") {
|
|
279
|
+
// The catalog on disk is kept as it was; say so, or a native-only refresh refused for safety
|
|
280
|
+
// reads exactly like a successful sync (#6529).
|
|
281
|
+
warning = cat.skippedReason === "foreign_owner"
|
|
282
|
+
? FOREIGN_CODEX_HOME_OWNER_MESSAGE
|
|
283
|
+
: cat.skippedReason === "owner_unknown" ? UNKNOWN_CODEX_HOME_OWNER_MESSAGE
|
|
284
|
+
: unbackedRoutedRemovalMessage(cat.protectedRoutedNamespaces ?? 1);
|
|
285
|
+
log?.error(`[opencodex] ${warning}`);
|
|
286
|
+
} else if (cat.added > 0) {
|
|
276
287
|
log?.log(` + ${cat.added} models appended to Codex catalog (${cat.path})`);
|
|
277
288
|
} else if (!cat.catalogExists) {
|
|
278
289
|
warning = "catalog sync skipped: no Codex catalog source found; keeping Codex's native catalog.";
|
|
@@ -322,6 +333,7 @@ export async function syncModelsToCodex(
|
|
|
322
333
|
status: "applied",
|
|
323
334
|
ok: result.success,
|
|
324
335
|
added,
|
|
336
|
+
...(refreshOutcome ? { refreshOutcome } : {}),
|
|
325
337
|
catalogPath,
|
|
326
338
|
catalogExists,
|
|
327
339
|
catalogWritten,
|
|
@@ -370,7 +382,15 @@ async function refreshCatalogForSync(
|
|
|
370
382
|
cacheSynced = cat.cacheSynced;
|
|
371
383
|
catalogPath = cat.catalogExists ? cat.path : null;
|
|
372
384
|
comboOmissions = cat.comboOmissions ?? [];
|
|
373
|
-
if (cat.
|
|
385
|
+
if (cat.skippedReason === "unbacked_routed_removal" || cat.skippedReason === "foreign_owner" || cat.skippedReason === "owner_unknown") {
|
|
386
|
+
// The catalog on disk is kept as it was; say so, or a native-only refresh refused for safety
|
|
387
|
+
// reads exactly like a successful sync (#6529).
|
|
388
|
+
warning = cat.skippedReason === "foreign_owner"
|
|
389
|
+
? FOREIGN_CODEX_HOME_OWNER_MESSAGE
|
|
390
|
+
: cat.skippedReason === "owner_unknown" ? UNKNOWN_CODEX_HOME_OWNER_MESSAGE
|
|
391
|
+
: unbackedRoutedRemovalMessage(cat.protectedRoutedNamespaces ?? 1);
|
|
392
|
+
log?.error(`[opencodex] ${warning}`);
|
|
393
|
+
} else if (cat.added > 0) {
|
|
374
394
|
log?.log(` + ${cat.added} models appended to Codex catalog (${cat.path})`);
|
|
375
395
|
} else if (!cat.catalogExists) {
|
|
376
396
|
warning = "catalog sync skipped: no Codex catalog source found; keeping Codex's native catalog.";
|
package/src/combos/failover.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
|
|
12
12
|
interface TargetCooldown {
|
|
13
13
|
cooldownUntil: number;
|
|
14
|
+
status?: number;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
const DEFAULT_COOLDOWN_MS = 60_000;
|
|
@@ -195,6 +196,23 @@ export function remainingComboCooldownMs(comboId: string, now = Date.now()): num
|
|
|
195
196
|
return soonest;
|
|
196
197
|
}
|
|
197
198
|
|
|
199
|
+
/** Snapshot active quota cooldowns without inspecting request eligibility or changing state. */
|
|
200
|
+
export function snapshotComboQuotaCooldowns<T extends Pick<OcxComboTarget, "provider" | "model">>(
|
|
201
|
+
comboId: string,
|
|
202
|
+
targets: Iterable<T>,
|
|
203
|
+
now = Date.now(),
|
|
204
|
+
): Array<{ target: T; cooldownUntil: number }> {
|
|
205
|
+
const snapshot: Array<{ target: T; cooldownUntil: number }> = [];
|
|
206
|
+
for (const target of targets) {
|
|
207
|
+
const key = cooldownMapKey(comboId, target);
|
|
208
|
+
const cooldown = targetCooldowns.get(key);
|
|
209
|
+
if (!cooldown || cooldown.cooldownUntil <= now) continue;
|
|
210
|
+
if (cooldown.status !== 429 && cooldown.status !== 402) continue;
|
|
211
|
+
snapshot.push({ target, cooldownUntil: cooldown.cooldownUntil });
|
|
212
|
+
}
|
|
213
|
+
return snapshot;
|
|
214
|
+
}
|
|
215
|
+
|
|
198
216
|
export function comboCooldownRetryAfterSeconds(comboId: string, now = Date.now()): string | undefined {
|
|
199
217
|
const remainingMs = remainingComboCooldownMs(comboId, now);
|
|
200
218
|
if (remainingMs === undefined) return undefined;
|
|
@@ -244,6 +262,7 @@ export function coolComboTarget(
|
|
|
244
262
|
targetCooldowns.set(cooldownMapKey(comboId, target), {
|
|
245
263
|
// Local fallbacks are capped at ten minutes; explicit server delays at one day.
|
|
246
264
|
cooldownUntil: now + (serverDelayMs ?? Math.min(Math.max(cooldownMs, 1), MAX_COOLDOWN_MS)),
|
|
265
|
+
status: options?.status,
|
|
247
266
|
});
|
|
248
267
|
sweepExpiredOnWrite(now);
|
|
249
268
|
return true;
|
package/src/combos/index.ts
CHANGED
package/src/combos/resolve.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
coolComboTarget,
|
|
7
7
|
earliestComboCooldown,
|
|
8
8
|
isComboTargetInCooldown,
|
|
9
|
+
snapshotComboQuotaCooldowns as snapshotTargetQuotaCooldowns,
|
|
9
10
|
type ComboFailureCooldownScope,
|
|
10
11
|
} from "./failover";
|
|
11
12
|
import { quotaResetRemainingMs } from "./reset-window";
|
|
@@ -67,6 +68,17 @@ function targetProviderIsUsable(config: OcxConfig, target: OcxComboTarget, now:
|
|
|
67
68
|
return !cachedProviderQuotaIsExhausted(getCachedProviderRoutingQuota(target.provider, provider, now), now, target.model);
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
/** Capture usable providers' quota cooldowns; request eligibility is deferred to exhaustion. */
|
|
72
|
+
export function snapshotComboQuotaCooldowns(
|
|
73
|
+
config: OcxConfig,
|
|
74
|
+
comboId: string,
|
|
75
|
+
now = Date.now(),
|
|
76
|
+
): Array<{ target: NormalizedComboTarget; cooldownUntil: number }> {
|
|
77
|
+
const targets = getCombo(config, comboId)?.targets.filter(target =>
|
|
78
|
+
targetProviderIsUsable(config, target, now)) ?? [];
|
|
79
|
+
return snapshotTargetQuotaCooldowns(comboId, targets, now);
|
|
80
|
+
}
|
|
81
|
+
|
|
70
82
|
function quotaWindowExhausted(percent: number | undefined, resetAt: number | undefined, now: number): boolean {
|
|
71
83
|
if (typeof percent !== "number" || !Number.isFinite(percent) || percent < 100) return false;
|
|
72
84
|
return typeof resetAt !== "number" || !Number.isFinite(resetAt) || resetAt > now;
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "node:fs";
|
|
14
14
|
import { basename, dirname, join } from "node:path";
|
|
15
15
|
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
16
|
-
import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
|
|
16
|
+
import { assertNotRealCodexHomeUnderTest, assertNotRealHomeUnderTest } from "../lib/test-home-guard";
|
|
17
17
|
import {
|
|
18
18
|
forgetEphemeralSecretPath,
|
|
19
19
|
hardenSecretPath,
|
|
@@ -114,14 +114,19 @@ export function resolveWriteTarget(path: string): string {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
function assertResolvedTargetAllowed(path: string, target: string): void {
|
|
117
|
+
// Codex-home files (catalog, models cache, journal, config.toml, profile) are addressed through
|
|
118
|
+
// CODEX_HOME, which falls back to os.homedir() when a test's teardown deletes it, and os.homedir()
|
|
119
|
+
// ignores HOME. That is how test runs rewrote the real catalog (#6529), so every atomic write
|
|
120
|
+
// whose directory is the real Codex home is refused under an armed test process.
|
|
121
|
+
let realParent: string | null = null;
|
|
122
|
+
try {
|
|
123
|
+
realParent = realpathSync(dirname(target));
|
|
124
|
+
} catch {
|
|
125
|
+
realParent = null;
|
|
126
|
+
}
|
|
127
|
+
assertNotRealCodexHomeUnderTest(realParent ?? dirname(target));
|
|
117
128
|
if (target === path) {
|
|
118
|
-
|
|
119
|
-
try {
|
|
120
|
-
realParent = realpathSync(dirname(target));
|
|
121
|
-
} catch {
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
if (realParent !== dirname(target)) assertNotRealHomeUnderTest(realParent);
|
|
129
|
+
if (realParent !== null && realParent !== dirname(target)) assertNotRealHomeUnderTest(realParent);
|
|
125
130
|
return;
|
|
126
131
|
}
|
|
127
132
|
assertNotRealHomeUnderTest(dirname(target));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isSubagentModelEntry, rawSubagentModelForce } from "./subagent-models";
|
|
2
|
+
import { protocolConfigSchema } from "./schema/config-schema";
|
|
2
3
|
import { createHash } from "node:crypto";
|
|
3
4
|
import { lstatSync, readFileSync } from "node:fs";
|
|
4
5
|
import { join } from "node:path";
|
|
@@ -66,6 +67,7 @@ import {
|
|
|
66
67
|
runtimeRoleSchema,
|
|
67
68
|
spendSchema,
|
|
68
69
|
chatgptDesktopSchema,
|
|
70
|
+
chatgptDesktopConfigIssue,
|
|
69
71
|
compactionRoutingSchema,
|
|
70
72
|
skillsConfigSchema,
|
|
71
73
|
memoryModelsSchema,
|
|
@@ -109,6 +111,8 @@ function validFileConfigDiagnostics(config: OcxConfig, rawParsed: unknown): Conf
|
|
|
109
111
|
if (normalized.chatgptDesktop?.appServerShim === true && process.platform !== "darwin") {
|
|
110
112
|
warnings.push("chatgptDesktop.appServerShim is experimental and macOS only; ignored on this platform");
|
|
111
113
|
}
|
|
114
|
+
const chatgptDesktopIssue = chatgptDesktopConfigIssue(rawParsed);
|
|
115
|
+
if (chatgptDesktopIssue) warnings.push(`${chatgptDesktopIssue}; the whole chatgptDesktop block is ignored, so the ChatGPT desktop integration reads as off`);
|
|
112
116
|
warnings.push(...inheritedFastWireConflictProviderNames(normalized).map(inheritedFastWireConflictWarning));
|
|
113
117
|
warnings.push(...degradedCodexAccountPriorityWarnings(rawParsed, normalized));
|
|
114
118
|
warnings.push(...degradedListenerWarnings(rawParsed, normalized));
|
|
@@ -647,7 +651,19 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
|
|
|
647
651
|
if (memoryModels !== undefined && !memoryModelsSchema.safeParse(memoryModels).success) {
|
|
648
652
|
return { ok: false, error: "schema_invalid: memoryModels: requires a nonblank model and an optional declared reasoningEffort per configured phase, and no other fields" };
|
|
649
653
|
}
|
|
650
|
-
const
|
|
654
|
+
const protocols = rawConfigRecord(value)?.protocols;
|
|
655
|
+
if (protocols !== undefined && !protocolConfigSchema.safeParse(protocols).success) {
|
|
656
|
+
return { ok: false, error: "schema_invalid: protocols: expected valid protocol policy and boolean rollout flags" };
|
|
657
|
+
}
|
|
658
|
+
const rawAnthropicPool = rawConfigRecord(value)?.anthropicAccountPool;
|
|
659
|
+
const anthropicPool = rawConfigRecord(rawAnthropicPool);
|
|
660
|
+
if (rawAnthropicPool !== undefined && !anthropicPool) {
|
|
661
|
+
return { ok: false, error: "schema_invalid: anthropicAccountPool: must be an object" };
|
|
662
|
+
}
|
|
663
|
+
if (anthropicPool && Object.hasOwn(anthropicPool, "nativeMessages") && typeof anthropicPool.nativeMessages !== "boolean") {
|
|
664
|
+
return { ok: false, error: "schema_invalid: anthropicAccountPool.nativeMessages: must be a boolean" };
|
|
665
|
+
}
|
|
666
|
+
const routeValue = anthropicPool?.routes;
|
|
651
667
|
if (routeValue !== undefined) {
|
|
652
668
|
const parsed = parseAnthropicModelRoutes(routeValue);
|
|
653
669
|
if (!parsed.ok) return { ok: false, error: `schema_invalid: anthropicAccountPool.routes: ${parsed.error}` };
|
|
@@ -730,6 +746,8 @@ export function configDiagnosticsFromRaw(raw: string): ConfigDiagnostics {
|
|
|
730
746
|
if (salvaged) {
|
|
731
747
|
const config = normalizeApiKeyIds(salvaged.parsed);
|
|
732
748
|
const warnings = degradedListenerWarnings(parsed, config);
|
|
749
|
+
const chatgptDesktopIssue = chatgptDesktopConfigIssue(parsed);
|
|
750
|
+
if (chatgptDesktopIssue) warnings.push(`${chatgptDesktopIssue}; the whole chatgptDesktop block is ignored, so the ChatGPT desktop integration reads as off`);
|
|
733
751
|
return {
|
|
734
752
|
config,
|
|
735
753
|
source: "fallback",
|
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
transientRetryOn5xxPolicySchema,
|
|
42
42
|
runtimeRoleSchema,
|
|
43
43
|
spendSchema,
|
|
44
|
+
chatgptDesktopConfigIssue,
|
|
44
45
|
} from "./schema/leaf-validators";
|
|
45
46
|
import { hasWarnedInheritedFastWireConflict, markWarnedInheritedFastWireConflict } from "./warn-memo";
|
|
46
47
|
|
|
@@ -124,6 +125,8 @@ export function warnDegradedCompactionRouting(rawParsed: unknown, validated: Ocx
|
|
|
124
125
|
export function warnDegradedTopLevelOptIns(rawParsed: unknown, validated: OcxConfig): void {
|
|
125
126
|
if (compactionRecoveryConfigError(rawParsed)) console.warn("⚠️ invalid compactionRecovery disabled; the original compaction failure is preserved");
|
|
126
127
|
if (blockedModelRedirectsError(rawParsed)) console.warn("⚠️ invalid blockedModelRedirects ignored; provider routing remains available");
|
|
128
|
+
const chatgptDesktop = chatgptDesktopConfigIssue(rawParsed);
|
|
129
|
+
if (chatgptDesktop) console.warn(`⚠️ config.json ${chatgptDesktop} — the whole chatgptDesktop block is ignored, so the ChatGPT desktop integration reads as off`);
|
|
127
130
|
warnDegradedStreamMode(rawParsed, validated);
|
|
128
131
|
warnDegradedCompactionRouting(rawParsed, validated);
|
|
129
132
|
warnDegradedMemoryModels(rawParsed, validated);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as z from "zod/v4";
|
|
2
|
+
import { redactSecretString } from "../../lib/redact";
|
|
3
|
+
|
|
4
|
+
/** Experimental macOS ChatGPT app-server shim; only a strict optional boolean is accepted. */
|
|
5
|
+
export const chatgptDesktopSchema = z.object({ appServerShim: z.boolean().optional() }).strict();
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Why a present `chatgptDesktop` block fails the schema, or null when it is valid or absent. The
|
|
9
|
+
* read path degrades a failing block to absent, so the integration reads as off while the file
|
|
10
|
+
* still says `appServerShim: true`; this names the field so that is never silent. A leftover key
|
|
11
|
+
* from an older or ported config (for example `unblockSend`) is the usual cause.
|
|
12
|
+
*/
|
|
13
|
+
export function chatgptDesktopConfigIssue(rawParsed: unknown): string | null {
|
|
14
|
+
if (!rawParsed || typeof rawParsed !== "object" || Array.isArray(rawParsed)) return null;
|
|
15
|
+
const block = (rawParsed as Record<string, unknown>).chatgptDesktop;
|
|
16
|
+
if (block === undefined) return null;
|
|
17
|
+
const result = chatgptDesktopSchema.safeParse(block);
|
|
18
|
+
if (result.success) return null;
|
|
19
|
+
const issue = result.error.issues[0];
|
|
20
|
+
const field = issue?.path.join(".");
|
|
21
|
+
return redactSecretString(`chatgptDesktop${field ? `.${field}` : ""}: ${issue?.message ?? "invalid configuration"}`);
|
|
22
|
+
}
|
|
@@ -74,6 +74,18 @@ import { parseDesktopProfile } from "../../claude/desktop-profile";
|
|
|
74
74
|
import { isInterceptBindingId, isInterceptBindingRoute } from "../../claude/intercept/model-bindings";
|
|
75
75
|
import { DEFAULT_APP_OWNED_MEMORY_BUDGET_BYTES, MAX_APP_OWNED_MEMORY_BUDGET_MB, MIN_APP_OWNED_MEMORY_BUDGET_MB } from "../../lib/app-owned-memory";
|
|
76
76
|
|
|
77
|
+
/** Strict write contract; file-load recovery is applied only by the enclosing schema. */
|
|
78
|
+
export const protocolConfigSchema = z.object({
|
|
79
|
+
unrepresentable: z.enum(["legacy", "reject"]).optional(),
|
|
80
|
+
rollout: z.object({
|
|
81
|
+
nativeChatCombos: z.boolean().optional(),
|
|
82
|
+
managedMessagesNative: z.boolean().optional(),
|
|
83
|
+
managedMessagesNativeOAuth: z.boolean().optional(),
|
|
84
|
+
directEncoders: z.boolean().optional(),
|
|
85
|
+
shadowPlan: z.boolean().optional(),
|
|
86
|
+
}).strict().optional(),
|
|
87
|
+
}).strict();
|
|
88
|
+
|
|
77
89
|
export const configSchema = z.object({
|
|
78
90
|
chatgptDesktop: chatgptDesktopSchema.optional().catch(undefined),
|
|
79
91
|
codexNativeSteering: z.boolean().optional().catch(false),
|
|
@@ -96,18 +108,23 @@ export const configSchema = z.object({
|
|
|
96
108
|
// which can reopen a surface the operator meant to close. src/protocols/settings.ts parses it
|
|
97
109
|
// and fails closed instead.
|
|
98
110
|
apiSurfaces: z.unknown().optional(),
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
rollout
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
+
// Keep malformed native policy disabled even when an enabled pool supplies defaults.
|
|
112
|
+
protocols: protocolConfigSchema.optional().catch(ctx => {
|
|
113
|
+
const raw = ctx.input;
|
|
114
|
+
const protocols = raw && typeof raw === "object" && !Array.isArray(raw) ? raw as Record<string, unknown> : {};
|
|
115
|
+
const rollout = protocols.rollout;
|
|
116
|
+
const fields = rollout && typeof rollout === "object" && !Array.isArray(rollout) ? rollout as Record<string, unknown> : {};
|
|
117
|
+
return {
|
|
118
|
+
unrepresentable: protocols.unrepresentable === "reject" ? "reject" as const : "legacy" as const,
|
|
119
|
+
rollout: {
|
|
120
|
+
nativeChatCombos: fields.nativeChatCombos === true,
|
|
121
|
+
managedMessagesNative: false,
|
|
122
|
+
managedMessagesNativeOAuth: false,
|
|
123
|
+
directEncoders: fields.directEncoders === true,
|
|
124
|
+
shadowPlan: fields.shadowPlan === true,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}),
|
|
111
128
|
// A malformed present client block must remain diagnosable from raw config and
|
|
112
129
|
// fail closed through src/client/state.ts; unrelated provider state still loads.
|
|
113
130
|
client: clientConnectionSchema.optional().catch(undefined),
|
|
@@ -308,8 +325,9 @@ export const configSchema = z.object({
|
|
|
308
325
|
// path below and wipe providers/pool accounts. Warning emitted in loadConfig.
|
|
309
326
|
streamMode: z.enum(["auto", "legacy-tee", "eager-relay"]).optional().catch(undefined),
|
|
310
327
|
blockedModelRedirects: blockedModelRedirectsSchema.optional().catch(undefined),
|
|
311
|
-
//
|
|
312
|
-
|
|
328
|
+
// Degrade malformed hand edits locally; candidate writes reject them before parsing.
|
|
329
|
+
// An invalid native preference retains the legacy route instead of enabling native by default.
|
|
330
|
+
anthropicAccountPool: z.object({ nativeMessages: z.boolean().optional().catch(false) }).passthrough().optional().catch(undefined),
|
|
313
331
|
// Same degrade-don't-reject rationale as the fields above: a hand-edited
|
|
314
332
|
// non-string must not trip the backup-and-defaults repair path. Unset then
|
|
315
333
|
// takes the canonical sideband path (src/server/live.ts normalizeSidebandRoot).
|