From 7d7b41caa661af1d59505421892fde8a76da807f Mon Sep 17 00:00:00 2001
From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Date: Tue, 29 Sep 2026 17:14:52 -0700
Subject: [PATCH 1/5] fix(codex): recognise Codex's quoted project-trust
spellings in config.toml (#22592)
Codex's settings screen writes project trust as ["projects"."/p"] and
"trust_level" = "trusted". Orca's matchers only knew the bare spelling, so a
trust write appended a second [projects."/p"] table (or a second trust_level
line) and every codex command then failed with "duplicate key". The config
mirror kept both spellings in Orca-managed homes for the same reason.
- Project table headers are now read through the existing TOML key-path
parser, so bare, quoted, literal-quoted, mixed and spaced spellings are the
same table for trust writes and the managed-home mirror/dedupe.
- trust_level is found by decoded key, in both the trust writer and the
mirror's trust reader, and an existing key is rewritten, never duplicated.
- On the next trust write, a table older Orca appended (exactly
[projects."
"] holding only trust_level = "trusted") that duplicates the
user's table, or the bare line it inserted under a quoted "trust_level", is
removed; the user's table wins and the atomic writer keeps config.toml.bak.
Any other duplicate, or a repair that would still leave one, leaves the file
untouched and logs once.
---
...oml-duplicate-declarations-test-fixture.ts | 32 +++
...nfig-toml-project-duplicate-repair.test.ts | 183 ++++++++++++++++++
.../config-toml-project-duplicate-repair.ts | 158 +++++++++++++++
.../codex/config-toml-project-trust-level.ts | 55 ++++++
...onfig-toml-project-trust-spellings.test.ts | 171 ++++++++++++++++
src/main/codex/config-toml-project-trust.ts | 17 +-
.../config-toml-runtime-owned-sections.ts | 8 +-
src/main/codex/config-toml-syntax.ts | 12 +-
8 files changed, 619 insertions(+), 17 deletions(-)
create mode 100644 src/main/codex/config-toml-duplicate-declarations-test-fixture.ts
create mode 100644 src/main/codex/config-toml-project-duplicate-repair.test.ts
create mode 100644 src/main/codex/config-toml-project-duplicate-repair.ts
create mode 100644 src/main/codex/config-toml-project-trust-level.ts
create mode 100644 src/main/codex/config-toml-project-trust-spellings.test.ts
diff --git a/src/main/codex/config-toml-duplicate-declarations-test-fixture.ts b/src/main/codex/config-toml-duplicate-declarations-test-fixture.ts
new file mode 100644
index 000000000000..2f01d2804d52
--- /dev/null
+++ b/src/main/codex/config-toml-duplicate-declarations-test-fixture.ts
@@ -0,0 +1,32 @@
+import { parseTomlKeyPath, parseTomlTableHeaderPath } from './config-toml-key-path'
+import { getTomlTableHeader } from './config-toml-line-scan'
+
+/** Duplicate tables or keys Codex's TOML parser would reject; fixtures hold no multiline values. */
+export function findDuplicateTomlDeclarations(content: string): string[] {
+ const duplicates: string[] = []
+ const tables = new Set()
+ let table = '[]'
+ let keys = new Set()
+ for (const rawLine of content.split('\n')) {
+ const line = rawLine.replace(/\r$/, '')
+ const header = getTomlTableHeader(line)
+ if (header !== null) {
+ table = JSON.stringify(parseTomlTableHeaderPath(header)?.segments ?? header)
+ if (tables.has(table)) {
+ duplicates.push(`table ${table}`)
+ }
+ tables.add(table)
+ keys = new Set()
+ continue
+ }
+ const key = parseTomlKeyPath(line)
+ if (key && line[key.end] === '=') {
+ const keyName = JSON.stringify(key.segments)
+ if (keys.has(keyName)) {
+ duplicates.push(`key ${keyName} in ${table}`)
+ }
+ keys.add(keyName)
+ }
+ }
+ return duplicates
+}
diff --git a/src/main/codex/config-toml-project-duplicate-repair.test.ts b/src/main/codex/config-toml-project-duplicate-repair.test.ts
new file mode 100644
index 000000000000..77b5ed22c971
--- /dev/null
+++ b/src/main/codex/config-toml-project-duplicate-repair.test.ts
@@ -0,0 +1,183 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { readFileSync, writeFileSync } from 'node:fs'
+import { findDuplicateTomlDeclarations } from './config-toml-duplicate-declarations-test-fixture'
+import { repairOrcaDuplicateProjectTrust } from './config-toml-project-duplicate-repair'
+import { upsertProjectTrustLevel, upsertProjectTrustLevelInContent } from './config-toml-trust'
+import {
+ createTrustConfigFixture,
+ removeTrustConfigFixture
+} from './config-toml-trust-test-fixtures'
+
+// Why: each case uses its own path because refusals are logged once per process.
+let tmpDir: string
+let configPath: string
+let warn: ReturnType
+
+beforeEach(() => {
+ const fixture = createTrustConfigFixture()
+ tmpDir = fixture.tmpDir
+ configPath = fixture.configPath
+ warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
+})
+
+afterEach(() => {
+ warn.mockRestore()
+ removeTrustConfigFixture(tmpDir)
+})
+
+describe('repairing configs broken by #22592', () => {
+ it('removes the table Orca appended beside Codex quoted spelling and keeps a .bak', () => {
+ const projectPath = '/Users/me/Projects/agentic-WPS'
+ // The reported file: Codex's table, then the duplicate an older Orca appended.
+ const broken = [
+ 'model = "gpt-5.5"',
+ '',
+ `["projects"."${projectPath}"]`,
+ '"trust_level" = "trusted"',
+ '',
+ `[projects."${projectPath}"]`,
+ 'trust_level = "trusted"',
+ ''
+ ].join('\n')
+ writeFileSync(configPath, broken)
+ expect(findDuplicateTomlDeclarations(broken)).not.toEqual([])
+
+ upsertProjectTrustLevel(configPath, projectPath, 'trusted')
+
+ const repaired = readFileSync(configPath, 'utf-8')
+ expect(repaired).toBe(
+ [
+ 'model = "gpt-5.5"',
+ '',
+ `["projects"."${projectPath}"]`,
+ 'trust_level = "trusted"',
+ ''
+ ].join('\n')
+ )
+ expect(findDuplicateTomlDeclarations(repaired)).toEqual([])
+ expect(readFileSync(`${configPath}.bak`, 'utf-8')).toBe(broken)
+ expect(warn).not.toHaveBeenCalled()
+ })
+
+ it('repairs a duplicate for another project than the one being trusted', () => {
+ const broken = [
+ "['projects'.'/repo-a']",
+ 'trust_level = "untrusted"',
+ 'notes = "user"',
+ '',
+ '[projects."/repo-a"]',
+ 'trust_level = "trusted"',
+ ''
+ ].join('\n')
+
+ const updated = upsertProjectTrustLevelInContent(broken, '/repo-b', 'trusted', {
+ alreadyCanonical: true
+ })
+
+ expect(updated).toBe(
+ [
+ "['projects'.'/repo-a']",
+ 'trust_level = "untrusted"',
+ 'notes = "user"',
+ '',
+ '[projects."/repo-b"]',
+ 'trust_level = "trusted"',
+ ''
+ ].join('\n')
+ )
+ })
+
+ it('removes the bare trust_level line Orca inserted under a quoted key', () => {
+ const broken = [
+ '["projects"."/repo-c"]',
+ 'trust_level = "trusted"',
+ '"trust_level" = "trusted"',
+ ''
+ ].join('\r\n')
+
+ const repaired = repairOrcaDuplicateProjectTrust(broken)
+
+ expect(repaired).toBe(['["projects"."/repo-c"]', '"trust_level" = "trusted"', ''].join('\r\n'))
+ })
+
+ it('repairs Windows-path tables that decode to the same key', () => {
+ const broken = [
+ "['projects'.'C:\\Users\\me\\repo']",
+ "trust_level = 'trusted'",
+ '',
+ '[projects."C:\\\\Users\\\\me\\\\repo"]',
+ 'trust_level = "trusted"',
+ ''
+ ].join('\n')
+
+ const repaired = repairOrcaDuplicateProjectTrust(broken)
+
+ expect(repaired).toBe(
+ ["['projects'.'C:\\Users\\me\\repo']", "trust_level = 'trusted'", ''].join('\n')
+ )
+ })
+
+ it('keeps Windows slash variants, which are distinct TOML keys', () => {
+ const config = [
+ "['projects'.'C:\\Users\\me\\slashes']",
+ 'trust_level = "trusted"',
+ '',
+ '[projects."C:/Users/me/slashes"]',
+ 'trust_level = "trusted"',
+ ''
+ ].join('\n')
+
+ expect(repairOrcaDuplicateProjectTrust(config)).toBe(config)
+ expect(warn).not.toHaveBeenCalled()
+ })
+
+ it('leaves a user-authored duplicate alone and logs it once', () => {
+ const broken = [
+ '["projects"."/repo-d"]',
+ 'trust_level = "trusted"',
+ '',
+ '[projects."/repo-d"]',
+ 'trust_level = "trusted"',
+ 'notes = "hand-written"',
+ ''
+ ].join('\n')
+
+ const first = upsertProjectTrustLevelInContent(broken, '/repo-d', 'trusted', {
+ alreadyCanonical: true
+ })
+ const second = upsertProjectTrustLevelInContent(broken, '/repo-d', 'trusted', {
+ alreadyCanonical: true
+ })
+
+ expect(first).toBe(broken)
+ expect(second).toBe(broken)
+ expect(warn).toHaveBeenCalledTimes(1)
+ })
+
+ it('leaves the file untouched when another duplicate would still break it', () => {
+ const broken = [
+ '["projects"."/repo-e"]',
+ 'trust_level = "trusted"',
+ '',
+ '[profiles.fast]',
+ 'model = "a"',
+ '',
+ '["profiles"."fast"]',
+ 'model = "b"',
+ '',
+ '[projects."/repo-e"]',
+ 'trust_level = "trusted"',
+ ''
+ ].join('\n')
+
+ expect(repairOrcaDuplicateProjectTrust(broken)).toBe(broken)
+ expect(warn).toHaveBeenCalledTimes(1)
+ })
+
+ it('does not touch a valid config', () => {
+ const config = ['["projects"."/repo-f"]', '"trust_level" = "trusted"', ''].join('\n')
+
+ expect(repairOrcaDuplicateProjectTrust(config)).toBe(config)
+ expect(warn).not.toHaveBeenCalled()
+ })
+})
diff --git a/src/main/codex/config-toml-project-duplicate-repair.ts b/src/main/codex/config-toml-project-duplicate-repair.ts
new file mode 100644
index 000000000000..84d34a1ec9c8
--- /dev/null
+++ b/src/main/codex/config-toml-project-duplicate-repair.ts
@@ -0,0 +1,158 @@
+import { parseTomlTableHeaderPath } from './config-toml-key-path'
+import {
+ createTomlLineScanState,
+ getTomlTableHeader,
+ isTomlStructuralLine,
+ updateTomlLineScanState
+} from './config-toml-line-scan'
+import { findProjectTrustLevelEntries } from './config-toml-project-trust-level'
+import { escapeTomlBasicString } from './config-toml-syntax'
+
+type TomlTable = {
+ /** Line index of the header; the body runs to the next table's header. */
+ headerLine: number
+ endLine: number
+ header: string
+ segments: string[] | null
+ isArray: boolean
+}
+
+const ORCA_TRUST_LINE = 'trust_level = "trusted"'
+const loggedRefusals = new Set()
+
+/**
+ * Removes the duplicates older Orca builds wrote beside a Codex-spelled project
+ * table (#22592): an appended `[projects."
"]` table holding only
- * `trust_level = "trusted"`, or that line inserted under a quoted `"trust_level"`.
- * The user's table always wins; anything else leaves the content untouched.
+ * Removes the duplicates older Orca builds wrote beside Codex-spelled trust
+ * tables (#22592): an Orca-shaped `[projects."
"]`, `[hooks.state.""]` or
+ * empty `[hooks.state]` that repeats a table the user (or Codex) wrote in
+ * another spelling, and the bare `trust_level` line Orca inserted under a
+ * quoted `"trust_level"`. The user's table always wins; anything else leaves
+ * the content untouched.
*/
-export function repairOrcaDuplicateProjectTrust(content: string): string {
+export function repairOrcaDuplicateTrustTables(content: string): string {
const lines = content.split('\n')
- const tables = readTomlTables(lines)
+ const groups = new Map()
+ for (const table of readTomlTables(lines)) {
+ if (getTrustTableKind(table) !== null) {
+ const key = JSON.stringify(table.segments)
+ groups.set(key, [...(groups.get(key) ?? []), table])
+ }
+ }
const removedLines = new Set()
- const firstTableByPath = new Map()
- for (const table of tables) {
- const projectPath = getProjectPath(table)
- if (projectPath === null) {
- continue
+ for (const group of groups.values()) {
+ const userTables = group.filter((table) => !isOrcaWrittenTable(lines, table))
+ const survivor = group.length === 1 ? group[0] : userTables[0]
+ // Why: an identical spelling or two user tables cannot come from #22592, so only a person can merge them.
+ if (
+ !survivor ||
+ (group.length > 1 &&
+ (userTables.length !== 1 ||
+ group.some((table) => table !== survivor && table.header === survivor.header)))
+ ) {
+ return refuseRepair(content, `duplicate table ${group[0]?.header}`)
+ }
+ for (const table of group) {
+ if (table !== survivor) {
+ for (let index = table.headerLine; index < table.endLine; index += 1) {
+ removedLines.add(index)
+ }
+ }
}
- const first = firstTableByPath.get(projectPath)
- if (!first) {
- firstTableByPath.set(projectPath, table)
- const insertedLine = findOrcaInsertedTrustLine(lines, table)
+ if (getTrustTableKind(survivor) === 'project') {
+ const insertedLine = findOrcaInsertedTrustLine(lines, survivor)
if (insertedLine === 'unsafe') {
- return refuseRepair(content, `duplicate trust_level for ${projectPath}`)
+ return refuseRepair(content, `duplicate trust_level in ${survivor.header}`)
}
if (insertedLine !== null) {
removedLines.add(insertedLine)
}
- continue
- }
- if (!isOrcaAppendedTable(lines, table, projectPath) || first.header === table.header) {
- return refuseRepair(content, `duplicate project table for ${projectPath}`)
- }
- for (let index = table.headerLine; index < table.endLine; index += 1) {
- removedLines.add(index)
}
}
if (removedLines.size === 0) {
@@ -93,20 +105,50 @@ function readTomlTables(lines: string[]): TomlTable[] {
return tables
}
-function getProjectPath(table: TomlTable): string | null {
+type TrustTableKind = 'project' | 'hook-state' | 'hook-state-parent'
+
+function getTrustTableKind(table: TomlTable): TrustTableKind | null {
const segments = table.segments
- return !table.isArray && segments?.length === 2 && segments[0] === 'projects'
- ? (segments[1] ?? null)
- : null
+ if (table.isArray || !segments) {
+ return null
+ }
+ if (segments.length === 2 && segments[0] === 'projects') {
+ return 'project'
+ }
+ if (segments[0] !== 'hooks' || segments[1] !== 'state') {
+ return null
+ }
+ return segments.length === 2 ? 'hook-state-parent' : segments.length === 3 ? 'hook-state' : null
}
-function isOrcaAppendedTable(lines: string[], table: TomlTable, projectPath: string): boolean {
- if (stripCr(lines[table.headerLine]) !== `[projects."${escapeTomlBasicString(projectPath)}"]`) {
- return false
+// Why: only the exact bytes Orca's writers emit are safe to delete without asking.
+function isOrcaWrittenTable(lines: string[], table: TomlTable): boolean {
+ const header = stripCr(lines[table.headerLine])
+ const body = lines
+ .slice(table.headerLine + 1, table.endLine)
+ .map(stripCr)
+ .filter((line) => line.trim() !== '')
+ const leaf = table.segments?.at(-1) ?? ''
+ switch (getTrustTableKind(table)) {
+ case 'project':
+ return (
+ header === `[projects."${escapeTomlBasicString(leaf)}"]` &&
+ body.length === 1 &&
+ body[0] === ORCA_TRUST_LINE
+ )
+ case 'hook-state':
+ return (
+ (header === `[hooks.state."${escapeTomlBasicString(leaf)}"]` ||
+ header === `[hooks.state.'${leaf}']`) &&
+ body.length === 2 &&
+ /^enabled = (?:true|false)$/.test(body[0] ?? '') &&
+ /^trusted_hash = "(?:[^"\\]|\\.)*"$/.test(body[1] ?? '')
+ )
+ case 'hook-state-parent':
+ return header === '[hooks.state]' && body.length === 0
+ case null:
+ return false
}
- const body = lines.slice(table.headerLine + 1, table.endLine).map(stripCr)
- const content = body.filter((line) => line.trim() !== '')
- return content.length === 1 && content[0] === ORCA_TRUST_LINE
}
// Why: older Orca inserted its bare line directly under the header when it missed a quoted key.
@@ -136,7 +178,7 @@ function findDuplicateTable(content: string): string | null {
const body = lines.slice(table.headerLine + 1, table.endLine).join('\n')
if (
seen.has(key) ||
- (getProjectPath(table) !== null && findProjectTrustLevelEntries(body).length > 1)
+ (getTrustTableKind(table) === 'project' && findProjectTrustLevelEntries(body).length > 1)
) {
return table.header
}
diff --git a/src/main/codex/config-toml-project-trust.ts b/src/main/codex/config-toml-project-trust.ts
index cb62ecfc0133..9335605cb15b 100644
--- a/src/main/codex/config-toml-project-trust.ts
+++ b/src/main/codex/config-toml-project-trust.ts
@@ -12,7 +12,7 @@ import {
parseProjectTomlHeaderPath
} from './config-toml-syntax'
import { findProjectTrustLevelEntries } from './config-toml-project-trust-level'
-import { repairOrcaDuplicateProjectTrust } from './config-toml-project-duplicate-repair'
+import { repairOrcaDuplicateTrustTables } from './config-toml-project-duplicate-repair'
export function upsertProjectTrustContent(
existingContent: string,
@@ -20,7 +20,7 @@ export function upsertProjectTrustContent(
trustLevel: CodexProjectTrustLevel,
options?: { alreadyCanonical?: boolean }
): string {
- const existing = repairOrcaDuplicateProjectTrust(stripLeadingBom(existingContent))
+ const existing = repairOrcaDuplicateTrustTables(stripLeadingBom(existingContent))
const trustedProjectPath = options?.alreadyCanonical
? projectPath
: canonicalizeLocalProjectPath(projectPath)
diff --git a/src/main/codex/config-toml-runtime-owned-sections.ts b/src/main/codex/config-toml-runtime-owned-sections.ts
index c1fc734ac3e1..47bee5c8e594 100644
--- a/src/main/codex/config-toml-runtime-owned-sections.ts
+++ b/src/main/codex/config-toml-runtime-owned-sections.ts
@@ -83,10 +83,11 @@ export function isRuntimePreservedTomlSection(header: string): boolean {
}
export function isRuntimeHookTrustTomlSection(header: string): boolean {
- const trimmed = header.trim()
+ const table = parseTomlTableHeaderPath(header)
// Why: Codex's config writer materializes the parent table on Windows. It is
// part of runtime-owned trust and must survive the next config mirror too.
- return trimmed === '[hooks.state]' || trimmed.startsWith('[hooks.state.')
+ // Its `["hooks"."state"]` spelling is the same table (#22592).
+ return !!table && !table.isArray && table.segments[0] === 'hooks' && table.segments[1] === 'state'
}
export function isRuntimeProjectTomlSection(header: string): boolean {
diff --git a/src/main/codex/config-toml-syntax.ts b/src/main/codex/config-toml-syntax.ts
index 9ef3f303bb74..a9f9ed5efc4c 100644
--- a/src/main/codex/config-toml-syntax.ts
+++ b/src/main/codex/config-toml-syntax.ts
@@ -6,11 +6,6 @@ import {
} from './config-toml-line-scan'
import { parseTomlTableHeaderPath } from './config-toml-key-path'
-type ParsedTomlString = {
- value: string
- endIndex: number
-}
-
export function escapeTomlBasicString(value: string): string {
return value
.replaceAll('\\', '\\\\')
@@ -23,79 +18,26 @@ export function escapeTomlBasicString(value: string): string {
}
export function parseHookStateTomlHeaderKey(line: string): string | null {
- const trimmed = line.trimStart()
- const prefixMatch = /^\[[ \t]*hooks[ \t]*\.[ \t]*state[ \t]*\.[ \t]*/.exec(trimmed)
- return prefixMatch ? parseTomlTableStringKey(trimmed, prefixMatch[0].length) : null
+ return parseTomlTableHeaderLeafKey(line, ['hooks', 'state'])
}
-// Why (#22592): Codex writes `["projects"."/p"]`; every spelling of the key path must match.
export function parseProjectTomlHeaderPath(line: string): string | null {
- const header = getTomlTableHeader(line.replace(/\r$/, ''))
- const table = header === null ? null : parseTomlTableHeaderPath(header)
- if (!table || table.isArray || table.segments.length !== 2 || table.segments[0] !== 'projects') {
- return null
- }
- return table.segments[1] ?? null
+ return parseTomlTableHeaderLeafKey(line, ['projects'])
}
-function parseTomlTableStringKey(line: string, startIndex: number): string | null {
- const parsedKey = parseTomlSingleLineString(line, startIndex)
- if (!parsedKey) {
- return null
- }
- let index = skipTomlInlineWhitespace(line, parsedKey.endIndex)
- if (line[index] !== ']') {
+// Why (#22592): Codex writes `["projects"."/p"]`; every spelling of the key path must match.
+function parseTomlTableHeaderLeafKey(line: string, parent: readonly string[]): string | null {
+ const header = getTomlTableHeader(line.replace(/\r$/, ''))
+ const table = header === null ? null : parseTomlTableHeaderPath(header)
+ if (
+ !table ||
+ table.isArray ||
+ table.segments.length !== parent.length + 1 ||
+ parent.some((segment, index) => table.segments[index] !== segment)
+ ) {
return null
}
- index = skipTomlInlineWhitespace(line, index + 1)
- return index === line.length || line[index] === '#' ? parsedKey.value : null
-}
-
-function parseTomlSingleLineString(line: string, startIndex: number): ParsedTomlString | null {
- if (line[startIndex] === '"') {
- return parseTomlBasicSingleLineString(line, startIndex + 1)
- }
- if (line[startIndex] === "'") {
- return parseTomlLiteralSingleLineString(line, startIndex + 1)
- }
- return null
-}
-
-function parseTomlBasicSingleLineString(line: string, startIndex: number): ParsedTomlString | null {
- let value = ''
- let index = startIndex
- while (index < line.length) {
- const char = line[index]
- if (char === '"') {
- return { value, endIndex: index + 1 }
- }
- if (char === '\\' && index + 1 < line.length) {
- value += unescapeTomlBasicStringEscape(line[index + 1]!)
- index += 2
- continue
- }
- value += char
- index += 1
- }
- return null
-}
-
-function parseTomlLiteralSingleLineString(
- line: string,
- startIndex: number
-): ParsedTomlString | null {
- const endIndex = line.indexOf("'", startIndex)
- return endIndex === -1
- ? null
- : { value: line.slice(startIndex, endIndex), endIndex: endIndex + 1 }
-}
-
-function skipTomlInlineWhitespace(line: string, startIndex: number): number {
- let index = startIndex
- while (line[index] === ' ' || line[index] === '\t') {
- index += 1
- }
- return index
+ return table.segments.at(-1) ?? null
}
export function findNextTomlTableHeader(text: string): number {
From 57542d7db8f6307df99d69e3fe6ba1f2c28b3b3f Mon Sep 17 00:00:00 2001
From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
Date: Tue, 29 Sep 2026 19:10:06 -0700
Subject: [PATCH 4/5] fix(codex): carry plain-Codex plugin and project hook
trust into Orca's Codex homes (#22592)
Codex keeps hook trust in $CODEX_HOME/config.toml under hooks.state, keyed
by the hook's source. Plugin keys (`id@mkt:path`) and project keys
(`/.codex/...`) are the same in every home, but the mirror dropped
every hooks.state table from ~/.codex, so Codex inside Orca asked users to
re-trust plugin and project hooks they had already trusted in plain Codex.
- classifyHookTrustKey splits keys into home-scoped (the home's own
hooks.json/config.toml, re-keyed by install as before) and shared.
- The mirror now carries shared hook trust from ~/.codex in every spelling.
A key the managed home already holds keeps the managed copy, a key
repeated in ~/.codex is carried once, and the parent [hooks.state] table
is never copied, so the result never declares a table twice.
- mergeSystemCodexConfigIntoRuntime moves to codex-config-mirror-merge.ts
to keep codex-config-mirror.ts under the line limit.
- Tests cover plugin/project carry in each spelling, user-hook keys staying
out, repeated launches, managed-copy precedence, Windows key spellings,
parent tables, and user-hook trust re-keying (trusted_hash and enabled)
from every ~/.codex spelling.
---
config/tsconfig.cli.json | 1 +
...dex-config-mirror-hook-trust-carry.test.ts | 276 ++++++++++++++++++
src/main/codex/codex-config-mirror-merge.ts | 78 +++++
src/main/codex/codex-config-mirror.test.ts | 25 +-
src/main/codex/codex-config-mirror.ts | 86 +-----
.../config-toml-runtime-owned-sections.ts | 68 ++++-
...-service-user-hook-trust-spellings.test.ts | 135 +++++++++
7 files changed, 581 insertions(+), 88 deletions(-)
create mode 100644 src/main/codex/codex-config-mirror-hook-trust-carry.test.ts
create mode 100644 src/main/codex/codex-config-mirror-merge.ts
create mode 100644 src/main/codex/hook-service-user-hook-trust-spellings.test.ts
diff --git a/config/tsconfig.cli.json b/config/tsconfig.cli.json
index 3a1cf9584c7f..cd47d038c2a8 100644
--- a/config/tsconfig.cli.json
+++ b/config/tsconfig.cli.json
@@ -116,6 +116,7 @@
"../src/main/codex/codex-app-server-record-reader.ts",
"../src/main/codex/codex-app-server-session.ts",
"../src/main/codex/codex-config-mirror.ts",
+ "../src/main/codex/codex-config-mirror-merge.ts",
"../src/main/codex/codex-config-path-reference-rewrite.ts",
"../src/main/codex/codex-config-settings-preservation.ts",
"../src/main/codex/codex-config-settings-removal.ts",
diff --git a/src/main/codex/codex-config-mirror-hook-trust-carry.test.ts b/src/main/codex/codex-config-mirror-hook-trust-carry.test.ts
new file mode 100644
index 000000000000..a9633c6075c4
--- /dev/null
+++ b/src/main/codex/codex-config-mirror-hook-trust-carry.test.ts
@@ -0,0 +1,276 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { mkdtempSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import type * as NodeOs from 'node:os'
+import { join } from 'node:path'
+
+// Why: temp homes exceed sun_path on macOS but not on Linux; keep asserted config bytes host-independent.
+vi.mock('./codex-daemon-socket-path-guard', async (importOriginal) => ({
+ ...(await importOriginal