fix(install): declare a transport in generated Codex agent role TOMLs

Codex deserializes ~/.codex/agents/*.toml standalone: an
[mcp_servers.codebase-memory-mcp] table with only enabled_tools fails
with "invalid transport" and the whole role is dropped, so all three
generated roles have been dead on Codex since they first shipped in
v0.9.1-rc.1.

Render command = <installed binary> plus
args = ["--tool-profile=scout|analysis"] in direct Codex profiles,
mirroring the Kiro renderer, and keep enabled_tools. Recognize the
rc.1 transport-less rendering as a released shape so install migrates
those files in place and uninstall still removes them; pass the
installed binary path in the Codex uninstall set so exact-content
ownership keeps matching.

Fixes #1391

Signed-off-by: Ivan Dergachev <dergachoff@gmail.com>
This commit is contained in:
Ivan Dergachev committed 2026-08-01 17:36:21 +04:00
1 parent d6be58ef9d
commit 07885ef6a9
5 files changed
+166 -40

No files matched your search

+65 -17
View File
@@ -7,6 +7,7 @@
*/
#include "cli/agent_profiles.h"
#include "cli/config_toml_edit.h"
#include "yyjson/yyjson.h"
#include <stdbool.h>
@@ -451,9 +452,51 @@ static char *render_kiro_profile(cbm_graph_tier_t tier, cbm_graph_access_t acces
return result;
}
/* Codex deserializes role files standalone: a server table without command/url
* is rejected as "invalid transport" and the whole role is dropped, so direct
* profiles must declare the transport. rc1_transportless reproduces the
* v0.9.1-rc.1 rendering so installs can migrate those files. */
static bool append_codex_profile(profile_buffer_t *buffer, cbm_graph_tier_t tier,
cbm_graph_access_t access, const char *binary_path,
const char *prompt, bool rc1_transportless) {
if (!profile_buffer_append(buffer, "name = \"") ||
!profile_buffer_append(buffer, cbm_graph_tier_slug(tier)) ||
!profile_buffer_append(buffer, "\"\ndescription = \"") ||
!profile_buffer_append(buffer, profile_description(tier, access)) ||
!profile_buffer_append(buffer, "\"\nsandbox_mode = \"read-only\"\ndeveloper_instructions = "
"\"\"\"\n") ||
!profile_buffer_append(buffer, prompt) || !profile_buffer_append(buffer, "\"\"\"\n")) {
return false;
}
if (access != CBM_GRAPH_ACCESS_DIRECT) {
return true;
}
if (!profile_buffer_append(buffer, "\n[mcp_servers.codebase-memory-mcp]\n")) {
return false;
}
if (!rc1_transportless) {
char escaped_binary[8192];
if (!binary_path || !binary_path[0] ||
cbm_toml_escape_basic_string(binary_path, escaped_binary, sizeof(escaped_binary)) !=
0) {
return false;
}
if (!profile_buffer_append(buffer, "command = \"") ||
!profile_buffer_append(buffer, escaped_binary) ||
!profile_buffer_append(buffer, "\"\nargs = [\"--tool-profile=") ||
!profile_buffer_append(buffer, tier_server_profile(tier)) ||
!profile_buffer_append(buffer, "\"]\n")) {
return false;
}
}
return profile_buffer_append(buffer, "enabled_tools = [") &&
append_toml_mcp_tools(buffer, CBM_GRAPH_DIALECT_CODEX, tier, false) &&
profile_buffer_append(buffer, "]\n");
}
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 *prompt) {
const char *binary_path, const char *prompt) {
const char *slug = cbm_graph_tier_slug(tier);
const char *display = cbm_graph_tier_display_name(tier);
const char *description = profile_description(tier, access);
@@ -471,21 +514,7 @@ static bool render_profile_text(profile_buffer_t *buffer, cbm_graph_profile_dial
}
return true;
case CBM_GRAPH_DIALECT_CODEX:
if (!profile_buffer_append(buffer, "name = \"") || !profile_buffer_append(buffer, slug) ||
!profile_buffer_append(buffer, "\"\ndescription = \"") ||
!profile_buffer_append(buffer, description) ||
!profile_buffer_append(
buffer, "\"\nsandbox_mode = \"read-only\"\ndeveloper_instructions = \"\"\"\n") ||
!profile_buffer_append(buffer, prompt) || !profile_buffer_append(buffer, "\"\"\"\n")) {
return false;
}
if (direct && (!profile_buffer_append(
buffer, "\n[mcp_servers.codebase-memory-mcp]\nenabled_tools = [") ||
!append_toml_mcp_tools(buffer, dialect, tier, false) ||
!profile_buffer_append(buffer, "]\n"))) {
return false;
}
return true;
return append_codex_profile(buffer, tier, access, binary_path, prompt, false);
case CBM_GRAPH_DIALECT_GEMINI:
if (!append_yaml_identity(buffer, slug, description) ||
!profile_buffer_append(buffer,
@@ -627,7 +656,26 @@ char *cbm_render_graph_profile(cbm_graph_profile_dialect_t dialect, cbm_graph_ti
}
profile_buffer_t buffer;
profile_buffer_init(&buffer);
bool ok = render_profile_text(&buffer, dialect, tier, access, prompt);
bool ok = render_profile_text(&buffer, dialect, tier, access, binary_path, prompt);
free(prompt);
if (!ok) {
profile_buffer_discard(&buffer);
return NULL;
}
return profile_buffer_finish(&buffer);
}
char *cbm_render_graph_profile_codex_rc1(cbm_graph_tier_t tier) {
if (!tier_valid(tier)) {
return NULL;
}
char *prompt = cbm_render_graph_prompt(tier, CBM_GRAPH_ACCESS_DIRECT);
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);
free(prompt);
if (!ok) {
profile_buffer_discard(&buffer);
+5 -1
View File
@@ -50,10 +50,14 @@ const char *cbm_graph_tier_display_name(cbm_graph_tier_t tier);
bool cbm_graph_dialect_direct_capable(cbm_graph_profile_dialect_t dialect);
/* Returns malloc-owned profile content, or NULL for invalid/unsafe combinations.
* binary_path is required for a direct Kiro profile and ignored otherwise. */
* binary_path is required for direct Kiro and Codex profiles and ignored otherwise. */
char *cbm_render_graph_profile(cbm_graph_profile_dialect_t dialect, cbm_graph_tier_t tier,
cbm_graph_access_t access, const char *binary_path);
/* v0.9.1-rc.1 direct Codex rendering (server table without a transport), kept
* so install/uninstall can recognize and migrate those files. */
char *cbm_render_graph_profile_codex_rc1(cbm_graph_tier_t tier);
/* 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);
+21 -4
View File
@@ -7390,17 +7390,25 @@ static void install_tiered_agent_profiles(cbm_tiered_profile_set_t profiles, boo
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);
const char *released[2];
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);
free(current);
if (result != CLI_OK) {
@@ -7437,15 +7445,23 @@ static void uninstall_tiered_agent_profiles(cbm_tiered_profile_set_t profiles, b
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);
const char *released[2];
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);
free(current);
if (result < CLI_OK) {
@@ -10050,11 +10066,13 @@ static void uninstall_cli_agents(const cbm_detected_agents_t *agents, const char
char ip[CLI_BUF_1K];
char skills_dir[CLI_BUF_1K];
char ap[CLI_BUF_1K];
char installed_binary[CLI_BUF_1K];
cbm_codex_config_dir(home, config_dir, sizeof(config_dir));
snprintf(cp, sizeof(cp), "%s/config.toml", config_dir);
snprintf(ip, sizeof(ip), "%s/AGENTS.md", config_dir);
snprintf(skills_dir, sizeof(skills_dir), "%s/skills", config_dir);
snprintf(ap, sizeof(ap), "%s/agents/codebase-memory.toml", config_dir);
cbm_agent_installed_binary_path(home, installed_binary, sizeof(installed_binary));
uninstall_agent_mcp_instr((mcp_uninstall_args_t){"Codex CLI", cp, ip}, dry_run,
cbm_remove_codex_mcp_owned);
uninstall_agent_skill("Codex CLI", skills_dir, dry_run);
@@ -10062,6 +10080,7 @@ static void uninstall_cli_agents(const cbm_detected_agents_t *agents, const char
(cbm_tiered_profile_set_t){
.label = "Codex CLI",
.verify_path = ap,
.binary_path = installed_binary,
.legacy_verify_content = legacy_codex_verify_agent_content,
.dialect = CBM_GRAPH_DIALECT_CODEX,
},
@@ -10071,10 +10090,8 @@ static void uninstall_cli_agents(const cbm_detected_agents_t *agents, const char
record_agent_config_error(true, "Codex CLI", "hook_uninstall", cp);
}
char hooks_json[CLI_BUF_1K];
char installed_binary[CLI_BUF_1K];
char hook_command[CLI_BUF_8K];
snprintf(hooks_json, sizeof(hooks_json), "%s/hooks.json", config_dir);
cbm_agent_installed_binary_path(home, installed_binary, sizeof(installed_binary));
if (cbm_file_exists(hooks_json) &&
(cbm_build_augment_command(installed_binary, hook_command, sizeof(hook_command)) !=
CLI_OK ||
+39 -2
View File
@@ -78,8 +78,10 @@ TEST(agent_profiles_direct_dialects_are_coverage_aware_and_read_only) {
for (size_t i = 0U; i < sizeof(direct_dialects) / sizeof(direct_dialects[0]); i++) {
const direct_dialect_expectation_t *expectation = &direct_dialects[i];
for (int tier = 0; tier < (int)CBM_GRAPH_TIER_COUNT; tier++) {
const char *binary =
expectation->dialect == CBM_GRAPH_DIALECT_KIRO ? "/opt/codebase memory/cbm" : NULL;
const char *binary = expectation->dialect == CBM_GRAPH_DIALECT_KIRO ||
expectation->dialect == CBM_GRAPH_DIALECT_CODEX
? "/opt/codebase memory/cbm"
: NULL;
char *profile = cbm_render_graph_profile(expectation->dialect, (cbm_graph_tier_t)tier,
CBM_GRAPH_ACCESS_DIRECT, binary);
if (!profile) {
@@ -225,6 +227,40 @@ TEST(agent_profiles_kiro_is_valid_json_and_escapes_binary_path) {
PASS();
}
TEST(agent_profiles_codex_declares_transport_and_escapes_binary_path) {
const char *binary = "C:\\cbm bin\\codebase-memory-mcp.exe";
char *scout = cbm_render_graph_profile(CBM_GRAPH_DIALECT_CODEX, CBM_GRAPH_TIER_SCOUT,
CBM_GRAPH_ACCESS_DIRECT, binary);
char *verify = cbm_render_graph_profile(CBM_GRAPH_DIALECT_CODEX, CBM_GRAPH_TIER_VERIFY,
CBM_GRAPH_ACCESS_DIRECT, binary);
ASSERT_NOT_NULL(scout);
ASSERT_NOT_NULL(verify);
int valid = strstr(scout, "[mcp_servers.codebase-memory-mcp]\n"
"command = \"C:\\\\cbm bin\\\\codebase-memory-mcp.exe\"\n"
"args = [\"--tool-profile=scout\"]\n"
"enabled_tools = [") != NULL &&
strstr(verify, "command = \"C:\\\\cbm bin\\\\codebase-memory-mcp.exe\"\n"
"args = [\"--tool-profile=analysis\"]\n") != NULL;
free(scout);
free(verify);
ASSERT_TRUE(valid);
ASSERT_NULL(cbm_render_graph_profile(CBM_GRAPH_DIALECT_CODEX, CBM_GRAPH_TIER_VERIFY,
CBM_GRAPH_ACCESS_DIRECT, NULL));
ASSERT_NULL(cbm_render_graph_profile(CBM_GRAPH_DIALECT_CODEX, CBM_GRAPH_TIER_VERIFY,
CBM_GRAPH_ACCESS_DIRECT, ""));
char *handoff = cbm_render_graph_profile(CBM_GRAPH_DIALECT_CODEX, CBM_GRAPH_TIER_VERIFY,
CBM_GRAPH_ACCESS_HANDOFF, NULL);
ASSERT_NOT_NULL(handoff);
ASSERT_TRUE(strstr(handoff, "[mcp_servers.") == NULL);
free(handoff);
char *rc1 = cbm_render_graph_profile_codex_rc1(CBM_GRAPH_TIER_VERIFY);
ASSERT_NOT_NULL(rc1);
ASSERT_TRUE(strstr(rc1, "[mcp_servers.codebase-memory-mcp]\nenabled_tools = [") != NULL);
ASSERT_TRUE(strstr(rc1, "command = ") == NULL);
free(rc1);
PASS();
}
TEST(agent_profiles_vibe_uses_matching_prompt_identifier_and_contract) {
for (int tier = 0; tier < (int)CBM_GRAPH_TIER_COUNT; tier++) {
const char *slug = cbm_graph_tier_slug((cbm_graph_tier_t)tier);
@@ -274,6 +310,7 @@ SUITE(agent_profiles) {
RUN_TEST(agent_profiles_handoff_only_dialects_fail_closed_for_direct_access);
RUN_TEST(agent_profiles_server_level_dialects_hard_enforce_read_only_tools);
RUN_TEST(agent_profiles_kiro_is_valid_json_and_escapes_binary_path);
RUN_TEST(agent_profiles_codex_declares_transport_and_escapes_binary_path);
RUN_TEST(agent_profiles_vibe_uses_matching_prompt_identifier_and_contract);
RUN_TEST(agent_profiles_render_deterministically_and_reject_invalid_inputs);
}
+36 -16
View File
@@ -13,6 +13,7 @@
#include "../src/foundation/compat_thread.h"
#include "test_framework.h"
#include "test_helpers.h"
#include <cli/agent_profiles.h>
#include <cli/cli.h>
#include <cli/progress_sink.h>
#include <daemon/bootstrap.h>
@@ -4894,11 +4895,15 @@ TEST(cli_durable_profiles_follow_current_vendor_paths) {
files_ok = files_ok && test_file_contains_all(path, claude_terms, 7U);
snprintf(path, sizeof(path), "%s/agents/codebase-memory.toml", codex_home);
const char *const codex_terms[] = {
"name = \"codebase-memory\"", "description = ",
"developer_instructions = ", "sandbox_mode = \"read-only\"",
"[mcp_servers.codebase-memory-mcp]", "check_index_coverage"};
files_ok = files_ok && test_file_contains_all(path, codex_terms, 6U);
const char *const codex_terms[] = {"name = \"codebase-memory\"",
"description = ",
"developer_instructions = ",
"sandbox_mode = \"read-only\"",
"[mcp_servers.codebase-memory-mcp]",
"command = \"/opt/codebase-memory-mcp\"",
"args = [\"--tool-profile=analysis\"]",
"check_index_coverage"};
files_ok = files_ok && test_file_contains_all(path, codex_terms, 8U);
char *profile = read_test_file_alloc(path);
files_ok = files_ok && profile && !strstr(profile, "model =") &&
!strstr(profile, "index_repository") && !strstr(profile, "delete_project") &&
@@ -5371,25 +5376,40 @@ TEST(cli_tiered_codex_profiles_migrate_preserve_and_uninstall) {
"name = \"codebase-memory-scout\"\nuser_note = \"preserve scout\"\n";
write_test_file(verify_path, legacy_verify);
write_test_file(scout_path, foreign_scout);
char *rc1_auditor = cbm_render_graph_profile_codex_rc1(CBM_GRAPH_TIER_AUDIT);
if (!rc1_auditor)
FAIL("rc.1 auditor rendering must be available");
write_test_file(auditor_path, rc1_auditor);
free(rc1_auditor);
char *plan = cbm_build_install_plan_json(tmpdir, "/opt/codebase-memory-mcp");
/* Uninstall renders expected content with the installed binary path, so
* install with that same path to exercise exact-content removal. */
char installed_binary[640];
char expected_command[768];
snprintf(installed_binary, sizeof(installed_binary), "%s/.local/bin/codebase-memory-mcp",
tmpdir);
snprintf(expected_command, sizeof(expected_command), "command = \"%s\"", installed_binary);
char *plan = cbm_build_install_plan_json(tmpdir, installed_binary);
bool plan_ok =
plan && strstr(plan, scout_path) && strstr(plan, verify_path) && strstr(plan, auditor_path);
free(plan);
int install_rc = cbm_install_agent_configs(tmpdir, "/opt/codebase-memory-mcp", false, false);
int install_rc = cbm_install_agent_configs(tmpdir, installed_binary, false, false);
char *scout = read_test_file_alloc(scout_path);
char *verify = read_test_file_alloc(verify_path);
char *auditor = read_test_file_alloc(auditor_path);
bool installed = install_rc != 0 && scout && strcmp(scout, foreign_scout) == 0 && verify &&
strcmp(verify, legacy_verify) != 0 && strstr(verify, "Tier 2") &&
strstr(verify, "name = \"codebase-memory\"") &&
strstr(verify, "check_index_coverage") && auditor &&
strstr(auditor, "Tier 3") && strstr(auditor, "check_index_coverage") &&
!strstr(verify, "index_repository") && !strstr(verify, "delete_project") &&
!strstr(verify, "manage_adr") && !strstr(verify, "ingest_traces") &&
!strstr(auditor, "index_repository") && !strstr(auditor, "delete_project") &&
!strstr(auditor, "manage_adr") && !strstr(auditor, "ingest_traces");
bool installed =
install_rc != 0 && scout && strcmp(scout, foreign_scout) == 0 && verify &&
strcmp(verify, legacy_verify) != 0 && strstr(verify, "Tier 2") &&
strstr(verify, "name = \"codebase-memory\"") && strstr(verify, expected_command) &&
strstr(verify, "args = [\"--tool-profile=analysis\"]") &&
strstr(verify, "check_index_coverage") && auditor && strstr(auditor, expected_command) &&
strstr(auditor, "args = [\"--tool-profile=analysis\"]") && strstr(auditor, "Tier 3") &&
strstr(auditor, "check_index_coverage") && !strstr(verify, "index_repository") &&
!strstr(verify, "delete_project") && !strstr(verify, "manage_adr") &&
!strstr(verify, "ingest_traces") && !strstr(auditor, "index_repository") &&
!strstr(auditor, "delete_project") && !strstr(auditor, "manage_adr") &&
!strstr(auditor, "ingest_traces");
free(scout);
free(verify);
free(auditor);