Skip to content
Open
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Rust build artifacts
/target/
/devit-landing-preview/
## Landing preview (now versioned)
# devit-landing-preview/
/site/
**/*.rs.bk
Cargo.lock
Expand Down
2 changes: 1 addition & 1 deletion crates/agent/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ impl Agent {
Ok(answer)
}

/// Génère un message de commit (Conventional Commits) à partir du goal, d'un résumé et d'un extrait de diff.
/// Generate a Conventional Commit message from a goal, a summary, and a diff snippet.
/// Retourne une ligne courte (≤ 72 chars) ; body optionnel non inclus (MVP).
pub async fn commit_message(
&self,
Expand Down
58 changes: 29 additions & 29 deletions crates/cli/src/core/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ impl PolicyEngine {
}

let reason = format!(
"Niveau untrusted : confirmation requise pour {} changement(s)",
"Untrusted level: confirmation required for {} change(s)",
context.file_changes.len()
);
Ok(PolicyDecision::allow_with_confirmation(reason))
Expand All @@ -223,12 +223,12 @@ impl PolicyEngine {
return Ok(PolicyDecision::allow_with_confirmation(reason));
}

// Ask : demander confirmation sauf pour les changements très simples
// Ask: request confirmation except for very simple changes
if self.is_simple_change(context) {
let reason = "Changement simple, autorisé automatiquement".to_string();
let reason = "Simple change, automatically allowed".to_string();
Ok(PolicyDecision::allow(reason))
} else {
let reason = "Changement nécessitant confirmation utilisateur".to_string();
let reason = "Change requires user confirmation".to_string();
Ok(PolicyDecision::allow_with_confirmation(reason))
}
}
Expand All @@ -243,7 +243,7 @@ impl PolicyEngine {

if context.file_changes.len() > context.config.max_files_moderate {
let reason = format!(
"Trop de fichiers ({} > {}), dégradé vers ask",
"Too many files ({} > {}), downgraded to Ask",
context.file_changes.len(),
context.config.max_files_moderate
);
Expand All @@ -254,7 +254,7 @@ impl PolicyEngine {

if total_lines > context.config.max_lines_moderate {
let reason = format!(
"Trop de lignes changées ({} > {}), dégradé vers ask",
"Too many lines changed ({} > {}), downgraded to Ask",
total_lines, context.config.max_lines_moderate
);
return Ok(PolicyDecision::downgrade(reason, ApprovalLevel::Ask, true));
Expand All @@ -275,22 +275,22 @@ impl PolicyEngine {
}

if context.file_changes.iter().any(|fc| fc.is_binary) {
let reason = "Les fichiers binaires nécessitent le niveau Ask".to_string();
let reason = "Binary files require Ask level".to_string();
return Ok(PolicyDecision::downgrade(reason, ApprovalLevel::Ask, true));
}

if context.file_changes.iter().any(|fc| fc.touches_gitmodules) {
return Ok(PolicyDecision::deny(
"Modification de .gitmodules réservée au niveau privileged".to_string(),
".gitmodules modification is restricted to privileged level".to_string(),
));
}

if context.file_changes.iter().any(|fc| fc.touches_submodule) {
let reason = "Changement de sous-module nécessite le niveau Ask".to_string();
let reason = "Submodule change requires Ask level".to_string();
return Ok(PolicyDecision::downgrade(reason, ApprovalLevel::Ask, true));
}

let reason = "Changement autorisé au niveau moderate".to_string();
let reason = "Change allowed at moderate level".to_string();
Ok(PolicyDecision::allow(reason))
}

Expand All @@ -304,7 +304,7 @@ impl PolicyEngine {

if context.file_changes.iter().any(|fc| fc.touches_gitmodules) {
return Ok(PolicyDecision::deny(
"Modification de .gitmodules réservée au niveau privileged".to_string(),
".gitmodules modification is restricted to privileged level".to_string(),
));
}

Expand All @@ -316,7 +316,7 @@ impl PolicyEngine {
for file_change in &context.file_changes {
if file_change.is_binary {
if !self.is_whitelisted_binary(file_change, &context.config) {
let reason = format!("Binaire non autorisé: {}", file_change.path.display());
let reason = format!("Unauthorized binary: {}", file_change.path.display());
return Ok(PolicyDecision::downgrade(reason, ApprovalLevel::Ask, true));
}
}
Expand All @@ -330,7 +330,7 @@ impl PolicyEngine {
let reason = if requires_confirmation {
"Sensitive path modified: confirmation required".to_string()
} else {
"Changement autorisé au niveau trusted".to_string()
"Change allowed at trusted level".to_string()
};

if requires_confirmation {
Expand Down Expand Up @@ -363,7 +363,7 @@ impl PolicyEngine {

if !path_allowed {
let reason = format!(
"Chemin non autorisé en mode privileged: {}",
"Path not allowed in privileged mode: {}",
file_change.path.display()
);
return Ok(PolicyDecision::deny(reason));
Expand Down Expand Up @@ -804,7 +804,7 @@ pub struct FileChange {
/// Si le changement touche un sous-module Git
pub touches_submodule: bool,

/// Si le changement touche .gitmodules
/// Whether the change touches .gitmodules
pub touches_gitmodules: bool,

/// Taille du fichier en octets (pour les binaires)
Expand Down Expand Up @@ -853,18 +853,18 @@ pub struct PolicyDecision {
/// Si l'opération est autorisée
pub allow: bool,

/// Si une confirmation utilisateur est requise
/// Whether user confirmation is required
pub requires_confirmation: bool,

/// Raison de la décision
pub reason: String,

/// Niveau d'approbation dégradé si applicable
/// Downgraded approval level, if any
pub downgraded_to: Option<ApprovalLevel>,
}

impl PolicyDecision {
/// Crée une décision d'autorisation.
/// Create an allow decision.
pub fn allow(reason: String) -> Self {
Self {
allow: true,
Expand All @@ -874,7 +874,7 @@ impl PolicyDecision {
}
}

/// Crée une décision d'autorisation avec confirmation.
/// Create an allow decision requiring confirmation.
pub fn allow_with_confirmation(reason: String) -> Self {
Self {
allow: true,
Expand All @@ -884,7 +884,7 @@ impl PolicyDecision {
}
}

/// Crée une décision de refus.
/// Create a deny decision.
pub fn deny(reason: String) -> Self {
Self {
allow: false,
Expand All @@ -894,7 +894,7 @@ impl PolicyDecision {
}
}

/// Crée une décision avec dégradation de niveau.
/// Create a decision with a downgraded approval level.
pub fn downgrade(
reason: String,
downgraded_to: ApprovalLevel,
Expand All @@ -913,7 +913,7 @@ impl PolicyDecision {
mod tests {
use super::*;

/// Crée un contexte de test avec des valeurs par défaut.
/// Create a test context with default values.
fn create_test_context(
file_changes: Vec<FileChange>,
approval_level: ApprovalLevel,
Expand All @@ -931,7 +931,7 @@ mod tests {
}
}

/// Crée un changement de fichier simple pour les tests.
/// Create a simple file change for tests.
fn create_simple_file_change(path: &str) -> FileChange {
FileChange {
path: PathBuf::from(path),
Expand All @@ -949,7 +949,7 @@ mod tests {
}
}

/// Crée un Policy Engine pour les tests.
/// Create a Policy Engine for tests.
fn create_test_engine() -> PolicyEngine {
PolicyEngine::new(
ApprovalLevel::Privileged {
Expand Down Expand Up @@ -1046,7 +1046,7 @@ mod tests {
fn test_ask_complex_change_requires_confirmation() {
let engine = create_test_engine();
let mut change = create_simple_file_change("src/main.rs");
change.lines_added = 50; // Dépasse le seuil simple
change.lines_added = 50; // Exceeds simple threshold
let changes = vec![change];
let context = create_test_context(changes, ApprovalLevel::Ask);

Expand All @@ -1061,7 +1061,7 @@ mod tests {
fn test_moderate_too_many_files_downgrades() {
let engine = create_test_engine();
let mut changes = Vec::new();
// Créer plus de fichiers que la limite moderate
// Create more files than the moderate limit
for i in 0..15 {
changes.push(create_simple_file_change(&format!("src/file{}.rs", i)));
}
Expand Down Expand Up @@ -1154,7 +1154,7 @@ mod tests {
assert!(decision.allow);
assert!(decision.requires_confirmation);
assert_eq!(decision.downgraded_to, Some(ApprovalLevel::Ask));
assert!(decision.reason.contains("Binaire non autorisé"));
assert!(decision.reason.contains("Unauthorized binary"));
}

#[test]
Expand Down Expand Up @@ -1215,7 +1215,7 @@ mod tests {

assert!(!decision.allow);
assert!(!decision.requires_confirmation);
assert!(decision.reason.contains("non autorisé en mode privileged"));
assert!(decision.reason.contains("not allowed in privileged mode"));
}

#[test]
Expand Down Expand Up @@ -1245,7 +1245,7 @@ mod tests {
let decision = engine.evaluate_changes(&context).unwrap();

assert!(decision.allow);
// Symlink interne : peut demander confirmation si changement non simple
// Internal symlink: may require confirmation if change is not simple
assert!(decision.requires_confirmation);
}

Expand Down
36 changes: 17 additions & 19 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -597,9 +597,7 @@ async fn main() -> Result<()> {
}
Some(Commands::Run { goal, use_mcp }) => {
if policy_requires_yes && !assume_yes {
eprintln!(
"La politique 'on-request' nécessite --yes pour exécuter cette commande."
);
eprintln!("Policy 'on-request' requires --yes to run this command.");
std::process::exit(1);
}
let response = handle_run(goal, use_mcp, use_json_output).await;
Expand Down Expand Up @@ -797,7 +795,7 @@ async fn main() -> Result<()> {
ext_allow.as_deref(),
json_out.as_deref(),
)?;
println!("index écrit: {}", written.display());
println!("index written: {}", written.display());
}
},
Some(Commands::CommitMsg {
Expand Down Expand Up @@ -1045,10 +1043,10 @@ fn read_patch(input: &str) -> Result<String> {

fn ensure_git_repo() -> Result<()> {
if !git::is_git_available() {
anyhow::bail!("git n'est pas disponible dans le PATH.");
anyhow::bail!("git is not available in PATH.");
}
if !git::in_repo() {
anyhow::bail!("pas dans un dépôt git (git rev-parse --is-inside-work-tree).");
anyhow::bail!("not inside a git repository (git rev-parse --is-inside-work-tree).");
}
Ok(())
}
Expand Down Expand Up @@ -1230,7 +1228,7 @@ fn tool_call_json(
"fs_patch_apply" => {
ensure_git_repo()?;
if cfg.policy.sandbox.to_lowercase() == "read-only" {
anyhow::bail!("policy.sandbox=read-only: apply refusé (aucune écriture autorisée)");
anyhow::bail!("policy.sandbox=read-only: apply denied (no write operations allowed)");
}
let patch = args.get("patch").and_then(|v| v.as_str()).unwrap_or("");
let mode = args.get("mode").and_then(|v| v.as_str()).unwrap_or("index");
Expand Down Expand Up @@ -1294,7 +1292,7 @@ fn tool_call_json(
.and_then(|v| v.as_bool())
.unwrap_or(false);
if patch.is_empty() {
anyhow::bail!("fs_patch_apply: champ 'patch' requis (contenu du diff)");
anyhow::bail!("fs_patch_apply: 'patch' field is required (diff content)");
}
// Precommit gate
if precommit_only {
Expand Down Expand Up @@ -1363,14 +1361,14 @@ fn tool_call_json(
}
let ask = requires_approval_tool(&cfg.policy, "git", yes, "write");
if ask && !ask_approval()? {
anyhow::bail!("Annulé par l'utilisateur.");
anyhow::bail!("Cancelled by user.");
}
let ok = match mode {
"worktree" => git::apply_worktree(patch)?,
_ => git::apply_index(patch)?,
};
if !ok {
anyhow::bail!("Échec git apply ({mode})");
anyhow::bail!(format!("git apply failed ({mode})"));
}
// tests impacted pipeline
let tests_enabled = match tests_mode.as_str() {
Expand Down Expand Up @@ -1620,11 +1618,11 @@ fn tool_call_json(
"shell_exec" => {
let cmd = args.get("cmd").and_then(|v| v.as_str()).unwrap_or("");
if cmd.is_empty() {
anyhow::bail!("shell_exec: champ 'cmd' requis");
anyhow::bail!("shell_exec: 'cmd' field is required");
}
let ask = requires_approval_tool(&cfg.policy, "shell", yes, "exec");
if ask && !ask_approval()? {
anyhow::bail!("Annulé par l'utilisateur.");
anyhow::bail!("Cancelled by user.");
}
#[cfg(feature = "sandbox")]
let (code, out) = sandbox::run_shell_sandboxed_capture(cmd, &cfg.policy, &cfg.sandbox)?;
Expand All @@ -1636,7 +1634,7 @@ fn tool_call_json(
}
Ok(serde_json::json!({"exit_code": code, "output": out}))
}
_ => anyhow::bail!(format!("outil inconnu: {name}")),
_ => anyhow::bail!(format!("unknown tool: {name}")),
}
}

Expand All @@ -1656,7 +1654,7 @@ fn tool_call_legacy(
"fs_patch_apply" => {
ensure_git_repo()?;
if cfg.policy.sandbox.to_lowercase() == "read-only" {
anyhow::bail!("policy.sandbox=read-only: apply refusé (aucune écriture autorisée)");
anyhow::bail!("policy.sandbox=read-only: apply denied (no write operations allowed)");
}
let patch = read_patch(input)?;
if precommit_only {
Expand Down Expand Up @@ -1693,10 +1691,10 @@ fn tool_call_legacy(
git::apply_check(&patch)?;
let ask = requires_approval_tool(&cfg.policy, "git", yes, "write");
if ask && !ask_approval()? {
anyhow::bail!("Annulé par l'utilisateur.");
anyhow::bail!("Cancelled by user.");
}
if !git::apply_index(&patch)? {
anyhow::bail!("Échec git apply --index (patch-only).");
anyhow::bail!("git apply --index failed (patch-only).");
}
// run impacted tests (auto on for non-danger profiles)
let profile = cfg
Expand Down Expand Up @@ -1732,7 +1730,7 @@ fn tool_call_legacy(
"shell_exec" => {
let ask = requires_approval_tool(&cfg.policy, "shell", yes, "exec");
if ask && !ask_approval()? {
anyhow::bail!("Annulé par l'utilisateur.");
anyhow::bail!("Cancelled by user.");
}
let cmd = if input == "-" {
anyhow::bail!("shell_exec requires a command string as input");
Expand All @@ -1752,7 +1750,7 @@ fn tool_call_legacy(
}
Ok(())
}
_ => anyhow::bail!(format!("outil inconnu: {name}")),
_ => anyhow::bail!(format!("unknown tool: {name}")),
}
}

Expand Down Expand Up @@ -3184,7 +3182,7 @@ async fn run_sandboxed_test(
}
Ok(Ok(Err(e))) => {
// Fallback to non-sandboxed execution if binary not allowed
if e.to_string().contains("binaire non autorisé")
if e.to_string().contains("unauthorized binary")
|| e.to_string().contains("binary not allowed")
{
tracing::warn!(
Expand Down
Loading