fix(#108): restreint Code Mode Codex au namespace MCP IdeA et documente les tools au démarrage

Codex Code Mode pouvait appeler n'importe quel outil MCP directement,
contournant la médiation d'approbation. On force
[features.code_mode].direct_only_tool_namespaces = ["mcp__idea"] sur
chaque surface qui écrit le config.toml Codex (permission projector,
lifecycle, migration run-dir, assistant de ticket), et on ajoute
initialize.instructions côté serveur MCP pour orienter Codex vers le
bon outil idea_* dès la connexion, sans dépendre de la recherche
sémantique différée.

QA : domain 283/0, application 126/0 + agent_lifecycle 73/0 +
change_agent_profile 19/0 + ticket_assistant 5/0, infrastructure 339/0
dont mcp_server 37/0, backend 68/0 (7 ignored). Les échecs web-server
observés sur cargo test --workspace (Too many open files, cookies)
sont une contamination de ressources inter-tests ; les deux tests
concernés repassent isolément.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 22:51:38 +02:00
parent af6b76935c
commit d0b65a92bd
9 changed files with 228 additions and 32 deletions

View File

@ -19,6 +19,7 @@ use domain::permission::{
EffectivePermissions, PermissionProjection, PermissionProjector, Posture, ProjectedFile,
ProjectionContext, ProjectorKey,
};
use domain::profile::{CODEX_CODE_MODE_FEATURES_TABLE, CODEX_CODE_MODE_FEATURES_TOML};
use domain::NetworkPolicy;
use super::toml_string;
@ -70,13 +71,15 @@ impl PermissionProjector for CodexPermissionProjector {
// `set_top_level_toml_value`). The mcp_servers/trust tables are NOT a
// permission concern and stay with the MCP wiring (LP3-3).
let mut contents = format!(
"sandbox_mode = {}\napproval_policy = {}\n\n[{}]\nnetwork_access = {}\n",
"sandbox_mode = {}\napproval_policy = {}\n",
toml_string(sandbox),
toml_string(approval),
SANDBOX_WORKSPACE_WRITE_TABLE,
network_access,
);
append_codex_model_config(&mut contents, ctx);
contents.push_str(&format!(
"\n[{}]\nnetwork_access = {}\n\n{}",
SANDBOX_WORKSPACE_WRITE_TABLE, network_access, CODEX_CODE_MODE_FEATURES_TOML,
));
let mut args = vec![
"--sandbox".to_owned(),
@ -111,12 +114,17 @@ fn codex_model_and_network_file(
ctx: &ProjectionContext,
network: Option<NetworkPolicy>,
) -> ProjectedFile {
let mut contents = format!(
"[{}]\nnetwork_access = {}\n",
let mut contents = String::new();
append_codex_model_config(&mut contents, ctx);
if !contents.is_empty() {
contents.push('\n');
}
contents.push_str(&format!(
"[{}]\nnetwork_access = {}\n\n{}",
SANDBOX_WORKSPACE_WRITE_TABLE,
codex_network_access(network),
);
append_codex_model_config(&mut contents, ctx);
CODEX_CODE_MODE_FEATURES_TOML,
));
ProjectedFile::MergeToml {
rel_path: CONFIG_REL_PATH.to_owned(),
managed_tables: codex_managed_tables(ctx),
@ -137,7 +145,10 @@ fn codex_managed_keys(ctx: &ProjectionContext, include_permissions: bool) -> Vec
}
fn codex_managed_tables(_ctx: &ProjectionContext) -> Vec<String> {
vec![SANDBOX_WORKSPACE_WRITE_TABLE.to_owned()]
vec![
SANDBOX_WORKSPACE_WRITE_TABLE.to_owned(),
CODEX_CODE_MODE_FEATURES_TABLE.to_owned(),
]
}
fn append_codex_model_config(contents: &mut String, ctx: &ProjectionContext) {
@ -198,6 +209,13 @@ mod tests {
resolve(Some(&PermissionSet::new(vec![], fallback)), None).unwrap()
}
fn expected_managed_tables() -> Vec<String> {
vec![
SANDBOX_WORKSPACE_WRITE_TABLE.to_owned(),
CODEX_CODE_MODE_FEATURES_TABLE.to_owned(),
]
}
// ---- product invariant + ownership ----------------------------------
#[test]
@ -213,15 +231,18 @@ mod tests {
contents,
} => {
assert_eq!(rel_path, CONFIG_REL_PATH);
assert_eq!(
managed_tables,
&vec![SANDBOX_WORKSPACE_WRITE_TABLE.to_owned()]
);
assert_eq!(managed_tables, &expected_managed_tables());
assert!(managed_keys.is_empty());
assert!(
contents.contains("[sandbox_workspace_write]\nnetwork_access = false"),
"network is denied by default: {contents:?}"
);
assert!(
contents.contains(
"[features.code_mode]\ndirect_only_tool_namespaces = [\"mcp__idea\"]"
),
"Code Mode direct tools must be restricted to IdeA MCP: {contents:?}"
);
}
ProjectedFile::Replace { .. } => panic!("Codex must emit a MergeToml file"),
}
@ -255,10 +276,7 @@ mod tests {
} => {
assert!(managed_keys.contains(&"model".to_owned()));
assert!(!managed_keys.contains(&"model_provider".to_owned()));
assert_eq!(
managed_tables,
&vec![SANDBOX_WORKSPACE_WRITE_TABLE.to_owned()]
);
assert_eq!(managed_tables, &expected_managed_tables());
assert!(contents.contains("model = \"gpt-5\""), "{contents}");
assert!(!contents.contains("model_provider"), "{contents}");
assert!(!contents.contains("model_providers"), "{contents}");
@ -296,10 +314,7 @@ mod tests {
contents,
} => {
assert_eq!(rel_path, CONFIG_REL_PATH);
assert_eq!(
managed_tables,
&vec![SANDBOX_WORKSPACE_WRITE_TABLE.to_owned()]
);
assert_eq!(managed_tables, &expected_managed_tables());
assert_eq!(
managed_keys,
&vec!["sandbox_mode".to_owned(), "approval_policy".to_owned()]
@ -316,6 +331,12 @@ mod tests {
contents.contains("[sandbox_workspace_write]\nnetwork_access = false"),
"network is denied by default: {contents:?}"
);
assert!(
contents.contains(
"[features.code_mode]\ndirect_only_tool_namespaces = [\"mcp__idea\"]"
),
"Code Mode direct tools must be restricted to IdeA MCP: {contents:?}"
);
}
ProjectedFile::Replace { .. } => panic!("Codex must emit a MergeToml file"),
}