@@ -2817,6 +2817,43 @@ def builder_profile_bytes() -> bytes:
28172817 ).encode ("utf-8" )
28182818
28192819
2820+ def builder_config_enabled (config : bytes , block : bytes ) -> bool :
2821+ """Return True when the canonical builder block is present verbatim in the
2822+ config.toml bytes. The block is written contiguously and the Codex runtime
2823+ only appends after it, so a substring test is precise and tolerant of the
2824+ runtime's later ``[projects.*]`` additions."""
2825+ return block in config
2826+
2827+
2828+ def config_with_builder_block (config : bytes , block : bytes ) -> bytes :
2829+ """Return config guaranteed to contain the canonical builder block exactly
2830+ once, appended as a co-owned addition after the managed setup base.
2831+ Idempotent: if the block is already present the config is returned unchanged,
2832+ so a repeated enable never duplicates it."""
2833+ if builder_config_enabled (config , block ):
2834+ return config
2835+ if not config .endswith (b"\n " ):
2836+ config += b"\n "
2837+ return config + b"\n " + block
2838+
2839+
2840+ def _builder_enabled_on_disk (target : Path , block : bytes ) -> bool :
2841+ """Read the target's config.toml and report whether the canonical builder
2842+ block is enabled in it. Any read failure is treated as not-enabled
2843+ (conservative: it triggers a re-enable rather than masking a missing block)."""
2844+ try :
2845+ content , _ = read_target_file (
2846+ target ,
2847+ "config.toml" ,
2848+ f"managed path { target / 'config.toml' } " ,
2849+ owner_only = False ,
2850+ max_bytes = MANAGED_PAYLOAD_MAX_BYTES ,
2851+ )
2852+ except (CodexSetupError , OSError , ValueError ):
2853+ return False
2854+ return builder_config_enabled (content , block )
2855+
2856+
28202857def validate_builder_mutated_config (content : bytes , original : bytes ) -> None :
28212858 if not content .startswith (original ) or not original .endswith (b"\n " ):
28222859 fail ("official Codex plugin commands replaced the managed setup configuration" )
@@ -2996,15 +3033,18 @@ def inspect_builder_profile(target: Path, expected: bytes) -> str:
29963033
29973034def builder_status (target : Path ) -> dict [str , Any ]:
29983035 plugin_version , source_manifest = builder_source_contract ()
3036+ expected_block = builder_profile_bytes ()
29993037 profile_state = "missing"
30003038 cache_state = "missing"
3039+ config_enabled = False
30013040 if ensure_target_directory (target , create = False ):
3002- profile_state = inspect_builder_profile (target , builder_profile_bytes () )
3041+ profile_state = inspect_builder_profile (target , expected_block )
30033042 cache_state = inspect_builder_cache (target , plugin_version , source_manifest )
3043+ config_enabled = _builder_enabled_on_disk (target , expected_block )
30043044 installation = inspect_software_installation (target )
3005- installed = profile_state == "current" and cache_state == "current"
3045+ installed = config_enabled and profile_state == "current" and cache_state == "current"
30063046 state = "installed" if installed else "missing"
3007- if not installed and (profile_state != "missing" or cache_state != "missing" ):
3047+ if not installed and (config_enabled or profile_state != "missing" or cache_state != "missing" ):
30083048 state = "incomplete"
30093049 return {
30103050 "schema_version" : 1 ,
@@ -3018,6 +3058,7 @@ def builder_status(target: Path) -> dict[str, Any]:
30183058 "profile" : BUILDER_PROFILE_NAME ,
30193059 "profile_state" : profile_state ,
30203060 "cache_state" : cache_state ,
3061+ "config_enabled" : config_enabled ,
30213062 "plugin_version" : plugin_version ,
30223063 "codex_version" : installation .version if installation is not None else None ,
30233064 }
@@ -3919,6 +3960,69 @@ def restore_builder_files(
39193960 fail (f"builder transaction rollback content mismatch for { target / name } " )
39203961
39213962
3963+ def _enable_builder_in_config (
3964+ target : Path ,
3965+ guard : TargetGuard ,
3966+ expected_profile : bytes ,
3967+ plugin_version : str ,
3968+ ) -> dict [str , Any ]:
3969+ """Add the canonical builder block to config.toml when the cache and profile
3970+ are already current but the base-config enable is absent -- e.g. a setup
3971+ apply or switch rewrote config.toml to the pure setup base. This restores the
3972+ default-on builder for a plain ``codex`` launch without re-materializing the
3973+ cache or invoking the official Codex plugin commands."""
3974+ original_snapshots , original_contents = capture_builder_files (target )
3975+ original_config = original_contents ["config.toml" ]
3976+ if original_config is None :
3977+ fail ("managed config.toml disappeared from the builder transaction snapshot" )
3978+ guard .expected_managed = {
3979+ name : original_snapshots [name ] for name in (* MANAGED_FILES , STAMP_NAME )
3980+ }
3981+ guard .mutated_paths .clear ()
3982+ guard .manager_results .clear ()
3983+ try :
3984+ current_config = snapshot_target_file (target , "config.toml" , owner_only = False )
3985+ desired : dict [str , bytes | None ] = {
3986+ "config.toml" : config_with_builder_block (original_config , expected_profile ),
3987+ }
3988+ replace_managed_state (
3989+ target ,
3990+ desired ,
3991+ {"config.toml" : current_config },
3992+ names = ("config.toml" ,),
3993+ )
3994+ require_effective_clean_managed (target )
3995+ require_current_software (target )
3996+ if not _builder_enabled_on_disk (target , expected_profile ):
3997+ fail ("nddev-builder base-config enable postcondition failed" )
3998+ assert_builder_paths_unchanged (
3999+ target ,
4000+ original_snapshots ,
4001+ ("AGENTS.md" , STAMP_NAME , BUILDER_PROFILE_NAME ),
4002+ )
4003+ except BaseException as operation_error :
4004+ try :
4005+ restore_builder_files (target , original_snapshots , original_contents )
4006+ require_effective_clean_managed (target )
4007+ except BaseException as rollback_error :
4008+ raise CodexSetupError (
4009+ "install-builder failed and configuration rollback also failed: "
4010+ f"{ type (operation_error ).__name__ } : { operation_error } "
4011+ ) from rollback_error
4012+ raise
4013+ finally :
4014+ guard .mutated_paths .clear ()
4015+ guard .manager_results .clear ()
4016+ return {
4017+ "schema_version" : 1 ,
4018+ "command" : "install-builder" ,
4019+ "target" : str (target ),
4020+ "changed" : True ,
4021+ "plugin_version" : plugin_version ,
4022+ "profile" : BUILDER_PROFILE_NAME ,
4023+ }
4024+
4025+
39224026def install_builder (target : Path ) -> dict [str , Any ]:
39234027 plugin_version , source_manifest = builder_source_contract ()
39244028 expected_profile = builder_profile_bytes ()
@@ -3932,15 +4036,17 @@ def install_builder(target: Path) -> dict[str, Any]:
39324036 if cache_state == "drifted" :
39334037 fail ("current nddev-builder plugin cache manifest is invalid" )
39344038 if profile_state == "current" and cache_state == "current" :
3935- revalidate_guard (guard , allow_missing = False )
3936- return {
3937- "schema_version" : 1 ,
3938- "command" : "install-builder" ,
3939- "target" : str (target ),
3940- "changed" : False ,
3941- "plugin_version" : plugin_version ,
3942- "profile" : BUILDER_PROFILE_NAME ,
3943- }
4039+ if _builder_enabled_on_disk (target , expected_profile ):
4040+ revalidate_guard (guard , allow_missing = False )
4041+ return {
4042+ "schema_version" : 1 ,
4043+ "command" : "install-builder" ,
4044+ "target" : str (target ),
4045+ "changed" : False ,
4046+ "plugin_version" : plugin_version ,
4047+ "profile" : BUILDER_PROFILE_NAME ,
4048+ }
4049+ return _enable_builder_in_config (target , guard , expected_profile , plugin_version )
39444050
39454051 original_snapshots , original_contents = capture_builder_files (target )
39464052 original_cache = capture_builder_cache_transaction (target , plugin_version )
@@ -3990,8 +4096,8 @@ def install_builder(target: Path) -> dict[str, Any]:
39904096
39914097 current_config = snapshot_target_file (target , "config.toml" , owner_only = False )
39924098 current_profile = snapshot_target_file (target , BUILDER_PROFILE_NAME , owner_only = False )
3993- desired = {
3994- "config.toml" : original_config ,
4099+ desired : dict [ str , bytes | None ] = {
4100+ "config.toml" : config_with_builder_block ( original_config , expected_profile ) ,
39954101 BUILDER_PROFILE_NAME : expected_profile ,
39964102 }
39974103 expected = {
@@ -4010,6 +4116,8 @@ def install_builder(target: Path) -> dict[str, Any]:
40104116 fail ("nddev-builder profile installation postcondition failed" )
40114117 if inspect_builder_cache (target , plugin_version , source_manifest ) != "current" :
40124118 fail ("nddev-builder cache installation postcondition failed" )
4119+ if not _builder_enabled_on_disk (target , expected_profile ):
4120+ fail ("nddev-builder base-config enable postcondition failed" )
40134121 assert_builder_paths_unchanged (
40144122 target ,
40154123 original_snapshots ,
@@ -4157,6 +4265,7 @@ def human_output(value: dict[str, Any]) -> str:
41574265 if command == "builder-status" :
41584266 return (
41594267 f"{ value ['state' ]} : { value ['target' ]} ; "
4268+ f"enabled={ value ['config_enabled' ]} ; "
41604269 f"profile={ value ['profile_state' ]} ; cache={ value ['cache_state' ]} ; "
41614270 f"plugin={ value ['plugin_version' ]} "
41624271 )
0 commit comments