Skip to content

Commit 2d3bc01

Browse files
authored
feat: enable the nddev-builder in the base config for default-on codex (#17)
install-builder now writes the canonical nddev-builder marketplace and plugin-enable block into the managed config.toml base (a co-owned addition after the setup base), so a plain codex launch loads the builder by default instead of only through --profile nddev-builder. Codex 0.134.0+ removed the default-profile selector, so a profile-only delivery could not be default-on. The isolated nddev-builder.config.toml profile is still written for explicit --profile selection, and drift detection tolerates the enable as a runtime-style addition while keeping the setup base intact. A setup apply/switch that rewrites config.toml to the pure base drops the enable while the cache and profile persist; install-builder then restores it idempotently without re-materializing the cache or invoking the official Codex plugin commands. builder-status reports config_enabled and an installed-but-not-enabled target now reports incomplete. Claude-Session: https://claude.ai/code/session_01Y29c9sJRhChvxDcUjab5Xo
1 parent 95068cf commit 2d3bc01

8 files changed

Lines changed: 154 additions & 25 deletions

File tree

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,24 @@
22

33
All notable changes to this project are documented here.
44

5+
## [0.3.7] - 2026-07-19
6+
7+
### Changed
8+
9+
- `install-builder` now enables the nddev-builder marketplace and plugin in the
10+
managed `config.toml` base -- a co-owned addition after the setup base -- so a
11+
plain `codex` launch loads the builder by default instead of only through
12+
`--profile nddev-builder`. The isolated `nddev-builder.config.toml` profile is
13+
still written for explicit `--profile` selection, and drift detection tolerates
14+
the enable as a runtime-style addition while keeping the setup base intact.
15+
- When a setup `apply` or `switch` rewrites `config.toml` to the pure setup base,
16+
the co-owned builder enable is dropped while the cache and profile persist.
17+
`install-builder` then restores the base-config enable idempotently without
18+
re-materializing the cache or invoking the official Codex plugin commands.
19+
- `builder-status` reports `config_enabled`; a target whose cache and profile are
20+
current but whose base-config enable is absent now reports `incomplete` rather
21+
than `installed`.
22+
523
## [0.3.6] - 2026-07-18
624

725
### Changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# NDDev Codex Setup Manager
22

33
`nddev-codex-app` is a dependency-free manager for a caller-selected Codex
4-
home. Version `0.3.6` installs the exact tested official Codex CLI standalone
4+
home. Version `0.3.7` installs the exact tested official Codex CLI standalone
55
release into that target and switches one of two complete NDDev configuration
66
sets without deleting unrelated target state. The repository also publishes
77
the independently installable `nddev-builder` Codex marketplace.

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.3.6
1+
0.3.7

build/manifest.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"location": "sibling .<target-name>.nddev-codex-backups",
66
"slots": 10
77
},
8-
"build_version": "0.3.6",
8+
"build_version": "0.3.7",
99
"command_policy": {
1010
"json_supported": [
1111
"list",
@@ -82,7 +82,8 @@
8282
"profile_selection": "--profile nddev-builder",
8383
"install_command": "python3 cli-tools/nddev_codex.py install-builder --target <absolute-target> [--json]",
8484
"status_command": "python3 cli-tools/nddev_codex.py builder-status --target <absolute-target> [--json]",
85-
"primary_config_restored_after_install": true,
85+
"builder_enabled_in_base_config": true,
86+
"setup_base_preserved_after_install": true,
8687
"runtime_owned_cache": true,
8788
"cache_validation": "bounded-exact-source-tree"
8889
},

build/release-evidence.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22
"schema_version": 2,
33
"module": {
44
"repository": "NDDev-it-com/nddev-codex-app",
5-
"setup_digest": "sha256:1116e54640131f5f56a4290bccbc33b1af236536f68d89824c4ff7fd24be24a3"
5+
"setup_digest": "sha256:3863c7a301d3438632e4574df7dd78f8e22e73f696a1986a509dedaf69fd7490"
66
},
77
"harness": {
88
"repository": "NDDev-it-com/nddev-harnesses",
9-
"commit": "091cdabb5c28f7adeeab563564379b0b69f88075"
9+
"commit": "83c4d1bee7cebf40314d09a53b97efaaea972d63"
1010
},
1111
"adapter": {
1212
"id": "codex",
13-
"version": "0.3.6"
13+
"version": "0.3.7"
1414
},
1515
"vendor": {
1616
"cli_version": "0.144.6",
@@ -29,8 +29,8 @@
2929
}
3030
],
3131
"lanes": [],
32-
"generated_at_utc": "2026-07-18T15:04:16Z",
33-
"expires_at_utc": "2027-01-14T15:04:16Z",
32+
"generated_at_utc": "2026-07-19T00:29:22Z",
33+
"expires_at_utc": "2027-01-15T00:29:22Z",
3434
"promotion": {
3535
"decision": "pending",
3636
"waivers": []

build/version.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"build_version": "0.3.6",
2+
"build_version": "0.3.7",
33
"codex_permission_profiles_since": "0.138.0",
44
"codex_cli_tested": "0.144.6",
55
"nddev_builder_plugin_version": "0.3.2",

cli-tools/nddev_codex.py

Lines changed: 123 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
28202857
def 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

29973034
def 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+
39224026
def 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
)

config/nddev-contract.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@
4040
"cache_owner": "official-codex-plugin-lifecycle",
4141
"cache_validation": "bounded-exact-source-tree",
4242
"install_command": "python3 cli-tools/nddev_codex.py install-builder --target <absolute-target> [--json]",
43-
"manager_restores_primary_config": true,
43+
"builder_enabled_in_base_config": true,
44+
"setup_base_preserved_after_install": true,
4445
"marketplace_add": "<absolute-target>/bin/codex plugin marketplace add <absolute-repository-root> --json",
4546
"orthogonal_to_setups": true,
4647
"plugin_add": "<absolute-target>/bin/codex plugin add nddev-builder@nddev-builder --json",

0 commit comments

Comments
 (0)