From 5e05b5aced76c27d2802dd7aa6e485f4f3e14c13 Mon Sep 17 00:00:00 2001 From: JUN Date: Mon, 28 Sep 2026 00:44:11 +0900 Subject: [PATCH 1/3] fix(codex): log unattended Codex write service-home refusals A service-home admission refusal in syncModelsToCodex returned without any log line. Startup then marked /readyz failed with no evidence and `ocx sync` only said the sync did not complete. Log one line naming the authority and pointing to POST /api/sync for the specific reason. The admission message can contain private home and service-definition paths, so it stays out of the log. Reimplements the diagnostic slice of #5782 without the raw message. Co-authored-by: tcflying <77509083+tcflying@users.noreply.github.com> --- src/codex/sync.ts | 10 ++++++ .../codex-integration/codex-sync-api.test.ts | 35 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/codex/sync.ts b/src/codex/sync.ts index 87b336eea62..b31a0859afc 100644 --- a/src/codex/sync.ts +++ b/src/codex/sync.ts @@ -132,6 +132,16 @@ export async function syncModelsToCodex( // the injector, before it gets a chance to create any artifact. const admission = (deps.admitCodexWrite ?? admitCodexWrite)(); if (admission.kind === "refused" && admission.authority === "service-home") { + // Startup turns this result into a failed /readyz and records nothing else, and + // `ocx sync` only says the sync did not complete, so an unattended refusal must + // leave one line behind (#5782). The admission message can name private home and + // service-definition paths, so the log carries the authority only; POST /api/sync + // returns the specific reason to an authenticated operator. + log?.error( + "[opencodex] Codex write refused (service-home): this proxy could not prove it owns the " + + "installed service homes, so catalog sync and Codex config injection were skipped. " + + "POST /api/sync returns the specific reason.", + ); return { status: "refused", authority: "service-home", diff --git a/tests/codex-integration/codex-sync-api.test.ts b/tests/codex-integration/codex-sync-api.test.ts index b19ef2ae55f..c632b6df225 100644 --- a/tests/codex-integration/codex-sync-api.test.ts +++ b/tests/codex-integration/codex-sync-api.test.ts @@ -165,6 +165,41 @@ describe("GUI/CLI Codex sync backend", () => { expect(errors).toEqual([]); }); + test("a service-home refusal leaves one path-free log line and writes nothing (#5782)", async () => { + const privatePath = "/srv/private-operator/.opencodex"; + const logs: string[] = []; + const errors: string[] = []; + let refreshed = false; + let injected = false; + const result = await syncModelsToCodex(12345, config, { log: line => logs.push(String(line)), error: line => errors.push(String(line)) }, { + admitCodexWrite: () => ({ + kind: "refused" as const, + authority: "service-home" as const, + message: `Refusing to write: a service is installed for OPENCODEX_HOME=${privatePath}.`, + }), + refreshCodexModelCatalog: async () => { + refreshed = true; + throw new Error("must not refresh"); + }, + injectCodexConfig: async () => { + injected = true; + return { success: true, message: "must not inject" }; + }, + currentExternalCodexModelProvider: () => null, + }); + + expect(result.status).toBe("refused"); + expect(result.ok).toBe(false); + expect(result.message).toContain(privatePath); + expect(refreshed).toBe(false); + expect(injected).toBe(false); + expect(logs).toEqual([]); + expect(errors).toHaveLength(1); + expect(errors[0]).toContain("service-home"); + expect(errors[0]).toContain("POST /api/sync"); + expect(errors[0]).not.toContain(privatePath); + }); + test("catalog sync proceeds after a bounded reasoning refresh fails on both sync paths", async () => { const calls: string[] = []; const routedConfig = { From a0d48270837cf7f5c44608424db8bb4983daa279 Mon Sep 17 00:00:00 2001 From: JUN Date: Mon, 28 Sep 2026 00:48:25 +0900 Subject: [PATCH 2/3] docs(structure): describe the path-free service-home refusal log --- structure/config.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/structure/config.md b/structure/config.md index 79deb53630f..bf558606907 100644 --- a/structure/config.md +++ b/structure/config.md @@ -270,6 +270,10 @@ missing on disk (presence only; a present key with another value is left alone). `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 leave the existing catalog and cache untouched, and their concrete messages are emitted on stderr. +The unattended service-home admission refusal, which `src/codex/sync.ts` checks before that +preflight, is the exception: its message can name private home and service-definition paths, so +the sync logs one path-free line naming the authority. Startup still marks `/readyz` failed, and +an authenticated `POST /api/sync` returns the concrete reason in its 409 body. Exactly one conversation-history refusal scopes the relabel unit instead of vetoing the apply transition, and only because it is permanent. Codex allocates paginated rollout ordinals inside its own writer, so `history_paginated_requires_native_writer` is not retryable: when the admitted From 75f863c6f4781a3a1565c69e1a92b7cfbf46066c Mon Sep 17 00:00:00 2001 From: JUN Date: Mon, 28 Sep 2026 00:48:46 +0900 Subject: [PATCH 3/3] docs(structure): keep config.md within its line budget --- structure/config.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/structure/config.md b/structure/config.md index bf558606907..1ae6fc51842 100644 --- a/structure/config.md +++ b/structure/config.md @@ -270,10 +270,7 @@ missing on disk (presence only; a present key with another value is left alone). `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 leave the existing catalog and cache untouched, and their concrete messages are emitted on stderr. -The unattended service-home admission refusal, which `src/codex/sync.ts` checks before that -preflight, is the exception: its message can name private home and service-definition paths, so -the sync logs one path-free line naming the authority. Startup still marks `/readyz` failed, and -an authenticated `POST /api/sync` returns the concrete reason in its 409 body. +The earlier service-home admission refusal logs one path-free line; `POST /api/sync` returns its reason. Exactly one conversation-history refusal scopes the relabel unit instead of vetoing the apply transition, and only because it is permanent. Codex allocates paginated rollout ordinals inside its own writer, so `history_paginated_requires_native_writer` is not retryable: when the admitted