Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/codex/catalog/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ import { MAIN_CODEX_ACCOUNT_ID } from "../main-account";
import {
availableAccountGatedNativeModels,
isCodexModelEntitlementSnapshotCurrent,
resolveCodexModelEntitlements,
type CodexModelEntitlementSnapshot,
} from "../model-entitlements";
import { resolveAdmittedCodexModelEntitlements } from "../model-entitlement-admission";


import { CODEX_CUSTOM_MODEL_CATALOG_KIND, CODEX_PROVIDER_MODEL_CATALOG_KIND, activeCodexModelsCachePath, applyCatalogMetadata, applyMultiAgentMode, applyNativeOpenAiContextOverride, applyRoutedCodexToolMode, catalogBackupPathFor, catalogHasRoutedEntries, catalogModelSlug, ensureStrictCatalogFields, findNativeTemplate, findSupportedNativeTemplate, isDefaultCatalogPath, isRoutedModelCompatibilityExcluded, legacyCatalogBackupPath, normalizeRoutedCatalogEntry, normalizeServiceTiers, readCatalog, readCatalogBackup, readCodexCatalogPath, readCodexCatalogPathForHome, readConfiguredAutoReviewModel, readNativeBaseline } from "./parsing";
Expand Down Expand Up @@ -1836,7 +1836,7 @@ export async function syncCatalogModels(
comboOmissions,
providerModelOutcomes,
}),
resolveCodexModelEntitlements(config),
resolveAdmittedCodexModelEntitlements(config),
]);
const committed = withCatalogWriteSerialization(owningCodexHome, permit => {
// Desired state can flip OFF during the provider await above. The catalog
Expand Down
4 changes: 2 additions & 2 deletions src/codex/convergence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,9 @@ import { MAIN_CODEX_ACCOUNT_ID } from "./main-account";
import {
availableAccountGatedNativeModels,
isCodexModelEntitlementSnapshotCurrent,
resolveCodexModelEntitlements,
type CodexModelEntitlementSnapshot,
} from "./model-entitlements";
import { resolveAdmittedCodexModelEntitlements } from "./model-entitlement-admission";
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "./catalog/native-models";
import { providerCodexAccountMode } from "../providers/registry";
import { OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
Expand Down Expand Up @@ -412,7 +412,7 @@ export async function gatherCodexCatalogCandidate(
providerModelOutcomes,
discoveryPolicySnapshots: discoveryPolicies,
}),
resolveCodexModelEntitlements(snapshot.config),
resolveAdmittedCodexModelEntitlements(snapshot.config),
]);
const processLocal = processEvidence(source);
const sourceEvidence = sealCatalogGatherEvidenceSession(session);
Expand Down
63 changes: 63 additions & 0 deletions src/codex/model-entitlement-admission.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { AdmissionLease } from "../lib/admission";
import type { OcxConfig } from "../types";
import { MAIN_CODEX_ACCOUNT_ID } from "./main-account";
import {
resolveCodexModelEntitlements,
type CodexModelEntitlementResolveOptions,
type CodexModelEntitlementSnapshot,
} from "./model-entitlements";
import { tryAcquireNativeMainProfileClaim } from "./native-main-admission";
import { withNativeMainSharedClaim } from "./native-main-claim";
import { resolveNativeProfileContext } from "./native-profile-store";
import { NativeProfileError } from "./native-profile-types";

interface ModelEntitlementAdmissionDeps {
readonly acquireNativeMain?: () => AdmissionLease | null;
readonly resolve?: typeof resolveCodexModelEntitlements;
readonly withSharedClaim?: <T>(operation: () => Promise<T>) => Promise<T>;
}

function excludeNativeMain(
options: CodexModelEntitlementResolveOptions,
): CodexModelEntitlementResolveOptions {
return {
...options,
excludeAccountIds: new Set([
...(options.excludeAccountIds ?? []),
MAIN_CODEX_ACCOUNT_ID,
]),
};
}

/**
* Resolve background/data-plane entitlements inside both native-main fences.
*
* Pool discovery remains available when startup recovery or a profile drain
* owns the physical credential. When main is admitted, the process-local lease
* and cross-process shared claim cover its complete read/possible refresh.
*/
export async function resolveAdmittedCodexModelEntitlements(
config: Pick<OcxConfig, "codexAccounts">,
options: CodexModelEntitlementResolveOptions = {},
deps: ModelEntitlementAdmissionDeps = {},
): Promise<CodexModelEntitlementSnapshot> {
const resolve = deps.resolve ?? resolveCodexModelEntitlements;
const lease = (deps.acquireNativeMain ?? tryAcquireNativeMainProfileClaim)();
if (!lease) return resolve(config, excludeNativeMain(options));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Retry catalog sync after startup admission opens

When startup ownership acquisition or journal recovery is still pending, startNativeMainStartupLifecycle reports native-main traffic as blocked, but the startup flow in src/cli/index.ts proceeds from startServer to syncCodexOnStartIfEnabled without awaiting that lifecycle. This branch therefore lets syncCatalogModels commit a Pool-only entitlement snapshot, removing main-only account-gated models from the on-disk Codex catalog; when recovery later succeeds, nothing automatically reconverges that catalog, so the models remain absent until a separate sync occurs. Treat this as a non-committable/retryable snapshot for catalog writers, or schedule convergence when the startup gate opens.

Useful? React with 👍 / 👎.


try {
const operation = () => resolve(config, options);
const withSharedClaim = deps.withSharedClaim
?? (<T>(work: () => Promise<T>) => withNativeMainSharedClaim(resolveNativeProfileContext(), work));
try {
return await withSharedClaim(operation);
} catch (error) {
// A foreign exclusive holder or an unsupported claim filesystem makes
// main unavailable; it must not suppress independent Pool discovery.
if (!(error instanceof NativeProfileError)) throw error;
return await resolve(config, excludeNativeMain(options));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Release the main lease before Pool-only fallback

When the cross-process claim is busy or unavailable, this fallback performs the entire Pool-only resolution before the finally releases the process-local native-main lease. A concurrent local profile switch therefore sees a native-main request in progress and waits for unrelated Pool credential refreshes/model fetches; those can outlast the switch's 10-second drain deadline and make the switch fail with MAIN_REQUESTS_ACTIVE even though the fallback never reads main. Release the lease before starting the independent Pool-only resolution.

Useful? React with 👍 / 👎.

}
} finally {
lease.release();
}
}
4 changes: 2 additions & 2 deletions src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ import { codexAccountNamespaceEntries, isMainCodexAccountTarget } from "../codex
import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
import {
availableAccountGatedNativeModels,
resolveCodexModelEntitlements,
} from "../codex/model-entitlements";
import { resolveAdmittedCodexModelEntitlements } from "../codex/model-entitlement-admission";
export {
clearThreadAccountMap,
formatCodexProviderForLog,
Expand Down Expand Up @@ -1160,7 +1160,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
// Codex sends its own client_version on this request, and upstream filters the
// entitlement roster by it. Passing it through is what stops an entitled account
// being told it cannot use models a newer client can (#2886).
resolveCodexModelEntitlements(config, { clientVersion: url.searchParams.get("client_version") }),
resolveAdmittedCodexModelEntitlements(config, { clientVersion: url.searchParams.get("client_version") }),
]);
} catch (error) {
if (error instanceof CatalogGatherBusyError) {
Expand Down
75 changes: 75 additions & 0 deletions tests/codex-model-entitlement-admission.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { describe, expect, test } from "bun:test";

import { MAIN_CODEX_ACCOUNT_ID } from "../src/codex/main-account";
import { resolveAdmittedCodexModelEntitlements } from "../src/codex/model-entitlement-admission";
import type { CodexModelEntitlementResolveOptions } from "../src/codex/model-entitlements";
import { NativeProfileError } from "../src/codex/native-profile-types";

const emptySnapshot = {
modelsByAccount: new Map<string, ReadonlySet<string>>(),
confirmedAccountIds: new Set<string>(),
credentialIdentities: new Map<string, string>(),
};

describe("Codex model entitlement admission", () => {
test("excludes native main before credential discovery when lifecycle admission is blocked", async () => {
let received: CodexModelEntitlementResolveOptions | undefined;

await resolveAdmittedCodexModelEntitlements({ codexAccounts: [] }, {}, {
acquireNativeMain: () => null,
resolve: async (_config, options) => {
received = options;
return emptySnapshot;
},
});

expect(received?.excludeAccountIds?.has(MAIN_CODEX_ACCOUNT_ID)).toBe(true);
});

test("holds lifecycle and shared claims through credential discovery", async () => {
const events: string[] = [];
let released = false;

await resolveAdmittedCodexModelEntitlements({ codexAccounts: [] }, {}, {
acquireNativeMain: () => ({ release: () => {
released = true;
events.push("lifecycle-release");
} }),
withSharedClaim: async operation => {
events.push("shared-enter");
const result = await operation();
events.push("shared-release");
return result;
},
resolve: async () => {
expect(released).toBe(false);
events.push("credential-discovery");
return emptySnapshot;
},
});

expect(events).toEqual([
"shared-enter",
"credential-discovery",
"shared-release",
"lifecycle-release",
]);
});

test("falls back to Pool-only discovery when the shared claim is unavailable", async () => {
const exclusions: boolean[] = [];

await resolveAdmittedCodexModelEntitlements({ codexAccounts: [] }, {}, {
acquireNativeMain: () => ({ release: () => undefined }),
withSharedClaim: async () => {
throw new NativeProfileError("NATIVE_MAIN_CLAIM_BUSY", "busy", 503, true);
},
resolve: async (_config, options) => {
exclusions.push(options.excludeAccountIds?.has(MAIN_CODEX_ACCOUNT_ID) === true);
return emptySnapshot;
},
});

expect(exclusions).toEqual([true]);
});
});
Loading