From dc735c01fbbb4eddbbb73ff4df6007ab68413a4d Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Sat, 26 Sep 2026 01:07:11 +0200 Subject: [PATCH] fix(install): migrate v0.10.8 OpenCode agent profiles on upgrade (#2264) Upgrading from v0.10.8 to v0.11.0 on a machine with OpenCode reported all three OpenCode subagent profiles as "preserved modified profile" plus an op=agent_install error each, and the activation stopped ("one or more agent configurations failed; the published/current executable was kept"), although nobody had edited those files. Root cause: #1933 (v0.11.0) added the tool_search / tool_search_regex permissions to the rendered OpenCode profile. Install and uninstall recognise a profile as ours only when it matches the current rendering or one of the known released renderings (the other access mode, the v0.9.1-rc.1 Codex shape, the pre-tier Verify file). The v0.10.8 OpenCode shape was not in that set, so cbm_text_migrate_owned_document classified the untouched v0.10.8 files as user-modified. Reproduced end to end: install with the v0.10.8 release binary, then install with v0.11.0/main, into an isolated HOME; the three profiles are 1729/1880/1910 bytes, the same sizes as in the report. cbm_render_graph_profile_opencode_v0108() renders the pre-#1933 shape (both access modes), and install/uninstall now list it as a released rendering, so those files are upgraded in place and uninstall removes them as owned. A profile the user actually edited is still preserved. The released-list assembly that install and uninstall duplicated moves into one helper, and the two legacy renderers share one implementation (cli.c drops three raw free sites; the memory-core baseline is tightened 173 -> 170). The Codex and OpenCode op=mcp_install errors in the same report do not reproduce from a pristine v0.10.8 config and depend on the reporter's own config.toml / opencode.json; they are not addressed here. Signed-off-by: Martin Vogel --- scripts/memory-core-baseline.txt | 2 +- src/cli/agent_profiles.c | 50 ++++++++++----- src/cli/agent_profiles.h | 4 ++ src/cli/cli.c | 106 +++++++++++++++++-------------- tests/test_cli.c | 104 ++++++++++++++++++++++++++++++ 5 files changed, 204 insertions(+), 62 deletions(-) diff --git a/scripts/memory-core-baseline.txt b/scripts/memory-core-baseline.txt index fe1cb426c..602fe5958 100644 --- a/scripts/memory-core-baseline.txt +++ b/scripts/memory-core-baseline.txt @@ -17,7 +17,7 @@ internal/cbm/sqlite_writer.c 74 src/cli/activation_transaction.c 80 src/cli/agent_clients.c 73 src/cli/agent_profiles.c 7 -src/cli/cli.c 173 +src/cli/cli.c 170 src/cli/client_adapter.c 3 src/cli/config_edit_path.c 2 src/cli/config_json_like.c 142 diff --git a/src/cli/agent_profiles.c b/src/cli/agent_profiles.c index 7ae55b82e..7e72fc560 100644 --- a/src/cli/agent_profiles.c +++ b/src/cli/agent_profiles.c @@ -498,6 +498,23 @@ static bool append_codex_profile(profile_buffer_t *buffer, cbm_graph_tier_t tier profile_buffer_append(buffer, "]\n"); } +/* before_tool_search reproduces the v0.10.8 rendering: #1933 (v0.11.0) added + * the tool_search / tool_search_regex permissions, and install/uninstall + * must still recognise the older bytes as ours (#2264). */ +static bool append_opencode_profile(profile_buffer_t *buffer, cbm_graph_tier_t tier, + cbm_graph_access_t access, const char *prompt, + bool before_tool_search) { + return profile_buffer_append(buffer, "---\ndescription: ") && + profile_buffer_append(buffer, profile_description(tier, access)) && + profile_buffer_append(buffer, "\nmode: subagent\npermission:\n \"*\": deny\n read: " + "allow\n grep: allow\n glob: allow\n") && + (before_tool_search || + profile_buffer_append(buffer, " tool_search: allow\n tool_search_regex: allow\n")) && + (access != CBM_GRAPH_ACCESS_DIRECT || + append_permission_mcp_tools(buffer, CBM_GRAPH_DIALECT_OPENCODE, tier)) && + profile_buffer_append(buffer, "---\n") && profile_buffer_append(buffer, prompt); +} + static bool render_profile_text(profile_buffer_t *buffer, cbm_graph_profile_dialect_t dialect, cbm_graph_tier_t tier, cbm_graph_access_t access, const char *binary_path, const char *prompt) { @@ -547,17 +564,7 @@ static bool render_profile_text(profile_buffer_t *buffer, cbm_graph_profile_dial } return true; case CBM_GRAPH_DIALECT_OPENCODE: - if (!profile_buffer_append(buffer, "---\ndescription: ") || - !profile_buffer_append(buffer, description) || - !profile_buffer_append( - buffer, "\nmode: subagent\npermission:\n \"*\": deny\n read: allow\n grep: " - "allow\n glob: allow\n tool_search: allow\n tool_search_regex: " - "allow\n") || - (direct && !append_permission_mcp_tools(buffer, dialect, tier)) || - !profile_buffer_append(buffer, "---\n") || !profile_buffer_append(buffer, prompt)) { - return false; - } - return true; + return append_opencode_profile(buffer, tier, access, prompt, false); case CBM_GRAPH_DIALECT_KILO: if (!profile_buffer_append(buffer, "---\ndescription: ") || !profile_buffer_append(buffer, description) || @@ -711,17 +718,22 @@ char *cbm_render_graph_profile(cbm_graph_profile_dialect_t dialect, cbm_graph_ti return profile_buffer_finish(&buffer); } -char *cbm_render_graph_profile_codex_rc1(cbm_graph_tier_t tier) { - if (!tier_valid(tier)) { +/* A released-but-superseded rendering, kept only so install/uninstall can + * recognise and migrate files an older release wrote. */ +static char *render_released_profile(cbm_graph_profile_dialect_t dialect, cbm_graph_tier_t tier, + cbm_graph_access_t access) { + if (!tier_valid(tier) || !access_valid(access)) { return NULL; } - char *prompt = cbm_render_graph_prompt(tier, CBM_GRAPH_ACCESS_DIRECT); + char *prompt = cbm_render_graph_prompt(tier, access); if (!prompt) { return NULL; } profile_buffer_t buffer; profile_buffer_init(&buffer); - bool ok = append_codex_profile(&buffer, tier, CBM_GRAPH_ACCESS_DIRECT, NULL, prompt, true); + bool ok = dialect == CBM_GRAPH_DIALECT_OPENCODE + ? append_opencode_profile(&buffer, tier, access, prompt, true) + : append_codex_profile(&buffer, tier, access, NULL, prompt, true); free(prompt); if (!ok) { profile_buffer_discard(&buffer); @@ -729,3 +741,11 @@ char *cbm_render_graph_profile_codex_rc1(cbm_graph_tier_t tier) { } return profile_buffer_finish(&buffer); } + +char *cbm_render_graph_profile_opencode_v0108(cbm_graph_tier_t tier, cbm_graph_access_t access) { + return render_released_profile(CBM_GRAPH_DIALECT_OPENCODE, tier, access); +} + +char *cbm_render_graph_profile_codex_rc1(cbm_graph_tier_t tier) { + return render_released_profile(CBM_GRAPH_DIALECT_CODEX, tier, CBM_GRAPH_ACCESS_DIRECT); +} diff --git a/src/cli/agent_profiles.h b/src/cli/agent_profiles.h index 184b03bb1..aa85c665f 100644 --- a/src/cli/agent_profiles.h +++ b/src/cli/agent_profiles.h @@ -60,6 +60,10 @@ char *cbm_render_graph_profile(cbm_graph_profile_dialect_t dialect, cbm_graph_ti * so install/uninstall can recognize and migrate those files. */ char *cbm_render_graph_profile_codex_rc1(cbm_graph_tier_t tier); +/* v0.10.8 OpenCode rendering (before the tool_search permissions of #1933), + * kept so install/uninstall can recognize and migrate those files (#2264). */ +char *cbm_render_graph_profile_opencode_v0108(cbm_graph_tier_t tier, cbm_graph_access_t access); + /* Vibe stores the behavioral prompt separately from its TOML agent definition. * Other integrations may also use this as the canonical contract text. */ char *cbm_render_graph_prompt(cbm_graph_tier_t tier, cbm_graph_access_t access); diff --git a/src/cli/cli.c b/src/cli/cli.c index b868fdca2..6e33759e4 100644 --- a/src/cli/cli.c +++ b/src/cli/cli.c @@ -8717,6 +8717,54 @@ static cbm_graph_access_t cbm_tiered_profile_set_access(cbm_tiered_profile_set_t : CBM_GRAPH_ACCESS_HANDOFF; } +/* Every earlier rendering of one tier's profile that install/uninstall must + * still recognise as ours: the other access mode, the v0.9.1-rc.1 Codex + * shape, the pre-tier Verify file, and the v0.10.8 OpenCode shape (#2264). */ +typedef struct { + char *owned[4]; + size_t owned_count; + const char *content[5]; + size_t count; +} tiered_released_profiles_t; + +static void tiered_released_profiles_keep(tiered_released_profiles_t *released, char *rendered) { + if (rendered) { + released->owned[released->owned_count++] = rendered; + released->content[released->count++] = rendered; + } +} + +static void tiered_released_profiles_collect(cbm_tiered_profile_set_t profiles, + cbm_graph_tier_t tier, cbm_graph_access_t access, + tiered_released_profiles_t *released) { + memset(released, 0, sizeof(*released)); + cbm_graph_access_t alternate_access = + access == CBM_GRAPH_ACCESS_DIRECT ? CBM_GRAPH_ACCESS_HANDOFF : CBM_GRAPH_ACCESS_DIRECT; + tiered_released_profiles_keep( + released, + cbm_render_graph_profile(profiles.dialect, tier, alternate_access, profiles.binary_path)); + if (profiles.dialect == CBM_GRAPH_DIALECT_CODEX && access == CBM_GRAPH_ACCESS_DIRECT) { + tiered_released_profiles_keep(released, cbm_render_graph_profile_codex_rc1(tier)); + } + if (profiles.dialect == CBM_GRAPH_DIALECT_OPENCODE) { + tiered_released_profiles_keep(released, + cbm_render_graph_profile_opencode_v0108(tier, access)); + tiered_released_profiles_keep( + released, cbm_render_graph_profile_opencode_v0108(tier, alternate_access)); + } + if (tier == CBM_GRAPH_TIER_VERIFY && profiles.legacy_verify_content) { + released->content[released->count++] = profiles.legacy_verify_content; + } +} + +static void tiered_released_profiles_free(tiered_released_profiles_t *released) { + for (size_t i = 0U; i < released->owned_count; i++) { + free(released->owned[i]); + } + released->owned_count = 0U; + released->count = 0U; +} + static void install_tiered_agent_profiles(cbm_tiered_profile_set_t profiles, bool dry_run) { cbm_graph_access_t access = cbm_tiered_profile_set_access(profiles); for (int value = 0; value < (int)CBM_GRAPH_TIER_COUNT; value++) { @@ -8740,30 +8788,13 @@ static void install_tiered_agent_profiles(cbm_tiered_profile_set_t profiles, boo record_agent_config_error(false, profiles.label, "agent_render", path); continue; } - cbm_graph_access_t alternate_access = - access == CBM_GRAPH_ACCESS_DIRECT ? CBM_GRAPH_ACCESS_HANDOFF : CBM_GRAPH_ACCESS_DIRECT; - char *alternate = cbm_render_graph_profile(profiles.dialect, tier, alternate_access, - profiles.binary_path); - char *codex_rc1 = - profiles.dialect == CBM_GRAPH_DIALECT_CODEX && access == CBM_GRAPH_ACCESS_DIRECT - ? cbm_render_graph_profile_codex_rc1(tier) - : NULL; - const char *released[3]; - size_t released_count = 0U; - if (alternate) { - released[released_count++] = alternate; - } - if (codex_rc1) { - released[released_count++] = codex_rc1; - } - if (tier == CBM_GRAPH_TIER_VERIFY && profiles.legacy_verify_content) { - released[released_count++] = profiles.legacy_verify_content; - } - int result = prepare_config_parent(path) - ? cbm_text_migrate_owned_document(path, current, released, released_count) - : CLI_ERR; - free(codex_rc1); - free(alternate); + tiered_released_profiles_t released; + tiered_released_profiles_collect(profiles, tier, access, &released); + int result = + prepare_config_parent(path) + ? cbm_text_migrate_owned_document(path, current, released.content, released.count) + : CLI_ERR; + tiered_released_profiles_free(&released); free(current); if (result != CLI_OK) { if (result > CLI_OK) { @@ -8795,28 +8826,11 @@ static void uninstall_tiered_agent_profiles(cbm_tiered_profile_set_t profiles, b record_agent_config_error(true, profiles.label, "agent_render", path); continue; } - cbm_graph_access_t alternate_access = - access == CBM_GRAPH_ACCESS_DIRECT ? CBM_GRAPH_ACCESS_HANDOFF : CBM_GRAPH_ACCESS_DIRECT; - char *alternate = cbm_render_graph_profile(profiles.dialect, tier, alternate_access, - profiles.binary_path); - char *codex_rc1 = - profiles.dialect == CBM_GRAPH_DIALECT_CODEX && access == CBM_GRAPH_ACCESS_DIRECT - ? cbm_render_graph_profile_codex_rc1(tier) - : NULL; - const char *released[3]; - size_t released_count = 0U; - if (alternate) { - released[released_count++] = alternate; - } - if (codex_rc1) { - released[released_count++] = codex_rc1; - } - if (tier == CBM_GRAPH_TIER_VERIFY && profiles.legacy_verify_content) { - released[released_count++] = profiles.legacy_verify_content; - } - int result = cbm_text_remove_owned_document_any(path, current, released, released_count); - free(codex_rc1); - free(alternate); + tiered_released_profiles_t released; + tiered_released_profiles_collect(profiles, tier, access, &released); + int result = + cbm_text_remove_owned_document_any(path, current, released.content, released.count); + tiered_released_profiles_free(&released); free(current); if (result < CLI_OK) { record_agent_config_error(true, profiles.label, "agent_uninstall", path); diff --git a/tests/test_cli.c b/tests/test_cli.c index 4f95e9c43..6306504aa 100644 --- a/tests/test_cli.c +++ b/tests/test_cli.c @@ -7412,6 +7412,109 @@ TEST(cli_tiered_codex_profiles_migrate_preserve_and_uninstall) { PASS(); } +/* The OpenCode subagent profile as v0.10.8 wrote it: today's rendering minus + * the tool_search / tool_search_regex permission lines #1933 added in + * v0.11.0. Derived by deleting exactly those lines, so the test states the + * released bytes without going through any production legacy renderer. */ +static char *opencode_profile_before_tool_search(cbm_graph_tier_t tier) { + char *current = + cbm_render_graph_profile(CBM_GRAPH_DIALECT_OPENCODE, tier, CBM_GRAPH_ACCESS_DIRECT, NULL); + if (!current) { + return NULL; + } + const char *const added[] = {" tool_search: allow\n", " tool_search_regex: allow\n"}; + for (size_t i = 0U; i < sizeof(added) / sizeof(added[0]); i++) { + char *at = strstr(current, added[i]); + if (!at) { + free(current); + return NULL; + } + size_t len = strlen(added[i]); + memmove(at, at + len, strlen(at + len) + 1U); + } + return current; +} + +/* #2264: upgrading v0.10.8 -> v0.11.0 reported all three OpenCode profiles + * as "preserved modified profile" + op=agent_install errors and stopped the + * activation, although nobody had edited them: the release render changed + * and the previous render was not recognised as ours. */ +TEST(cli_tiered_opencode_profiles_migrate_v0108_render) { + char tmpdir[256]; + snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-tiered-opencode-XXXXXX"); + if (!cbm_mkdtemp(tmpdir)) + FAIL("cbm_mkdtemp failed"); + + const char *const env_names[] = { + "HOME", "PATH", "CODEX_HOME", "QWEN_HOME", "COPILOT_HOME", + "CLINE_DATA_DIR", "KIRO_HOME", "OPENCODE_CONFIG", "OPENCODE_CONFIG_DIR"}; + char *saved_env[sizeof(env_names) / sizeof(env_names[0])]; + for (size_t i = 0U; i < sizeof(env_names) / sizeof(env_names[0]); i++) { + saved_env[i] = save_test_env(env_names[i]); + cbm_unsetenv(env_names[i]); + } + cbm_setenv("HOME", tmpdir, 1); + cbm_setenv("PATH", tmpdir, 1); + + char agents_dir[512]; + snprintf(agents_dir, sizeof(agents_dir), "%s/.config/opencode/agents", tmpdir); + test_mkdirp(agents_dir); + char paths[CBM_GRAPH_TIER_COUNT][640]; + bool seeded = true; + for (int t = 0; t < (int)CBM_GRAPH_TIER_COUNT; t++) { + cbm_graph_tier_t tier = (cbm_graph_tier_t)t; + snprintf(paths[t], sizeof(paths[t]), "%s/%s.md", agents_dir, cbm_graph_tier_slug(tier)); + char *old = opencode_profile_before_tool_search(tier); + seeded = seeded && old != NULL; + if (old) { + write_test_file(paths[t], old); + free(old); + } + } + + char installed_binary[640]; + snprintf(installed_binary, sizeof(installed_binary), "%s/.local/bin/codebase-memory-mcp", + tmpdir); + int install_rc = cbm_install_agent_configs(tmpdir, installed_binary, false, false); + bool migrated = true; + for (int t = 0; t < (int)CBM_GRAPH_TIER_COUNT; t++) { + char *after = read_test_file_alloc(paths[t]); + migrated = migrated && after && strstr(after, " tool_search: allow\n") && + strstr(after, " tool_search_regex: allow\n"); + free(after); + } + + /* Uninstall must also recognise the v0.10.8 bytes as ours. */ + for (int t = 0; t < (int)CBM_GRAPH_TIER_COUNT; t++) { + char *old = opencode_profile_before_tool_search((cbm_graph_tier_t)t); + if (old) { + write_test_file(paths[t], old); + free(old); + } + } + char *argv[] = {"uninstall", "--yes"}; + int uninstall_rc = cli_test_cmd_uninstall(2, argv); + bool removed = true; + struct stat state; + for (int t = 0; t < (int)CBM_GRAPH_TIER_COUNT; t++) { + removed = removed && stat(paths[t], &state) != 0; + } + + for (size_t i = 0U; i < sizeof(env_names) / sizeof(env_names[0]); i++) { + restore_test_env(env_names[i], saved_env[i]); + } + test_rmdir_r(tmpdir); + if (!seeded) + FAIL("v0.10.8 OpenCode profile bytes could not be derived"); + ASSERT_EQ(install_rc, 0); + if (!migrated) + FAIL("v0.10.8 OpenCode profiles must be upgraded in place, not preserved as modified"); + ASSERT_EQ(uninstall_rc, 0); + if (!removed) + FAIL("uninstall must remove v0.10.8 OpenCode profiles as owned"); + PASS(); +} + TEST(cli_tiered_vibe_installs_matching_agent_prompt_sets) { char tmpdir[256]; snprintf(tmpdir, sizeof(tmpdir), "/tmp/cli-tiered-vibe-XXXXXX"); @@ -16428,6 +16531,7 @@ SUITE(cli) { RUN_TEST(cli_warp_installs_shared_skill_without_mcp_or_permissions); RUN_TEST(cli_owned_durable_profiles_preserve_user_files); RUN_TEST(cli_tiered_codex_profiles_migrate_preserve_and_uninstall); + RUN_TEST(cli_tiered_opencode_profiles_migrate_v0108_render); RUN_TEST(cli_tiered_vibe_installs_matching_agent_prompt_sets); RUN_TEST(cli_tiered_grok_installs_profiles_and_withholds_hooks); RUN_TEST(cli_grok_mcp_preserves_foreign_table);