Repository navigation
toolprune: protect Codex packages, and recheck protection when a list is used - #188
Conversation
… is used On Codex, tool pruning works on capability packages, so naming a tool in `toolprune_protect` cannot keep one: a person who set `toolprune_protect=collaboration` still had sub-agents pruned. The Codex fold now leaves the packages the list names out of its proposal, and gives back one applied before it was protected, as the generic fold does for tools: only the plugin's own lines, never in a config the repository commits, and an unreadable config leaves the fold to finish. A pending `auto` list and the proposal `toolprune apply` reads are checked against the protection in force when they are used, and protected entries leave the stored proposal. Copilot lists name MCP servers and are left as they are. Partly addresses microsoft#48 (its Copilot case, a server whose tool became protected, is not covered). Five tests in tests/test_live_hooks.py; four fail on main.
Muhammad Abdullah Butt (1AbdullahButt)
left a comment
There was a problem hiding this comment.
I read the Codex fold, unprotected, and the two places it is now applied (apply_pending and cli._proposal) against main. I checked that _Protected.__contains__ matches patterns with fnmatchcase, so collab* works on package names the same way it does on tools. I also checked that no package name collides with PROTECTED_TOOLS / INTERACTIVE_PROTECTED, so unprotected can't drop a package by accident. And run_session in these tests calls hook.main in-process, so the monkeypatch of _git_tracked / restore really does reach the fold. The behaviour looks right to me.
One small thing about consistency: the generic fold gives a tool back only when not imported (tokeneconomizer/plugin/fold.py:155), and its docstring says a session read from history "changes nothing else: no deny rule is held, written or given back". _fold_toolprune doesn't pass imported to _fold_toolprune_codex (fold.py:128). Until now that didn't matter, because the Codex path never edited a file during the fold. With the new give-back, a Codex history import would be the first thing to change .codex/config.toml from an imported session. Only Claude Code has keeps_history = True today, so this can't happen yet. Passing imported through and adding it to the guard at fold.py:292 would keep the rule true if Codex history import arrives later:
if (not imported and any(c in protected for c in applied) and not ctx.config.shadow
and not denylist._git_tracked(ctx.repo.root, denylist.CODEX_CONFIG_REL)):Not a blocker.
Reviewed with help from Claude Code.
Record logical_capabilities before a newly protected package is given back, so the session that ran with it switched off is not counted as loading it.
Partly addresses #48.
On Codex, tool pruning works on capability packages (
collaboration,goals,image_media), not on single tools. The Codex fold never readtoolprune_protect. A person who settoolprune_protect=collaborationto keep sub-agents still had them pruned once the package had gone unused long enough. #48 suggested leaving Codex as it is because the setting names tools. But on Codex a package is the smallest thing that can be pruned, so naming the package is the only way to keep one.What changed
.codex/config.tomlthe repository commits (applyrefuses those too). An unreadable config is left as it is and the fold finishes.fold.unprotected(ctx, items)drops what is protected now.apply_pendingandcli._proposal(defaulttoolprune apply,show,measure) both call it, so a pendingautolist or a stored proposal formed before a protection was added no longer writes that tool.apply_pendingalso removes such entries from the stored proposal, which Pending automatic deny lists ignore newly configured tool protections #48 asked for.sql, so they are left as they are. Pending automatic deny lists ignore newly configured tool protections #48's Copilot case (dropping a server once one of its catalogued tools is protected) is not covered here.toolprune_protectrow inplugin/README.mdnames the Codex packages.Tests
tests/test_live_hooks.py:test_codex_never_proposes_a_protected_capability: thirty prompts, soimage_mediacrosses its evidence threshold too. An unprotected control proposes all three packages; withcollab*,image_mediaprotected, the next fold proposes onlygoals.test_a_codex_package_applied_before_it_was_protected_comes_backtest_a_protected_codex_package_in_a_committed_or_unreadable_config_staystest_a_tool_protected_after_the_list_formed_is_not_written: the pending list,_proposaland the stored proposal.test_copilot_server_lists_are_not_filtered_as_tool_namesAll but the committed-or-unreadable test fail on
main. That one guards the new give-back path, sincemaingives nothing back.python -m pytest testsgives 855 passed and 16 skipped on Python 3.10 and 3.13 (macOS), andexperiments/testsgives 394 passed.