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
2 changes: 1 addition & 1 deletion scripts/memory-core-baseline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
50 changes: 35 additions & 15 deletions src/cli/agent_profiles.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) ||
Expand Down Expand Up @@ -711,21 +718,34 @@ 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);
return NULL;
}
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);
}
4 changes: 4 additions & 0 deletions src/cli/agent_profiles.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
106 changes: 60 additions & 46 deletions src/cli/cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -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++) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
104 changes: 104 additions & 0 deletions tests/test_cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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);
Expand Down
Loading