mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-26 18:35:05 +00: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.
|
# Preserve ECC's explicit multi-agent setting.
|
||||||
multi_agent = true
|
multi_agent = true
|
||||||
|
|
||||||
# Named profiles are available for explicit selection; defining one does not select it.
|
# Codex 0.134+ reads named profiles from <CODEX_HOME>/<name>.config.toml.
|
||||||
# The default above remains on-request. Approval and sandbox policy are separate.
|
# Install strict.config.toml and yolo.config.toml beside config.toml, then use
|
||||||
# Select a supported profile explicitly with `codex -p <name>`.
|
# `codex -p strict` or `codex -p yolo`. Defining a file does not select it.
|
||||||
[profiles.strict]
|
# For an existing home, move legacy [profiles.<name>] settings into that file
|
||||||
approval_policy = "on-request"
|
# and remove the legacy table and top-level profile selector after review.
|
||||||
sandbox_mode = "read-only"
|
# Approval and sandbox policy remain separate; the default above is on-request.
|
||||||
web_search = "cached"
|
|
||||||
|
|
||||||
[profiles.yolo]
|
|
||||||
approval_policy = "never"
|
|
||||||
sandbox_mode = "workspace-write"
|
|
||||||
web_search = "live"
|
|
||||||
|
|
||||||
[agents]
|
[agents]
|
||||||
# Multi-agent role limits and local role definitions.
|
# 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 |
|
| `AGENTS.md` | Cross-harness operating rules | Read before any repo work |
|
||||||
| `.codex/AGENTS.md` | Codex-only guidance | Read after root instructions |
|
| `.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 |
|
| `.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 |
|
| `.agents/skills/` | Codex-facing skill copies | Use when Codex needs native skill loading |
|
||||||
| `skills/` | Canonical skill source | Update first for new workflow knowledge |
|
| `skills/` | Canonical skill source | Update first for new workflow knowledge |
|
||||||
@@ -165,3 +165,16 @@ npm run command-registry:check
|
|||||||
npm run catalog:check
|
npm run catalog:check
|
||||||
node tests/run-all.js
|
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
|
else
|
||||||
warn "persistent_instructions is not set (recommended but optional)"
|
warn "persistent_instructions is not set (recommended but optional)"
|
||||||
fi
|
fi
|
||||||
check_config_pattern '^\[profiles\.strict\]' "profiles.strict exists"
|
require_file "$CODEX_HOME/strict.config.toml" "strict named profile"
|
||||||
check_config_pattern '^\[profiles\.yolo\]' "profiles.yolo exists"
|
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
|
# Current default connector set (docs/MCP-CONNECTOR-POLICY.md): exactly
|
||||||
# one connector. Former defaults (github, memory, sequential-thinking,
|
# 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 ROOT_KEYS = ['approval_policy', 'sandbox_mode', 'web_search', 'notify', 'persistent_instructions'];
|
||||||
const TABLE_PATHS = [
|
const TABLE_PATHS = [
|
||||||
'features',
|
'features',
|
||||||
'profiles.strict',
|
|
||||||
'profiles.yolo',
|
|
||||||
'agents',
|
'agents',
|
||||||
'agents.explorer',
|
'agents.explorer',
|
||||||
'agents.reviewer',
|
'agents.reviewer',
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ function isCodexUserConfig(plan, operation) {
|
|||||||
}
|
}
|
||||||
const relativePath = path.relative(plan.targetRoot, operation.destinationPath);
|
const relativePath = path.relative(plan.targetRoot, operation.destinationPath);
|
||||||
const name = process.platform === 'win32' ? relativePath.toLowerCase() : relativePath;
|
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) {
|
function readConfigDigest(plan, destinationPath) {
|
||||||
|
|||||||
@@ -338,6 +338,16 @@ for agent_file in "$CODEX_AGENTS_SRC"/*.toml; do
|
|||||||
fi
|
fi
|
||||||
done
|
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
|
# Skills are NOT synced here — Codex CLI reads directly from
|
||||||
# ~/.agents/skills/ (installed by ECC installer / npx skills).
|
# ~/.agents/skills/ (installed by ECC installer / npx skills).
|
||||||
# Copying into ~/.codex/skills/ was unnecessary.
|
# Copying into ~/.codex/skills/ was unnecessary.
|
||||||
|
|||||||
@@ -165,21 +165,25 @@ const checks = [
|
|||||||
assert.strictEqual(project.agents.max_threads, 6);
|
assert.strictEqual(project.agents.max_threads, 6);
|
||||||
assert.strictEqual(project.agents.max_depth, 1);
|
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();
|
const user = readUserReference();
|
||||||
assert.deepStrictEqual(Object.keys(user).sort(), [
|
assert.deepStrictEqual(Object.keys(user).sort(), [
|
||||||
'agents', 'approval_policy', 'features', 'mcp_servers', 'notify',
|
'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.approval_policy, 'on-request');
|
||||||
assert.strictEqual(user.sandbox_mode, 'workspace-write');
|
assert.strictEqual(user.sandbox_mode, 'workspace-write');
|
||||||
assert.strictEqual(user.web_search, 'live');
|
assert.strictEqual(user.web_search, 'live');
|
||||||
assert.deepStrictEqual(user.notify,
|
assert.deepStrictEqual(user.notify,
|
||||||
['terminal-notifier', '-title', 'Codex ECC', '-message', 'Task completed!', '-sound', 'default']);
|
['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' },
|
strict: { approval_policy: 'on-request', sandbox_mode: 'read-only', web_search: 'cached' },
|
||||||
yolo: { approval_policy: 'never', sandbox_mode: 'workspace-write', web_search: 'live' },
|
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');
|
assert.ok(!Object.hasOwn(user, 'profile'), 'Defining a profile must not select it');
|
||||||
}],
|
}],
|
||||||
['user reference preserves guidance, role mappings and existing MCP data', () => {
|
['user reference preserves guidance, role mappings and existing MCP data', () => {
|
||||||
@@ -233,8 +237,7 @@ const checks = [
|
|||||||
assert.deepStrictEqual(merged[key], custom[key]);
|
assert.deepStrictEqual(merged[key], custom[key]);
|
||||||
}
|
}
|
||||||
assert.strictEqual(merged.profiles.strict.sandbox_mode, 'workspace-write');
|
assert.strictEqual(merged.profiles.strict.sandbox_mode, 'workspace-write');
|
||||||
assert.strictEqual(merged.profiles.strict.approval_policy, 'on-request');
|
assert.deepStrictEqual(merged.profiles, custom.profiles, 'Legacy choices require explicit manual migration');
|
||||||
assert.deepStrictEqual(merged.profiles.yolo, readUserReference().profiles.yolo);
|
|
||||||
});
|
});
|
||||||
}],
|
}],
|
||||||
['legacy merger rejects missing user reference without falling back to project config', () => {
|
['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 USER_REFERENCE = 'user-config.example.toml';
|
||||||
const PROJECT_CONFIG = '# Project-only configuration\n[features]\nmulti_agent = true\n';
|
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 = {
|
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',
|
'config.toml': '# ECC defaults\nmodel = "example-model"\n',
|
||||||
'AGENTS.md': '# ECC instructions\n\nFollow the project conventions.\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.sandbox_mode, 'workspace-write');
|
||||||
assert.strictEqual(parsed.web_search, 'live');
|
assert.strictEqual(parsed.web_search, 'live');
|
||||||
assert.strictEqual(parsed.features.multi_agent, true);
|
assert.strictEqual(parsed.features.multi_agent, true);
|
||||||
assert.strictEqual(parsed.profiles.strict.approval_policy, 'on-request');
|
assert.strictEqual(parsed.profiles, undefined, 'Do not emit legacy profile tables');
|
||||||
assert.strictEqual(parsed.profiles.yolo.approval_policy, 'never');
|
|
||||||
assert.strictEqual(parsed.agents.max_threads, 6);
|
assert.strictEqual(parsed.agents.max_threads, 6);
|
||||||
assert.strictEqual(parsed.agents.explorer.config_file, 'agents/explorer.toml');
|
assert.strictEqual(parsed.agents.explorer.config_file, 'agents/explorer.toml');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1263,9 +1262,12 @@ if (
|
|||||||
assert.ok(!Object.prototype.hasOwnProperty.call(parsedConfig, 'multi_agent'));
|
assert.ok(!Object.prototype.hasOwnProperty.call(parsedConfig, 'multi_agent'));
|
||||||
assert.ok(parsedConfig.features);
|
assert.ok(parsedConfig.features);
|
||||||
assert.strictEqual(parsedConfig.features.multi_agent, true);
|
assert.strictEqual(parsedConfig.features.multi_agent, true);
|
||||||
assert.ok(parsedConfig.profiles);
|
assert.strictEqual(parsedConfig.profiles, undefined);
|
||||||
assert.strictEqual(parsedConfig.profiles.strict.approval_policy, 'on-request');
|
for (const name of ['strict', 'yolo']) {
|
||||||
assert.strictEqual(parsedConfig.profiles.yolo.approval_policy, 'never');
|
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.ok(parsedConfig.agents);
|
||||||
assert.strictEqual(parsedConfig.agents.max_threads, 6);
|
assert.strictEqual(parsedConfig.agents.max_threads, 6);
|
||||||
assert.strictEqual(parsedConfig.agents.max_depth, 1);
|
assert.strictEqual(parsedConfig.agents.max_depth, 1);
|
||||||
|
|||||||
Reference in new issue
Block a user