Builds onaedbf8a, which fixed the discard-everything-after-the-block bug. Two related faults remained, both reproducible on a config that has drifted: 1. The block's interior was still dropped. Where an older kit let the end marker drift below content Codex had written, real [plugins.*] and [projects.*] sections ended up *inside* the block, so discarding it deleted them. This was the live state on the macOS machine: the markers spanned lines 15-88 with plugins, marketplaces, projects and mcp_servers.graphiti between them. The interior is now filtered like the rest, which also heals the layout in a single run. 2. Bare root keys were still landing under a table. With the block emitted after `before`, and `before` ending in [sandbox_workspace_write], TOML bound model/web_search/project_doc_max_bytes to that table rather than to the document root. Output is now ordered: root keys, managed block, then tables. Against a fixture with a plugin inside the block and an mcp_server after it,aedbf8ayields: swallowed plugin kept: False sandbox keys: [network_access, model, model_reasoning_effort, project_doc_max_bytes, web_search] root keys: [approval_policy] and this commit yields: swallowed plugin kept: True after-block mcp kept: True sandbox keys: [network_access] root keys: [approval_policy, model, model_reasoning_effort, project_doc_max_bytes, web_search] BOM handling, profile file layout and model choices fromaedbf8aare unchanged. Verified on macOS: repeated runs are byte-identical, all 13 plugins / 3 projects / 6 mcp_servers survive, and `codex exec --profile deep` works. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
148 lines
5.2 KiB
Bash
Executable File
148 lines
5.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
|
|
STAMP="$(date +%Y%m%d-%H%M%S)"
|
|
BACKUP_DIR="$CODEX_HOME/backups/$STAMP"
|
|
MANAGED_CONFIG="$REPO_ROOT/templates/global/config-managed.toml"
|
|
|
|
mkdir -p "$CODEX_HOME" "$BACKUP_DIR" "$CODEX_HOME/memories"
|
|
|
|
if [[ -f "$CODEX_HOME/config.toml" ]]; then
|
|
cp "$CODEX_HOME/config.toml" "$BACKUP_DIR/config.toml.bak"
|
|
fi
|
|
|
|
if [[ -f "$CODEX_HOME/AGENTS.md" ]]; then
|
|
cp "$CODEX_HOME/AGENTS.md" "$BACKUP_DIR/AGENTS.md.bak"
|
|
fi
|
|
|
|
cp "$REPO_ROOT/templates/global/AGENTS.md" "$CODEX_HOME/AGENTS.md"
|
|
|
|
python3 - "$CODEX_HOME/config.toml" "$MANAGED_CONFIG" <<'PY'
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
config_path = Path(sys.argv[1])
|
|
managed_path = Path(sys.argv[2])
|
|
|
|
managed_start = "# >>> codex-ops-kit managed block >>>"
|
|
managed_end = "# <<< codex-ops-kit managed block <<<"
|
|
# "profile" and the "profiles.*" tables are legacy: Codex 0.144+ refuses to load
|
|
# a config containing them. They stay in these strip lists (despite no longer
|
|
# being emitted) so an upgrade from an older kit removes them from a live config.
|
|
managed_root_keys = {
|
|
"model",
|
|
"model_reasoning_effort",
|
|
"profile",
|
|
"project_doc_max_bytes",
|
|
"web_search",
|
|
}
|
|
managed_sections = {
|
|
"history",
|
|
"profiles.economy",
|
|
"profiles.balanced",
|
|
"profiles.deep",
|
|
}
|
|
|
|
# utf-8-sig drops a leading BOM if one is present. Preserving it risks stranding
|
|
# the BOM mid-file once content is merged around the block, which is invalid TOML.
|
|
text = config_path.read_text(encoding="utf-8-sig") if config_path.exists() else ""
|
|
managed = managed_path.read_text(encoding="utf-8-sig").rstrip()
|
|
|
|
|
|
def strip_managed(chunk):
|
|
"""Drop kit-owned keys/tables so only machine-specific settings remain."""
|
|
filtered_lines = []
|
|
current_section = None
|
|
|
|
for line in chunk.splitlines():
|
|
stripped = line.strip()
|
|
if stripped.startswith("[") and stripped.endswith("]"):
|
|
current_section = stripped[1:-1].strip()
|
|
if current_section in managed_sections:
|
|
continue
|
|
filtered_lines.append(line)
|
|
continue
|
|
|
|
if current_section in managed_sections:
|
|
continue
|
|
|
|
if "=" in line and not line.lstrip().startswith("#"):
|
|
key = line.split("=", 1)[0].strip()
|
|
if current_section is None and key in managed_root_keys:
|
|
continue
|
|
|
|
filtered_lines.append(line)
|
|
|
|
return "\n".join(filtered_lines).strip()
|
|
|
|
|
|
def split_root(chunk):
|
|
"""Separate bare root keys from [table] sections, preserving order within each."""
|
|
root, tables, seen_table = [], [], False
|
|
for line in chunk.splitlines():
|
|
stripped = line.strip()
|
|
if stripped.startswith("[") and stripped.endswith("]"):
|
|
seen_table = True
|
|
(tables if seen_table else root).append(line)
|
|
return "\n".join(root).strip(), "\n".join(tables).strip()
|
|
|
|
|
|
# Settings may sit on either side of the managed block -- notably `codex mcp add`
|
|
# appends [mcp_servers.*] to the end of the file. Keep both sides; rewriting only
|
|
# what precedes the block silently discards everything after it.
|
|
#
|
|
# The block's *interior* is filtered too, never dropped. Where an older kit let the
|
|
# end marker drift below content Codex had written, real [plugins.*]/[projects.*]
|
|
# sections ended up inside the block; discarding it wholesale would delete them.
|
|
# Filtering keeps whatever the kit does not own and heals the layout in one run.
|
|
if managed_start in text and managed_end in text:
|
|
before, rest = text.split(managed_start, 1)
|
|
interior, after = rest.split(managed_end, 1)
|
|
else:
|
|
before = text
|
|
interior = ""
|
|
after = ""
|
|
|
|
preserved = "\n\n".join(
|
|
part
|
|
for part in (strip_managed(before), strip_managed(interior), strip_managed(after))
|
|
if part
|
|
)
|
|
preserved_root, preserved_tables = split_root(preserved)
|
|
|
|
block = f"{managed_start}\n{managed}\n{managed_end}\n"
|
|
|
|
# Bare root keys must precede every [table] header, or TOML binds them to the table
|
|
# above them rather than to the document root -- appending the block last quietly
|
|
# filed model/web_search under whichever table happened to come before it.
|
|
parts = [
|
|
part for part in (preserved_root, block.rstrip(), preserved_tables) if part
|
|
]
|
|
config_path.write_text("\n\n".join(parts) + "\n", encoding="utf-8")
|
|
PY
|
|
|
|
# Profile files. Codex 0.144+ reads per-profile settings from <name>.config.toml
|
|
# next to config.toml rather than from [profiles.<name>] tables. These files are
|
|
# wholly kit-managed, so they are replaced outright (after backup).
|
|
for profile in economy balanced deep; do
|
|
profile_src="$REPO_ROOT/templates/global/$profile.config.toml"
|
|
profile_dest="$CODEX_HOME/$profile.config.toml"
|
|
if [[ -f "$profile_dest" ]]; then
|
|
cp "$profile_dest" "$BACKUP_DIR/$profile.config.toml.bak"
|
|
fi
|
|
cp "$profile_src" "$profile_dest"
|
|
done
|
|
|
|
mkdir -p "$CODEX_HOME/memories/portable-kit"
|
|
cp "$REPO_ROOT"/memory/*.md "$CODEX_HOME/memories/portable-kit/"
|
|
|
|
echo "Installed Codex kit into: $CODEX_HOME"
|
|
echo "Backup created at: $BACKUP_DIR"
|
|
echo
|
|
echo "Next steps:"
|
|
echo "- Defaults match the balanced profile; plain 'codex' needs no flag."
|
|
echo "- Use 'codex --profile economy' for lighter work and '--profile deep' for hard tasks."
|
|
echo "- Run bash scripts/audit_codex_home.sh periodically."
|