diff --git a/docs-site/src/content/docs/ko/reference/cli/lifecycle.md b/docs-site/src/content/docs/ko/reference/cli/lifecycle.md index d4c19ebd46..ce7dfba755 100644 --- a/docs-site/src/content/docs/ko/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/ko/reference/cli/lifecycle.md @@ -185,6 +185,10 @@ probe이며, `--wait`는 준비 또는 timeout까지 polling하지만 종단 `fa 해당할 때 서비스 마이그레이션을 설명합니다. 이 진단에 표시되는 경로는 OS 사용자 이름을 마스킹합니다. doctor는 복구 힌트를 보여 주지만 직접 적용하지는 않습니다. +프로젝트 설정 진단은 `developer_instructions` 같은 TOML 여러 줄 문자열 안의 공급자 예시를 +무시합니다. 종료 구분자 바로 앞에 이스케이프된 따옴표가 있어도, 문자열이 끝난 뒤의 실제 +공급자 및 프로필 설정은 계속 검사합니다. + **OAuth 안정성** 섹션은 자격 증명 저장소에 쓰기 가능한지, `OPENCODEX_HOME` 아래에 refresh single-flight/lock 파일을 만들 수 있는지, 건강하지 않은 OAuth 또는 Codex pool 계정(마스킹된 ID)과 복구용 `Action:`, 그리고 Codex 전달 경로가 공식 클라이언트 메타데이터를 꾸며 내지 않는다는 diff --git a/docs-site/src/content/docs/reference/cli/lifecycle.md b/docs-site/src/content/docs/reference/cli/lifecycle.md index a7d61a0bee..ac04a371ac 100644 --- a/docs-site/src/content/docs/reference/cli/lifecycle.md +++ b/docs-site/src/content/docs/reference/cli/lifecycle.md @@ -214,6 +214,10 @@ and pending history migration. The Codex app-home targeting section also detects Orca runtime-home mismatch and explains service migration when applicable. Paths shown by this diagnostic redact the OS username. Doctor prints repair hints but does not apply them. +Project-config diagnostics ignore provider examples inside TOML multiline strings, including +`developer_instructions`. Real provider and profile settings after the closing delimiter are still +checked, even when an escaped quote immediately precedes that delimiter. + The **OAuth reliability** section reports whether credential storage is writable, whether refresh single-flight/lock files can be created under `OPENCODEX_HOME`, non-healthy OAuth or Codex pool accounts (redacted ids) with a recovery `Action:`, and a static OK that the Codex forward path does diff --git a/src/codex/project-config-warnings.ts b/src/codex/project-config-warnings.ts index 47b6afee27..50721a689d 100644 --- a/src/codex/project-config-warnings.ts +++ b/src/codex/project-config-warnings.ts @@ -69,7 +69,9 @@ function multilineCloseIndex( backslashes += 1; } if (backslashes % 2 === 0) break; - index = line.indexOf(delimiter, index + delimiter.length); + // An escaped quote can overlap the real terminator (backslash plus four quotes). + // Keep overlapping candidates instead of skipping the entire rejected delimiter. + index = line.indexOf(delimiter, index + 1); } return index; } diff --git a/tests/codex-integration/project-config-warnings.test.ts b/tests/codex-integration/project-config-warnings.test.ts index 16f9dd258d..1767ee655a 100644 --- a/tests/codex-integration/project-config-warnings.test.ts +++ b/tests/codex-integration/project-config-warnings.test.ts @@ -129,6 +129,49 @@ describe("parseTomlDocument", () => { const valid = parseTomlDocument('model_provider = "provider\\\\name"'); expect(valid.root.model_provider).toBe("provider\\name"); }, 2_000); + + for (const scenario of [ + { name: "root override", sameLine: false, tail: ['model_provider = "custom"'], + code: "model_provider_root", via: "root", profileName: null }, + { name: "same-line string", sameLine: true, tail: ['model_provider = "custom"'], + code: "model_provider_root", via: "root", profileName: null }, + { name: "selected profile", sameLine: false, + tail: ['profile = "work"', '[profiles.work]', 'model_provider = "custom"'], + code: "profile_selector", via: "profile", profileName: "work" }, + { name: "selected provider table", sameLine: false, + tail: ['model_provider = "custom"', '[model_providers.custom]', 'name = "Custom"'], + code: "model_providers_table", via: "root", profileName: null }, + ] as const) { + test(`overlapping multiline terminator preserves ${scenario.name} diagnostics`, () => { + const text = ['developer_instructions = """' + (scenario.sameLine ? "" : "\n") + + "foo" + "\\" + '"'.repeat(4), ...scenario.tail].join("\n"); + // Independent TOML parsing proves the escaped quote is followed by a real terminator. + expect(Bun.TOML.parse(text).developer_instructions).toBe('foo"'); + expect(resolveEffectiveProjectModelProvider(text)).toEqual({ + provider: "custom", profileName: scenario.profileName, via: scenario.via, + }); + const warnings = analyzeProjectCodexConfig(text, "fixture/.codex/config.toml"); + expect(warnings).toHaveLength(1); + expect(warnings[0]).toMatchObject({ code: scenario.code, detail: "custom" }); + expect(warnings[0]!.profileName).toBe(scenario.profileName ?? undefined); + if (scenario.code === "model_providers_table") { + expect(parseTomlDocument(text).sections.get("model_providers.custom")?.name).toBe("Custom"); + } + }); + } + + test("escaped three quotes keep fake routing inside the multiline body", () => { + const text = ['developer_instructions = """', "foo" + "\\" + '"'.repeat(3), + 'model_provider = "custom"', '[model_providers.custom]', 'name = "Custom"', + '"""', 'model_provider = "openai"'].join("\n"); + const parsedByBun = Bun.TOML.parse(text); + expect(parsedByBun.model_provider).toBe("openai"); + expect(parsedByBun.developer_instructions).toContain('[model_providers.custom]'); + const parsed = parseTomlDocument(text); + expect(parsed.root.model_provider).toBe("openai"); + expect(parsed.sections.has("model_providers.custom")).toBe(false); + expect(analyzeProjectCodexConfig(text, "fixture/.codex/config.toml")).toEqual([]); + }); }); describe("parseTrustedProjectPathsFromCodexConfig", () => {