Files
Ivan Dergachev 07885ef6a9 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>
2026-08-01 17:36:21 +04:00

317 lines
17 KiB
C

/* test_agent_profiles.c — Canonical Scout/Verify/Audit renderer contracts. */
#include "test_framework.h"
#include <cli/agent_profiles.h>
#include <yyjson/yyjson.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
cbm_graph_profile_dialect_t dialect;
const char *syntax_fragment;
const char *read_fragment;
const char *grep_fragment;
} direct_dialect_expectation_t;
static const direct_dialect_expectation_t direct_dialects[] = {
{CBM_GRAPH_DIALECT_CLAUDE, "permissionMode: plan", " - Read\n", " - Grep\n"},
{CBM_GRAPH_DIALECT_CODEX, "sandbox_mode = \"read-only\"", "read", "grep"},
{CBM_GRAPH_DIALECT_GEMINI, "kind: local", " - read_file\n", " - grep_search\n"},
{CBM_GRAPH_DIALECT_QWEN, "approvalMode: plan", " - read_file\n", " - grep_search\n"},
{CBM_GRAPH_DIALECT_COPILOT, "codebase-memory-mcp/check_index_coverage", " - read\n",
"source read/grep fallback"},
{CBM_GRAPH_DIALECT_OPENCODE, " \"*\": deny", " read: allow", " grep: allow"},
{CBM_GRAPH_DIALECT_KILO, "mode: subagent", " read: allow", " grep: allow"},
{CBM_GRAPH_DIALECT_KIRO, "\"includeMcpJson\": false", "\"read\"", "\"grep\""},
{CBM_GRAPH_DIALECT_JUNIE, "mcpServers: [\"codebase-memory-", "\"Read\"", "\"Grep\""},
{CBM_GRAPH_DIALECT_QODER, "mcp__codebase-memory-mcp__check_index_coverage",
"tools: Read,Grep,Glob,mcp__codebase-memory-mcp__", "Read,Grep"},
{CBM_GRAPH_DIALECT_CODEBUDDY, "permissionMode: plan", "tools: Read,Grep,Glob,", "Read,Grep"},
{CBM_GRAPH_DIALECT_FACTORY, "mcp__codebase-memory-mcp__check_index_coverage",
"tools: [\"Read\", \"LS\", \"Grep\", \"Glob\"", "source read/grep fallback"},
{CBM_GRAPH_DIALECT_VIBE, "agent_type = \"subagent\"", "\"read_file\"", "\"grep_search\""},
};
static const cbm_graph_profile_dialect_t handoff_only_dialects[] = {
CBM_GRAPH_DIALECT_AUGMENT,
CBM_GRAPH_DIALECT_CURSOR,
CBM_GRAPH_DIALECT_ROVO,
CBM_GRAPH_DIALECT_POCHI,
};
static int profile_has_mutator(const char *profile) {
static const char *const mutators[] = {
"index_repository",
"delete_project",
"manage_adr",
"ingest_traces",
};
for (size_t i = 0U; i < sizeof(mutators) / sizeof(mutators[0]); i++) {
if (strstr(profile, mutators[i])) {
return 1;
}
}
return 0;
}
TEST(agent_profiles_stable_tier_identity) {
ASSERT_STR_EQ(cbm_graph_tier_slug(CBM_GRAPH_TIER_SCOUT), "codebase-memory-scout");
ASSERT_STR_EQ(cbm_graph_tier_slug(CBM_GRAPH_TIER_VERIFY), "codebase-memory");
ASSERT_STR_EQ(cbm_graph_tier_slug(CBM_GRAPH_TIER_AUDIT), "codebase-memory-auditor");
ASSERT_STR_EQ(cbm_graph_tier_display_name(CBM_GRAPH_TIER_SCOUT), "Codebase Memory Scout");
ASSERT_STR_EQ(cbm_graph_tier_display_name(CBM_GRAPH_TIER_VERIFY), "Codebase Memory Verify");
ASSERT_STR_EQ(cbm_graph_tier_display_name(CBM_GRAPH_TIER_AUDIT), "Codebase Memory Auditor");
ASSERT_TRUE(cbm_graph_dialect_direct_capable(CBM_GRAPH_DIALECT_CLAUDE));
ASSERT_TRUE(cbm_graph_dialect_direct_capable(CBM_GRAPH_DIALECT_KIRO));
ASSERT_FALSE(cbm_graph_dialect_direct_capable(CBM_GRAPH_DIALECT_AUGMENT));
ASSERT_FALSE(cbm_graph_dialect_direct_capable(CBM_GRAPH_DIALECT_CURSOR));
ASSERT_FALSE(cbm_graph_dialect_direct_capable(CBM_GRAPH_DIALECT_ROVO));
ASSERT_FALSE(cbm_graph_dialect_direct_capable(CBM_GRAPH_DIALECT_POCHI));
ASSERT_FALSE(cbm_graph_dialect_direct_capable(CBM_GRAPH_DIALECT_COUNT));
ASSERT_NULL(cbm_graph_tier_slug(CBM_GRAPH_TIER_COUNT));
ASSERT_NULL(cbm_graph_tier_display_name(CBM_GRAPH_TIER_COUNT));
PASS();
}
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 ||
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) {
FAIL("every documented direct dialect must render all three tiers");
}
int valid = strstr(profile, "codebase-memory") != NULL &&
strstr(profile, "check_index_coverage") != NULL &&
strstr(profile, expectation->syntax_fragment) != NULL &&
strstr(profile, expectation->read_fragment) != NULL &&
strstr(profile, expectation->grep_fragment) != NULL &&
strstr(profile, "source read/grep fallback") != NULL &&
!profile_has_mutator(profile);
free(profile);
if (!valid) {
FAIL("direct profiles must expose coverage plus source fallback and omit mutators");
}
}
}
PASS();
}
TEST(agent_profiles_tiers_encode_distinct_evidence_budgets) {
char *scout = cbm_render_graph_profile(CBM_GRAPH_DIALECT_CLAUDE, CBM_GRAPH_TIER_SCOUT,
CBM_GRAPH_ACCESS_DIRECT, NULL);
char *verify = cbm_render_graph_profile(CBM_GRAPH_DIALECT_CLAUDE, CBM_GRAPH_TIER_VERIFY,
CBM_GRAPH_ACCESS_DIRECT, NULL);
char *audit = cbm_render_graph_profile(CBM_GRAPH_DIALECT_CLAUDE, CBM_GRAPH_TIER_AUDIT,
CBM_GRAPH_ACCESS_DIRECT, NULL);
int valid = scout && verify && audit && strstr(scout, "3-4 narrow graph calls") &&
strstr(scout, "positive, provisional") && strstr(scout, "all/none claims") &&
!strstr(scout, "mcp__codebase-memory-mcp__query_graph") &&
!strstr(scout, "mcp__codebase-memory-mcp__detect_changes") &&
strstr(verify, "default tier") && strstr(verify, "task-directed evidence") &&
strstr(verify, "scope coverage before negative claims") &&
strstr(verify, "mcp__codebase-memory-mcp__query_graph") &&
strstr(verify, "mcp__codebase-memory-mcp__detect_changes") &&
strstr(audit, "bounded scope") && strstr(audit, "current graph generation") &&
strstr(audit, "complete relevant pagination") && strstr(audit, "scope coverage") &&
strstr(audit, "source fallback") &&
strstr(audit, "mcp__codebase-memory-mcp__query_graph") &&
strstr(audit, "mcp__codebase-memory-mcp__detect_changes");
free(scout);
free(verify);
free(audit);
ASSERT_TRUE(valid);
PASS();
}
TEST(agent_profiles_handoff_requires_parent_evidence_without_child_mcp) {
for (int dialect = 0; dialect < (int)CBM_GRAPH_DIALECT_COUNT; dialect++) {
for (int tier = 0; tier < (int)CBM_GRAPH_TIER_COUNT; tier++) {
char *profile =
cbm_render_graph_profile((cbm_graph_profile_dialect_t)dialect,
(cbm_graph_tier_t)tier, CBM_GRAPH_ACCESS_HANDOFF, NULL);
if (!profile) {
FAIL("every dialect must be able to render a parent-handoff profile");
}
int valid = strstr(profile, "parent agent must supply") &&
strstr(profile, "coverage evidence") &&
strstr(profile, "must not call or claim access to MCP") &&
!strstr(profile, "mcpServers") &&
!strstr(profile, "mcp__codebase-memory-mcp__") &&
!strstr(profile, "mcp_codebase-memory-mcp_") &&
!strstr(profile, "@codebase-memory-mcp/") &&
!strstr(profile, "codebase-memory-mcp/");
free(profile);
if (!valid) {
FAIL("handoff profiles must require parent coverage and expose no child MCP");
}
}
}
PASS();
}
TEST(agent_profiles_handoff_only_dialects_fail_closed_for_direct_access) {
for (size_t i = 0U; i < sizeof(handoff_only_dialects) / sizeof(handoff_only_dialects[0]); i++) {
char *profile = cbm_render_graph_profile(handoff_only_dialects[i], CBM_GRAPH_TIER_VERIFY,
CBM_GRAPH_ACCESS_DIRECT, "/opt/cbm");
ASSERT_NULL(profile);
}
PASS();
}
TEST(agent_profiles_server_level_dialects_hard_enforce_read_only_tools) {
char *junie_scout = cbm_render_graph_profile(CBM_GRAPH_DIALECT_JUNIE, CBM_GRAPH_TIER_SCOUT,
CBM_GRAPH_ACCESS_DIRECT, NULL);
char *junie = cbm_render_graph_profile(CBM_GRAPH_DIALECT_JUNIE, CBM_GRAPH_TIER_VERIFY,
CBM_GRAPH_ACCESS_DIRECT, NULL);
char *qoder = cbm_render_graph_profile(CBM_GRAPH_DIALECT_QODER, CBM_GRAPH_TIER_VERIFY,
CBM_GRAPH_ACCESS_DIRECT, NULL);
char *factory = cbm_render_graph_profile(CBM_GRAPH_DIALECT_FACTORY, CBM_GRAPH_TIER_VERIFY,
CBM_GRAPH_ACCESS_DIRECT, NULL);
ASSERT_NOT_NULL(junie_scout);
ASSERT_NOT_NULL(junie);
ASSERT_NOT_NULL(qoder);
ASSERT_NOT_NULL(factory);
ASSERT(strstr(junie_scout, "mcpServers: [\"codebase-memory-scout\"]") != NULL);
ASSERT(strstr(junie, "mcpServers: [\"codebase-memory-analysis\"]") != NULL);
ASSERT(strstr(junie, "hard-enforces the analysis tool profile") != NULL);
ASSERT(strstr(qoder, "mcp__codebase-memory-mcp__check_index_coverage") != NULL);
ASSERT(strstr(factory, "mcp__codebase-memory-mcp__check_index_coverage") != NULL);
ASSERT(strstr(qoder, "mcpServers:") != NULL);
ASSERT(strstr(qoder, "codebase-memory-mcp") != NULL);
ASSERT(strstr(factory, "mcpServers") == NULL);
ASSERT(strstr(junie, "instruction-enforced") == NULL);
ASSERT(strstr(qoder, "instruction-enforced") == NULL);
ASSERT(strstr(factory, "instruction-enforced") == NULL);
ASSERT(!profile_has_mutator(junie));
ASSERT(!profile_has_mutator(qoder));
ASSERT(!profile_has_mutator(factory));
free(junie_scout);
free(junie);
free(qoder);
free(factory);
PASS();
}
TEST(agent_profiles_kiro_is_valid_json_and_escapes_binary_path) {
const char *binary = "/opt/cbm path/\"quoted\"";
char *profile = cbm_render_graph_profile(CBM_GRAPH_DIALECT_KIRO, CBM_GRAPH_TIER_AUDIT,
CBM_GRAPH_ACCESS_DIRECT, binary);
ASSERT_NOT_NULL(profile);
yyjson_doc *doc = yyjson_read(profile, strlen(profile), 0);
yyjson_val *root = doc ? yyjson_doc_get_root(doc) : NULL;
yyjson_val *servers = root ? yyjson_obj_get(root, "mcpServers") : NULL;
yyjson_val *server = servers ? yyjson_obj_get(servers, "codebase-memory-mcp") : NULL;
yyjson_val *command = server ? yyjson_obj_get(server, "command") : NULL;
yyjson_val *args = server ? yyjson_obj_get(server, "args") : NULL;
yyjson_val *profile_flag = args && yyjson_is_arr(args) ? yyjson_arr_get(args, 0U) : NULL;
yyjson_val *profile_name = args && yyjson_is_arr(args) ? yyjson_arr_get(args, 1U) : NULL;
yyjson_val *tools = root ? yyjson_obj_get(root, "tools") : NULL;
int valid = root && yyjson_is_obj(root) && command && yyjson_is_str(command) &&
strcmp(yyjson_get_str(command), binary) == 0 && args && yyjson_is_arr(args) &&
yyjson_arr_size(args) == 2U && profile_flag && yyjson_is_str(profile_flag) &&
strcmp(yyjson_get_str(profile_flag), "--tool-profile") == 0 && profile_name &&
yyjson_is_str(profile_name) &&
strcmp(yyjson_get_str(profile_name), "analysis") == 0 && tools &&
yyjson_is_arr(tools) &&
strstr(profile, "@codebase-memory-mcp/check_index_coverage") != NULL;
yyjson_doc_free(doc);
free(profile);
ASSERT_TRUE(valid);
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);
char *profile = cbm_render_graph_profile(CBM_GRAPH_DIALECT_VIBE, (cbm_graph_tier_t)tier,
CBM_GRAPH_ACCESS_DIRECT, NULL);
char *prompt = cbm_render_graph_prompt((cbm_graph_tier_t)tier, CBM_GRAPH_ACCESS_DIRECT);
int valid = profile && prompt && strstr(profile, slug) &&
strstr(profile, "system_prompt_id") && strstr(prompt, "check_index_coverage") &&
strstr(prompt, "source read/grep fallback");
free(profile);
free(prompt);
if (!valid) {
FAIL("Vibe profile and canonical prompt must share the tier slug and contract");
}
}
PASS();
}
TEST(agent_profiles_render_deterministically_and_reject_invalid_inputs) {
char *first = cbm_render_graph_profile(CBM_GRAPH_DIALECT_QWEN, CBM_GRAPH_TIER_VERIFY,
CBM_GRAPH_ACCESS_DIRECT, NULL);
char *second = cbm_render_graph_profile(CBM_GRAPH_DIALECT_QWEN, CBM_GRAPH_TIER_VERIFY,
CBM_GRAPH_ACCESS_DIRECT, NULL);
ASSERT_NOT_NULL(first);
ASSERT_NOT_NULL(second);
ASSERT_STR_EQ(first, second);
free(first);
free(second);
ASSERT_NULL(cbm_render_graph_profile(CBM_GRAPH_DIALECT_COUNT, CBM_GRAPH_TIER_VERIFY,
CBM_GRAPH_ACCESS_DIRECT, NULL));
ASSERT_NULL(cbm_render_graph_profile(CBM_GRAPH_DIALECT_CLAUDE, CBM_GRAPH_TIER_COUNT,
CBM_GRAPH_ACCESS_DIRECT, NULL));
ASSERT_NULL(cbm_render_graph_profile(CBM_GRAPH_DIALECT_CLAUDE, CBM_GRAPH_TIER_VERIFY,
CBM_GRAPH_ACCESS_COUNT, NULL));
ASSERT_NULL(cbm_render_graph_profile(CBM_GRAPH_DIALECT_KIRO, CBM_GRAPH_TIER_VERIFY,
CBM_GRAPH_ACCESS_DIRECT, NULL));
ASSERT_NULL(cbm_render_graph_prompt(CBM_GRAPH_TIER_COUNT, CBM_GRAPH_ACCESS_DIRECT));
ASSERT_NULL(cbm_render_graph_prompt(CBM_GRAPH_TIER_VERIFY, CBM_GRAPH_ACCESS_COUNT));
PASS();
}
SUITE(agent_profiles) {
RUN_TEST(agent_profiles_stable_tier_identity);
RUN_TEST(agent_profiles_direct_dialects_are_coverage_aware_and_read_only);
RUN_TEST(agent_profiles_tiers_encode_distinct_evidence_budgets);
RUN_TEST(agent_profiles_handoff_requires_parent_evidence_without_child_mcp);
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);
}