feat: finalise ticket99 - implémentation agent model configuration v2
- agent/lifecycle.rs: lifecycle management per profile
- agent/provider_catalogue.rs: provider registration with model support
- agent/usecases.rs: usecases for profile-based agent invocation
- agent/mod.rs: expose agent capabilities via AgentManager
- backend/dto.rs: AgentModelConfig, AgentProviderConfig DTOs
- domain/profile.rs: extend Profile avec agent capabilities
- domain/permission.rs: permission checks pour agent access
- infrastructure/assistant/mod.rs: agent integration
- infrastructure/permission/{claude,codex}.rs: permission handlers
- web-server/lib.rs: agent endpoints
- commands.rs: agent commands
- frontend/adapters/{http,profile,mock,domain}.ts: adapters
- frontend/first-run/FirstRunWizard.{test.tsx,tsx}: first-run flow
This commit is contained in:
@ -235,6 +235,7 @@ impl TicketAssistantEnvironmentPreparer {
|
||||
&declaration,
|
||||
cwd.as_str(),
|
||||
project.root.as_str(),
|
||||
profile.model.as_deref(),
|
||||
);
|
||||
self.write_file(&path, rendered.as_bytes()).await?;
|
||||
env.push((home_env.clone(), parent_dir(cwd, target)));
|
||||
@ -511,12 +512,13 @@ fn codex_config_toml(
|
||||
mcp_declaration: &str,
|
||||
run_dir: &str,
|
||||
project_root: &str,
|
||||
model: Option<&str>,
|
||||
) -> String {
|
||||
let mut text = replace_toml_table_block(
|
||||
existing.unwrap_or_default(),
|
||||
"mcp_servers.idea",
|
||||
mcp_declaration.trim_end(),
|
||||
);
|
||||
let mut text = existing.unwrap_or_default().to_owned();
|
||||
if let Some(model) = model {
|
||||
text = set_top_level_toml_value(&text, "model", model);
|
||||
}
|
||||
text = replace_toml_table_block(&text, "mcp_servers.idea", mcp_declaration.trim_end());
|
||||
text = ensure_codex_project_trust(&text, run_dir);
|
||||
text = ensure_codex_project_trust(&text, project_root);
|
||||
if !text.ends_with('\n') {
|
||||
@ -525,6 +527,33 @@ fn codex_config_toml(
|
||||
text
|
||||
}
|
||||
|
||||
fn set_top_level_toml_value(input: &str, key: &str, value: &str) -> String {
|
||||
let line = format!("{key} = {}", toml_quoted(value));
|
||||
set_top_level_toml_line(input, key, &line)
|
||||
}
|
||||
|
||||
fn set_top_level_toml_line(input: &str, key: &str, replacement: &str) -> String {
|
||||
let needle = format!("{key} =");
|
||||
let mut out = Vec::new();
|
||||
let mut replaced = false;
|
||||
for line in input.lines() {
|
||||
let trimmed = line.trim_start();
|
||||
if !replaced && trimmed.starts_with(&needle) {
|
||||
out.push(replacement.to_owned());
|
||||
replaced = true;
|
||||
} else {
|
||||
out.push(line.to_owned());
|
||||
}
|
||||
}
|
||||
if !replaced {
|
||||
if !out.is_empty() && !out.first().is_some_and(|line| line.trim().starts_with('[')) {
|
||||
out.push(String::new());
|
||||
}
|
||||
out.insert(0, replacement.to_owned());
|
||||
}
|
||||
out.join("\n")
|
||||
}
|
||||
|
||||
fn replace_toml_table_block(existing: &str, table: &str, replacement: &str) -> String {
|
||||
let header = format!("[{table}]");
|
||||
let mut out = Vec::new();
|
||||
|
||||
@ -228,9 +228,6 @@ mod tests {
|
||||
project_root: root,
|
||||
run_dir,
|
||||
model: None,
|
||||
model_provider: None,
|
||||
model_provider_base_url: None,
|
||||
model_provider_env_key: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -285,9 +282,6 @@ mod tests {
|
||||
project_root: "/proj",
|
||||
run_dir: "/run",
|
||||
model: Some("claude-sonnet-4-5"),
|
||||
model_provider: None,
|
||||
model_provider_base_url: None,
|
||||
model_provider_env_key: None,
|
||||
};
|
||||
let proj = ClaudePermissionProjector.project(None, None, &ctx);
|
||||
assert!(proj.args.is_empty());
|
||||
|
||||
@ -133,49 +133,17 @@ fn codex_managed_keys(ctx: &ProjectionContext, include_permissions: bool) -> Vec
|
||||
if ctx.model.is_some() {
|
||||
keys.push("model".to_owned());
|
||||
}
|
||||
if ctx.model_provider.is_some() {
|
||||
keys.push("model_provider".to_owned());
|
||||
}
|
||||
keys
|
||||
}
|
||||
|
||||
fn codex_managed_tables(ctx: &ProjectionContext) -> Vec<String> {
|
||||
let mut tables = vec![SANDBOX_WORKSPACE_WRITE_TABLE.to_owned()];
|
||||
if let Some(provider) = ctx.model_provider {
|
||||
if ctx.model_provider_base_url.is_some() {
|
||||
tables.push(codex_model_provider_table(provider));
|
||||
}
|
||||
}
|
||||
tables
|
||||
fn codex_managed_tables(_ctx: &ProjectionContext) -> Vec<String> {
|
||||
vec![SANDBOX_WORKSPACE_WRITE_TABLE.to_owned()]
|
||||
}
|
||||
|
||||
fn append_codex_model_config(contents: &mut String, ctx: &ProjectionContext) {
|
||||
if let Some(model) = ctx.model {
|
||||
contents.push_str(&format!("model = {}\n", toml_string(model)));
|
||||
}
|
||||
let Some(provider) = ctx.model_provider else {
|
||||
return;
|
||||
};
|
||||
contents.push_str(&format!("model_provider = {}\n", toml_string(provider)));
|
||||
if let Some(base_url) = ctx.model_provider_base_url {
|
||||
let table = codex_model_provider_table(provider);
|
||||
let name = provider_display_name(ctx);
|
||||
let env_key = ctx.model_provider_env_key.unwrap_or("OPENAI_API_KEY");
|
||||
contents.push_str(&format!(
|
||||
"\n[{table}]\nname = {}\nbase_url = {}\nenv_key = {}\nwire_api = \"responses\"\n",
|
||||
toml_string(name),
|
||||
toml_string(base_url),
|
||||
toml_string(env_key),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn provider_display_name<'a>(ctx: &'a ProjectionContext<'a>) -> &'a str {
|
||||
ctx.model_provider.unwrap_or("custom")
|
||||
}
|
||||
|
||||
fn codex_model_provider_table(provider: &str) -> String {
|
||||
format!("model_providers.{}", toml_string(provider))
|
||||
}
|
||||
|
||||
fn codex_network_env(network: Option<NetworkPolicy>) -> Vec<(String, String)> {
|
||||
@ -221,9 +189,6 @@ mod tests {
|
||||
project_root: "/proj",
|
||||
run_dir: "/run/agent",
|
||||
model: None,
|
||||
model_provider: None,
|
||||
model_provider_base_url: None,
|
||||
model_provider_env_key: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -273,14 +238,11 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_projection_without_permissions_writes_model_provider_and_custom_table() {
|
||||
fn model_projection_without_permissions_writes_only_model() {
|
||||
let ctx = ProjectionContext {
|
||||
project_root: "/proj",
|
||||
run_dir: "/run/agent",
|
||||
model: Some("gpt-5"),
|
||||
model_provider: Some("openai-compatible"),
|
||||
model_provider_base_url: Some("https://models.example.test/v1"),
|
||||
model_provider_env_key: Some("OPENAI_API_KEY"),
|
||||
};
|
||||
let proj = CodexPermissionProjector.project(None, None, &ctx);
|
||||
assert!(proj.args.is_empty());
|
||||
@ -292,27 +254,16 @@ mod tests {
|
||||
..
|
||||
} => {
|
||||
assert!(managed_keys.contains(&"model".to_owned()));
|
||||
assert!(managed_keys.contains(&"model_provider".to_owned()));
|
||||
assert!(
|
||||
managed_tables.contains(&"model_providers.\"openai-compatible\"".to_owned())
|
||||
assert!(!managed_keys.contains(&"model_provider".to_owned()));
|
||||
assert_eq!(
|
||||
managed_tables,
|
||||
&vec![SANDBOX_WORKSPACE_WRITE_TABLE.to_owned()]
|
||||
);
|
||||
assert!(contents.contains("model = \"gpt-5\""), "{contents}");
|
||||
assert!(
|
||||
contents.contains("model_provider = \"openai-compatible\""),
|
||||
"{contents}"
|
||||
);
|
||||
assert!(
|
||||
contents.contains("[model_providers.\"openai-compatible\"]"),
|
||||
"{contents}"
|
||||
);
|
||||
assert!(
|
||||
contents.contains("base_url = \"https://models.example.test/v1\""),
|
||||
"{contents}"
|
||||
);
|
||||
assert!(
|
||||
contents.contains("env_key = \"OPENAI_API_KEY\""),
|
||||
"{contents}"
|
||||
);
|
||||
assert!(!contents.contains("model_provider"), "{contents}");
|
||||
assert!(!contents.contains("model_providers"), "{contents}");
|
||||
assert!(!contents.contains("base_url"), "{contents}");
|
||||
assert!(!contents.contains("env_key"), "{contents}");
|
||||
}
|
||||
ProjectedFile::Replace { .. } => panic!("Codex must emit a MergeToml file"),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user