diff --git a/.codex/strict.config.toml b/.codex/strict.config.toml new file mode 100644 index 000000000..84b2fe31b --- /dev/null +++ b/.codex/strict.config.toml @@ -0,0 +1,4 @@ +# Explicit Codex CLI profile: codex -p strict +approval_policy = "on-request" +sandbox_mode = "read-only" +web_search = "cached" diff --git a/.codex/user-config.example.toml b/.codex/user-config.example.toml index 23f436a6c..ada22dfb0 100644 --- a/.codex/user-config.example.toml +++ b/.codex/user-config.example.toml @@ -90,18 +90,12 @@ startup_timeout_sec = 30 # Preserve ECC's explicit multi-agent setting. multi_agent = true -# Named profiles are available for explicit selection; defining one does not select it. -# The default above remains on-request. Approval and sandbox policy are separate. -# Select a supported profile explicitly with `codex -p `. -[profiles.strict] -approval_policy = "on-request" -sandbox_mode = "read-only" -web_search = "cached" - -[profiles.yolo] -approval_policy = "never" -sandbox_mode = "workspace-write" -web_search = "live" +# Codex 0.134+ reads named profiles from /.config.toml. +# Install strict.config.toml and yolo.config.toml beside config.toml, then use +# `codex -p strict` or `codex -p yolo`. Defining a file does not select it. +# For an existing home, move legacy [profiles.] settings into that file +# and remove the legacy table and top-level profile selector after review. +# Approval and sandbox policy remain separate; the default above is on-request. [agents] # Multi-agent role limits and local role definitions. diff --git a/.codex/yolo.config.toml b/.codex/yolo.config.toml new file mode 100644 index 000000000..2e910db86 --- /dev/null +++ b/.codex/yolo.config.toml @@ -0,0 +1,4 @@ +# Explicit Codex CLI profile: codex -p yolo +approval_policy = "never" +sandbox_mode = "workspace-write" +web_search = "live" diff --git a/docs/CODEX-NAVIGATION-GUIDE.md b/docs/CODEX-NAVIGATION-GUIDE.md index 8ff4c294c..3b8802153 100644 --- a/docs/CODEX-NAVIGATION-GUIDE.md +++ b/docs/CODEX-NAVIGATION-GUIDE.md @@ -26,7 +26,7 @@ policy belongs in `AGENTS.md` and `CONTRIBUTING.md`. |---------|---------------|-----------| | `AGENTS.md` | Cross-harness operating rules | Read before any repo work | | `.codex/AGENTS.md` | Codex-only guidance | Read after root instructions | -| `.codex/config.toml` | Codex sandbox, MCP, profiles, agent roles | Inspect when setup or MCP behavior matters | +| `.codex/config.toml` | Project guidance and agent roles | Inspect when setup or MCP behavior matters | | `.codex/agents/` | Codex multi-agent role layers | Use for explorer, reviewer, and docs researcher roles | | `.agents/skills/` | Codex-facing skill copies | Use when Codex needs native skill loading | | `skills/` | Canonical skill source | Update first for new workflow knowledge | @@ -165,3 +165,16 @@ npm run command-registry:check npm run catalog:check node tests/run-all.js ``` + +## Named user profiles + +Codex CLI 0.134 and later loads `codex -p strict` and `codex -p yolo` from +`/strict.config.toml` and `/yolo.config.toml`. ECC ships +those files beside its user reference. The receipt-backed home installer and +legacy sync preserve existing customized profile files. + +For an existing config with `[profiles.]`, review and move its settings +into `.config.toml`, then remove the old table and any top-level `profile` +selector. ECC's add-only legacy merger leaves those user choices untouched. +Verify the migration locally with `codex -p mcp list`; this checks +configuration loading without invoking a model or certifying its runtime. diff --git a/scripts/codex/check-codex-global-state.sh b/scripts/codex/check-codex-global-state.sh index fb944c4cc..d98dcdfe5 100755 --- a/scripts/codex/check-codex-global-state.sh +++ b/scripts/codex/check-codex-global-state.sh @@ -106,8 +106,10 @@ if [[ -f "$CONFIG_FILE" ]]; then else warn "persistent_instructions is not set (recommended but optional)" fi - check_config_pattern '^\[profiles\.strict\]' "profiles.strict exists" - check_config_pattern '^\[profiles\.yolo\]' "profiles.yolo exists" + require_file "$CODEX_HOME/strict.config.toml" "strict named profile" + require_file "$CODEX_HOME/yolo.config.toml" "yolo named profile" + check_config_absent '^[[:space:]]*\[profiles[.]' "legacy profile tables are absent (migrate to named .config.toml files)" + check_config_absent '^[[:space:]]*profile[[:space:]]*=' "legacy profile selector is absent" # Current default connector set (docs/MCP-CONNECTOR-POLICY.md): exactly # one connector. Former defaults (github, memory, sequential-thinking, diff --git a/scripts/codex/merge-codex-config.js b/scripts/codex/merge-codex-config.js index cd6d9c2c8..c01e84633 100644 --- a/scripts/codex/merge-codex-config.js +++ b/scripts/codex/merge-codex-config.js @@ -26,8 +26,6 @@ try { const ROOT_KEYS = ['approval_policy', 'sandbox_mode', 'web_search', 'notify', 'persistent_instructions']; const TABLE_PATHS = [ 'features', - 'profiles.strict', - 'profiles.yolo', 'agents', 'agents.explorer', 'agents.reviewer', diff --git a/scripts/lib/install/codex-user-config.js b/scripts/lib/install/codex-user-config.js index 90b6c8c84..5fca5efcf 100644 --- a/scripts/lib/install/codex-user-config.js +++ b/scripts/lib/install/codex-user-config.js @@ -11,7 +11,8 @@ function isCodexUserConfig(plan, operation) { } const relativePath = path.relative(plan.targetRoot, operation.destinationPath); const name = process.platform === 'win32' ? relativePath.toLowerCase() : relativePath; - return name === 'config.toml' || name === (process.platform === 'win32' ? 'agents.md' : 'AGENTS.md'); + return name === 'config.toml' || name === 'strict.config.toml' || name === 'yolo.config.toml' + || name === (process.platform === 'win32' ? 'agents.md' : 'AGENTS.md'); } function readConfigDigest(plan, destinationPath) { diff --git a/scripts/sync-ecc-to-codex.sh b/scripts/sync-ecc-to-codex.sh index 60e75d472..18a11f939 100755 --- a/scripts/sync-ecc-to-codex.sh +++ b/scripts/sync-ecc-to-codex.sh @@ -338,6 +338,16 @@ for agent_file in "$CODEX_AGENTS_SRC"/*.toml; do fi done +# Named profiles are independent user configuration. Seed only absent files; +# customized or legacy choices require explicit user migration, never overwrite. +for profile_name in strict yolo; do + profile_dest="$CODEX_HOME/$profile_name.config.toml" + if [[ ! -e "$profile_dest" && ! -L "$profile_dest" ]]; then + record_managed_path "$profile_dest" + run_or_echo cp "$REPO_ROOT/.codex/$profile_name.config.toml" "$profile_dest" + fi +done + # Skills are NOT synced here — Codex CLI reads directly from # ~/.agents/skills/ (installed by ECC installer / npx skills). # Copying into ~/.codex/skills/ was unnecessary. diff --git a/tests/codex-config.test.js b/tests/codex-config.test.js index 89fc40236..4eff276f8 100644 --- a/tests/codex-config.test.js +++ b/tests/codex-config.test.js @@ -165,21 +165,25 @@ const checks = [ assert.strictEqual(project.agents.max_threads, 6); assert.strictEqual(project.agents.max_depth, 1); }], - ['user reference preserves root defaults and explicit profiles in their own tables', () => { + ['user reference uses separate named profile files', () => { const user = readUserReference(); assert.deepStrictEqual(Object.keys(user).sort(), [ 'agents', 'approval_policy', 'features', 'mcp_servers', 'notify', - 'persistent_instructions', 'profiles', 'sandbox_mode', 'web_search', + 'persistent_instructions', 'sandbox_mode', 'web_search', ]); assert.strictEqual(user.approval_policy, 'on-request'); assert.strictEqual(user.sandbox_mode, 'workspace-write'); assert.strictEqual(user.web_search, 'live'); assert.deepStrictEqual(user.notify, ['terminal-notifier', '-title', 'Codex ECC', '-message', 'Task completed!', '-sound', 'default']); - assert.deepStrictEqual(user.profiles, { + assert.ok(!Object.hasOwn(user, 'profiles')); + for (const [name, expected] of Object.entries({ strict: { approval_policy: 'on-request', sandbox_mode: 'read-only', web_search: 'cached' }, yolo: { approval_policy: 'never', sandbox_mode: 'workspace-write', web_search: 'live' }, - }); + })) { + assert.deepStrictEqual(TOML.parse(fs.readFileSync( + path.join(repoRoot, '.codex', `${name}.config.toml`), 'utf8')), expected); + } assert.ok(!Object.hasOwn(user, 'profile'), 'Defining a profile must not select it'); }], ['user reference preserves guidance, role mappings and existing MCP data', () => { @@ -233,8 +237,7 @@ const checks = [ assert.deepStrictEqual(merged[key], custom[key]); } assert.strictEqual(merged.profiles.strict.sandbox_mode, 'workspace-write'); - assert.strictEqual(merged.profiles.strict.approval_policy, 'on-request'); - assert.deepStrictEqual(merged.profiles.yolo, readUserReference().profiles.yolo); + assert.deepStrictEqual(merged.profiles, custom.profiles, 'Legacy choices require explicit manual migration'); }); }], ['legacy merger rejects missing user reference without falling back to project config', () => { diff --git a/tests/lib/install-codex-config-preservation.test.js b/tests/lib/install-codex-config-preservation.test.js index ece571d76..be2156284 100644 --- a/tests/lib/install-codex-config-preservation.test.js +++ b/tests/lib/install-codex-config-preservation.test.js @@ -17,8 +17,10 @@ const codexAdapter = require('../../scripts/lib/install-targets/codex-home'); const USER_REFERENCE = 'user-config.example.toml'; const PROJECT_CONFIG = '# Project-only configuration\n[features]\nmulti_agent = true\n'; -const SHARED_FILES = ['config.toml', 'AGENTS.md']; +const SHARED_FILES = ['config.toml', 'AGENTS.md', 'strict.config.toml', 'yolo.config.toml']; const TEMPLATES = { + 'strict.config.toml': 'sandbox_mode = "read-only"\n', + 'yolo.config.toml': 'sandbox_mode = "workspace-write"\n', 'config.toml': '# ECC defaults\nmodel = "example-model"\n', 'AGENTS.md': '# ECC instructions\n\nFollow the project conventions.\n', }; diff --git a/tests/scripts/codex-hooks.test.js b/tests/scripts/codex-hooks.test.js index a4f6e388b..2161ea8c9 100644 --- a/tests/scripts/codex-hooks.test.js +++ b/tests/scripts/codex-hooks.test.js @@ -881,8 +881,7 @@ if ( assert.strictEqual(parsed.sandbox_mode, 'workspace-write'); assert.strictEqual(parsed.web_search, 'live'); assert.strictEqual(parsed.features.multi_agent, true); - assert.strictEqual(parsed.profiles.strict.approval_policy, 'on-request'); - assert.strictEqual(parsed.profiles.yolo.approval_policy, 'never'); + assert.strictEqual(parsed.profiles, undefined, 'Do not emit legacy profile tables'); assert.strictEqual(parsed.agents.max_threads, 6); assert.strictEqual(parsed.agents.explorer.config_file, 'agents/explorer.toml'); } finally { @@ -1263,9 +1262,12 @@ if ( assert.ok(!Object.prototype.hasOwnProperty.call(parsedConfig, 'multi_agent')); assert.ok(parsedConfig.features); assert.strictEqual(parsedConfig.features.multi_agent, true); - assert.ok(parsedConfig.profiles); - assert.strictEqual(parsedConfig.profiles.strict.approval_policy, 'on-request'); - assert.strictEqual(parsedConfig.profiles.yolo.approval_policy, 'never'); + assert.strictEqual(parsedConfig.profiles, undefined); + for (const name of ['strict', 'yolo']) { + assert.ok(fs.existsSync(path.join(codexDir, `${name}.config.toml`))); + } + assert.strictEqual(TOML.parse(fs.readFileSync(path.join(codexDir, 'strict.config.toml'), 'utf8')).approval_policy, 'on-request'); + assert.strictEqual(TOML.parse(fs.readFileSync(path.join(codexDir, 'yolo.config.toml'), 'utf8')).approval_policy, 'never'); assert.ok(parsedConfig.agents); assert.strictEqual(parsedConfig.agents.max_threads, 6); assert.strictEqual(parsedConfig.agents.max_depth, 1);