feat: v0.7.0 — am run/start --provider/--model : résolution cloud via le registre (base_url + keyring + modèle), fallback cloud quand le modèle local est inconnu, canal model_env/model_arg/env_map, jamais de token en clair (closes #92)

- apply_provider : providers::resolve_for (flag > agent > défaut) + apply_env_map_resolved (api_key = @secret, base_url/model du provider résolu)
- start/run/restart : --provider et --model dans le CLI, le REPL et la complétion ; specs help + man pages
- Tests unitaires : résolution, cascade, canal déclaré, erreur provider inconnu (liste), absence de fuite (env sans valeur sk-), fallback embarqué
This commit is contained in:
2026-08-19 21:05:39 -04:00
parent 68eb170f57
commit 6d51ce9504
12 changed files with 320 additions and 16 deletions
+1 -1
View File
@@ -501,7 +501,7 @@ S'appuie sur #36 (secrets), #40 (profils), #71 (modèle), #66 (sync) — livrés
| [#89](https://git.dracodev.net/Projets/agent-manager/issues/89) | ✅ Secrets partagés par provider (namespace keyring + résolution `@secret` fallback) | S | #88 | | [#89](https://git.dracodev.net/Projets/agent-manager/issues/89) | ✅ Secrets partagés par provider (namespace keyring + résolution `@secret` fallback) | S | #88 |
| [#90](https://git.dracodev.net/Projets/agent-manager/issues/90) | ✅ AgentDef provider/model + flags `--provider/--model/--no-config` à l'install | M | #88 | | [#90](https://git.dracodev.net/Projets/agent-manager/issues/90) | ✅ AgentDef provider/model + flags `--provider/--model/--no-config` à l'install | M | #88 |
| [#91](https://git.dracodev.net/Projets/agent-manager/issues/91) | ✅ Configuration post-install : bloc `config:` par agent (env_map + fichiers) | L | #89, #90 | | [#91](https://git.dracodev.net/Projets/agent-manager/issues/91) | ✅ Configuration post-install : bloc `config:` par agent (env_map + fichiers) | L | #89, #90 |
| [#92](https://git.dracodev.net/Projets/agent-manager/issues/92) | `am run/start --provider` : override au lancement (providers cloud inclus) | M | #90 | | [#92](https://git.dracodev.net/Projets/agent-manager/issues/92) | ✅ `am run/start --provider` : override au lancement (providers cloud inclus) | M | #90 |
Critère de sortie du jalon : un agent installé est opérationnel sans Critère de sortie du jalon : un agent installé est opérationnel sans
configuration manuelle (tokens au keyring, provider/modèle par défaut). configuration manuelle (tokens au keyring, provider/modèle par défaut).
+4 -1
View File
@@ -4,7 +4,7 @@
.SH NAME .SH NAME
restart \- Restart an agent: stop, then start with the same options restart \- Restart an agent: stop, then start with the same options
.SH SYNOPSIS .SH SYNOPSIS
\fBrestart\fR [\fB\-b\fR|\fB\-\-background\fR] [\fB\-f\fR|\fB\-\-foreground\fR] [\fB\-\-args\fR] [\fB\-\-env\fR] [\fB\-\-notify\fR] [\fB\-\-profile\fR] [\fB\-\-model\fR] [\fB\-\-parallel\fR] [\fB\-\-container\fR] [\fB\-\-force\fR] [\fB\-\-timeout\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fIAGENT\fR] \fBrestart\fR [\fB\-b\fR|\fB\-\-background\fR] [\fB\-f\fR|\fB\-\-foreground\fR] [\fB\-\-args\fR] [\fB\-\-env\fR] [\fB\-\-notify\fR] [\fB\-\-profile\fR] [\fB\-\-model\fR] [\fB\-\-provider\fR] [\fB\-\-parallel\fR] [\fB\-\-container\fR] [\fB\-\-force\fR] [\fB\-\-timeout\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fIAGENT\fR]
.SH DESCRIPTION .SH DESCRIPTION
Restart an agent: stop, then start with the same options Restart an agent: stop, then start with the same options
.SH OPTIONS .SH OPTIONS
@@ -30,6 +30,9 @@ Apply an environment profile (env + args, defined in the config)
\fB\-\-model\fR \fI<MODEL>\fR \fB\-\-model\fR \fI<MODEL>\fR
Local model to use for this run (validated against the local runtimes) Local model to use for this run (validated against the local runtimes)
.TP .TP
\fB\-\-provider\fR \fI<PROVIDER>\fR
Provider to use for this run (issue #92): registry lookup, base_url + keyring token + model
.TP
\fB\-\-parallel\fR \fB\-\-parallel\fR
Start every member of a group simultaneously (issue #57) Start every member of a group simultaneously (issue #57)
.TP .TP
+4 -1
View File
@@ -4,7 +4,7 @@
.SH NAME .SH NAME
run \- Run the agent command directly with the given arguments (no process management) run \- Run the agent command directly with the given arguments (no process management)
.SH SYNOPSIS .SH SYNOPSIS
\fBrun\fR [\fB\-\-model\fR] [\fB\-\-container\fR] [\fB\-h\fR|\fB\-\-help\fR] <\fIAGENT\fR> [\fIARGS...\fR] \fBrun\fR [\fB\-\-model\fR] [\fB\-\-provider\fR] [\fB\-\-container\fR] [\fB\-h\fR|\fB\-\-help\fR] <\fIAGENT\fR> [\fIARGS...\fR]
.SH DESCRIPTION .SH DESCRIPTION
Run the agent command directly with the given arguments (no process management) Run the agent command directly with the given arguments (no process management)
.SH OPTIONS .SH OPTIONS
@@ -12,6 +12,9 @@ Run the agent command directly with the given arguments (no process management)
\fB\-\-model\fR \fI<MODEL>\fR \fB\-\-model\fR \fI<MODEL>\fR
Local model to use for this run (validated against the local runtimes) Local model to use for this run (validated against the local runtimes)
.TP .TP
\fB\-\-provider\fR \fI<PROVIDER>\fR
Provider to use for this run (issue #92): registry lookup, base_url + keyring token + model
.TP
\fB\-\-container\fR \fB\-\-container\fR
Run the agent inside a container (issue #58) Run the agent inside a container (issue #58)
.TP .TP
+4 -1
View File
@@ -4,7 +4,7 @@
.SH NAME .SH NAME
start \- Start an agent (foreground by default, or detached with \-\-background) start \- Start an agent (foreground by default, or detached with \-\-background)
.SH SYNOPSIS .SH SYNOPSIS
\fBstart\fR [\fB\-b\fR|\fB\-\-background\fR] [\fB\-f\fR|\fB\-\-foreground\fR] [\fB\-\-args\fR] [\fB\-\-env\fR] [\fB\-\-notify\fR] [\fB\-\-profile\fR] [\fB\-\-model\fR] [\fB\-\-parallel\fR] [\fB\-\-container\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fIAGENT\fR] \fBstart\fR [\fB\-b\fR|\fB\-\-background\fR] [\fB\-f\fR|\fB\-\-foreground\fR] [\fB\-\-args\fR] [\fB\-\-env\fR] [\fB\-\-notify\fR] [\fB\-\-profile\fR] [\fB\-\-model\fR] [\fB\-\-provider\fR] [\fB\-\-parallel\fR] [\fB\-\-container\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fIAGENT\fR]
.SH DESCRIPTION .SH DESCRIPTION
Start an agent (foreground by default, or detached with \-\-background) Start an agent (foreground by default, or detached with \-\-background)
.SH OPTIONS .SH OPTIONS
@@ -30,6 +30,9 @@ Apply an environment profile (env + args, defined in the config)
\fB\-\-model\fR \fI<MODEL>\fR \fB\-\-model\fR \fI<MODEL>\fR
Local model to use for this run (validated against the local runtimes) Local model to use for this run (validated against the local runtimes)
.TP .TP
\fB\-\-provider\fR \fI<PROVIDER>\fR
Provider to use for this run (issue #92): registry lookup, base_url + keyring token + model
.TP
\fB\-\-parallel\fR \fB\-\-parallel\fR
Start every member of a group simultaneously (issue #57) Start every member of a group simultaneously (issue #57)
.TP .TP
+26 -3
View File
@@ -63,23 +63,46 @@ pub fn provider_pref(agent: &AgentDef) -> Option<&str> {
/// model/base_url/provider -> values of the resolved provider. Explicit /// model/base_url/provider -> values of the resolved provider. Explicit
/// values already present in agent.env are never overwritten. /// values already present in agent.env are never overwritten.
pub fn apply_env_map(env: &mut BTreeMap<String, String>, agent: &AgentDef, config: &Config) { pub fn apply_env_map(env: &mut BTreeMap<String, String>, agent: &AgentDef, config: &Config) {
apply_env_map_resolved(env, agent, config, None, None);
}
/// Same as `apply_env_map` but with the provider and model fixed by the
/// caller (issue #92: `am run/start --provider/--model`): the flags win
/// over the agent declaration and the settings default.
pub fn apply_env_map_resolved(
env: &mut BTreeMap<String, String>,
agent: &AgentDef,
config: &Config,
provider_override: Option<&str>,
model_override: Option<&str>,
) {
let Some(cfg) = &agent.config else { let Some(cfg) = &agent.config else {
return; return;
}; };
if cfg.env_map.is_empty() { if cfg.env_map.is_empty() {
return; return;
} }
let Some(pname) = provider_pref(agent).or_else(|| crate::providers::default_name(config)) else { let Some(pname) = provider_override
.filter(|p| !p.is_empty())
.or_else(|| provider_pref(agent))
.or_else(|| crate::providers::default_name(config))
else {
return; return;
}; };
let Some(def) = crate::providers::get(config, pname) else { let Some(def) = crate::providers::get(config, pname) else {
return; return;
}; };
let model = agent.model.as_deref().or(def.default_model.as_deref()); let model = model_override
.filter(|m| !m.is_empty())
.or(agent.model.as_deref())
.or(def.default_model.as_deref());
for (slot, var) in &cfg.env_map { for (slot, var) in &cfg.env_map {
let value = match slot.as_str() { let value = match slot.as_str() {
"api_key" => "@secret".to_string(), "api_key" => "@secret".to_string(),
"model" => model.unwrap_or_default().to_string(), "model" => {
let Some(m) = model else { continue };
m.to_string()
}
"base_url" => def.base_url.clone(), "base_url" => def.base_url.clone(),
"provider" => pname.to_string(), "provider" => pname.to_string(),
_ => continue, // unknown slots are ignored _ => continue, // unknown slots are ignored
+6
View File
@@ -495,6 +495,9 @@ pub enum Command {
/// Local model to use for this run (validated against the local runtimes) /// Local model to use for this run (validated against the local runtimes)
#[arg(long, value_name = "MODEL")] #[arg(long, value_name = "MODEL")]
model: Option<String>, model: Option<String>,
/// Provider to use for this run (issue #92): registry lookup, base_url + keyring token + model
#[arg(long, value_name = "PROVIDER")]
provider: Option<String>,
/// Run the agent inside a container (issue #58) /// Run the agent inside a container (issue #58)
#[arg(long)] #[arg(long)]
container: bool, container: bool,
@@ -566,6 +569,9 @@ pub struct StartArgs {
/// Local model to use for this run (validated against the local runtimes) /// Local model to use for this run (validated against the local runtimes)
#[arg(long, value_name = "MODEL")] #[arg(long, value_name = "MODEL")]
pub model: Option<String>, pub model: Option<String>,
/// Provider to use for this run (issue #92): registry lookup, base_url + keyring token + model
#[arg(long, value_name = "PROVIDER")]
pub provider: Option<String>,
/// Start every member of a group simultaneously (issue #57) /// Start every member of a group simultaneously (issue #57)
#[arg(long, action = ArgAction::SetTrue)] #[arg(long, action = ArgAction::SetTrue)]
pub parallel: bool, pub parallel: bool,
+9 -2
View File
@@ -226,8 +226,15 @@ pub fn execute_command(app: &App, cmd: &Command) -> Result<i32> {
Command::Doctor { fix, watch } => { Command::Doctor { fix, watch } => {
doctor_cmd::run(app, *fix, *watch) doctor_cmd::run(app, *fix, *watch)
} }
Command::Run { agent, model, container, args } => { Command::Run { agent, model, provider, container, args } => {
run_cmd::run(app, agent, model.as_deref(), *container, args) run_cmd::run(
app,
agent,
model.as_deref(),
provider.as_deref(),
*container,
args,
)
} }
Command::Service(svc) => match svc { Command::Service(svc) => match svc {
crate::cli::ServiceCmd::Install { agent, autostart } => { crate::cli::ServiceCmd::Install { agent, autostart } => {
+242 -4
View File
@@ -71,13 +71,30 @@ pub fn start_with_cwd(app: &App, opts: &StartArgs, cwd: Option<&std::path::Path>
)?; )?;
// --model (issue #71): resolve against the local runtimes, then // --model (issue #71): resolve against the local runtimes, then
// transmit through the agent's declared channel (env var or flag). // transmit through the agent's declared channel (env var or flag).
// --provider (issue #92): resolve from the LLM provider registry.
let mut extra_args = extra_args; let mut extra_args = extra_args;
let mut extra_env = extra_env; let mut extra_env = extra_env;
if let Some(m) = opts.model.as_deref() { if let Some(p) = opts.provider.as_deref() {
let agent = require_agent(app, &target)?; let agent = require_agent(app, &target)?;
let (a, e) = apply_model(app, agent, m)?; let (a, e) = apply_provider(app, agent, Some(p), opts.model.as_deref())?
.expect("a provider flag was given: apply_provider always resolves or errors");
extra_args.extend(a); extra_args.extend(a);
extra_env.extend(e); extra_env.extend(e);
} else if let Some(m) = opts.model.as_deref() {
let agent = require_agent(app, &target)?;
match apply_model(app, agent, m) {
Ok((a, e)) => {
extra_args.extend(a);
extra_env.extend(e);
}
Err(local_err) => match apply_provider(app, agent, None, Some(m))? {
Some((a, e)) => {
extra_args.extend(a);
extra_env.extend(e);
}
None => return Err(local_err),
},
}
} }
if let Some(group) = crate::catalog::Catalog::parse_group_selector(&target) { if let Some(group) = crate::catalog::Catalog::parse_group_selector(&target) {
let members = app.catalog.group_members(group); let members = app.catalog.group_members(group);
@@ -320,6 +337,7 @@ pub fn run(
app: &App, app: &App,
target: &str, target: &str,
model: Option<&str>, model: Option<&str>,
provider: Option<&str>,
container: bool, container: bool,
extra: &[OsString], extra: &[OsString],
) -> Result<i32> { ) -> Result<i32> {
@@ -333,10 +351,28 @@ pub fn run(
.map(|o| o.to_string_lossy().to_string()) .map(|o| o.to_string_lossy().to_string())
.collect(); .collect();
let mut extra_env: BTreeMap<String, String> = BTreeMap::new(); let mut extra_env: BTreeMap<String, String> = BTreeMap::new();
if let Some(m) = model { // Issue #92: --provider resolves from the LLM provider registry (cloud);
let (a, e) = apply_model(app, agent, m)?; // --model alone keeps the local-runtimes-first behavior (issue #71) with
// a cloud fallback when the model is unknown locally.
if let Some(p) = provider {
let (a, e) = apply_provider(app, agent, Some(p), model)?
.expect("a provider flag was given: apply_provider always resolves or errors");
extra_args.extend(a); extra_args.extend(a);
extra_env.extend(e); extra_env.extend(e);
} else if let Some(m) = model {
match apply_model(app, agent, m) {
Ok((a, e)) => {
extra_args.extend(a);
extra_env.extend(e);
}
Err(local_err) => match apply_provider(app, agent, None, Some(m))? {
Some((a, e)) => {
extra_args.extend(a);
extra_env.extend(e);
}
None => return Err(local_err),
},
}
} }
let exec = resolve_exec(app, agent, &extra_args, &extra_env)?; let exec = resolve_exec(app, agent, &extra_args, &extra_env)?;
if app.dry_run() { if app.dry_run() {
@@ -474,6 +510,70 @@ pub fn apply_model(
Ok((args, env)) Ok((args, env))
} }
/// Issue #92: resolve `--provider`/`--model` against the LLM provider
/// registry (cloud) and transmit through the agent's declared channels:
/// `model_env` / `model_arg`, or the `config.env_map` slots (issue #91).
/// The token travels as the `@secret` reference resolved by
/// resolve_env_secrets — never on the command line. Returns None when no
/// provider is configured at all (the caller falls back to the
/// local-runtime path).
fn apply_provider(
app: &App,
agent: &AgentDef,
provider: Option<&str>,
model: Option<&str>,
) -> Result<Option<(Vec<String>, BTreeMap<String, String>)>> {
let Some((pname, _def, resolved_model)) = crate::providers::resolve_for(
&app.config,
crate::agent_config::provider_pref(agent),
agent.model.as_deref(),
provider,
model,
) else {
if provider.is_some() {
let known = crate::providers::names(&app.config);
let hint = if known.is_empty() {
"aucun (am providers add <nom> --base-url <url>)".to_string()
} else {
known.join(", ")
};
bail!(crate::tr_fmt!(
"provider '{}' inconnu — providers enregistrés: {}",
provider.unwrap_or(""),
hint
));
}
return Ok(None);
};
let mut args: Vec<String> = Vec::new();
let mut env: BTreeMap<String, String> = BTreeMap::new();
// env_map resolved against THIS provider (api_key stays @secret).
crate::agent_config::apply_env_map_resolved(
&mut env,
agent,
&app.config,
Some(pname),
resolved_model,
);
// The model through the agent's declared channel (model_env > model_arg);
// otherwise the env_map "model" slot already carried it.
if let Some(m) = resolved_model {
if let Some(var) = &agent.model_env {
env.insert(var.clone(), m.to_string());
} else if let Some(flag) = &agent.model_arg {
args.push(flag.clone());
args.push(m.to_string());
}
}
app.log.info(&crate::tr_fmt!(
"provider {} (modèle {}) au lancement de {}",
pname,
resolved_model.unwrap_or("-"),
agent.name
));
Ok(Some((args, env)))
}
/// Seconds elapsed since an RFC 3339 timestamp (None when unparsable). /// Seconds elapsed since an RFC 3339 timestamp (None when unparsable).
fn seconds_since(ts: Option<&str>) -> Option<u64> { fn seconds_since(ts: Option<&str>) -> Option<u64> {
let t = chrono::DateTime::parse_from_rfc3339(ts?).ok()?; let t = chrono::DateTime::parse_from_rfc3339(ts?).ok()?;
@@ -483,3 +583,141 @@ fn seconds_since(ts: Option<&str>) -> Option<u64> {
.num_seconds(); .num_seconds();
Some(secs.max(0) as u64) Some(secs.max(0) as u64)
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{AgentConfig, AgentDef, Config};
use clap::Parser;
fn test_app(providers_yaml: &str) -> App {
let guard = tempfile::tempdir().unwrap();
let dir = guard.path().to_path_buf();
std::mem::forget(guard);
let cfg = dir.join("config.yaml");
std::fs::write(
&cfg,
format!(
"version: \"1.0\"\nsettings:\n auto_install_deps: false\n confirm_before_run: false\n{providers_yaml}agents: []\n"
),
)
.unwrap();
let cli = crate::cli::Cli::parse_from(["am", "--config", cfg.to_str().unwrap()]);
crate::app::App::from_cli(cli).unwrap()
}
fn agent(model_env: bool, model_arg: bool) -> AgentDef {
let mut env_map = BTreeMap::new();
env_map.insert("api_key".to_string(), "MY_KEY".to_string());
env_map.insert("model".to_string(), "MY_MODEL".to_string());
env_map.insert("base_url".to_string(), "MY_URL".to_string());
AgentDef {
name: "demo".to_string(),
display_name: None,
description: None,
category: None,
website: None,
install: None,
dependencies: vec![],
run: Some("demo".to_string()),
args: vec![],
env: BTreeMap::new(),
version: None,
pin_version: None,
model_env: model_env.then(|| "MY_MODEL".to_string()),
model_arg: model_arg.then(|| "--model".to_string()),
provider: None,
model: None,
config: Some(AgentConfig {
env_map,
files: vec![],
provider_default: None,
}),
tags: vec![],
installable: false,
note: None,
hidden: false,
platforms: vec![],
healthcheck: None,
container: None,
cost_model: None,
}
}
const OPENAI: &str = " default_provider: openai\n providers:\n openai:\n base_url: https://api.openai.com/v1\n default_model: gpt-5.2\n models: [gpt-5.2]\n";
#[test]
fn apply_provider_resolves_and_never_leaks_the_token() {
let app = test_app(OPENAI);
let (args, env) = apply_provider(&app, &agent(false, false), Some("openai"), Some("gpt-5.2"))
.unwrap()
.unwrap();
// The token slot is the @secret reference — never a value.
assert_eq!(env.get("MY_KEY").unwrap(), "@secret");
assert_eq!(env.get("MY_MODEL").unwrap(), "gpt-5.2");
assert_eq!(env.get("MY_URL").unwrap(), "https://api.openai.com/v1");
assert!(!env.values().any(|v| v.contains("sk-")), "{env:?}");
assert!(args.is_empty());
}
#[test]
fn apply_provider_uses_the_declared_model_channel() {
let app = test_app(OPENAI);
// model_env wins over the env_map slot.
let (args, env) = apply_provider(&app, &agent(true, false), Some("openai"), None)
.unwrap()
.unwrap();
assert_eq!(env.get("MY_MODEL").unwrap(), "gpt-5.2");
assert!(args.is_empty());
// model_arg is pushed on the command line.
let (args, _) = apply_provider(&app, &agent(false, true), Some("openai"), Some("gpt-5.2"))
.unwrap()
.unwrap();
assert_eq!(args, vec!["--model".to_string(), "gpt-5.2".to_string()]);
}
#[test]
fn apply_provider_unknown_is_an_error_listing_the_registry() {
let app = test_app(OPENAI);
let err = apply_provider(&app, &agent(false, false), Some("nope"), None).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("nope"), "{msg}");
assert!(msg.contains("openai"), "registry list missing: {msg}");
}
#[test]
fn apply_provider_falls_back_to_the_embedded_default_provider() {
// The embedded default catalog ships providers (anthropic★ since
// issue #90): even an empty user config resolves through the cloud.
let app = test_app("");
let (_, env) = apply_provider(&app, &agent(false, false), None, Some("x"))
.unwrap()
.expect("the embedded default provider resolves");
// Model "x" is unknown: the env_map model slot is skipped, but the
// provider slot is wired.
assert_eq!(env.get("MY_URL").unwrap(), "https://api.anthropic.com/v1");
assert_eq!(env.get("MY_KEY").unwrap(), "@secret");
}
#[test]
fn model_flag_falls_back_to_the_cloud_registry() {
let app = test_app(OPENAI);
// apply_model bails (no local runtime in the test env) → the caller
// falls back to apply_provider with the default provider.
let local = apply_model(&app, &agent(false, false), "gpt-5.2").unwrap_err();
assert!(local.to_string().contains("not found on any local runtime"));
let (_, env) = apply_provider(&app, &agent(false, false), None, Some("gpt-5.2"))
.unwrap()
.unwrap();
assert_eq!(env.get("MY_MODEL").unwrap(), "gpt-5.2");
assert_eq!(env.get("MY_URL").unwrap(), "https://api.openai.com/v1");
}
#[test]
fn config_parses_the_provider_registry_and_agents() {
let _cfg: Config = serde_yaml::from_str(&format!(
"version: \"1.0\"\nsettings:\n{OPENAI}agents: []\n"
))
.unwrap();
}
}
+15 -1
View File
@@ -156,6 +156,18 @@ const START_FLAGS: &[HelpFlag] = &[
value: "", value: "",
desc: "Run inside the agent's container profile (issue #58)", desc: "Run inside the agent's container profile (issue #58)",
}, },
HelpFlag {
short: "",
long: "--model",
value: "MODEL",
desc: "Model to use: local runtime first, then the provider registry (issue #71/#92)",
},
HelpFlag {
short: "",
long: "--provider",
value: "PROVIDER",
desc: "Provider of the registry to use (base_url + keyring token + model) (issue #92)",
},
]; ];
pub static HELP_SPECS: &[HelpSpec] = &[ pub static HELP_SPECS: &[HelpSpec] = &[
@@ -911,7 +923,8 @@ pub static HELP_SPECS: &[HelpSpec] = &[
about: "Run the agent command directly with the given arguments (no process management).", about: "Run the agent command directly with the given arguments (no process management).",
search_terms: &["exec", "pass-through", "container", "docker", "podman"], search_terms: &["exec", "pass-through", "container", "docker", "podman"],
flags: &[ flags: &[
HelpFlag { short: "", long: "--model", value: "MODEL", desc: "Local model to use (issue #71)" }, HelpFlag { short: "", long: "--model", value: "MODEL", desc: "Model to use: local runtime first, then the provider registry (issue #71/#92)" },
HelpFlag { short: "", long: "--provider", value: "PROVIDER", desc: "Provider of the registry to use (base_url + keyring token + model) (issue #92)" },
HelpFlag { short: "", long: "--container", value: "", desc: "Run inside the agent's container profile (issue #58)" }, HelpFlag { short: "", long: "--container", value: "", desc: "Run inside the agent's container profile (issue #58)" },
], ],
subcommands: &[], subcommands: &[],
@@ -922,6 +935,7 @@ pub static HELP_SPECS: &[HelpSpec] = &[
io: None, io: None,
examples: &[ examples: &[
HelpExample { desc: "Run an agent command directly.", code: "run claude-code" }, HelpExample { desc: "Run an agent command directly.", code: "run claude-code" },
HelpExample { desc: "Use a provider of the registry.", code: "run claude-code --provider anthropic --model claude-sonnet-4" },
HelpExample { desc: "Pass --help through to the agent (after --).", code: "run claude-code -- --help" }, HelpExample { desc: "Pass --help through to the agent (after --).", code: "run claude-code -- --help" },
], ],
}, },
+1
View File
@@ -185,6 +185,7 @@ pub const CATALOG: &[(&str, &str)] = &[
("aucun provider configuré — voir: am providers add <nom> --base-url <url>", "no provider configured — see: am providers add <name> --base-url <url>"), ("aucun provider configuré — voir: am providers add <nom> --base-url <url>", "no provider configured — see: am providers add <name> --base-url <url>"),
("modèle '{}' absent de la liste du provider '{}' — modèles: {}", "model '{}' is not in provider '{}' model list — models: {}"), ("modèle '{}' absent de la liste du provider '{}' — modèles: {}", "model '{}' is not in provider '{}' model list — models: {}"),
("configuré avec le provider {} (modèle {})", "configured with provider {} (model {})"), ("configuré avec le provider {} (modèle {})", "configured with provider {} (model {})"),
("provider {} (modèle {}) au lancement de {}", "provider {} (model {}) when launching {}"),
("agent sans adaptateur de config — voir sa doc", "agent without a config adapter — see its docs"), ("agent sans adaptateur de config — voir sa doc", "agent without a config adapter — see its docs"),
("config par variables d'env — injectée au démarrage", "env-var configuration — injected at start"), ("config par variables d'env — injectée au démarrage", "env-var configuration — injected at start"),
("aucun provider résolu — fichiers de config non écrits", "no provider resolved — config files not written"), ("aucun provider résolu — fichiers de config non écrits", "no provider resolved — config files not written"),
+6
View File
@@ -254,6 +254,7 @@ impl AmCompleter {
"--category", "--args", "--env", "--notify", "--timeout", "--yes", "--category", "--args", "--env", "--notify", "--timeout", "--yes",
"--verbose", "--quiet", "--json", "--dry-run", "--no-color", "--verbose", "--quiet", "--json", "--dry-run", "--no-color",
"--profile", "--installed", "--tag", "--output", "--random", "--profile", "--installed", "--tag", "--output", "--random",
"--model", "--provider", "--no-config",
], ],
agents, agents,
catalog_agents, catalog_agents,
@@ -1732,6 +1733,8 @@ fn handle_line(
agent: rest.first().cloned(), agent: rest.first().cloned(),
background: flag("--background") || flag("-b"), background: flag("--background") || flag("-b"),
profile: opt_value("--profile"), profile: opt_value("--profile"),
model: opt_value("--model"),
provider: opt_value("--provider"),
..Default::default() ..Default::default()
}), }),
"stop" => Command::Stop { "stop" => Command::Stop {
@@ -1744,6 +1747,8 @@ fn handle_line(
agent: rest.first().cloned(), agent: rest.first().cloned(),
background: flag("--background") || flag("-b"), background: flag("--background") || flag("-b"),
profile: opt_value("--profile"), profile: opt_value("--profile"),
model: opt_value("--model"),
provider: opt_value("--provider"),
..Default::default() ..Default::default()
}, },
force: flag("--force"), force: flag("--force"),
@@ -1839,6 +1844,7 @@ fn handle_line(
Command::Run { Command::Run {
agent, agent,
model: opt_value("--model"), model: opt_value("--model"),
provider: opt_value("--provider"),
container: flag("--container"), container: flag("--container"),
args: rest[1..].iter().map(|s| s.as_str().into()).collect(), args: rest[1..].iter().map(|s| s.as_str().into()).collect(),
} }
+2 -2
View File
@@ -45,7 +45,7 @@ agents:
#[test] #[test]
fn run_emits_one_run_event_with_exit_code() { fn run_emits_one_run_event_with_exit_code() {
let app = app_with_agent("run", &["run", "echo-agent"], ECHO_AGENT); let app = app_with_agent("run", &["run", "echo-agent"], ECHO_AGENT);
let code = run_cmd::run(&app, "echo-agent", None, false, &[OsString::from("hello")]).unwrap(); let code = run_cmd::run(&app, "echo-agent", None, None, false, &[OsString::from("hello")]).unwrap();
assert_eq!(code, 0); assert_eq!(code, 0);
let evs = events(&app); let evs = events(&app);
assert_eq!(evs.len(), 1, "exactly one event expected, got {:?}", evs); assert_eq!(evs.len(), 1, "exactly one event expected, got {:?}", evs);
@@ -58,7 +58,7 @@ fn run_emits_one_run_event_with_exit_code() {
#[test] #[test]
fn dry_run_writes_no_event() { fn dry_run_writes_no_event() {
let app = app_with_agent("dry", &["run", "echo-agent", "--dry-run"], ECHO_AGENT); let app = app_with_agent("dry", &["run", "echo-agent", "--dry-run"], ECHO_AGENT);
let code = run_cmd::run(&app, "echo-agent", None, false, &[]).unwrap(); let code = run_cmd::run(&app, "echo-agent", None, None, false, &[]).unwrap();
assert_eq!(code, 0); assert_eq!(code, 0);
assert!(events(&app).is_empty(), "dry-run must not append events"); assert!(events(&app).is_empty(), "dry-run must not append events");
assert!(events::journal_files(&app.events_dir()).is_empty()); assert!(events::journal_files(&app.events_dir()).is_empty());