fix(codex): ship named CLI profile files with ownership protection

Reproduced issue #3485 on installed Codex CLI 0.162.0. Keep user legacy choices for explicit migration and verify disposable install, repair, reinstall and removal.
This commit is contained in:
haelyra committed 2026-10-11 01:55:54 -04:00
1 parent 2d515e4fc2
commit 4f5970bf4f
11 files changed
+63 -30

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
# Explicit Codex CLI profile: codex -p strict
approval_policy = "on-request"
sandbox_mode = "read-only"
web_search = "cached"
+6 -12
View File
@@ -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 <name>`.
[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 <CODEX_HOME>/<name>.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.<name>] 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.
+4
View File
@@ -0,0 +1,4 @@
# Explicit Codex CLI profile: codex -p yolo
approval_policy = "never"
sandbox_mode = "workspace-write"
web_search = "live"
+14 -1
View File
@@ -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
`<CODEX_HOME>/strict.config.toml` and `<CODEX_HOME>/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.<name>]`, review and move its settings
into `<name>.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 <name> mcp list`; this checks
configuration loading without invoking a model or certifying its runtime.
+4 -2
View File
@@ -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,
-2
View File
@@ -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',
+2 -1
View File
@@ -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) {
+10
View File
@@ -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.
+9 -6
View File
@@ -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', () => {
@@ -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',
};
+7 -5
View File
@@ -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);