mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-23 03:27:50 +09:00
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:
11 files changed
+63
-30
No files matched your search
@@ -0,0 +1,4 @@
|
||||
# Explicit Codex CLI profile: codex -p strict
|
||||
approval_policy = "on-request"
|
||||
sandbox_mode = "read-only"
|
||||
web_search = "cached"
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# Explicit Codex CLI profile: codex -p yolo
|
||||
approval_policy = "never"
|
||||
sandbox_mode = "workspace-write"
|
||||
web_search = "live"
|
||||
@@ -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.
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user