From 1f3932f31c3c237b8ab7f0bfaf07807b14e42f1d Mon Sep 17 00:00:00 2001 From: luvs01 Date: Fri, 7 Aug 2026 14:32:44 +0900 Subject: [PATCH] fix(codex): guard native toggle teardown ownership --- .../management/native-integration-routes.ts | 7 +++++++ tests/native-codex-toggle.test.ts | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/server/management/native-integration-routes.ts b/src/server/management/native-integration-routes.ts index 72d0098eca7..1f4c2a06d25 100644 --- a/src/server/management/native-integration-routes.ts +++ b/src/server/management/native-integration-routes.ts @@ -234,6 +234,13 @@ async function handleCodexToggle(ctx: ManagementContext): Promise { } const enabled = body.enabled; + if (!enabled) { + // Restoring Codex mutates shared native artifacts. Refuse before even + // persisting intent when the installed service belongs to another home. + const owned = assertNativeTeardownOwned(); + if (!owned.ok) return refusal(409, "codex", "home_mismatch", owned.message); + } + const { setCodexIntegrationEnabled } = await import("../../codex/desired-state"); const persisted = setCodexIntegrationEnabled(enabled); /* diff --git a/tests/native-codex-toggle.test.ts b/tests/native-codex-toggle.test.ts index 92d00685761..3f0a3639005 100644 --- a/tests/native-codex-toggle.test.ts +++ b/tests/native-codex-toggle.test.ts @@ -104,6 +104,25 @@ describe("request validation", () => { }); describe("turning Codex off", () => { + test("refuses teardown owned by a service installed from another home", async () => { + writeFileSync(join(fixtureRoot, "service-state.json"), JSON.stringify({ + version: 2, + codexHome: process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"), + opencodexHome: join(fixtureRoot, "foreign-opencodex-home"), + backend: "scheduler", + })); + + const result = await put(baseConfig(), { enabled: false }); + + expect(result.status).toBe(409); + expect(result.body).toMatchObject({ + code: "native_integration_refused", + clientId: "codex", + reason: "home_mismatch", + }); + expect(persistedCodexIntent()).toBeUndefined(); + }); + test("persists the decision so it survives the next start", async () => { const result = await put(baseConfig(), { enabled: false }); expect(result.status).toBe(200);