Skip to content
Merged
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
9 changes: 8 additions & 1 deletion src/codex/catalog-auto-refresh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,15 @@ export function selectDriftHealCatalogPath(

/** Repair only the missing config roots; catalog convergence remains the later tick step. */
async function healCodexConfigDrift(config: OcxConfig, entryGeneration: number): Promise<"none" | "healed" | "not-healed"> {
const [{ codexConfigDrift }, { JOURNAL_PATH, journaledInjectedOpenaiBaseUrl, journaledInjectedRealtimeWsBaseUrl }, { shouldSyncCodexOnStart }, { injectCodexConfig }, { DEFAULT_CATALOG_PATH, resolveCodexConfigPath }, { loadConfig }] =
const [{ codexConfigDrift }, { JOURNAL_PATH, journaledInjectedOpenaiBaseUrl, journaledInjectedRealtimeWsBaseUrl }, { shouldSyncCodexOnStart }, { injectCodexConfig }, { DEFAULT_CATALOG_PATH, resolveCodexConfigPath }, { loadConfig }, { inspectNativeCodexOwnership }] =
await Promise.all([
import("./config-drift-heal"),
import("./journal"),
import("./desired-state"),
import("./inject"),
import("./paths"),
import("../config"),
import("../integrations/native/ownership-preflight"),
]);
const capturedSettings = JSON.stringify(config);
const current = () => entryGeneration === generation && JSON.stringify(loadConfig()) === capturedSettings;
Expand All @@ -142,11 +143,17 @@ async function healCodexConfigDrift(config: OcxConfig, entryGeneration: number):
// Keep the missing routing roots visible to the next tick. Catalog-only convergence below
// may recreate the file now, but it does not re-inject config.toml in this tick.
if (catalogPath === null) return "not-healed";
// Ownership is probed directly, not through admission: admission can refuse on the
// config or generation authority before it ever inspects native ownership, and a
// veto that rides that result would let those refusals pass on a foreign home.
const serviceHomeOwned = () => inspectNativeCodexOwnership().ownership === "owned";
if (!serviceHomeOwned()) return "not-healed";
await injectCodexConfig(runtime.port, config, {
catalogPath,
lockTimeoutMs: TICK_DEADLINE_MS,
beforeClientWrite: () => {
if (!current()) throw new Error("Catalog drift heal tick is stale");
if (!serviceHomeOwned()) throw new Error("Catalog drift heal lost service-home ownership");
},
}).catch(() => null);
if (!current()) return "none";
Expand Down
2 changes: 1 addition & 1 deletion structure/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ each app-managed rewrite writes `model_provider = "custom"` plus such a table, s
injected root keys alongside), which re-injects cleanly because the injector strips a root
`model_provider` line first. Recovery is drift detection (`src/codex/config-drift-heal.ts`): the
auto-refresh tick re-injects the config when a root key the journal says was injected is missing on disk (presence only; a present key with another value is left alone).
The heal calls the injector directly (no provider discovery or catalog write), waits at most the tick's 1-second commit-lock deadline, and its `beforeClientWrite` guard refuses the write once the timer generation changed or the persisted config no longer matches the tick's snapshot; the catalog path is a bounded read-only lookup of the journal's `injectedCatalogPath` (a regular, parseable catalog) falling back to the default; with no usable catalog the heal is deferred to a later tick, and "healed" is reported only after the keys are observed on disk.
The heal calls the injector directly (no provider discovery or catalog write), but first requires the same positive service-home ownership used by unattended sync. It waits at most the tick's 1-second commit-lock deadline, and its `beforeClientWrite` guard rechecks that ownership and refuses the write once it is lost, the timer generation changed, or the persisted config no longer matches the tick's snapshot; the catalog path is a bounded read-only lookup of the journal's `injectedCatalogPath` (a regular, parseable catalog) falling back to the default; with no usable catalog the heal is deferred to a later tick, and "healed" is reported only after the keys are observed on disk.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

`ocx sync` and `ocx restore back` run the injector's non-writing preflight before provider
discovery or catalog/cache replacement. Deterministic config and ownership refusals therefore
Expand Down
107 changes: 107 additions & 0 deletions tests/codex-integration/catalog-auto-refresh-scheduler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ describe("catalog auto-refresh drift heal", () => {
const desired = await import("../../src/codex/desired-state");
const processState = await import("../../src/config/process-state");
const inject = await import("../../src/codex/inject");
const ownership = await import("../../src/integrations/native/ownership-preflight");
writeCatalogAutoRefreshConfig({ enabled: true, intervalMinutes: 60 });
const fixture = JSON.parse(readFileSync(getConfigPath(), "utf8")) as Record<string, unknown>;
fixture.apiKeys = [{ key: "fixture-key", name: "fixture", createdAt: "2026-01-01T00:00:00.000Z" }];
Expand All @@ -288,6 +289,7 @@ describe("catalog auto-refresh drift heal", () => {
spyOn(desired, "shouldSyncCodexOnStart").mockReturnValue(true),
spyOn(drift, "codexConfigDrift").mockReturnValue({ drifted: true, missingKeys: ["openai_base_url"] }),
spyOn(processState, "readRuntimePort").mockReturnValue({ pid: process.pid, port: 43_210 } as never),
spyOn(ownership, "inspectNativeCodexOwnership").mockReturnValue({ ownership: "owned", reason: "fixture" }),
spyOn(inject, "injectCodexConfig").mockImplementation((async (_port, _config, options) => {
injectorCalls += 1;
if (editBeforeWrite) {
Expand Down Expand Up @@ -327,6 +329,7 @@ describe("catalog auto-refresh drift heal", () => {
const processState = await import("../../src/config/process-state");
const sync = await import("../../src/codex/sync");
const inject = await import("../../src/codex/inject");
const ownership = await import("../../src/integrations/native/ownership-preflight");
const configPath = join(openCodexHome, "healed-config.toml");
const injected: Array<{ port: number; lockTimeoutMs: number | undefined }> = [];
const info: string[] = [];
Expand All @@ -337,6 +340,7 @@ describe("catalog auto-refresh drift heal", () => {
configPath,
)),
spyOn(processState, "readRuntimePort").mockReturnValue({ pid: process.pid, port: 43_210 } as never),
spyOn(ownership, "inspectNativeCodexOwnership").mockReturnValue({ ownership: "owned", reason: "fixture" }),
spyOn(sync, "syncModelsToCodex").mockImplementation((async () => { throw new Error("full sync must not run"); }) as never),
spyOn(inject, "injectCodexConfig").mockImplementation((async (port, _config, options) => {
options?.beforeClientWrite?.();
Expand Down Expand Up @@ -369,12 +373,104 @@ describe("catalog auto-refresh drift heal", () => {
expect(ceded.info.some(line => line.includes("not re-injected this tick"))).toBe(true);
});

test("a foreign service home prevents the drift healer from reaching the injector", async () => {
const drift = await import("../../src/codex/config-drift-heal");
const desired = await import("../../src/codex/desired-state");
const processState = await import("../../src/config/process-state");
const ownership = await import("../../src/integrations/native/ownership-preflight");
const inject = await import("../../src/codex/inject");
const spies = [
spyOn(desired, "shouldSyncCodexOnStart").mockReturnValue(true),
spyOn(drift, "codexConfigDrift").mockReturnValue({ drifted: true, missingKeys: ["openai_base_url"] }),
spyOn(processState, "readRuntimePort").mockReturnValue({ pid: process.pid, port: 43_210 } as never),
spyOn(ownership, "inspectNativeCodexOwnership").mockReturnValue({
ownership: "foreign",
reason: "fixture foreign install",
}),
spyOn(inject, "injectCodexConfig"),
spyOn(console, "info").mockImplementation(() => {}),
];
try {
writeCatalogAutoRefreshConfig({ enabled: true, intervalMinutes: 60 });
await runCatalogAutoRefreshTickForTests();
expect(inject.injectCodexConfig).not.toHaveBeenCalled();
} finally {
for (const spy of spies) spy.mockRestore();
}
});

test.each(["config", "generation"] as const)("admission refused on %s does not bypass the ownership veto on a foreign home", async (authority) => {
const drift = await import("../../src/codex/config-drift-heal");
const desired = await import("../../src/codex/desired-state");
const processState = await import("../../src/config/process-state");
const admission = await import("../../src/codex/admission");
const ownership = await import("../../src/integrations/native/ownership-preflight");
const inject = await import("../../src/codex/inject");
// A write guard that derives veto from the admission result would pass here:
// the refusal authority is not service-home. Ownership must veto on its own.
const spies = [
spyOn(desired, "shouldSyncCodexOnStart").mockReturnValue(true),
spyOn(drift, "codexConfigDrift").mockReturnValue({ drifted: true, missingKeys: ["openai_base_url"] }),
spyOn(processState, "readRuntimePort").mockReturnValue({ pid: process.pid, port: 43_210 } as never),
spyOn(admission, "admitCodexWrite").mockReturnValue({
kind: "refused",
authority,
message: "fixture refusal",
}),
spyOn(ownership, "inspectNativeCodexOwnership").mockReturnValue({
ownership: "foreign",
reason: "fixture foreign install",
}),
spyOn(inject, "injectCodexConfig"),
spyOn(console, "info").mockImplementation(() => {}),
];
try {
writeCatalogAutoRefreshConfig({ enabled: true, intervalMinutes: 60 });
await runCatalogAutoRefreshTickForTests();
expect(inject.injectCodexConfig).not.toHaveBeenCalled();
} finally {
for (const spy of spies) spy.mockRestore();
}
});

test("service-home ownership is rechecked at the injector write boundary", async () => {
const drift = await import("../../src/codex/config-drift-heal");
const desired = await import("../../src/codex/desired-state");
const processState = await import("../../src/config/process-state");
const ownership = await import("../../src/integrations/native/ownership-preflight");
const inject = await import("../../src/codex/inject");
const own = spyOn(ownership, "inspectNativeCodexOwnership")
.mockReturnValueOnce({ ownership: "owned", reason: "fixture" })
.mockReturnValue({ ownership: "foreign", reason: "fixture foreign install" });
const injector = spyOn(inject, "injectCodexConfig").mockImplementation((async (_port, _config, options) => {
options?.beforeClientWrite?.();
return { success: true, message: "fixture" };
}) as typeof inject.injectCodexConfig);
const spies = [
spyOn(desired, "shouldSyncCodexOnStart").mockReturnValue(true),
spyOn(drift, "codexConfigDrift").mockReturnValue({ drifted: true, missingKeys: ["openai_base_url"] }),
spyOn(processState, "readRuntimePort").mockReturnValue({ pid: process.pid, port: 43_210 } as never),
own,
injector,
spyOn(console, "info").mockImplementation(() => {}),
];
try {
writeCatalogAutoRefreshConfig({ enabled: true, intervalMinutes: 60 });
await runCatalogAutoRefreshTickForTests();
expect(own).toHaveBeenCalledTimes(2);
expect(injector).toHaveBeenCalledTimes(1);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} finally {
for (const spy of spies) spy.mockRestore();
}
});

test.each(["stop", "restart", "settings", "off"])("a deferred injector cannot write or log success after %s", async (change) => {
const drift = await import("../../src/codex/config-drift-heal");
const desired = await import("../../src/codex/desired-state");
const processState = await import("../../src/config/process-state");
const sync = await import("../../src/codex/sync");
const inject = await import("../../src/codex/inject");
const ownership = await import("../../src/integrations/native/ownership-preflight");
let entered!: () => void;
const enteredInjector = new Promise<void>(resolve => { entered = resolve; });
let release!: () => void;
Expand All @@ -385,6 +481,7 @@ describe("catalog auto-refresh drift heal", () => {
spyOn(desired, "shouldSyncCodexOnStart").mockReturnValue(true),
spyOn(drift, "codexConfigDrift").mockReturnValue({ drifted: true, missingKeys: ["openai_base_url"] }),
spyOn(processState, "readRuntimePort").mockReturnValue({ pid: process.pid, port: 43_210 } as never),
spyOn(ownership, "inspectNativeCodexOwnership").mockReturnValue({ ownership: "owned", reason: "fixture" }),
spyOn(sync, "syncModelsToCodex").mockImplementation((async () => {
entered();
await released;
Expand Down Expand Up @@ -450,6 +547,8 @@ describe("catalog auto-refresh drift heal", () => {
const syncPath = fileURLToPath(new URL("../../src/codex/sync.ts", import.meta.url));
const injectPath = fileURLToPath(new URL("../../src/codex/inject.ts", import.meta.url));
const configPath = fileURLToPath(new URL("../../src/config.ts", import.meta.url));
const admissionPath = fileURLToPath(new URL("../../src/codex/admission.ts", import.meta.url));
const ownershipPath = fileURLToPath(new URL("../../src/integrations/native/ownership-preflight.ts", import.meta.url));
const script = `
const { spyOn } = require("bun:test");
const fs = require("node:fs");
Expand All @@ -461,6 +560,8 @@ describe("catalog auto-refresh drift heal", () => {
const processState = require(${JSON.stringify(processStatePath)});
const sync = require(${JSON.stringify(syncPath)});
const inject = require(${JSON.stringify(injectPath)});
const admission = require(${JSON.stringify(admissionPath)});
const ownership = require(${JSON.stringify(ownershipPath)});
const catalogPath = path.join(process.env.CODEX_HOME, "alternate-catalog.json");
fs.writeFileSync(catalogPath, JSON.stringify({ models: [{ slug: "alternate" }] }));
fs.writeFileSync(path.join(process.env.CODEX_HOME, "config.toml"), 'model = "gpt-5"\\n');
Expand All @@ -469,6 +570,8 @@ describe("catalog auto-refresh drift heal", () => {
spyOn(desired, "shouldSyncCodexOnStart").mockReturnValue(true);
spyOn(drift, "codexConfigDrift").mockReturnValue({ drifted: true, missingKeys: ["openai_base_url"] });
spyOn(processState, "readRuntimePort").mockReturnValue({ pid: process.pid, port: 43210 });
spyOn(admission, "admitCodexWrite").mockReturnValue({ kind: "admitted" });
spyOn(ownership, "inspectNativeCodexOwnership").mockReturnValue({ ownership: "owned", reason: "fixture" });
spyOn(sync, "syncModelsToCodex").mockImplementation(async () => { throw new Error("full sync called"); });
let received = null;
spyOn(inject, "injectCodexConfig").mockImplementation(async (_port, _config, options) => { received = options.catalogPath; options.beforeClientWrite(); return { success: true, message: "fixture" }; });
Expand Down Expand Up @@ -509,12 +612,16 @@ describe("catalog auto-refresh drift heal", () => {
const processState = require(${JSON.stringify(source("config/process-state.ts"))});
const inject = require(${JSON.stringify(source("codex/inject.ts"))});
const management = require(${JSON.stringify(source("codex/management-convergence.ts"))});
const admission = require(${JSON.stringify(source("codex/admission.ts"))});
const ownership = require(${JSON.stringify(source("integrations/native/ownership-preflight.ts"))});
const catalog = path.join(process.env.CODEX_HOME, "opencodex-catalog.json");
fs.writeFileSync(path.join(process.env.CODEX_HOME, "config.toml"), 'model = "gpt-5"\\n');
fs.writeFileSync(config.getConfigPath(), JSON.stringify({ ...config.getDefaultConfig(), defaultProvider: "xai", providers: { xai: { adapter: "openai-responses", baseUrl: "https://api.x.ai/v1" } }, catalogAutoRefresh: { enabled: true, intervalMinutes: 60 } }));
spyOn(desired, "shouldSyncCodexOnStart").mockReturnValue(true);
spyOn(drift, "codexConfigDrift").mockReturnValue({ drifted: true, missingKeys: ["openai_base_url"] });
spyOn(processState, "readRuntimePort").mockReturnValue({ pid: process.pid, port: 43210 });
spyOn(admission, "admitCodexWrite").mockReturnValue({ kind: "admitted" });
spyOn(ownership, "inspectNativeCodexOwnership").mockReturnValue({ ownership: "owned", reason: "fixture" });
const received = [];
spyOn(inject, "injectCodexConfig").mockImplementation(async (_port, _config, options) => { received.push(options.catalogPath); return { success: true, message: "fixture" }; });
let converges = 0;
Expand Down
Loading