Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,24 @@

All notable changes to this project are documented here.

## [0.3.7] - 2026-07-19

### Changed

- `install-builder` now enables the nddev-builder marketplace and plugin in 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`. 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.
- When a setup `apply` or `switch` rewrites `config.toml` to the pure setup base,
the co-owned builder enable is dropped while the cache and profile persist.
`install-builder` then restores the base-config enable idempotently without
re-materializing the cache or invoking the official Codex plugin commands.
- `builder-status` reports `config_enabled`; a target whose cache and profile are
current but whose base-config enable is absent now reports `incomplete` rather
than `installed`.

## [0.3.6] - 2026-07-18

### Changed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# NDDev Codex Setup Manager

`nddev-codex-app` is a dependency-free manager for a caller-selected Codex
home. Version `0.3.6` installs the exact tested official Codex CLI standalone
home. Version `0.3.7` installs the exact tested official Codex CLI standalone
release into that target and switches one of two complete NDDev configuration
sets without deleting unrelated target state. The repository also publishes
the independently installable `nddev-builder` Codex marketplace.
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.3.6
0.3.7
5 changes: 3 additions & 2 deletions build/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"location": "sibling .<target-name>.nddev-codex-backups",
"slots": 10
},
"build_version": "0.3.6",
"build_version": "0.3.7",
"command_policy": {
"json_supported": [
"list",
Expand Down Expand Up @@ -82,7 +82,8 @@
"profile_selection": "--profile nddev-builder",
"install_command": "python3 cli-tools/nddev_codex.py install-builder --target <absolute-target> [--json]",
"status_command": "python3 cli-tools/nddev_codex.py builder-status --target <absolute-target> [--json]",
"primary_config_restored_after_install": true,
"builder_enabled_in_base_config": true,
"setup_base_preserved_after_install": true,
"runtime_owned_cache": true,
"cache_validation": "bounded-exact-source-tree"
},
Expand Down
10 changes: 5 additions & 5 deletions build/release-evidence.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
"schema_version": 2,
"module": {
"repository": "NDDev-it-com/nddev-codex-app",
"setup_digest": "sha256:1116e54640131f5f56a4290bccbc33b1af236536f68d89824c4ff7fd24be24a3"
"setup_digest": "sha256:3863c7a301d3438632e4574df7dd78f8e22e73f696a1986a509dedaf69fd7490"
},
"harness": {
"repository": "NDDev-it-com/nddev-harnesses",
"commit": "091cdabb5c28f7adeeab563564379b0b69f88075"
"commit": "83c4d1bee7cebf40314d09a53b97efaaea972d63"
},
"adapter": {
"id": "codex",
"version": "0.3.6"
"version": "0.3.7"
},
"vendor": {
"cli_version": "0.144.6",
Expand All @@ -29,8 +29,8 @@
}
],
"lanes": [],
"generated_at_utc": "2026-07-18T15:04:16Z",
"expires_at_utc": "2027-01-14T15:04:16Z",
"generated_at_utc": "2026-07-19T00:29:22Z",
"expires_at_utc": "2027-01-15T00:29:22Z",
"promotion": {
"decision": "pending",
"waivers": []
Expand Down
2 changes: 1 addition & 1 deletion build/version.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"build_version": "0.3.6",
"build_version": "0.3.7",
"codex_permission_profiles_since": "0.138.0",
"codex_cli_tested": "0.144.6",
"nddev_builder_plugin_version": "0.3.2",
Expand Down
137 changes: 123 additions & 14 deletions cli-tools/nddev_codex.py
Original file line number Diff line number Diff line change
Expand Up @@ -2817,6 +2817,43 @@ def builder_profile_bytes() -> bytes:
).encode("utf-8")


def builder_config_enabled(config: bytes, block: bytes) -> bool:
"""Return True when the canonical builder block is present verbatim in the
config.toml bytes. The block is written contiguously and the Codex runtime
only appends after it, so a substring test is precise and tolerant of the
runtime's later ``[projects.*]`` additions."""
return block in config


def config_with_builder_block(config: bytes, block: bytes) -> bytes:
"""Return config guaranteed to contain the canonical builder block exactly
once, appended as a co-owned addition after the managed setup base.
Idempotent: if the block is already present the config is returned unchanged,
so a repeated enable never duplicates it."""
if builder_config_enabled(config, block):
return config
if not config.endswith(b"\n"):
config += b"\n"
return config + b"\n" + block


def _builder_enabled_on_disk(target: Path, block: bytes) -> bool:
"""Read the target's config.toml and report whether the canonical builder
block is enabled in it. Any read failure is treated as not-enabled
(conservative: it triggers a re-enable rather than masking a missing block)."""
try:
content, _ = read_target_file(
target,
"config.toml",
f"managed path {target / 'config.toml'}",
owner_only=False,
max_bytes=MANAGED_PAYLOAD_MAX_BYTES,
)
except (CodexSetupError, OSError, ValueError):
return False
return builder_config_enabled(content, block)


def validate_builder_mutated_config(content: bytes, original: bytes) -> None:
if not content.startswith(original) or not original.endswith(b"\n"):
fail("official Codex plugin commands replaced the managed setup configuration")
Expand Down Expand Up @@ -2996,15 +3033,18 @@ def inspect_builder_profile(target: Path, expected: bytes) -> str:

def builder_status(target: Path) -> dict[str, Any]:
plugin_version, source_manifest = builder_source_contract()
expected_block = builder_profile_bytes()
profile_state = "missing"
cache_state = "missing"
config_enabled = False
if ensure_target_directory(target, create=False):
profile_state = inspect_builder_profile(target, builder_profile_bytes())
profile_state = inspect_builder_profile(target, expected_block)
cache_state = inspect_builder_cache(target, plugin_version, source_manifest)
config_enabled = _builder_enabled_on_disk(target, expected_block)
installation = inspect_software_installation(target)
installed = profile_state == "current" and cache_state == "current"
installed = config_enabled and profile_state == "current" and cache_state == "current"
state = "installed" if installed else "missing"
if not installed and (profile_state != "missing" or cache_state != "missing"):
if not installed and (config_enabled or profile_state != "missing" or cache_state != "missing"):
state = "incomplete"
return {
"schema_version": 1,
Expand All @@ -3018,6 +3058,7 @@ def builder_status(target: Path) -> dict[str, Any]:
"profile": BUILDER_PROFILE_NAME,
"profile_state": profile_state,
"cache_state": cache_state,
"config_enabled": config_enabled,
"plugin_version": plugin_version,
"codex_version": installation.version if installation is not None else None,
}
Expand Down Expand Up @@ -3919,6 +3960,69 @@ def restore_builder_files(
fail(f"builder transaction rollback content mismatch for {target / name}")


def _enable_builder_in_config(
target: Path,
guard: TargetGuard,
expected_profile: bytes,
plugin_version: str,
) -> dict[str, Any]:
"""Add the canonical builder block to config.toml when the cache and profile
are already current but the base-config enable is absent -- e.g. a setup
apply or switch rewrote config.toml to the pure setup base. This restores the
default-on builder for a plain ``codex`` launch without re-materializing the
cache or invoking the official Codex plugin commands."""
original_snapshots, original_contents = capture_builder_files(target)
original_config = original_contents["config.toml"]
if original_config is None:
fail("managed config.toml disappeared from the builder transaction snapshot")
guard.expected_managed = {
name: original_snapshots[name] for name in (*MANAGED_FILES, STAMP_NAME)
}
guard.mutated_paths.clear()
guard.manager_results.clear()
try:
current_config = snapshot_target_file(target, "config.toml", owner_only=False)
desired: dict[str, bytes | None] = {
"config.toml": config_with_builder_block(original_config, expected_profile),
}
replace_managed_state(
target,
desired,
{"config.toml": current_config},
names=("config.toml",),
)
require_effective_clean_managed(target)
require_current_software(target)
if not _builder_enabled_on_disk(target, expected_profile):
fail("nddev-builder base-config enable postcondition failed")
assert_builder_paths_unchanged(
target,
original_snapshots,
("AGENTS.md", STAMP_NAME, BUILDER_PROFILE_NAME),
)
except BaseException as operation_error:
try:
restore_builder_files(target, original_snapshots, original_contents)
require_effective_clean_managed(target)
except BaseException as rollback_error:
raise CodexSetupError(
"install-builder failed and configuration rollback also failed: "
f"{type(operation_error).__name__}: {operation_error}"
) from rollback_error
raise
finally:
guard.mutated_paths.clear()
guard.manager_results.clear()
return {
"schema_version": 1,
"command": "install-builder",
"target": str(target),
"changed": True,
"plugin_version": plugin_version,
"profile": BUILDER_PROFILE_NAME,
}


def install_builder(target: Path) -> dict[str, Any]:
plugin_version, source_manifest = builder_source_contract()
expected_profile = builder_profile_bytes()
Expand All @@ -3932,15 +4036,17 @@ def install_builder(target: Path) -> dict[str, Any]:
if cache_state == "drifted":
fail("current nddev-builder plugin cache manifest is invalid")
if profile_state == "current" and cache_state == "current":
revalidate_guard(guard, allow_missing=False)
return {
"schema_version": 1,
"command": "install-builder",
"target": str(target),
"changed": False,
"plugin_version": plugin_version,
"profile": BUILDER_PROFILE_NAME,
}
if _builder_enabled_on_disk(target, expected_profile):
revalidate_guard(guard, allow_missing=False)
return {
"schema_version": 1,
"command": "install-builder",
"target": str(target),
"changed": False,
"plugin_version": plugin_version,
"profile": BUILDER_PROFILE_NAME,
}
return _enable_builder_in_config(target, guard, expected_profile, plugin_version)

original_snapshots, original_contents = capture_builder_files(target)
original_cache = capture_builder_cache_transaction(target, plugin_version)
Expand Down Expand Up @@ -3990,8 +4096,8 @@ def install_builder(target: Path) -> dict[str, Any]:

current_config = snapshot_target_file(target, "config.toml", owner_only=False)
current_profile = snapshot_target_file(target, BUILDER_PROFILE_NAME, owner_only=False)
desired = {
"config.toml": original_config,
desired: dict[str, bytes | None] = {
"config.toml": config_with_builder_block(original_config, expected_profile),
BUILDER_PROFILE_NAME: expected_profile,
}
expected = {
Expand All @@ -4010,6 +4116,8 @@ def install_builder(target: Path) -> dict[str, Any]:
fail("nddev-builder profile installation postcondition failed")
if inspect_builder_cache(target, plugin_version, source_manifest) != "current":
fail("nddev-builder cache installation postcondition failed")
if not _builder_enabled_on_disk(target, expected_profile):
fail("nddev-builder base-config enable postcondition failed")
assert_builder_paths_unchanged(
target,
original_snapshots,
Expand Down Expand Up @@ -4157,6 +4265,7 @@ def human_output(value: dict[str, Any]) -> str:
if command == "builder-status":
return (
f"{value['state']}: {value['target']}; "
f"enabled={value['config_enabled']}; "
f"profile={value['profile_state']}; cache={value['cache_state']}; "
f"plugin={value['plugin_version']}"
)
Expand Down
3 changes: 2 additions & 1 deletion config/nddev-contract.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"cache_owner": "official-codex-plugin-lifecycle",
"cache_validation": "bounded-exact-source-tree",
"install_command": "python3 cli-tools/nddev_codex.py install-builder --target <absolute-target> [--json]",
"manager_restores_primary_config": true,
"builder_enabled_in_base_config": true,
"setup_base_preserved_after_install": true,
"marketplace_add": "<absolute-target>/bin/codex plugin marketplace add <absolute-repository-root> --json",
"orthogonal_to_setups": true,
"plugin_add": "<absolute-target>/bin/codex plugin add nddev-builder@nddev-builder --json",
Expand Down