@bitkyc08/opencodex 2.36.0 → 2.38.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 +69 -10
- package/gui/dist/assets/index-C14iCj_Q.js +112 -0
- package/gui/dist/assets/index-D7PIz7_g.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/aside.svg +3 -0
- package/gui/dist/provider-icons/deepseek-harness.svg +3 -0
- package/gui/dist/provider-icons/oh-my-pi.svg +11 -0
- package/gui/dist/provider-icons/openclaw.svg +54 -0
- package/gui/dist/provider-icons/prime-agent.svg +21 -0
- package/gui/dist/provider-icons/zcode.svg +219 -0
- package/package.json +1 -1
- package/src/adapters/cursor/protobuf-request.ts +4 -1
- package/src/adapters/cursor/tool-definitions.ts +36 -4
- package/src/adapters/kiro-constants.ts +36 -2
- package/src/adapters/kiro.ts +13 -2
- package/src/cli/capabilities.ts +14 -0
- package/src/cli/claude.ts +12 -0
- package/src/cli/codex-cli-update.ts +96 -0
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/export-command.ts +18 -17
- package/src/cli/help.ts +2 -2
- package/src/cli/index.ts +3 -2
- package/src/cli/launcher-context.ts +53 -2
- package/src/cli/opencode.ts +126 -33
- package/src/cli/registry.ts +16 -10
- package/src/cli/system-command.ts +6 -1
- package/src/clients/config-export.ts +293 -28
- package/src/codex/account-store.ts +10 -4
- package/src/codex/autostart-health.ts +3 -3
- package/src/codex/catalog/provider-fetch.ts +20 -1
- package/src/codex/catalog/sync.ts +4 -3
- package/src/codex/cli-install-provenance.ts +795 -0
- package/src/codex/convergence.ts +4 -3
- package/src/codex/credential-mutation-epoch.ts +11 -0
- package/src/codex/main-account.ts +2 -0
- package/src/codex/model-entitlements.ts +489 -30
- package/src/codex/native-profile-manager.ts +4 -0
- package/src/codex/reset-credit-operation-ledger.ts +1411 -0
- package/src/codex/reset-credit-recovery.ts +20 -2
- package/src/codex/shim.ts +204 -18
- package/src/codex/user-identity.ts +2 -1
- package/src/config/paths.ts +18 -3
- package/src/config.ts +23 -0
- package/src/generated/compatibility-version.json +84 -48
- package/src/integrations/registry.ts +112 -0
- package/src/integrations/state.ts +67 -5
- package/src/integrations/writer.ts +25 -9
- package/src/lib/bounded-subprocess.ts +36 -0
- package/src/lib/strict-semver.ts +47 -0
- package/src/lib/windows-elevation.ts +32 -1
- package/src/lib/windows-secret-acl.ts +47 -25
- package/src/lib/windows-service-mutation-lock.ts +133 -0
- package/src/lib/windows-user-principal.ts +15 -17
- package/src/responses/spill-store.ts +334 -29
- package/src/responses/state.ts +488 -7
- package/src/server/index.ts +4 -3
- package/src/server/lifecycle.ts +5 -1
- package/src/server/management/model-rows.ts +11 -2
- package/src/server/management/provider-routes.ts +4 -0
- package/src/server/management/system-restart.ts +5 -5
- package/src/server/management-api.ts +7 -2
- package/src/server/startup-action-control.ts +3 -2
- package/src/service.ts +594 -33
- package/src/sidecar/candidates.ts +1 -1
- package/src/update/codex-cli-update-launch-policy.d.mts +18 -0
- package/src/update/codex-cli-update-launch-policy.mjs +30 -0
- package/src/update/index.ts +3 -2
- package/src/update/job.ts +10 -11
- package/gui/dist/assets/index-Cy7Z_pl0.css +0 -1
- package/gui/dist/assets/index-DO8liQVL.js +0 -112
|
@@ -27,6 +27,20 @@ export type CodexResetCreditConsumeCode =
|
|
|
27
27
|
| "nothing_to_reset"
|
|
28
28
|
| "no_credit";
|
|
29
29
|
|
|
30
|
+
declare const CODEX_RESERVED_OPERATION_ID_BRAND: unique symbol;
|
|
31
|
+
|
|
32
|
+
/** An operation id whose durable reservation was validated by the operation ledger. */
|
|
33
|
+
export type CodexReservedOperationId = string & {
|
|
34
|
+
readonly [CODEX_RESERVED_OPERATION_ID_BRAND]: true;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const CODEX_RESET_CREDIT_OPERATION_ID_PATTERN =
|
|
38
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
39
|
+
|
|
40
|
+
export function isCodexResetCreditOperationId(value: unknown): value is string {
|
|
41
|
+
return typeof value === "string" && CODEX_RESET_CREDIT_OPERATION_ID_PATTERN.test(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
30
44
|
export type CodexResetCreditRecoveryAuthorization = Readonly<{
|
|
31
45
|
enabled: boolean;
|
|
32
46
|
/**
|
|
@@ -189,7 +203,9 @@ const RESET_ELIGIBLE_CODES = {
|
|
|
189
203
|
insufficient_quota: true,
|
|
190
204
|
} as const satisfies Record<CodexResetEligibleExhaustionCode, true>;
|
|
191
205
|
|
|
192
|
-
function
|
|
206
|
+
export function snapshotCodexResetCreditRecoveryGeneration(
|
|
207
|
+
input: unknown,
|
|
208
|
+
): CodexResetCreditRecoveryGeneration {
|
|
193
209
|
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
194
210
|
throw new TypeError("generation must be an object");
|
|
195
211
|
}
|
|
@@ -251,6 +267,8 @@ function compareGenerationOrder(
|
|
|
251
267
|
return 0;
|
|
252
268
|
}
|
|
253
269
|
|
|
270
|
+
export const compareCodexResetCreditRecoveryGenerationOrder = compareGenerationOrder;
|
|
271
|
+
|
|
254
272
|
function authorizedResetRejection(authorization: CodexResetCreditRecoveryAuthorization): boolean {
|
|
255
273
|
const hasOwn = Object.prototype.hasOwnProperty;
|
|
256
274
|
if (!hasOwn.call(authorization, "enabled")
|
|
@@ -507,7 +525,7 @@ export class CodexResetCreditRecoveryCoordinator {
|
|
|
507
525
|
let requestSignal: AbortSignal | undefined;
|
|
508
526
|
try {
|
|
509
527
|
requestSignal = snapshotRequestSignal(options);
|
|
510
|
-
generationSnapshot =
|
|
528
|
+
generationSnapshot = snapshotCodexResetCreditRecoveryGeneration(generation);
|
|
511
529
|
} catch (error) {
|
|
512
530
|
rejectAttempt(error);
|
|
513
531
|
return attempt;
|
package/src/codex/shim.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { basename, delimiter, dirname, extname, join, posix } from "node:path";
|
|
4
|
+
import { basename, delimiter, dirname, extname, join, posix, win32 } from "node:path";
|
|
5
5
|
import {
|
|
6
6
|
chmodSync,
|
|
7
7
|
closeSync,
|
|
@@ -264,6 +264,28 @@ interface ShimFileState {
|
|
|
264
264
|
preserveOnly?: boolean;
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
export type CodexShimBackingForCommand =
|
|
268
|
+
| Readonly<{ status: "not-tracked" }>
|
|
269
|
+
| Readonly<{
|
|
270
|
+
status: "matched";
|
|
271
|
+
selectedRole: "wrapper" | "backing";
|
|
272
|
+
backingPath: string;
|
|
273
|
+
backingKind: "backup" | "real";
|
|
274
|
+
}>
|
|
275
|
+
| Readonly<{
|
|
276
|
+
status: "unknown";
|
|
277
|
+
reason:
|
|
278
|
+
| "state_invalid"
|
|
279
|
+
| "platform_mismatch"
|
|
280
|
+
| "ambiguous_match"
|
|
281
|
+
| "preserve_only"
|
|
282
|
+
| "backing_missing"
|
|
283
|
+
| "backing_mismatch"
|
|
284
|
+
| "binding_unavailable"
|
|
285
|
+
| "wrapper_unhealthy"
|
|
286
|
+
| "version_manager_refused";
|
|
287
|
+
}>;
|
|
288
|
+
|
|
267
289
|
interface ShimPathFingerprint {
|
|
268
290
|
dev: number;
|
|
269
291
|
ino: number;
|
|
@@ -616,8 +638,13 @@ function backupPathFor(path: string): string {
|
|
|
616
638
|
* deliberately excluded: a false positive here refuses a restore that would
|
|
617
639
|
* otherwise be correct.
|
|
618
640
|
*/
|
|
619
|
-
export function isVersionManagerOwnedCodexPath(
|
|
620
|
-
|
|
641
|
+
export function isVersionManagerOwnedCodexPath(
|
|
642
|
+
path: string,
|
|
643
|
+
platform: NodeJS.Platform = process.platform,
|
|
644
|
+
): boolean {
|
|
645
|
+
const normalized = (platform === "win32"
|
|
646
|
+
? win32.normalize(path).replace(/\\/g, "/")
|
|
647
|
+
: posix.normalize(path)).toLowerCase();
|
|
621
648
|
return normalized.includes("/mise/installs/")
|
|
622
649
|
|| normalized.includes("/mise/shims/")
|
|
623
650
|
|| normalized.includes("/.asdf/installs/")
|
|
@@ -1078,6 +1105,7 @@ exit $LASTEXITCODE
|
|
|
1078
1105
|
|
|
1079
1106
|
interface ShimStateReadResult {
|
|
1080
1107
|
state: ShimState | null;
|
|
1108
|
+
present: boolean;
|
|
1081
1109
|
warning?: string;
|
|
1082
1110
|
}
|
|
1083
1111
|
|
|
@@ -1087,7 +1115,17 @@ function fileErrorCode(error: unknown): string | undefined {
|
|
|
1087
1115
|
: undefined;
|
|
1088
1116
|
}
|
|
1089
1117
|
|
|
1090
|
-
function readBoundedRegularFile(path: string, maxBytes: number): { content: string } | { warning: string } | null {
|
|
1118
|
+
function readBoundedRegularFile(path: string, maxBytes: number): { bytes: Buffer; content: string } | { warning: string } | null {
|
|
1119
|
+
let lexicalBefore: Stats;
|
|
1120
|
+
try {
|
|
1121
|
+
lexicalBefore = lstatSync(path);
|
|
1122
|
+
if (lexicalBefore.isSymbolicLink() || !lexicalBefore.isFile()) {
|
|
1123
|
+
return { warning: `Codex shim state is not a direct regular file at ${path}; auto-restore skipped.` };
|
|
1124
|
+
}
|
|
1125
|
+
} catch (error) {
|
|
1126
|
+
if (fileErrorCode(error) === "ENOENT") return null;
|
|
1127
|
+
return { warning: `Codex shim state could not be inspected at ${path}.` };
|
|
1128
|
+
}
|
|
1091
1129
|
let fd: number;
|
|
1092
1130
|
try {
|
|
1093
1131
|
fd = openSync(path, "r");
|
|
@@ -1113,25 +1151,33 @@ function readBoundedRegularFile(path: string, maxBytes: number): { content: stri
|
|
|
1113
1151
|
return { warning: `Codex shim state exceeds the 1 MiB startup limit at ${path}; auto-restore skipped.` };
|
|
1114
1152
|
}
|
|
1115
1153
|
const after = fstatSync(fd);
|
|
1154
|
+
let lexicalAfter: Stats;
|
|
1155
|
+
try {
|
|
1156
|
+
lexicalAfter = lstatSync(path);
|
|
1157
|
+
} catch {
|
|
1158
|
+
return { warning: `Codex shim state changed while being read at ${path}; auto-restore skipped.` };
|
|
1159
|
+
}
|
|
1116
1160
|
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size
|
|
1117
|
-
|| before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs
|
|
1161
|
+
|| before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs
|
|
1162
|
+
|| lexicalBefore.dev !== before.dev || lexicalBefore.ino !== before.ino
|
|
1163
|
+
|| lexicalAfter.isSymbolicLink() || lexicalAfter.dev !== after.dev || lexicalAfter.ino !== after.ino) {
|
|
1118
1164
|
return { warning: `Codex shim state changed while being read at ${path}; auto-restore skipped.` };
|
|
1119
1165
|
}
|
|
1120
|
-
return { content: buffer.toString("utf8") };
|
|
1166
|
+
return { bytes: buffer, content: buffer.toString("utf8") };
|
|
1121
1167
|
} finally {
|
|
1122
1168
|
closeSync(fd);
|
|
1123
1169
|
}
|
|
1124
1170
|
}
|
|
1125
1171
|
|
|
1126
|
-
function readStateResult(): ShimStateReadResult {
|
|
1127
|
-
const bounded = readBoundedRegularFile(
|
|
1128
|
-
if (!bounded) return { state: null };
|
|
1129
|
-
if ("warning" in bounded) return { state: null, warning: bounded.warning };
|
|
1172
|
+
function readStateResult(path = statePath()): ShimStateReadResult {
|
|
1173
|
+
const bounded = readBoundedRegularFile(path, CODEX_SHIM_STATE_MAX_BYTES);
|
|
1174
|
+
if (!bounded) return { state: null, present: false };
|
|
1175
|
+
if ("warning" in bounded) return { state: null, present: true, warning: bounded.warning };
|
|
1130
1176
|
try {
|
|
1131
1177
|
const value = JSON.parse(bounded.content) as unknown;
|
|
1132
|
-
if (!value || typeof value !== "object") return { state: null };
|
|
1178
|
+
if (!value || typeof value !== "object") return { state: null, present: true };
|
|
1133
1179
|
const state = value as Record<string, unknown>;
|
|
1134
|
-
if (typeof state.platform !== "string") return { state: null };
|
|
1180
|
+
if (typeof state.platform !== "string") return { state: null, present: true };
|
|
1135
1181
|
const validFile = (item: unknown): item is ShimFileState => {
|
|
1136
1182
|
if (!item || typeof item !== "object") return false;
|
|
1137
1183
|
const file = item as Record<string, unknown>;
|
|
@@ -1142,13 +1188,13 @@ function readStateResult(): ShimStateReadResult {
|
|
|
1142
1188
|
&& (file.preserveOnly === undefined || typeof file.preserveOnly === "boolean");
|
|
1143
1189
|
};
|
|
1144
1190
|
if (state.wrappers !== undefined) {
|
|
1145
|
-
if (!Array.isArray(state.wrappers) || state.wrappers.length === 0 || !state.wrappers.every(validFile)) return { state: null };
|
|
1191
|
+
if (!Array.isArray(state.wrappers) || state.wrappers.length === 0 || !state.wrappers.every(validFile)) return { state: null, present: true };
|
|
1146
1192
|
} else if (!validFile(state)) {
|
|
1147
|
-
return { state: null };
|
|
1193
|
+
return { state: null, present: true };
|
|
1148
1194
|
}
|
|
1149
|
-
return { state: state as unknown as ShimState };
|
|
1195
|
+
return { state: state as unknown as ShimState, present: true };
|
|
1150
1196
|
} catch {
|
|
1151
|
-
return { state: null };
|
|
1197
|
+
return { state: null, present: true };
|
|
1152
1198
|
}
|
|
1153
1199
|
}
|
|
1154
1200
|
|
|
@@ -1156,6 +1202,146 @@ function readState(): ShimState | null {
|
|
|
1156
1202
|
return readStateResult().state;
|
|
1157
1203
|
}
|
|
1158
1204
|
|
|
1205
|
+
export function isLocalAbsoluteInspectionPath(path: string, platform: NodeJS.Platform): boolean {
|
|
1206
|
+
if (platform !== "win32") return posix.isAbsolute(path);
|
|
1207
|
+
const normalized = path.replace(/\//g, "\\");
|
|
1208
|
+
// UNC and device namespaces can initiate remote I/O while a nominally local
|
|
1209
|
+
// inspection is resolving user-controlled paths. Root-relative paths are
|
|
1210
|
+
// drive-context dependent, so require an explicit local drive as well.
|
|
1211
|
+
return win32.isAbsolute(path)
|
|
1212
|
+
&& /^[a-z]:\\/i.test(normalized)
|
|
1213
|
+
&& !normalized.startsWith("\\\\");
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
function windowsShimInspectionIsDeferred(platform: NodeJS.Platform): boolean {
|
|
1217
|
+
return platform === "win32";
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
/** Resolve one selected command through already-recorded shim state, without repair. */
|
|
1221
|
+
export function inspectCodexShimBackingForCommand(
|
|
1222
|
+
selectedCommand: string,
|
|
1223
|
+
platform: NodeJS.Platform = process.platform,
|
|
1224
|
+
configDir: string = getConfigDir(),
|
|
1225
|
+
): CodexShimBackingForCommand {
|
|
1226
|
+
// Pathname prechecks cannot prevent a writable Windows ancestor from being
|
|
1227
|
+
// replaced with a remote reparse point before the later state/fingerprint
|
|
1228
|
+
// reads. Keep the exported read-only helper fail-closed until those reads are
|
|
1229
|
+
// performed through a handle-bound Windows provenance layer.
|
|
1230
|
+
if (windowsShimInspectionIsDeferred(platform)) {
|
|
1231
|
+
return Object.freeze({ status: "unknown" as const, reason: "binding_unavailable" as const });
|
|
1232
|
+
}
|
|
1233
|
+
if (!isLocalAbsoluteInspectionPath(configDir, platform)) {
|
|
1234
|
+
return Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const });
|
|
1235
|
+
}
|
|
1236
|
+
const stateFile = join(configDir, "codex-shim.json");
|
|
1237
|
+
try {
|
|
1238
|
+
const stateEntry = lstatSync(stateFile);
|
|
1239
|
+
if (stateEntry.isSymbolicLink()) {
|
|
1240
|
+
return Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const });
|
|
1241
|
+
}
|
|
1242
|
+
} catch (error) {
|
|
1243
|
+
if (fileErrorCode(error) !== "ENOENT") {
|
|
1244
|
+
return Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const });
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
const result = readStateResult(stateFile);
|
|
1248
|
+
if (!result.state) {
|
|
1249
|
+
return result.present
|
|
1250
|
+
? Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const })
|
|
1251
|
+
: Object.freeze({ status: "not-tracked" as const });
|
|
1252
|
+
}
|
|
1253
|
+
const pathApi = platform === "win32" ? win32 : posix;
|
|
1254
|
+
const samePath = (left: string, right: string): boolean => {
|
|
1255
|
+
const normalizedLeft = pathApi.resolve(left);
|
|
1256
|
+
const normalizedRight = pathApi.resolve(right);
|
|
1257
|
+
return platform === "win32"
|
|
1258
|
+
? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
|
|
1259
|
+
: normalizedLeft === normalizedRight;
|
|
1260
|
+
};
|
|
1261
|
+
const files = stateFiles(result.state);
|
|
1262
|
+
if (files.some(file => !file.wrapperPath || !file.originalPath || !file.backupPath
|
|
1263
|
+
|| ![file.wrapperPath, file.originalPath, file.backupPath, file.realPath]
|
|
1264
|
+
.filter((path): path is string => typeof path === "string")
|
|
1265
|
+
.every(path => isLocalAbsoluteInspectionPath(path, platform)))) {
|
|
1266
|
+
return Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const });
|
|
1267
|
+
}
|
|
1268
|
+
const wrapperKeys = files.map(file => platform === "win32"
|
|
1269
|
+
? pathApi.resolve(file.wrapperPath).toLowerCase()
|
|
1270
|
+
: pathApi.resolve(file.wrapperPath));
|
|
1271
|
+
if (new Set(wrapperKeys).size !== wrapperKeys.length) {
|
|
1272
|
+
return Object.freeze({ status: "unknown" as const, reason: "state_invalid" as const });
|
|
1273
|
+
}
|
|
1274
|
+
const selectedFingerprint = shimPathFingerprint(selectedCommand);
|
|
1275
|
+
if (!selectedFingerprint) {
|
|
1276
|
+
return Object.freeze({ status: "unknown" as const, reason: "binding_unavailable" as const });
|
|
1277
|
+
}
|
|
1278
|
+
const selectedIdentity = selectedFingerprint.target ?? selectedFingerprint;
|
|
1279
|
+
const sameEffectiveIdentity = (fingerprint: ShimPathFingerprint | null): boolean => {
|
|
1280
|
+
if (!fingerprint) return false;
|
|
1281
|
+
const identity = fingerprint.target ?? fingerprint;
|
|
1282
|
+
return identity.dev === selectedIdentity.dev && identity.ino === selectedIdentity.ino;
|
|
1283
|
+
};
|
|
1284
|
+
const matches = files.flatMap(file => {
|
|
1285
|
+
const backingPath = file.realPath ?? file.backupPath;
|
|
1286
|
+
const roles: Array<"wrapper" | "backing"> = [];
|
|
1287
|
+
if (samePath(file.wrapperPath, selectedCommand)
|
|
1288
|
+
|| sameEffectiveIdentity(shimPathFingerprint(file.wrapperPath))) {
|
|
1289
|
+
roles.push("wrapper");
|
|
1290
|
+
}
|
|
1291
|
+
if (samePath(backingPath, selectedCommand)
|
|
1292
|
+
|| sameEffectiveIdentity(shimPathFingerprint(backingPath))) {
|
|
1293
|
+
roles.push("backing");
|
|
1294
|
+
}
|
|
1295
|
+
return roles.map(selectedRole => ({ file, backingPath, selectedRole }));
|
|
1296
|
+
});
|
|
1297
|
+
if (matches.length === 0) return Object.freeze({ status: "not-tracked" as const });
|
|
1298
|
+
if (result.state.platform !== platform) {
|
|
1299
|
+
return Object.freeze({ status: "unknown" as const, reason: "platform_mismatch" as const });
|
|
1300
|
+
}
|
|
1301
|
+
if (matches.length !== 1) {
|
|
1302
|
+
return Object.freeze({ status: "unknown" as const, reason: "ambiguous_match" as const });
|
|
1303
|
+
}
|
|
1304
|
+
const { file, backingPath, selectedRole } = matches[0]!;
|
|
1305
|
+
if (file.preserveOnly === true) {
|
|
1306
|
+
return Object.freeze({ status: "unknown" as const, reason: "preserve_only" as const });
|
|
1307
|
+
}
|
|
1308
|
+
const backing = statFingerprint(backingPath, true);
|
|
1309
|
+
if (!backing || backing.size <= 0 || samePath(backingPath, file.wrapperPath)) {
|
|
1310
|
+
return Object.freeze({ status: "unknown" as const, reason: "backing_missing" as const });
|
|
1311
|
+
}
|
|
1312
|
+
const wrapperProbe = stableShimPathProbe(file.wrapperPath);
|
|
1313
|
+
if (!wrapperProbe || !isHealthyShimProbe(wrapperProbe, result.state.platform)) {
|
|
1314
|
+
return Object.freeze({
|
|
1315
|
+
status: "unknown" as const,
|
|
1316
|
+
reason: isVersionManagerOwnedCodexPath(file.wrapperPath)
|
|
1317
|
+
? "version_manager_refused" as const
|
|
1318
|
+
: "wrapper_unhealthy" as const,
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
const wrapperIdentity = wrapperProbe.fingerprint.target ?? wrapperProbe.fingerprint;
|
|
1322
|
+
if (backing.dev === wrapperIdentity.dev && backing.ino === wrapperIdentity.ino) {
|
|
1323
|
+
return Object.freeze({ status: "unknown" as const, reason: "backing_mismatch" as const });
|
|
1324
|
+
}
|
|
1325
|
+
const wrapperExt = extname(file.wrapperPath).toLowerCase();
|
|
1326
|
+
const invokesBacking = platform !== "win32"
|
|
1327
|
+
? wrapperProbe.prefix.includes(`exec ${shQuote(backingPath)} "$@"`)
|
|
1328
|
+
: wrapperExt === ".cmd" || wrapperExt === ".bat"
|
|
1329
|
+
? wrapperProbe.prefix.includes(windowsBatchSet("OCX_REAL_CODEX", backingPath))
|
|
1330
|
+
&& wrapperProbe.prefix.includes('"%OCX_REAL_CODEX%" %*')
|
|
1331
|
+
: wrapperExt === ".ps1"
|
|
1332
|
+
? wrapperProbe.prefix.includes(`& ${psString(backingPath)} @args`)
|
|
1333
|
+
: wrapperProbe.prefix.includes(`exec ${shQuote(gitBashPath(backingPath))} "$@"`);
|
|
1334
|
+
if (!invokesBacking) {
|
|
1335
|
+
return Object.freeze({ status: "unknown" as const, reason: "backing_mismatch" as const });
|
|
1336
|
+
}
|
|
1337
|
+
return Object.freeze({
|
|
1338
|
+
status: "matched" as const,
|
|
1339
|
+
selectedRole,
|
|
1340
|
+
backingPath,
|
|
1341
|
+
backingKind: file.realPath !== undefined ? "real" as const : "backup" as const,
|
|
1342
|
+
});
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1159
1345
|
function statePath(): string {
|
|
1160
1346
|
return join(getConfigDir(), "codex-shim.json");
|
|
1161
1347
|
}
|
|
@@ -1283,7 +1469,7 @@ function stateFiles(state: ShimState): ShimFileState[] {
|
|
|
1283
1469
|
}
|
|
1284
1470
|
|
|
1285
1471
|
function primaryState(files: ShimFileState[]): ShimState {
|
|
1286
|
-
const first = files[0]
|
|
1472
|
+
const first = files[0]!;
|
|
1287
1473
|
return { platform: process.platform, ...first, wrappers: files };
|
|
1288
1474
|
}
|
|
1289
1475
|
|
|
@@ -2067,7 +2253,7 @@ export function autoRestoreCodexShim(options: {
|
|
|
2067
2253
|
const state = stateRead.state;
|
|
2068
2254
|
if (!state) {
|
|
2069
2255
|
if (stateRead.warning) return { status: "ineligible", message: stateRead.warning };
|
|
2070
|
-
return { status:
|
|
2256
|
+
return { status: stateRead.present ? "ineligible" : "not-installed" };
|
|
2071
2257
|
}
|
|
2072
2258
|
if (state.platform !== process.platform) return { status: "ineligible" };
|
|
2073
2259
|
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
import { isAbsolute, join, resolve } from "node:path";
|
|
19
19
|
|
|
20
20
|
import { resolveTrustedWindowsPowerShellExe } from "../lib/windows-elevation";
|
|
21
|
+
import { WINDOWS_PRINCIPAL_LOOKUP_TIMEOUT_MS } from "../lib/windows-user-principal";
|
|
21
22
|
|
|
22
23
|
import type {
|
|
23
24
|
ResolveCodexCoordinatorDatabasePath,
|
|
@@ -55,7 +56,7 @@ const SID_PATTERN = /^S-1-(?:\d+-)+\d+$/i;
|
|
|
55
56
|
* fails the lookup and the caller still refuses rather than writing. Only the
|
|
56
57
|
* ceiling moved, and it moved for the case where the lookup would have succeeded.
|
|
57
58
|
*/
|
|
58
|
-
const WINDOWS_POWERSHELL_LOOKUP_TIMEOUT_MS =
|
|
59
|
+
const WINDOWS_POWERSHELL_LOOKUP_TIMEOUT_MS = WINDOWS_PRINCIPAL_LOOKUP_TIMEOUT_MS;
|
|
59
60
|
|
|
60
61
|
function windowsIdentityLookupTimeoutMs(): number {
|
|
61
62
|
return WINDOWS_POWERSHELL_LOOKUP_TIMEOUT_MS;
|
package/src/config/paths.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { chmodSync, existsSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { hardenSecretDirAsync, windowsSecretAclApplies } from "../lib/windows-secret-acl";
|
|
5
5
|
import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -14,6 +14,7 @@ export function expandUserPath(raw: string): string {
|
|
|
14
14
|
return raw;
|
|
15
15
|
}
|
|
16
16
|
let resolvedConfigDirCache: { raw: string | undefined; path: string } | null = null;
|
|
17
|
+
const configDirHardeningFlights = new Map<string, Promise<void>>();
|
|
17
18
|
|
|
18
19
|
export function getConfigDir(): string {
|
|
19
20
|
const raw = process.env["OPENCODEX_HOME"]?.trim() || undefined;
|
|
@@ -34,7 +35,21 @@ export function hardenConfigDir(): void {
|
|
|
34
35
|
assertNotRealHomeUnderTest(dir);
|
|
35
36
|
if (!existsSync(dir)) return;
|
|
36
37
|
try { chmodSync(dir, 0o700); } catch { /* best-effort */ }
|
|
37
|
-
if (
|
|
38
|
-
|
|
38
|
+
if (windowsSecretAclApplies() && !configDirHardeningFlights.has(dir)) {
|
|
39
|
+
// This is an optional read-path harden. Waiting synchronously here used to stop the Bun
|
|
40
|
+
// event loop (including /healthz) for the full icacls timeout. Required mutation paths keep
|
|
41
|
+
// their own awaited/fail-closed hardening; ordinary config reads only start one soft flight.
|
|
42
|
+
const flight = hardenSecretDirAsync(dir, { required: false })
|
|
43
|
+
.then(() => undefined)
|
|
44
|
+
.catch(() => undefined)
|
|
45
|
+
.finally(() => {
|
|
46
|
+
if (configDirHardeningFlights.get(dir) === flight) configDirHardeningFlights.delete(dir);
|
|
47
|
+
});
|
|
48
|
+
configDirHardeningFlights.set(dir, flight);
|
|
39
49
|
}
|
|
40
50
|
}
|
|
51
|
+
|
|
52
|
+
/** Test-only: settle optional config-directory hardening without exposing it to production callers. */
|
|
53
|
+
export async function flushConfigDirHardeningForTests(): Promise<void> {
|
|
54
|
+
await Promise.all([...configDirHardeningFlights.values()]);
|
|
55
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -2380,6 +2380,29 @@ function configMutationDatabasePath(): string {
|
|
|
2380
2380
|
return path;
|
|
2381
2381
|
}
|
|
2382
2382
|
|
|
2383
|
+
/** Raised when an independent config-mutation transaction is requested recursively. */
|
|
2384
|
+
export class NestedConfigMutationError extends Error {
|
|
2385
|
+
constructor() {
|
|
2386
|
+
super("prepareConfigMutationDatabasePathForWrite must not run inside withConfigMutationLockSync");
|
|
2387
|
+
this.name = "NestedConfigMutationError";
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2391
|
+
/**
|
|
2392
|
+
* Prepare the shared config-mutation database path for an independent top-level
|
|
2393
|
+
* SQLite transaction. Callers must not invoke this while holding
|
|
2394
|
+
* {@link withConfigMutationLockSync}; a second `BEGIN IMMEDIATE` deliberately
|
|
2395
|
+
* fails busy instead of joining an uncommitted transaction.
|
|
2396
|
+
*
|
|
2397
|
+
* @throws {NestedConfigMutationError} If a config mutation lock is already held.
|
|
2398
|
+
*/
|
|
2399
|
+
export function prepareConfigMutationDatabasePathForWrite(): string {
|
|
2400
|
+
if (configMutationLockDepth > 0) {
|
|
2401
|
+
throw new NestedConfigMutationError();
|
|
2402
|
+
}
|
|
2403
|
+
return configMutationDatabasePath();
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2383
2406
|
let configMutationLockDepth = 0;
|
|
2384
2407
|
let configMutationDatabase: Database | null = null;
|
|
2385
2408
|
|