fix(codex): parse multiline provider pins in OAuth guard

This commit is contained in:
Brennan Benson
2026-07-17 02:37:30 -07:00
parent 13f32d4965
commit 1c328c56cb
3 changed files with 177 additions and 3 deletions
@@ -0,0 +1,63 @@
import { describe, expect, it } from 'vitest'
import { readCodexTopLevelModelProvider } from './codex-model-provider-config'
describe('readCodexTopLevelModelProvider', () => {
it.each([
['missing provider', 'model = "gpt-5"\n', null],
['OpenAI provider', 'model_provider = "openai" # default\n', 'openai'],
['literal custom provider', "model_provider = 'codex-lb'\n", 'codex-lb'],
['quoted key', '"model_provider" = "codex-lb"\n', 'codex-lb'],
['escaped quoted key', '"model\\u005Fprovider" = "codex-lb"\n', 'codex-lb'],
['escaped provider', 'model_provider = "codex\\u002Dlb"\n', 'codex-lb'],
['single-line multiline OpenAI', 'model_provider = """openai"""\n', 'openai'],
['single-line multiline custom', "model_provider = '''codex-lb'''\n", 'codex-lb'],
['CRLF custom provider', 'model = "gpt-5"\r\nmodel_provider = "codex-lb"\r\n', 'codex-lb'],
[
'continued multiline custom',
['model_provider = """\\', ' codex-lb"""', ''].join('\n'),
'codex-lb'
],
['profile provider', ['[profiles.custom]', 'model_provider = "codex-lb"', ''].join('\n'), null],
[
'provider-like multiline data',
[
'developer_instructions = """',
'[profiles.custom]',
'model_provider = "not-a-setting"',
'"""',
'model_provider = "openai"',
''
].join('\n'),
'openai'
],
[
'provider-like array data',
[
'notify = [',
' "model_provider = \\"not-a-setting\\"",',
']',
'model_provider = "openai"',
''
].join('\n'),
'openai'
]
])('reads %s', (_description, config, expected) => {
expect(readCodexTopLevelModelProvider(config)).toBe(expected)
})
it('uses the first duplicate assignment deterministically', () => {
expect(
readCodexTopLevelModelProvider(
['model_provider = "openai"', 'model_provider = "codex-lb"', ''].join('\n')
)
).toBe('openai')
})
it.each([
['unquoted value', 'model_provider = codex-lb\n'],
['unterminated value', 'model_provider = "codex-lb\n'],
['assignment after a table', '[profiles.default]\nmodel_provider = "codex-lb"\n']
])('ignores malformed or non-root %s', (_description, config) => {
expect(readCodexTopLevelModelProvider(config)).toBeNull()
})
})
+28 -3
View File
@@ -3,6 +3,7 @@ import {
getTomlTableHeader,
isTomlStructuralLine,
parseTomlSingleLineStringValue,
parseTomlStringValue,
updateTomlLineScanState
} from './config-toml-line-scan'
@@ -10,17 +11,41 @@ import {
// similarly named keys inside profiles or provider tables are not global pins.
export function readCodexTopLevelModelProvider(config: string): string | null {
let state = createTomlLineScanState()
let lineOffset = 0
for (const line of config.split('\n')) {
if (isTomlStructuralLine(state)) {
if (getTomlTableHeader(line)) {
return null
}
const match = /^[ \t]*(?:model_provider|"model_provider"|'model_provider')[ \t]*=/.exec(line)
if (match) {
return parseTomlSingleLineStringValue(line, match[0].length)?.value ?? null
const valueOffset = getModelProviderValueOffset(line)
if (valueOffset !== null) {
return parseTomlStringValue(config, lineOffset + valueOffset)?.value ?? null
}
}
state = updateTomlLineScanState(state, line)
lineOffset += line.length + 1
}
return null
}
function getModelProviderValueOffset(line: string): number | null {
let index = 0
while (line[index] === ' ' || line[index] === '\t') {
index += 1
}
if (line.startsWith('model_provider', index)) {
index += 'model_provider'.length
} else {
const parsedKey = parseTomlSingleLineStringValue(line, index)
if (parsedKey?.value !== 'model_provider') {
return null
}
index = parsedKey.end
}
while (line[index] === ' ' || line[index] === '\t') {
index += 1
}
return line[index] === '=' ? index + 1 : null
}
+86
View File
@@ -122,6 +122,9 @@ export function parseTomlSingleLineStringValue(
let value = ''
while (index < line.length) {
const char = line[index]
if (char === '\n' || char === '\r') {
return null
}
if (char === quote) {
return { value, start, end: index + 1 }
}
@@ -140,6 +143,58 @@ export function parseTomlSingleLineStringValue(
return null
}
// Why: serde accepts multiline TOML strings for String settings such as
// model_provider, so safety classifiers cannot treat triple quotes as absent.
export function parseTomlStringValue(source: string, offset: number): ParsedTomlString | null {
let index = offset
while (source[index] === ' ' || source[index] === '\t') {
index += 1
}
const delimiter = source.startsWith('"""', index)
? '"""'
: source.startsWith("'''", index)
? "'''"
: null
if (!delimiter) {
return parseTomlSingleLineStringValue(source, offset)
}
const start = index
index += delimiter.length
index = skipInitialTomlMultilineNewline(source, index)
let value = ''
while (index < source.length) {
if (source.startsWith(delimiter, index)) {
return { value, start, end: index + delimiter.length }
}
if (delimiter === '"""' && source[index] === '\\') {
const continuationEnd = getTomlMultilineContinuationEnd(source, index)
if (continuationEnd !== null) {
index = continuationEnd
continue
}
const escaped = parseTomlBasicStringEscape(source, index)
if (!escaped) {
return null
}
value += escaped.value
index = escaped.nextIndex
continue
}
if (source[index] === '\r') {
value += '\n'
index += source[index + 1] === '\n' ? 2 : 1
continue
}
value += source[index]
index += 1
}
return null
}
function skipTomlBasicString(line: string, startIndex: number): number {
let index = startIndex
while (index < line.length) {
@@ -161,6 +216,37 @@ function skipTomlLiteralString(line: string, startIndex: number): number {
return endIndex === -1 ? line.length : endIndex + 1
}
function skipInitialTomlMultilineNewline(source: string, index: number): number {
if (source[index] === '\r' && source[index + 1] === '\n') {
return index + 2
}
return source[index] === '\n' ? index + 1 : index
}
function getTomlMultilineContinuationEnd(source: string, slashIndex: number): number | null {
let index = slashIndex + 1
while (source[index] === ' ' || source[index] === '\t') {
index += 1
}
if (source[index] === '\r' && source[index + 1] === '\n') {
index += 2
} else if (source[index] === '\n') {
index += 1
} else {
return null
}
while (
source[index] === ' ' ||
source[index] === '\t' ||
source[index] === '\n' ||
source[index] === '\r'
) {
index += 1
}
return index
}
function parseTomlBasicStringEscape(
line: string,
slashIndex: number