#![forbid(unsafe_code)]
mod agent;
mod config;
mod defaults;
mod headless_runner;
mod memory;
mod services;
mod system;
mod tools;
mod tui;
use crate::agent::magi_wiring::{
resolve_magi_adapter_specs, static_override_notice, MagiEnvModels,
};
use crate::agent::provider::{build_openai_provider, AnthropicProvider, Provider, StaticProvider};
use crate::agent::Agent;
use crate::config::{
resolve_anthropic_model, resolve_openai_base_url, resolve_openai_model, resolve_provider,
HeadlessConfig, MagiConfig,
};
use crate::headless_runner::{resolve_run_timeout, run_consult, run_query};
use crate::memory::clock::SystemClock;
use crate::memory::embedding::OpenAiCompatibleEmbedder;
use crate::memory::store::SqliteVectorStore;
use crate::system::database::{EncryptedSqliteMemory, MemoryStore};
use crate::system::fs::{FileSystem, RealFileSystem};
use crate::system::grep::RipGrep;
use crate::system::workspace::Workspace;
use crate::tools::bash::BashTool;
use crate::tools::grep::GrepTool;
use crate::tools::knowledge::ProjectFactTool;
use crate::tools::ls::ListTool;
use crate::tools::read::FileReadTool;
use crate::tools::write::FileWriteTool;
use clap::Parser;
use cryptovault::CryptoVault;
use magi_core::orchestrator::{Magi, MagiBuilder};
use magi_rs::headless::exit::exit_code as headless_exit_code;
use magi_rs::headless::input::{parse_input, read_input_bounded, InputFormat};
use magi_rs::headless::limits::{HeadlessLimits, NORMAL_MAX_TOOL_CALLS};
use magi_rs::headless::log::{LogLevel, RunLog};
use magi_rs::headless::output::{write_json, write_text};
use magi_rs::headless::policy::{Policy, Tier};
use magi_rs::headless::resolution::{
resolve as resolve_params, CliOverrides, ConfigDefaults, Resolved,
};
use magi_rs::headless::types::{ErrorKind, RunOutcome, StopReason};
use magi_rs::headless::HeadlessError;
use magi_rs::vault::{
check_strength, create_passphrase, diagnose, format_diagnose_report, harden_process,
rekey_envelope, resolve_passphrase, run_vault_cmd, strip_trailing_newline, wire,
PassphrasePrompt, SecretStore, TtyIo, TtyPrompt, VaultCmd, VaultError, PASSPHRASE_ENV,
};
use std::env;
use std::io::Write as _;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use zeroize::Zeroizing;
type SharedSecretStore = Arc<Mutex<dyn SecretStore + Send>>;
#[derive(Clone)]
struct SecretArg(Zeroizing<String>);
impl std::ops::Deref for SecretArg {
type Target = str;
fn deref(&self) -> &str {
self.0.as_str()
}
}
impl std::fmt::Debug for SecretArg {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("SecretArg(<redacted>)")
}
}
fn parse_secret_arg(s: &str) -> Result<SecretArg, std::convert::Infallible> {
Ok(SecretArg(Zeroizing::new(s.to_string())))
}
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
#[arg(short, long)]
logout: bool,
#[arg(long)]
init_config: bool,
#[arg(short = 'p', long, global = true, value_parser = parse_secret_arg)]
passphrase: Option<SecretArg>,
#[command(subcommand)]
command: Option<TopCmd>,
}
#[derive(Clone, Copy, Debug, clap::ValueEnum)]
enum CliInputFormat {
Text,
Json,
}
impl CliInputFormat {
fn into_lib(self) -> InputFormat {
match self {
CliInputFormat::Text => InputFormat::Text,
CliInputFormat::Json => InputFormat::Json,
}
}
}
#[derive(Clone, Copy, Debug, clap::ValueEnum)]
enum CliOutputFormat {
Text,
Json,
}
#[derive(Clone, Copy, Debug, clap::ValueEnum)]
enum CliLogLevel {
Error,
Warn,
Info,
Debug,
}
impl CliLogLevel {
fn into_lib(self) -> LogLevel {
match self {
CliLogLevel::Error => LogLevel::Error,
CliLogLevel::Warn => LogLevel::Warn,
CliLogLevel::Info => LogLevel::Info,
CliLogLevel::Debug => LogLevel::Debug,
}
}
}
#[derive(clap::Args, Debug)]
struct HeadlessArgs {
#[arg(short = 'i', long)]
input: Option<PathBuf>,
#[arg(short = 'o', long)]
output: Option<PathBuf>,
#[arg(long, value_enum)]
input_format: Option<CliInputFormat>,
#[arg(long, value_enum)]
output_format: Option<CliOutputFormat>,
#[arg(short = 'w', long)]
workdir: Option<PathBuf>,
#[arg(long)]
no_memory: bool,
#[arg(long)]
auto: bool,
#[arg(long)]
full_auto: bool,
#[arg(long)]
timeout: Option<u64>,
#[arg(long, value_enum)]
log_level: Option<CliLogLevel>,
#[arg(long)]
log_dir: Option<PathBuf>,
#[arg(long)]
allow_system_override: bool,
#[arg(long)]
no_clobber: bool,
#[arg(long)]
consult: bool,
#[arg(long)]
model: Option<String>,
#[arg(long)]
provider: Option<String>,
#[arg(long)]
max_tool_calls: Option<u32>,
}
#[derive(clap::Subcommand, Debug)]
enum TopCmd {
#[command(subcommand)]
Vault(VaultCmd),
Init,
Query(HeadlessArgs),
Consult(HeadlessArgs),
}
#[derive(Debug)]
struct Config {
api_key: String,
model: String,
source: String,
}
pub(crate) use crate::defaults::DEFAULT_ANTHROPIC_MODEL as DEFAULT_MODEL;
const LOCAL_HOSTS: [&str; 4] = ["localhost", "127.0.0.1", "::1", "[::1]"];
fn is_localhost(base_url: &str) -> bool {
reqwest::Url::parse(base_url)
.ok()
.and_then(|url| url.host_str().map(|host| LOCAL_HOSTS.contains(&host)))
.unwrap_or(false)
}
fn magi_toml_exists(workspace: Option<&Workspace>) -> bool {
workspace.is_some_and(|ws| ws.config_path().exists())
}
fn discover_config(
config: &MagiConfig,
env_key: Option<&str>,
secret_store: Option<&SharedSecretStore>,
) -> Option<Config> {
let model = resolve_anthropic_model(config, env::var("ANTHROPIC_MODEL").ok().as_deref());
if let Some(key) = env_key {
return Some(Config {
api_key: key.trim().to_string(),
model,
source: "ENV".to_string(),
});
}
let ss = secret_store?;
let mut guard = ss.lock().unwrap_or_else(|p| p.into_inner());
let key = guard.get("ANTHROPIC_API_KEY").ok()?;
Some(Config {
api_key: key.as_str().trim().to_string(),
model,
source: "vault".to_string(),
})
}
fn resolve_openai_key(
env_key: Option<&str>,
secret_store: Option<&SharedSecretStore>,
) -> Option<String> {
if let Some(key) = env_key {
return Some(key.trim().to_string());
}
let ss = secret_store?;
let mut guard = ss.lock().unwrap_or_else(|p| p.into_inner());
guard
.get("OPENAI_API_KEY")
.ok()
.map(|z| z.as_str().trim().to_string())
}
const ANTHROPIC_KEY_ENV: &str = "ANTHROPIC_API_KEY";
const OPENAI_KEY_ENV: &str = "OPENAI_API_KEY";
const LEGACY_LAYOUT_WARNING: &str =
"warning: found a legacy .magi-rs-memory.db/magi.toml loose in this directory; \
the pre-.magi/ layout is no longer used — run `magi init` to create a .magi/ \
state directory (the legacy files are not read or migrated)";
struct ConsumedSecrets {
passphrase: Option<Zeroizing<String>>,
anthropic_key: Option<String>,
openai_key: Option<String>,
}
fn read_then_scrub_secret_env() -> ConsumedSecrets {
let passphrase = env::var(PASSPHRASE_ENV).ok().map(Zeroizing::new);
let anthropic_key = env::var(ANTHROPIC_KEY_ENV).ok();
let openai_key = env::var(OPENAI_KEY_ENV).ok();
env::remove_var(PASSPHRASE_ENV);
env::remove_var(ANTHROPIC_KEY_ENV);
env::remove_var(OPENAI_KEY_ENV);
ConsumedSecrets {
passphrase,
anthropic_key,
openai_key,
}
}
enum MemoryAttachment {
Encrypted(EncryptedSqliteMemory),
Ephemeral,
}
fn resolve_master_passphrase(
db_absent: bool,
passphrase_flag: Option<Zeroizing<String>>,
prompt: &mut dyn PassphrasePrompt,
) -> Result<Zeroizing<String>, VaultError> {
if !db_absent {
return resolve_passphrase(passphrase_flag, prompt);
}
if let Some(p) = passphrase_flag {
let p = strip_trailing_newline(p);
check_strength(p.as_str())?;
return Ok(p);
}
if !prompt.is_interactive() {
return Err(VaultError::PassphraseUnavailable);
}
create_passphrase(prompt, false)
}
fn is_wrong_passphrase(e: &anyhow::Error) -> bool {
matches!(
e.downcast_ref::<VaultError>(),
Some(VaultError::WrongPassphrase)
)
}
fn open_tui_memory(
db_path: &std::path::Path,
passphrase_flag: Option<Zeroizing<String>>,
prompt: &mut dyn PassphrasePrompt,
notices: &mut Vec<String>,
) -> MemoryAttachment {
let db_absent = !db_path.exists();
let mut passphrase = match resolve_master_passphrase(db_absent, passphrase_flag, prompt) {
Ok(p) => p,
Err(e) => {
notices.push(format!(
"WARNING: {e}; running WITHOUT persistence for this session (any \
existing on-disk database is left untouched)."
));
return MemoryAttachment::Ephemeral;
}
};
loop {
match EncryptedSqliteMemory::new(db_path.to_path_buf(), passphrase) {
Ok(store) => return MemoryAttachment::Encrypted(store),
Err(e) => {
if !is_wrong_passphrase(&e) {
notices.push(format!(
"WARNING: could not open the encrypted database ({e}); \
running WITHOUT persistence for this session."
));
return MemoryAttachment::Ephemeral;
}
if !prompt.is_interactive() {
notices.push(
"WARNING: incorrect passphrase and no interactive terminal to \
retry; running WITHOUT persistence for this session."
.to_string(),
);
return MemoryAttachment::Ephemeral;
}
let retry_msg = "Incorrect passphrase (if this DB predates v0.9.0's \
keyring, there is no migration: delete it manually to start \
fresh). Passphrase: ";
match prompt.read_passphrase(retry_msg, false) {
Ok(p) if !p.is_empty() => passphrase = p,
_ => {
notices.push(
"WARNING: passphrase entry cancelled; running WITHOUT \
persistence for this session."
.to_string(),
);
return MemoryAttachment::Ephemeral;
}
}
}
}
}
}
fn vault_error_exit_code(e: &VaultError) -> i32 {
match e {
VaultError::Aborted
| VaultError::WrongPassphrase
| VaultError::PassphraseUnavailable
| VaultError::WeakPassphrase(_)
| VaultError::ValueTooLarge(_)
| VaultError::SecretNotFound(_)
| VaultError::VaultMetaCorrupt
| VaultError::DbCorrupt { .. } => 1,
VaultError::Crypto(_) | VaultError::Storage(_) | VaultError::Io(_) => 2,
}
}
fn report_open_failure(e: &anyhow::Error) -> i32 {
match e.downcast_ref::<VaultError>() {
Some(ve) => {
eprintln!("error: {ve}");
vault_error_exit_code(ve)
}
None => {
eprintln!("error: {e}");
2
}
}
}
fn require_workspace(cwd: &std::path::Path) -> Result<crate::system::workspace::Workspace, i32> {
match crate::system::workspace::discover(cwd) {
Ok(Some(ws)) => Ok(ws),
Ok(None) => {
eprintln!(
"error: no .magi/ state directory found in this directory or any \
parent; run `magi init` to create one"
);
Err(1)
}
Err(e) => {
eprintln!("error: {e}");
Err(headless_error_exit_code(&e))
}
}
}
fn run_vault_subcommand(
cmd: VaultCmd,
passphrase_flag: Option<Zeroizing<String>>,
workspace_root: &std::path::Path,
hardening_warnings: &[String],
) -> i32 {
let ws = match require_workspace(workspace_root) {
Ok(ws) => ws,
Err(code) => return code,
};
let db_path = ws.db_path();
let db_absent = !db_path.exists();
let mut prompt = TtyPrompt;
let passphrase = match resolve_master_passphrase(db_absent, passphrase_flag, &mut prompt) {
Ok(p) => p,
Err(e) => {
eprintln!("error: {e}");
return vault_error_exit_code(&e);
}
};
let current_passphrase = Zeroizing::new(passphrase.as_str().to_string());
let store = match EncryptedSqliteMemory::new(db_path, passphrase) {
Ok(s) => s,
Err(e) => return report_open_failure(&e),
};
let dek = match store.data_key() {
Ok(d) => d,
Err(e) => {
eprintln!("error: {e}");
return vault_error_exit_code(&e);
}
};
for w in hardening_warnings.iter().chain(dek.warnings().iter()) {
eprintln!("warning: {w}");
}
let conn = store.shared_conn();
let mut vault_store = match wire(conn.clone(), dek) {
Ok(v) => v,
Err(e) => {
eprintln!("error: {e}");
return vault_error_exit_code(&e);
}
};
let mut rekey = |new: &str| -> Result<(), VaultError> {
rekey_envelope(
&CryptoVault::default(),
&conn,
current_passphrase.as_str(),
new,
)
};
let mut io = TtyIo::new();
match run_vault_cmd(cmd, &mut vault_store, &mut io, &mut rekey) {
Ok(()) => 0,
Err(e) => {
eprintln!("error: {e}");
vault_error_exit_code(&e)
}
}
}
fn resolve_diagnose_db_path(
workspace_root: &std::path::Path,
) -> Result<std::path::PathBuf, magi_rs::headless::HeadlessError> {
match crate::system::workspace::discover(workspace_root)? {
Some(ws) => Ok(ws.db_path()),
None => Ok(workspace_root.join(".magi-rs-memory.db")),
}
}
fn run_vault_diagnose(workspace_root: &std::path::Path, names: bool) -> i32 {
let db_path = match resolve_diagnose_db_path(workspace_root) {
Ok(p) => p,
Err(e) => {
eprintln!("error: {e}");
return headless_error_exit_code(&e);
}
};
if !db_path.exists() {
println!("no database found at {}", db_path.display());
return 0;
}
let conn = match rusqlite::Connection::open_with_flags(
&db_path,
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY,
) {
Ok(c) => c,
Err(e) => {
eprintln!("error: storage error: {e}");
return vault_error_exit_code(&VaultError::Storage(e.to_string()));
}
};
match diagnose(&conn, names) {
Ok(report) => {
for line in format_diagnose_report(&report) {
println!("{line}");
}
0
}
Err(e) => {
eprintln!("error: {e}");
vault_error_exit_code(&e)
}
}
}
fn run_logout(passphrase_flag: Option<Zeroizing<String>>, workspace_root: &std::path::Path) -> i32 {
let db_path = match crate::system::workspace::discover(workspace_root) {
Ok(Some(ws)) => ws.db_path(),
Ok(None) => {
println!("no stored session");
return 0;
}
Err(e) => {
eprintln!("error: {e}");
return headless_error_exit_code(&e);
}
};
if !db_path.exists() {
println!("no stored session");
return 0;
}
let mut prompt = TtyPrompt;
let passphrase = match resolve_passphrase(passphrase_flag, &mut prompt) {
Ok(p) => p,
Err(e) => {
eprintln!("error: {e}");
return vault_error_exit_code(&e);
}
};
let store = match EncryptedSqliteMemory::new(db_path, passphrase) {
Ok(s) => s,
Err(e) => return report_open_failure(&e),
};
let dek = match store.data_key() {
Ok(d) => d,
Err(e) => {
eprintln!("error: {e}");
return vault_error_exit_code(&e);
}
};
let mut vault_store = match wire(store.shared_conn(), dek) {
Ok(v) => v,
Err(e) => {
eprintln!("error: {e}");
return vault_error_exit_code(&e);
}
};
match vault_store.remove("ANTHROPIC_API_KEY") {
Ok(()) => {
println!("Logged out successfully.");
0
}
Err(VaultError::SecretNotFound(_)) => {
println!("no stored session");
0
}
Err(e) => {
eprintln!("error: {e}");
vault_error_exit_code(&e)
}
}
}
fn headless_error_exit_code(e: &magi_rs::headless::HeadlessError) -> i32 {
use magi_rs::headless::HeadlessError;
match e {
HeadlessError::InputInvalid(_) | HeadlessError::InputTooLarge(_) => 2,
HeadlessError::Io(_)
| HeadlessError::Storage(_)
| HeadlessError::Aborted
| HeadlessError::PassphraseUnavailable
| HeadlessError::Db(_) => 1,
}
}
fn resolve_init_passphrase(
passphrase_flag: Option<Zeroizing<String>>,
) -> Result<Option<Zeroizing<String>>, VaultError> {
if let Some(p) = passphrase_flag {
let p = strip_trailing_newline(p);
check_strength(p.as_str())?;
return Ok(Some(p));
}
Ok(None)
}
fn run_init(cwd: &std::path::Path, passphrase_flag: Option<Zeroizing<String>>) -> i32 {
match crate::system::workspace::discover(cwd) {
Ok(Some(ws)) => {
eprintln!(
"error: an existing .magi/ was found at {}; use it, or run \
`magi init` from a different root (refusing to nest a second \
.magi/ inside it)",
ws.magi_dir.display()
);
return 1;
}
Ok(None) => {}
Err(e) => {
eprintln!("error: {e}");
return headless_error_exit_code(&e);
}
}
let bootstrap_passphrase = match resolve_init_passphrase(passphrase_flag) {
Ok(p) => p,
Err(e) => {
eprintln!("error: {e}");
return vault_error_exit_code(&e);
}
};
let ws = match crate::system::workspace::init(cwd) {
Ok(ws) => ws,
Err(e) => {
eprintln!("error: {e}");
return headless_error_exit_code(&e);
}
};
println!("{}", ws.magi_dir.display());
if let Some(passphrase) = bootstrap_passphrase {
if let Err(e) = crate::system::database::EncryptedSqliteMemory::open_with_state_machine(
ws.db_path(),
passphrase,
) {
eprintln!("error: {e}");
return vault_error_exit_code(&e);
}
}
0
}
fn exit_code(code: i32) -> ExitCode {
ExitCode::from(u8::try_from(code).unwrap_or(1))
}
fn bootstrap_headless<F, Fut>(body: F) -> ExitCode
where
F: FnOnce(ConsumedSecrets) -> Fut,
Fut: std::future::Future<Output = anyhow::Result<ExitCode>>,
{
let secrets = read_then_scrub_secret_env();
let runtime = match tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
{
Ok(rt) => rt,
Err(e) => {
eprintln!("error: failed to build the async runtime: {e}");
return ExitCode::FAILURE;
}
};
match runtime.block_on(body(secrets)) {
Ok(code) => code,
Err(e) => {
eprintln!("error: {e}");
ExitCode::FAILURE
}
}
}
fn main() -> ExitCode {
bootstrap_headless(run)
}
async fn run(secrets: ConsumedSecrets) -> anyhow::Result<ExitCode> {
let ConsumedSecrets {
passphrase: env_passphrase,
anthropic_key,
openai_key,
} = secrets;
let mut args = Args::parse();
let passphrase_flag: Option<Zeroizing<String>> = args
.passphrase
.take()
.map(|s| s.0)
.or_else(|| env_passphrase.filter(|p| !p.is_empty()));
let workspace_root = env::current_dir()?;
if crate::system::workspace::detect_legacy_files(&workspace_root) {
eprintln!("{LEGACY_LAYOUT_WARNING}");
}
let hardening_warnings = harden_process();
match args.command.take() {
Some(TopCmd::Vault(VaultCmd::Diagnose { names })) => {
return Ok(exit_code(run_vault_diagnose(&workspace_root, names)));
}
Some(TopCmd::Vault(cmd)) => {
return Ok(exit_code(run_vault_subcommand(
cmd,
passphrase_flag,
&workspace_root,
&hardening_warnings,
)));
}
Some(TopCmd::Init) => {
return Ok(exit_code(run_init(&workspace_root, passphrase_flag)));
}
Some(TopCmd::Query(h)) => {
return Ok(exit_code(
run_query_subcommand(
h,
passphrase_flag,
&workspace_root,
anthropic_key,
openai_key,
)
.await,
));
}
Some(TopCmd::Consult(h)) => {
return Ok(exit_code(
run_consult_subcommand(
h,
passphrase_flag,
&workspace_root,
anthropic_key,
openai_key,
)
.await,
));
}
None => {}
}
if args.logout {
return Ok(exit_code(run_logout(passphrase_flag, &workspace_root)));
}
if args.init_config {
match crate::defaults::write_default_config(&workspace_root) {
Ok(path) => {
println!("Wrote default config to {}", path.display());
return Ok(ExitCode::SUCCESS);
}
Err(e) => {
eprintln!("{e}");
return Ok(ExitCode::FAILURE);
}
}
}
let mut startup_notices: Vec<String> = hardening_warnings
.iter()
.map(|w| format!("warning: {w}"))
.collect();
let workspace = match crate::system::workspace::discover(&workspace_root) {
Ok(ws) => ws,
Err(e) => {
startup_notices.push(format!(
"WARNING: could not resolve the .magi/ state directory ({e}); \
running WITHOUT persistence for this session."
));
None
}
};
let mut prompt = TtyPrompt;
let attachment = match workspace.as_ref() {
Some(ws) => open_tui_memory(
&ws.db_path(),
passphrase_flag,
&mut prompt,
&mut startup_notices,
),
None => {
startup_notices.push(
"WARNING: no .magi/ state directory found — running WITHOUT \
persistence. Run `magi init` to create one and enable saved \
history (any existing on-disk database is left untouched)."
.to_string(),
);
MemoryAttachment::Ephemeral
}
};
let (memory_store, secret_store): (Option<EncryptedSqliteMemory>, Option<SharedSecretStore>) =
match attachment {
MemoryAttachment::Encrypted(store) => match store.data_key() {
Ok(dek) => {
for w in dek.warnings() {
startup_notices.push(format!("warning: {w}"));
}
match wire(store.shared_conn(), dek) {
Ok(vstore) => (
Some(store),
Some(Arc::new(Mutex::new(vstore)) as SharedSecretStore),
),
Err(e) => {
startup_notices.push(format!(
"WARNING: could not open the secret vault ({e}); \
ANTHROPIC_API_KEY/OPENAI_API_KEY must come from the \
environment this session."
));
(Some(store), None)
}
}
}
Err(e) => {
startup_notices.push(format!(
"WARNING: could not derive the vault key ({e}); \
ANTHROPIC_API_KEY/OPENAI_API_KEY must come from the \
environment this session."
));
(Some(store), None)
}
},
MemoryAttachment::Ephemeral => (None, None),
};
let (magi_config, config_warning) = match workspace.as_ref() {
Some(ws) => MagiConfig::load(&ws.magi_dir),
None => (MagiConfig::default(), None),
};
let config = discover_config(
&magi_config,
anthropic_key.as_deref(),
secret_store.as_ref(),
);
let provider_kind = resolve_provider(&magi_config, env::var("MAGI_PROVIDER").ok().as_deref());
let mut oai_creds: Option<(String, String)> = None;
let (provider, provider_info, model_label): (Arc<dyn Provider>, String, String) =
if provider_kind == "openai" {
let api_key = resolve_openai_key(openai_key.as_deref(), secret_store.as_ref())
.unwrap_or_else(|| "ollama".to_string());
let base_url =
resolve_openai_base_url(&magi_config, env::var("OPENAI_BASE_URL").ok().as_deref());
let model =
resolve_openai_model(&magi_config, env::var("OPENAI_MODEL").ok().as_deref());
let info = format!("OpenAI-compatible ({base_url}) Model: {model}");
let model_label = model.clone();
oai_creds = Some((base_url.clone(), api_key.clone()));
(
build_openai_provider(&base_url, &api_key, &model),
info,
model_label,
)
} else if let Some(ref c) = config {
(
Arc::new(AnthropicProvider::new(c.api_key.clone(), c.model.clone())),
format!("Magi API ({}) Model: {}", c.source, c.model),
c.model.clone(),
)
} else {
(
Arc::new(StaticProvider),
"Static Mode: no API key found. Set ANTHROPIC_API_KEY or run \
`magi-rs vault set ANTHROPIC_API_KEY` (recommended). /login \
(OAuth) is best-effort and may be rate-limited."
.to_string(),
"static".to_string(),
)
};
startup_notices.push(provider_info);
if let Some(w) = config_warning {
startup_notices.push(w);
}
if let Err(e) = magi_config.memory.validate() {
startup_notices.push(format!("memory config warning: {e}"));
}
if let Err(e) = magi_config.embedding.validate() {
startup_notices.push(format!("embedding config warning: {e}"));
}
if crate::defaults::should_emit_default_notice(
&provider_kind,
magi_toml_exists(workspace.as_ref()),
) {
startup_notices.push(crate::defaults::no_config_notice());
}
let backend_label = if provider_kind == "openai" {
"openai"
} else {
"anthropic"
};
let consult_magi: Option<Arc<Magi>> = if provider.is_static() {
let specs = resolve_magi_adapter_specs(
backend_label,
&magi_config.magi,
&MagiEnvModels {
melchior: env::var("MAGI_MODEL_MELCHIOR").ok(),
balthasar: env::var("MAGI_MODEL_BALTHASAR").ok(),
caspar: env::var("MAGI_MODEL_CASPAR").ok(),
},
);
if let Some(notice) = static_override_notice(true, !specs.is_empty()) {
startup_notices.push(notice);
}
None
} else {
Some(build_magi_orchestrator(
provider.clone(),
backend_label,
&model_label,
&magi_config,
oai_creds,
config.as_ref().map(|c| c.api_key.clone()),
)?)
};
let mut agent = Agent::new(provider);
match memory_store {
Some(concrete_store) => {
let embed_key = resolve_openai_key(openai_key.as_deref(), secret_store.as_ref());
attach_persistent_memory(
&mut agent,
concrete_store,
&magi_config,
embed_key,
&mut startup_notices,
)
.await?;
}
None => {
startup_notices.push(
"WARNING: this session runs WITHOUT persistence — your conversation and \
project knowledge will NOT be saved (any existing on-disk database is left \
untouched). Provide the vault passphrase (-p, MAGI_PASSPHRASE, or the \
interactive prompt) to restore persistence."
.to_string(),
);
}
}
let fs: Arc<dyn FileSystem> = Arc::new(RealFileSystem::new());
agent.register_tool(Box::new(ListTool::new(fs.clone(), workspace_root.clone())?));
agent.register_tool(Box::new(FileReadTool::new(
fs.clone(),
workspace_root.clone(),
)?));
agent.register_tool(Box::new(FileWriteTool::new(
fs.clone(),
workspace_root.clone(),
)?));
agent.register_tool(Box::new(GrepTool::new(
Box::new(RipGrep::new("rg")),
workspace_root.clone(),
)?));
agent.register_tool(Box::new(BashTool::new(workspace_root.clone())?));
if let Some(ref magi) = consult_magi {
agent.register_tool(Box::new(crate::tools::consult::ConsultTool::new(
magi.clone(),
magi_config.magi.auto_approve,
)));
}
crate::tui::run_tui_ext(
agent,
startup_notices,
consult_magi,
workspace_root,
magi_config.magi.auto_approve,
secret_store,
)
.await?;
Ok(ExitCode::SUCCESS)
}
fn build_magi_orchestrator(
provider: Arc<dyn Provider>,
backend_label: &str,
model_label: &str,
magi_config: &MagiConfig,
oai_creds: Option<(String, String)>,
anthropic_key: Option<String>,
) -> anyhow::Result<Arc<Magi>> {
let env_models = MagiEnvModels {
melchior: env::var("MAGI_MODEL_MELCHIOR").ok(),
balthasar: env::var("MAGI_MODEL_BALTHASAR").ok(),
caspar: env::var("MAGI_MODEL_CASPAR").ok(),
};
let specs = resolve_magi_adapter_specs(backend_label, &magi_config.magi, &env_models);
let build_sibling = |model: &str| -> Option<Arc<dyn Provider>> {
if backend_label == "openai" {
oai_creds
.as_ref()
.map(|(b, k)| build_openai_provider(b, k, model))
} else {
anthropic_key.as_ref().map(|k| {
Arc::new(AnthropicProvider::new(k.clone(), model.to_string())) as Arc<dyn Provider>
})
}
};
let default_adapter = crate::agent::magi_adapter::MagiCoreProviderAdapter::new(
provider,
backend_label,
model_label,
);
if specs.is_empty() {
return Ok(Arc::new(Magi::new(Arc::new(default_adapter))));
}
let mut builder = MagiBuilder::new(Arc::new(default_adapter));
for spec in &specs {
if let Some(sibling) = build_sibling(&spec.model) {
let adapter = crate::agent::magi_adapter::MagiCoreProviderAdapter::new(
sibling,
spec.adapter_name.clone(),
spec.model.clone(),
);
builder = builder.with_provider(spec.agent, Arc::new(adapter));
}
}
Ok(Arc::new(builder.build().map_err(|e| {
anyhow::anyhow!("MAGI builder failed: {e}")
})?))
}
async fn attach_persistent_memory(
agent: &mut Agent,
concrete_store: EncryptedSqliteMemory,
magi_config: &MagiConfig,
embed_key: Option<String>,
notices: &mut Vec<String>,
) -> anyhow::Result<()> {
let vstore_result = concrete_store
.data_key()
.map_err(|e| crate::memory::error::MemoryError::Crypto(e.to_string()))
.and_then(|dek| SqliteVectorStore::new(concrete_store.shared_conn(), dek));
let memory: Arc<dyn MemoryStore> = Arc::new(concrete_store);
let sessions = memory.list_sessions().await?;
let session_id = if let Some((id, _)) = sessions.first() {
id.clone()
} else {
memory.create_session("default").await?
};
agent.set_memory(memory.clone(), session_id);
let _ = agent.load_history().await;
if let Ok(vstore) = vstore_result {
match OpenAiCompatibleEmbedder::new(&magi_config.embedding, embed_key) {
Err(err) => {
notices.push(format!(
"embedding client init failed ({err}); \
memory subsystem disabled (text-only persistence)"
));
}
Ok(embedder_inner) => {
let embedder = Arc::new(embedder_inner);
let clock = Arc::new(SystemClock);
let vstore = Arc::new(vstore);
let vstore_diag = Arc::clone(&vstore);
agent.set_memory_subsystem(vstore, embedder, clock, magi_config.memory.clone());
agent.on_session_open().await.ok();
if let Ok(d) = vstore_diag.diagnostics("root").await {
notices.push(format!(
"memory: {} active, {} archived, {} pending re-embed (~{} KB index)",
d.active_count,
d.archived_count,
d.pending_reembed_count,
d.ram_estimate_bytes / 1024,
));
}
if magi_config.memory.distill_enabled
&& !is_localhost(&magi_config.embedding.base_url)
{
notices.push(format!(
"Memory distiller will send bounded memory batches \
(≤ {} tokens) to {} — set distill_enabled = false \
in [memory] for zero cloud memory egress.",
magi_config.memory.distill_max_batch_tokens, magi_config.embedding.base_url,
));
}
}
}
}
agent.register_tool(Box::new(ProjectFactTool::new(memory.clone())));
Ok(())
}
fn open_headless_memory(
db_path: &Path,
passphrase: Zeroizing<String>,
) -> Result<Option<EncryptedSqliteMemory>, HeadlessError> {
match EncryptedSqliteMemory::new(db_path.to_path_buf(), passphrase) {
Ok(store) => Ok(Some(store)),
Err(e) => Err(match e.downcast::<VaultError>() {
Ok(ve) => HeadlessError::from(ve),
Err(other) => HeadlessError::Storage(other.to_string()),
}),
}
}
fn read_headless_input(
input: Option<&Path>,
max_input_bytes: usize,
) -> Result<Vec<u8>, HeadlessError> {
match input {
Some(path) => {
let file = std::fs::File::open(path).map_err(|e| HeadlessError::Io(e.to_string()))?;
read_input_bounded(file, max_input_bytes)
}
None => read_input_bounded(std::io::stdin().lock(), max_input_bytes),
}
}
fn headless_error_for_exit(kind: ErrorKind) -> HeadlessError {
match kind {
ErrorKind::InputInvalid => HeadlessError::InputInvalid(String::new()),
ErrorKind::PassphraseUnavailable => HeadlessError::PassphraseUnavailable,
ErrorKind::TierDenied => HeadlessError::Io(String::new()),
ErrorKind::DbCorrupt
| ErrorKind::WrongPassphrase
| ErrorKind::Provider
| ErrorKind::Timeout
| ErrorKind::Runtime => HeadlessError::Io(String::new()),
}
}
const EXIT_TIER_DENIED: i32 = 3;
fn exit_code_for_outcome(outcome: &RunOutcome) -> i32 {
if outcome
.error
.as_ref()
.is_some_and(|e| e.kind == ErrorKind::TierDenied)
{
return EXIT_TIER_DENIED;
}
let exit_err = outcome
.error
.as_ref()
.map(|e| headless_error_for_exit(e.kind));
let response_empty = outcome.response.as_deref().is_none_or(str::is_empty);
let tier_denied = outcome.stop_reason == StopReason::Denied;
headless_exit_code(
exit_err.as_ref(),
outcome.stop_reason,
response_empty,
tier_denied,
)
}
fn finish_no_clobber_write(
path: &Path,
write_result: std::io::Result<()>,
) -> Result<(), HeadlessError> {
write_result.map_err(|e| {
let _ = std::fs::remove_file(path);
HeadlessError::Io(e.to_string())
})
}
fn write_output_atomic(
path: &Path,
contents: &[u8],
no_clobber: bool,
) -> Result<(), HeadlessError> {
let parent = match path.parent() {
Some(p) if !p.as_os_str().is_empty() => p.to_path_buf(),
_ => PathBuf::from("."),
};
if !parent.exists() {
return Err(HeadlessError::InputInvalid(format!(
"output parent directory does not exist: {}",
parent.display()
)));
}
if no_clobber {
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path)
{
Ok(mut f) => finish_no_clobber_write(path, f.write_all(contents)),
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
Err(HeadlessError::InputInvalid(format!(
"output file already exists (--no-clobber): {}",
path.display()
)))
}
Err(e) => Err(HeadlessError::Io(e.to_string())),
}
} else {
let tmp = parent.join(format!(".magi-out.tmp.{:016x}", rand::random::<u64>()));
std::fs::write(&tmp, contents).map_err(|e| HeadlessError::Io(e.to_string()))?;
std::fs::rename(&tmp, path).map_err(|e| {
let _ = std::fs::remove_file(&tmp);
HeadlessError::Io(e.to_string())
})
}
}
fn write_headless_output(
h: &HeadlessArgs,
outcome: &RunOutcome,
out_json: bool,
tool_result_cap: usize,
) -> Result<(), HeadlessError> {
if let Some(path) = &h.output {
let mut buf: Vec<u8> = Vec::new();
if out_json {
write_json(&mut buf, outcome, tool_result_cap)?;
} else {
write_text(&mut buf, &mut std::io::stderr(), outcome);
}
write_output_atomic(path, &buf, h.no_clobber)
} else {
let stdout = std::io::stdout();
let mut out = stdout.lock();
if out_json {
write_json(&mut out, outcome, tool_result_cap)?;
} else {
write_text(&mut out, &mut std::io::stderr(), outcome);
}
out.flush().map_err(|e| HeadlessError::Io(e.to_string()))
}
}
fn finish_headless(h: &HeadlessArgs, outcome: &RunOutcome, tool_result_cap: usize) -> i32 {
let out_json = matches!(h.output_format, Some(CliOutputFormat::Json));
if let Err(e) = write_headless_output(h, outcome, out_json, tool_result_cap) {
eprintln!("error: {e}");
return headless_error_exit_code(&e);
}
exit_code_for_outcome(outcome)
}
fn resolve_log_level(
cli: Option<CliLogLevel>,
cfg: Option<&str>,
) -> Result<LogLevel, HeadlessError> {
if let Some(l) = cli {
return Ok(l.into_lib());
}
match cfg {
Some(s) => s.parse(),
None => Ok(LogLevel::Info),
}
}
fn build_run_log(
h: &HeadlessArgs,
workspace: Option<&Workspace>,
limits: &HeadlessLimits,
log_level_cfg: Option<&str>,
) -> Result<Option<RunLog>, HeadlessError> {
let level = resolve_log_level(h.log_level, log_level_cfg)?;
let logs_dir = if let Some(d) = &h.log_dir {
Some(d.clone())
} else if h.no_memory {
None
} else {
workspace.map(Workspace::logs_dir)
};
let Some(dir) = logs_dir else {
return Ok(None);
};
match RunLog::start(
&dir,
level,
limits.log_retention_runs,
limits.log_max_bytes,
limits.tool_result_cap,
) {
Ok(log) => Ok(Some(log)),
Err(e) => {
eprintln!("warning: could not start the run log ({e}); continuing without it");
Ok(None)
}
}
}
fn tier_from_flags(auto: bool, full_auto: bool) -> Tier {
if full_auto {
Tier::FullAuto
} else if auto {
Tier::Auto
} else {
Tier::Default
}
}
struct HeadlessContext {
workdir: PathBuf,
memory: Option<EncryptedSqliteMemory>,
magi_config: MagiConfig,
provider: Arc<dyn Provider>,
provider_kind: String,
model_label: String,
oai_creds: Option<(String, String)>,
anthropic_key_for_siblings: Option<String>,
resolved: Resolved,
prompt: String,
tier: Tier,
embed_key: Option<String>,
run_log: Option<RunLog>,
limits: HeadlessLimits,
}
fn resolve_allow_system_override(flag: bool, cfg: Option<bool>) -> bool {
flag || cfg.unwrap_or(false)
}
const MAX_SAFE_INPUT_BYTES: usize = usize::MAX - 1;
fn resolve_headless_limits(cfg: &HeadlessConfig) -> HeadlessLimits {
let d = HeadlessLimits::default();
HeadlessLimits {
max_input_bytes: cfg
.max_input_bytes
.unwrap_or(d.max_input_bytes)
.min(MAX_SAFE_INPUT_BYTES),
full_auto_max_tool_calls: cfg
.full_auto_max_tool_calls
.unwrap_or(d.full_auto_max_tool_calls),
log_retention_runs: cfg.log_retention.unwrap_or(d.log_retention_runs),
log_max_bytes: cfg.log_max_bytes.unwrap_or(d.log_max_bytes),
tool_result_cap: cfg.tool_result_cap_bytes.unwrap_or(d.tool_result_cap),
full_auto_timeout_secs: cfg.timeout_secs.unwrap_or(d.full_auto_timeout_secs),
}
}
async fn prepare_headless(
h: &HeadlessArgs,
mut passphrase_flag: Option<Zeroizing<String>>,
cwd: &Path,
anthropic_key: Option<String>,
openai_key: Option<String>,
) -> Result<HeadlessContext, i32> {
let workdir = h.workdir.clone().unwrap_or_else(|| cwd.to_path_buf());
let workspace = match crate::system::workspace::discover(&workdir) {
Ok(ws) => ws,
Err(e) => {
eprintln!("error: {e}");
return Err(headless_error_exit_code(&e));
}
};
if workspace.is_none() && !h.no_memory {
eprintln!(
"error: no .magi/ state directory found in this directory or any \
parent; run `magi init` to create one"
);
return Err(1);
}
let db_path = workspace
.as_ref()
.map(Workspace::db_path)
.filter(|p| p.exists());
let memory = if let Some(db) = &db_path {
if let Some(pass) = passphrase_flag.take() {
match open_headless_memory(db, pass) {
Ok(m) => m,
Err(e) => {
eprintln!("error: {e}");
return Err(headless_error_exit_code(&e));
}
}
} else if !h.no_memory {
let e = HeadlessError::PassphraseUnavailable;
eprintln!("error: {e}");
return Err(headless_error_exit_code(&e));
} else {
None
}
} else {
None
};
let secret_store: Option<SharedSecretStore> = memory.as_ref().and_then(|store| {
let dek = store.data_key().ok()?;
let vstore = wire(store.shared_conn(), dek).ok()?;
Some(Arc::new(Mutex::new(vstore)) as SharedSecretStore)
});
let (magi_config, cfg_warn) = match workspace.as_ref() {
Some(ws) => MagiConfig::load(&ws.magi_dir),
None => (MagiConfig::default(), None),
};
if let Some(w) = cfg_warn {
eprintln!("warning: {w}");
}
let limits = resolve_headless_limits(&magi_config.headless);
let bytes = match read_headless_input(h.input.as_deref(), limits.max_input_bytes) {
Ok(b) => b,
Err(e) => {
eprintln!("error: {e}");
return Err(headless_error_exit_code(&e));
}
};
let forced_fmt = h.input_format.map(CliInputFormat::into_lib);
let envelope = match parse_input(&bytes, forced_fmt) {
Ok(e) => e,
Err(e) => {
eprintln!("error: {e}");
return Err(headless_error_exit_code(&e));
}
};
let prompt = envelope.prompt.clone();
let default_provider =
resolve_provider(&magi_config, env::var("MAGI_PROVIDER").ok().as_deref());
let effective_provider = h
.provider
.clone()
.or_else(|| envelope.provider.clone())
.unwrap_or_else(|| default_provider.clone());
let default_model = if effective_provider == "openai" {
resolve_openai_model(&magi_config, env::var("OPENAI_MODEL").ok().as_deref())
} else {
resolve_anthropic_model(&magi_config, env::var("ANTHROPIC_MODEL").ok().as_deref())
};
let defaults = ConfigDefaults {
model: default_model,
provider: default_provider,
max_tool_calls: None,
consult: None,
system: String::new(),
};
let overrides = CliOverrides {
model: h.model.clone(),
provider: h.provider.clone(),
max_tool_calls: h.max_tool_calls,
consult: if h.consult { Some(true) } else { None },
};
let tier = tier_from_flags(h.auto, h.full_auto);
let operator_ceiling = h
.max_tool_calls
.or(if h.full_auto {
Some(limits.full_auto_max_tool_calls)
} else {
None
})
.or(defaults.max_tool_calls)
.unwrap_or(NORMAL_MAX_TOOL_CALLS);
let allow_system_override = resolve_allow_system_override(
h.allow_system_override,
magi_config.headless.allow_system_override,
);
let resolved = resolve_params(
envelope,
&defaults,
&overrides,
operator_ceiling,
allow_system_override,
);
let provider_kind = resolved.provider.clone();
let mut oai_creds: Option<(String, String)> = None;
let (provider, model_label, anthropic_key_for_siblings): (
Arc<dyn Provider>,
String,
Option<String>,
) = if provider_kind == "openai" {
let api_key = resolve_openai_key(openai_key.as_deref(), secret_store.as_ref())
.unwrap_or_else(|| "ollama".to_string());
let base_url =
resolve_openai_base_url(&magi_config, env::var("OPENAI_BASE_URL").ok().as_deref());
oai_creds = Some((base_url.clone(), api_key.clone()));
(
build_openai_provider(&base_url, &api_key, &resolved.model),
resolved.model.clone(),
None,
)
} else if let Some(cfg) = discover_config(
&magi_config,
anthropic_key.as_deref(),
secret_store.as_ref(),
) {
let key = cfg.api_key;
(
Arc::new(AnthropicProvider::new(key.clone(), resolved.model.clone())),
resolved.model.clone(),
Some(key),
)
} else {
(Arc::new(StaticProvider), "static".to_string(), None)
};
let embed_key = resolve_openai_key(openai_key.as_deref(), secret_store.as_ref());
let run_log = match build_run_log(
h,
workspace.as_ref(),
&limits,
magi_config.headless.log_level.as_deref(),
) {
Ok(log) => log,
Err(e) => {
eprintln!("error: {e}");
return Err(headless_error_exit_code(&e));
}
};
Ok(HeadlessContext {
workdir,
memory,
magi_config,
provider,
provider_kind,
model_label,
oai_creds,
anthropic_key_for_siblings,
resolved,
prompt,
tier,
embed_key,
run_log,
limits,
})
}
fn register_headless_tools(agent: &mut Agent, workdir: &Path) -> anyhow::Result<()> {
let fs: Arc<dyn FileSystem> = Arc::new(RealFileSystem::new());
agent.register_tool(Box::new(ListTool::new(fs.clone(), workdir.to_path_buf())?));
agent.register_tool(Box::new(FileReadTool::new(
fs.clone(),
workdir.to_path_buf(),
)?));
agent.register_tool(Box::new(FileWriteTool::new(
fs.clone(),
workdir.to_path_buf(),
)?));
agent.register_tool(Box::new(GrepTool::new(
Box::new(RipGrep::new("rg")),
workdir.to_path_buf(),
)?));
agent.register_tool(Box::new(BashTool::new(workdir.to_path_buf())?));
Ok(())
}
async fn run_query_subcommand(
h: HeadlessArgs,
passphrase_flag: Option<Zeroizing<String>>,
cwd: &Path,
anthropic_key: Option<String>,
openai_key: Option<String>,
) -> i32 {
let ctx = match prepare_headless(&h, passphrase_flag, cwd, anthropic_key, openai_key).await {
Ok(c) => c,
Err(code) => return code,
};
let HeadlessContext {
workdir,
magi_config,
provider,
provider_kind,
model_label,
oai_creds,
anthropic_key_for_siblings,
resolved,
prompt,
tier,
embed_key,
memory,
mut run_log,
limits,
..
} = ctx;
let backend_label = if provider_kind == "openai" {
"openai"
} else {
"anthropic"
};
let consult_magi: Option<Arc<Magi>> = if provider.is_static() {
None
} else {
match build_magi_orchestrator(
provider.clone(),
backend_label,
&model_label,
&magi_config,
oai_creds,
anthropic_key_for_siblings,
) {
Ok(m) => Some(m),
Err(e) => {
eprintln!("error: {e}");
return 1;
}
}
};
let mut agent = Agent::new(provider);
if !h.no_memory {
if let Some(store) = memory {
let mut notices = Vec::new();
if let Err(e) =
attach_persistent_memory(&mut agent, store, &magi_config, embed_key, &mut notices)
.await
{
eprintln!("error: {e}");
return 1;
}
for n in notices {
eprintln!("note: {n}");
}
}
}
if let Err(e) = register_headless_tools(&mut agent, &workdir) {
eprintln!("error: {e}");
return 1;
}
if let Some(ref magi) = consult_magi {
agent.register_tool(Box::new(crate::tools::consult::ConsultTool::new(
magi.clone(),
magi_config.magi.auto_approve,
)));
}
let policy = Policy::new(tier, resolved.max_tool_calls, h.timeout);
let timeout = resolve_run_timeout(&policy, limits.full_auto_timeout_secs);
let outcome = run_query(
resolved,
policy,
&mut agent,
&prompt,
timeout,
run_log.as_mut(),
)
.await;
finish_headless(&h, &outcome, limits.tool_result_cap)
}
async fn run_consult_subcommand(
h: HeadlessArgs,
passphrase_flag: Option<Zeroizing<String>>,
cwd: &Path,
anthropic_key: Option<String>,
openai_key: Option<String>,
) -> i32 {
let ctx = match prepare_headless(&h, passphrase_flag, cwd, anthropic_key, openai_key).await {
Ok(c) => c,
Err(code) => return code,
};
let HeadlessContext {
magi_config,
provider,
provider_kind,
model_label,
oai_creds,
anthropic_key_for_siblings,
resolved,
prompt,
mut run_log,
limits,
..
} = ctx;
let backend_label = if provider_kind == "openai" {
"openai"
} else {
"anthropic"
};
let magi = match build_magi_orchestrator(
provider,
backend_label,
&model_label,
&magi_config,
oai_creds,
anthropic_key_for_siblings,
) {
Ok(m) => m,
Err(e) => {
eprintln!("error: {e}");
return 1;
}
};
let timeout = h.timeout.map(Duration::from_secs);
let outcome = run_consult(resolved, magi, &prompt, timeout, run_log.as_mut()).await;
finish_headless(&h, &outcome, limits.tool_result_cap)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::agent::messages::Message;
use magi_rs::vault::MaskedDek;
#[test]
fn test_is_localhost_rejects_hostname_containing_localhost_substring() {
assert!(!is_localhost("https://notlocalhost.evil.com"));
assert!(!is_localhost("http://127.0.0.1.evil.com/v1"));
}
#[test]
fn test_is_localhost_accepts_canonical_local_hosts() {
assert!(is_localhost("http://localhost:11434/v1"));
assert!(is_localhost("http://127.0.0.1:11434/v1"));
assert!(is_localhost("http://[::1]:11434/v1"));
}
#[test]
fn test_is_localhost_treats_unparseable_url_as_non_local() {
assert!(!is_localhost("not a url"));
assert!(!is_localhost(""));
}
#[test]
fn test_resolve_headless_limits_applies_config_overrides() {
let cfg = HeadlessConfig {
max_input_bytes: Some(2048),
full_auto_max_tool_calls: Some(30),
log_retention: Some(7),
log_max_bytes: Some(1024),
tool_result_cap_bytes: Some(4096),
timeout_secs: Some(120),
..Default::default()
};
let limits = resolve_headless_limits(&cfg);
assert_eq!(limits.max_input_bytes, 2048);
assert_eq!(limits.full_auto_max_tool_calls, 30);
assert_eq!(limits.log_retention_runs, 7);
assert_eq!(limits.log_max_bytes, 1024);
assert_eq!(limits.tool_result_cap, 4096);
assert_eq!(limits.full_auto_timeout_secs, 120);
}
#[test]
fn test_resolve_headless_limits_defaults_when_unset() {
let limits = resolve_headless_limits(&HeadlessConfig::default());
assert_eq!(limits, HeadlessLimits::default());
}
#[test]
fn test_resolve_headless_limits_clamps_max_input_bytes_below_usize_max() {
let cfg = HeadlessConfig {
max_input_bytes: Some(usize::MAX),
..Default::default()
};
let limits = resolve_headless_limits(&cfg);
assert_eq!(
limits.max_input_bytes,
usize::MAX - 1,
"usize::MAX must be clamped so the downstream +1 cannot overflow"
);
}
#[test]
fn test_resolve_headless_limits_leaves_normal_max_input_bytes_unclamped() {
let cfg = HeadlessConfig {
max_input_bytes: Some(2048),
..Default::default()
};
let limits = resolve_headless_limits(&cfg);
assert_eq!(limits.max_input_bytes, 2048);
}
#[test]
fn test_read_headless_input_enforces_custom_effective_cap_from_file() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("prompt.txt");
std::fs::write(&path, vec![b'x'; 11]).expect("write fixture");
let small_cap = 10usize;
let err = read_headless_input(Some(&path), small_cap)
.expect_err("11 bytes must exceed the custom 10-byte cap");
assert!(matches!(err, HeadlessError::InputTooLarge(limit) if limit == small_cap));
let path_ok = dir.path().join("prompt_ok.txt");
std::fs::write(&path_ok, vec![b'x'; small_cap]).expect("write fixture");
let bytes = read_headless_input(Some(&path_ok), small_cap).expect("must fit exactly");
assert_eq!(bytes.len(), small_cap);
}
#[test]
fn test_resolve_log_level_config_wins_over_default_without_cli_flag() {
let level = resolve_log_level(None, Some("debug"))
.expect("a valid config string must resolve, not error");
assert_eq!(
level,
LogLevel::Debug,
"the [headless] log_level config value must take effect, not the info default"
);
}
#[test]
fn test_resolve_log_level_cli_flag_wins_over_config() {
let level = resolve_log_level(Some(CliLogLevel::Error), Some("debug"))
.expect("must resolve with both sources present");
assert_eq!(level, LogLevel::Error);
}
#[test]
fn test_resolve_log_level_defaults_to_info_when_unset() {
assert_eq!(
resolve_log_level(None, None).expect("must resolve"),
LogLevel::Info
);
}
#[test]
fn test_resolve_log_level_rejects_invalid_config_string() {
assert!(matches!(
resolve_log_level(None, Some("verbose")),
Err(HeadlessError::InputInvalid(_))
));
}
#[test]
fn test_resolve_allow_system_override_config_enables_without_cli_flag() {
assert!(
resolve_allow_system_override(false, Some(true)),
"a true [headless] allow_system_override must enable the gate on its own"
);
}
#[test]
fn test_resolve_allow_system_override_cli_flag_alone_enables() {
assert!(resolve_allow_system_override(true, None));
assert!(resolve_allow_system_override(true, Some(false)));
}
#[test]
fn test_resolve_allow_system_override_defaults_closed() {
assert!(!resolve_allow_system_override(false, None));
assert!(!resolve_allow_system_override(false, Some(false)));
}
#[test]
fn test_magi_toml_exists_checks_canonical_dot_magi_path() {
let tmp = tempfile::tempdir().unwrap();
let cwd = dunce::canonicalize(tmp.path()).unwrap();
assert!(!magi_toml_exists(None), "no workspace ⇒ no config file");
let magi_dir = cwd.join(".magi");
std::fs::create_dir(&magi_dir).unwrap();
std::fs::write(magi_dir.join("magi.toml"), "provider = \"anthropic\"\n").unwrap();
let ws = Workspace {
root: cwd.clone(),
magi_dir: magi_dir.clone(),
};
assert!(magi_toml_exists(Some(&ws)));
}
#[test]
fn test_magi_toml_exists_ignores_legacy_loose_cwd_file() {
let tmp = tempfile::tempdir().unwrap();
let cwd = dunce::canonicalize(tmp.path()).unwrap();
std::fs::write(cwd.join("magi.toml"), "provider = \"anthropic\"\n").unwrap();
let magi_dir = cwd.join(".magi");
std::fs::create_dir(&magi_dir).unwrap();
let ws = Workspace {
root: cwd.clone(),
magi_dir,
};
assert!(!magi_toml_exists(Some(&ws)));
}
fn with_var<R>(key: &str, val: Option<&str>, f: impl FnOnce() -> R) -> R {
struct Guard {
key: String,
prev: Option<String>,
}
impl Drop for Guard {
fn drop(&mut self) {
match &self.prev {
Some(v) => std::env::set_var(&self.key, v),
None => std::env::remove_var(&self.key),
}
}
}
let _g = Guard {
key: key.to_string(),
prev: std::env::var(key).ok(),
};
match val {
Some(v) => std::env::set_var(key, v),
None => std::env::remove_var(key),
}
f()
}
struct FakePrompt {
interactive: bool,
answers: Vec<String>,
reads: usize,
}
impl FakePrompt {
fn interactive(answers: Vec<&str>) -> Self {
Self {
interactive: true,
answers: answers.into_iter().map(str::to_string).collect(),
reads: 0,
}
}
fn non_interactive() -> Self {
Self {
interactive: false,
answers: Vec::new(),
reads: 0,
}
}
}
impl PassphrasePrompt for FakePrompt {
fn is_interactive(&self) -> bool {
self.interactive
}
fn read_passphrase(
&mut self,
_msg: &str,
_show: bool,
) -> Result<Zeroizing<String>, VaultError> {
let i = self.reads;
self.reads += 1;
Ok(Zeroizing::new(
self.answers.get(i).cloned().unwrap_or_default(),
))
}
}
fn vault_fixture() -> SharedSecretStore {
let conn = rusqlite::Connection::open_in_memory().expect("mem db");
let dek = MaskedDek::new(Zeroizing::new(vec![3u8; 32])).expect("32B");
let store = wire(Arc::new(Mutex::new(conn)), dek).expect("wire");
Arc::new(Mutex::new(store)) as SharedSecretStore
}
#[test]
fn test_resolve_provider_wiring() {
use crate::config::{resolve_provider, MagiConfig};
assert_eq!(
resolve_provider(
&MagiConfig {
provider: Some("anthropic".into()),
..Default::default()
},
Some("openai")
),
"openai"
);
assert_eq!(resolve_provider(&MagiConfig::default(), None), "openai");
}
#[test]
fn test_args_parses_init_config_flag() {
use clap::Parser;
let a = Args::parse_from(["magi-rs", "--init-config"]);
assert!(a.init_config);
let b = Args::parse_from(["magi-rs"]);
assert!(!b.init_config);
}
#[test]
fn test_first_run_strips_trailing_newline_from_flag_and_env_like_unlock() {
let mut prompt = FakePrompt::non_interactive();
let flag = Some(Zeroizing::new("correct horse battery staple\n".to_string()));
let got = resolve_master_passphrase(true, flag, &mut prompt).expect("first-run flag");
assert_eq!(got.as_str(), "correct horse battery staple");
}
#[test]
fn test_args_parses_passphrase_flag_and_vault_subcommand() {
use clap::Parser;
let a = Args::parse_from(["magi-rs", "-p", "hunter2"]);
assert_eq!(a.passphrase.as_deref(), Some("hunter2"));
assert!(a.command.is_none());
let b = Args::parse_from(["magi-rs", "vault", "ls"]);
assert!(matches!(b.command, Some(TopCmd::Vault(VaultCmd::Ls))));
let c = Args::parse_from(["magi-rs", "-p", "hunter2", "vault", "ls"]);
assert_eq!(c.passphrase.as_deref(), Some("hunter2"));
assert!(matches!(c.command, Some(TopCmd::Vault(VaultCmd::Ls))));
}
#[test]
fn test_vault_error_exit_code_assigns_one_or_two_to_every_variant() {
let exit_one = [
VaultError::Aborted,
VaultError::WrongPassphrase,
VaultError::PassphraseUnavailable,
VaultError::WeakPassphrase("x".into()),
VaultError::ValueTooLarge(1),
VaultError::SecretNotFound("x".into()),
VaultError::VaultMetaCorrupt,
VaultError::DbCorrupt {
db_path: std::path::PathBuf::from("x"),
detail: "data present without envelope".into(),
},
];
for e in &exit_one {
assert_eq!(vault_error_exit_code(e), 1, "{e}");
}
let exit_two = [
VaultError::Crypto("x".into()),
VaultError::Storage("x".into()),
VaultError::Io("x".into()),
];
for e in &exit_two {
assert_eq!(vault_error_exit_code(e), 2, "{e}");
}
}
#[test]
#[serial_test::serial]
fn test_api_key_resolution_prefers_env_over_vault() {
with_var("ANTHROPIC_MODEL", None, || {
let ss = vault_fixture();
{
let mut guard = ss.lock().unwrap();
guard.set("ANTHROPIC_API_KEY", "sk-from-vault").unwrap();
}
let config = MagiConfig::default();
assert!(discover_config(&config, None, None).is_none());
let cfg = discover_config(&config, None, Some(&ss)).expect("vault key");
assert_eq!(cfg.api_key, "sk-from-vault");
assert_eq!(cfg.source, "vault");
let cfg = discover_config(&config, Some("sk-from-env"), Some(&ss)).expect("env key");
assert_eq!(cfg.api_key, "sk-from-env");
assert_eq!(cfg.source, "ENV");
});
}
#[test]
#[serial_test::serial]
fn test_discover_config_model_prefers_env_over_toml_then_falls_back_to_toml() {
let ss = vault_fixture();
{
let mut guard = ss.lock().unwrap();
guard.set("ANTHROPIC_API_KEY", "sk-from-vault").unwrap();
}
let config = MagiConfig {
anthropic: crate::config::AnthropicConfig {
model: Some("claude-toml-model".into()),
},
..Default::default()
};
with_var("ANTHROPIC_MODEL", None, || {
let cfg = discover_config(&config, None, Some(&ss)).expect("vault key");
assert_eq!(cfg.model, "claude-toml-model");
});
with_var("ANTHROPIC_MODEL", Some("claude-env-model"), || {
let cfg = discover_config(&config, None, Some(&ss)).expect("vault key");
assert_eq!(cfg.model, "claude-env-model");
});
}
#[test]
#[serial_test::serial]
fn test_api_keys_are_trimmed_of_surrounding_whitespace() {
with_var("ANTHROPIC_MODEL", None, || {
let ss = vault_fixture();
{
let mut guard = ss.lock().unwrap();
guard
.set("ANTHROPIC_API_KEY", " sk-vault-anthropic\n")
.unwrap();
guard.set("OPENAI_API_KEY", "sk-vault-openai\t").unwrap();
}
let config = MagiConfig::default();
assert_eq!(
discover_config(&config, None, Some(&ss))
.expect("a")
.api_key,
"sk-vault-anthropic"
);
assert_eq!(
resolve_openai_key(None, Some(&ss)).as_deref(),
Some("sk-vault-openai")
);
assert_eq!(
resolve_openai_key(Some("sk-env-openai\n"), Some(&ss)).as_deref(),
Some("sk-env-openai")
);
assert_eq!(
discover_config(&config, Some(" sk-env-anthropic "), Some(&ss))
.expect("b")
.api_key,
"sk-env-anthropic"
);
});
}
#[test]
fn test_resolve_openai_key_prefers_env_over_vault() {
let ss = vault_fixture();
assert!(resolve_openai_key(None, None).is_none());
assert!(resolve_openai_key(None, Some(&ss)).is_none());
{
let mut guard = ss.lock().unwrap();
guard.set("OPENAI_API_KEY", "sk-oai-vault").unwrap();
}
assert_eq!(
resolve_openai_key(None, Some(&ss)).as_deref(),
Some("sk-oai-vault")
);
assert_eq!(
resolve_openai_key(Some("sk-oai-env"), Some(&ss)).as_deref(),
Some("sk-oai-env")
);
}
#[test]
#[serial_test::serial]
fn test_scrub_removes_passphrase_and_api_keys_from_process_env() {
with_var(PASSPHRASE_ENV, Some("pw-secret"), || {
with_var(ANTHROPIC_KEY_ENV, Some("sk-anthropic"), || {
with_var(OPENAI_KEY_ENV, Some("sk-openai"), || {
let consumed = read_then_scrub_secret_env();
assert_eq!(
consumed.passphrase.as_deref().map(String::as_str),
Some("pw-secret")
);
assert_eq!(consumed.anthropic_key.as_deref(), Some("sk-anthropic"));
assert_eq!(consumed.openai_key.as_deref(), Some("sk-openai"));
assert!(env::var(PASSPHRASE_ENV).is_err());
assert!(env::var(ANTHROPIC_KEY_ENV).is_err());
assert!(env::var(OPENAI_KEY_ENV).is_err());
});
});
});
}
#[test]
#[serial_test::serial]
fn test_tool_child_env_excludes_secrets_and_arbitrary_lc() {
with_var(PASSPHRASE_ENV, Some("pw-secret"), || {
with_var(ANTHROPIC_KEY_ENV, Some("sk-anthropic"), || {
with_var(OPENAI_KEY_ENV, Some("sk-openai"), || {
with_var("LC_EVIL", Some("evil"), || {
with_var("LC_ALL", Some("C.UTF-8"), || {
with_var("PATH", Some("/usr/bin"), || {
let child = crate::tools::proc_group::tool_child_env();
let lookup = |key: &str| -> Option<&str> {
child
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(key))
.map(|(_, v)| v.as_str())
};
assert!(lookup(PASSPHRASE_ENV).is_none());
assert!(lookup(ANTHROPIC_KEY_ENV).is_none());
assert!(lookup(OPENAI_KEY_ENV).is_none());
assert!(lookup("LC_EVIL").is_none());
assert_eq!(lookup("PATH"), Some("/usr/bin"));
assert_eq!(lookup("LC_ALL"), Some("C.UTF-8"));
});
});
});
});
});
});
}
#[test]
#[serial_test::serial]
fn test_open_tui_memory_degrades_to_ephemeral_when_no_tty_and_no_passphrase_on_first_run() {
with_var(PASSPHRASE_ENV, None, || {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("absent.db");
let mut prompt = FakePrompt::non_interactive();
let mut notices = Vec::new();
let attachment = open_tui_memory(&db_path, None, &mut prompt, &mut notices);
assert!(matches!(attachment, MemoryAttachment::Ephemeral));
assert!(!db_path.exists(), "must never create a DB it cannot open");
assert!(notices.iter().any(|n| n.contains("WARNING")));
});
}
#[test]
#[serial_test::serial]
fn test_open_tui_memory_creates_db_on_first_run_with_passphrase_flag() {
with_var(PASSPHRASE_ENV, None, || {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("fresh.db");
let flag = Some(Zeroizing::new("correct horse battery staple".to_string()));
let mut prompt = FakePrompt::non_interactive();
let mut notices = Vec::new();
let attachment = open_tui_memory(&db_path, flag, &mut prompt, &mut notices);
assert!(matches!(attachment, MemoryAttachment::Encrypted(_)));
assert!(db_path.exists());
});
}
#[test]
#[serial_test::serial]
fn test_wrong_passphrase_is_retryable_and_never_wipes() {
with_var(PASSPHRASE_ENV, None, || {
let tmp = tempfile::tempdir().unwrap();
let db_path = tmp.path().join("existing.db");
let rt = tokio::runtime::Runtime::new().unwrap();
{
let store = EncryptedSqliteMemory::new(
db_path.clone(),
Zeroizing::new("right-passphrase-0123456".to_string()),
)
.unwrap();
let sid = rt.block_on(store.create_session("p")).unwrap();
rt.block_on(store.add_message(&sid, &Message::user("keep me")))
.unwrap();
}
let mut prompt =
FakePrompt::interactive(vec!["wrong-passphrase-xyz", "right-passphrase-0123456"]);
let mut notices = Vec::new();
let attachment = open_tui_memory(&db_path, None, &mut prompt, &mut notices);
match attachment {
MemoryAttachment::Encrypted(store) => {
let sessions = rt.block_on(store.list_sessions()).unwrap();
assert_eq!(sessions.len(), 1, "prior history must survive intact");
}
MemoryAttachment::Ephemeral => {
panic!("the second (correct) attempt must recover the store")
}
}
});
}
#[tokio::test]
async fn test_rotating_anthropic_key_in_vault_never_invalidates_the_db() {
let tmp = tempfile::NamedTempFile::new().unwrap();
let store = EncryptedSqliteMemory::new(
tmp.path().to_path_buf(),
Zeroizing::new("db-master-passphrase-1".to_string()),
)
.unwrap();
let sid = store.create_session("p").await.unwrap();
store
.add_message(&sid, &Message::user("keep me"))
.await
.unwrap();
let dek = store.data_key().unwrap();
let mut vault_store = wire(store.shared_conn(), dek).unwrap();
vault_store.set("ANTHROPIC_API_KEY", "sk-old").unwrap();
vault_store.set("ANTHROPIC_API_KEY", "sk-rotated").unwrap();
let msgs = store.get_messages(&sid).await.unwrap();
assert_eq!(msgs, vec![Message::user("keep me")]);
assert_eq!(
vault_store.get("ANTHROPIC_API_KEY").unwrap().as_str(),
"sk-rotated"
);
}
#[tokio::test]
async fn test_db_moved_to_another_machine_opens_with_passphrase_alone() {
let dir_a = tempfile::tempdir().unwrap();
let path_a = dir_a.path().join("a.db");
let passphrase = Zeroizing::new("portable-passphrase-xyz-9".to_string());
{
let store = EncryptedSqliteMemory::new(path_a.clone(), passphrase.clone()).unwrap();
let sid = store.create_session("p").await.unwrap();
store.add_message(&sid, &Message::user("hi")).await.unwrap();
let conn = store.shared_conn();
let guard = conn.lock().unwrap();
guard
.execute_batch("PRAGMA wal_checkpoint(TRUNCATE);")
.unwrap();
}
let dir_b = tempfile::tempdir().unwrap();
let path_b = dir_b.path().join("copy.db");
std::fs::copy(&path_a, &path_b).unwrap();
let store_b = EncryptedSqliteMemory::new(path_b, passphrase).unwrap();
let sessions = store_b.list_sessions().await.unwrap();
assert_eq!(sessions.len(), 1);
let msgs = store_b.get_messages(&sessions[0].0).await.unwrap();
assert_eq!(msgs, vec![Message::user("hi")]);
}
#[test]
fn test_run_logout_reports_no_stored_session_when_db_absent() {
let tmp = tempfile::tempdir().unwrap();
let workspace = tmp.path().to_path_buf();
assert_eq!(run_logout(None, &workspace), 0);
}
#[test]
fn test_args_parses_init_subcommand() {
use clap::Parser;
let a = Args::parse_from(["magi-rs", "init"]);
assert!(matches!(a.command, Some(TopCmd::Init)));
let b = Args::parse_from(["magi-rs", "-p", "hunter2", "init"]);
assert_eq!(b.passphrase.as_deref(), Some("hunter2"));
assert!(matches!(b.command, Some(TopCmd::Init)));
}
fn envelope_row_count(db_path: &std::path::Path) -> i64 {
let conn = rusqlite::Connection::open(db_path).expect("open db");
conn.query_row(
"SELECT COUNT(*) FROM vault_meta WHERE key = 'wrapped_dek'",
[],
|r| r.get(0),
)
.expect("query envelope")
}
#[test]
#[serial_test::serial]
fn test_run_init_creates_magi_dir_and_refuses_second_run() {
with_var(PASSPHRASE_ENV, None, || {
let tmp = tempfile::tempdir().unwrap();
let cwd = dunce::canonicalize(tmp.path()).unwrap();
assert_eq!(run_init(&cwd, None), 0, "first init must succeed");
assert!(cwd.join(".magi").is_dir(), ".magi/ must exist after init");
assert!(cwd.join(".magi/.magi-rs-memory.db").exists());
assert!(cwd.join(".magi/magi.toml").exists());
assert!(cwd.join(".magi/logs").is_dir());
assert_ne!(
run_init(&cwd, None),
0,
"a second init must refuse (nested/existing .magi/)"
);
});
}
#[test]
#[serial_test::serial]
fn test_run_init_without_passphrase_leaves_no_envelope() {
with_var(PASSPHRASE_ENV, None, || {
let tmp = tempfile::tempdir().unwrap();
let cwd = dunce::canonicalize(tmp.path()).unwrap();
assert_eq!(run_init(&cwd, None), 0);
assert_eq!(
envelope_row_count(&cwd.join(".magi/.magi-rs-memory.db")),
0,
"no passphrase must leave the DB without an envelope"
);
});
}
#[test]
#[serial_test::serial]
fn test_run_init_with_passphrase_bootstraps_the_envelope() {
with_var(PASSPHRASE_ENV, None, || {
let tmp = tempfile::tempdir().unwrap();
let cwd = dunce::canonicalize(tmp.path()).unwrap();
let pass = Some(Zeroizing::new("correct horse battery staple".to_string()));
assert_eq!(run_init(&cwd, pass), 0);
assert_eq!(
envelope_row_count(&cwd.join(".magi/.magi-rs-memory.db")),
1,
"a supplied passphrase must bootstrap the envelope"
);
});
}
#[test]
fn test_args_parses_vault_diagnose_subcommand() {
use clap::Parser;
let a = Args::parse_from(["magi-rs", "vault", "diagnose"]);
assert!(matches!(
a.command,
Some(TopCmd::Vault(VaultCmd::Diagnose { names: false }))
));
let b = Args::parse_from(["magi-rs", "vault", "diagnose", "--names"]);
assert!(matches!(
b.command,
Some(TopCmd::Vault(VaultCmd::Diagnose { names: true }))
));
}
#[test]
fn test_run_vault_diagnose_reports_no_database_when_absent() {
let tmp = tempfile::tempdir().unwrap();
let workspace = dunce::canonicalize(tmp.path()).unwrap();
assert_eq!(run_vault_diagnose(&workspace, false), 0);
}
#[test]
#[serial_test::serial]
fn test_run_vault_diagnose_never_needs_a_passphrase_and_finds_magi_dir() {
with_var(PASSPHRASE_ENV, None, || {
let tmp = tempfile::tempdir().unwrap();
let cwd = dunce::canonicalize(tmp.path()).unwrap();
assert_eq!(run_init(&cwd, None), 0, "init must succeed");
assert_eq!(run_vault_diagnose(&cwd, false), 0);
});
}
#[test]
#[serial_test::serial]
fn test_run_vault_diagnose_never_mutates_or_unlocks_a_bootstrapped_db() {
with_var(PASSPHRASE_ENV, None, || {
let tmp = tempfile::tempdir().unwrap();
let cwd = dunce::canonicalize(tmp.path()).unwrap();
let pass = Some(Zeroizing::new("correct horse battery staple".to_string()));
assert_eq!(run_init(&cwd, pass), 0, "init -p must succeed");
let db_path = cwd.join(".magi/.magi-rs-memory.db");
let envelope_rows_before = envelope_row_count(&db_path);
assert_eq!(envelope_rows_before, 1, "init -p bootstraps an envelope");
assert_eq!(run_vault_diagnose(&cwd, true), 0);
assert_eq!(
envelope_row_count(&db_path),
envelope_rows_before,
"diagnose must never mutate vault_meta"
);
});
}
#[test]
#[serial_test::serial]
fn test_run_init_refuses_to_nest_inside_ancestor_magi_dir() {
with_var(PASSPHRASE_ENV, None, || {
let tmp = tempfile::tempdir().unwrap();
let root = dunce::canonicalize(tmp.path()).unwrap();
std::fs::create_dir_all(root.join(".magi")).unwrap();
let sub = root.join("a/b");
std::fs::create_dir_all(&sub).unwrap();
assert_ne!(run_init(&sub, None), 0, "must refuse to nest a .magi/");
assert!(
!sub.join(".magi").exists(),
"a refused nested init must create no .magi/"
);
});
}
#[test]
#[cfg(unix)]
#[serial_test::serial]
fn test_run_init_fails_and_creates_nothing_when_ancestor_is_symlink() {
with_var(PASSPHRASE_ENV, None, || {
let tmp = tempfile::tempdir().unwrap();
let root = dunce::canonicalize(tmp.path()).unwrap();
let real = root.join("real");
std::fs::create_dir_all(&real).unwrap();
let link = root.join("link");
std::os::unix::fs::symlink(&real, &link).unwrap();
let sub = link.join("sub");
std::fs::create_dir_all(&sub).unwrap();
assert_ne!(
run_init(&sub, None),
0,
"a walk aborted by a symlink must fail, not init"
);
assert!(
!sub.join(".magi").exists(),
"an aborted walk must create no .magi/"
);
});
}
fn base_hargs() -> HeadlessArgs {
HeadlessArgs {
input: None,
output: None,
input_format: None,
output_format: None,
workdir: None,
no_memory: false,
auto: false,
full_auto: false,
timeout: None,
log_level: None,
log_dir: None,
allow_system_override: false,
no_clobber: false,
consult: false,
model: None,
provider: None,
max_tool_calls: None,
}
}
fn outcome_with(
stop_reason: StopReason,
response: Option<&str>,
error_kind: Option<ErrorKind>,
) -> RunOutcome {
use magi_rs::headless::types::{AppliedCaps, ErrorPayload, Timings, Usage};
RunOutcome {
response: response.map(str::to_string),
model: "m".to_string(),
provider: "p".to_string(),
usage: Usage {
input_tokens: 0,
output_tokens: 0,
},
timings: Timings {
total_ms: 1,
ttfb_ms: None,
per_turn_ms: Vec::new(),
},
stop_reason,
tool_calls: Vec::new(),
transcript: Vec::new(),
consult: None,
applied_caps: AppliedCaps {
max_tool_calls: 15,
max_tool_calls_clamped: false,
timeout_secs: None,
system_override_applied: false,
},
error: error_kind.map(|kind| ErrorPayload {
message: String::new(),
kind,
}),
}
}
#[test]
fn test_tier_from_flags_full_auto_wins() {
assert!(matches!(tier_from_flags(false, false), Tier::Default));
assert!(matches!(tier_from_flags(true, false), Tier::Auto));
assert!(matches!(tier_from_flags(false, true), Tier::FullAuto));
assert!(matches!(tier_from_flags(true, true), Tier::FullAuto));
}
#[test]
fn test_exit_code_for_outcome_taxonomy() {
assert_eq!(
exit_code_for_outcome(&outcome_with(StopReason::Done, Some("hi"), None)),
0
);
assert_eq!(
exit_code_for_outcome(&outcome_with(
StopReason::Error,
None,
Some(ErrorKind::InputInvalid)
)),
2
);
assert_eq!(
exit_code_for_outcome(&outcome_with(StopReason::Denied, Some(""), None)),
3
);
assert_eq!(
exit_code_for_outcome(&outcome_with(
StopReason::Error,
None,
Some(ErrorKind::Timeout)
)),
1
);
assert_eq!(
exit_code_for_outcome(&outcome_with(
StopReason::Error,
None,
Some(ErrorKind::TierDenied)
)),
3
);
}
#[test]
fn test_write_output_atomic_overwrites_by_default() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("out.txt");
std::fs::write(&path, b"old").unwrap();
write_output_atomic(&path, b"new", false).expect("overwrite must succeed");
assert_eq!(std::fs::read(&path).unwrap(), b"new");
}
#[test]
fn test_write_output_atomic_no_clobber_refuses_existing() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("out.txt");
std::fs::write(&path, b"keep").unwrap();
let err = write_output_atomic(&path, b"new", true).expect_err("no-clobber must refuse");
assert!(matches!(err, HeadlessError::InputInvalid(_)));
assert_eq!(
std::fs::read(&path).unwrap(),
b"keep",
"existing file unchanged"
);
assert_eq!(
headless_error_exit_code(&err),
2,
"no-clobber refusal is misuse"
);
}
#[test]
fn test_write_output_atomic_no_clobber_creates_when_absent() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("fresh.txt");
write_output_atomic(&path, b"data", true).expect("create must succeed");
assert_eq!(std::fs::read(&path).unwrap(), b"data");
}
#[test]
fn test_finish_no_clobber_write_removes_partial_file_on_write_failure() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("out.txt");
std::fs::write(&path, b"partial").unwrap();
assert!(path.exists(), "precondition: the partial file exists");
let simulated_failure = Err(std::io::Error::other("simulated disk-full mid-write"));
let err = finish_no_clobber_write(&path, simulated_failure)
.expect_err("a write failure must surface as an error");
assert!(matches!(err, HeadlessError::Io(_)));
assert!(
!path.exists(),
"a failed write must leave NO partial file on disk"
);
}
#[test]
fn test_finish_no_clobber_write_leaves_file_intact_on_success() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("out.txt");
std::fs::write(&path, b"data").unwrap();
finish_no_clobber_write(&path, Ok(())).expect("a successful write must not error");
assert_eq!(
std::fs::read(&path).unwrap(),
b"data",
"a successful write must leave the file untouched by cleanup"
);
}
#[test]
fn test_args_parses_query_and_consult_subcommands() {
use clap::Parser;
let q = Args::parse_from(["magi-rs", "query", "--auto", "-i", "q.txt"]);
match q.command {
Some(TopCmd::Query(h)) => {
assert!(h.auto);
assert_eq!(h.input.as_deref(), Some(std::path::Path::new("q.txt")));
}
_ => panic!("expected the query subcommand"),
}
let c = Args::parse_from([
"magi-rs",
"consult",
"--full-auto",
"--output-format",
"json",
]);
match c.command {
Some(TopCmd::Consult(h)) => {
assert!(h.full_auto);
assert!(matches!(h.output_format, Some(CliOutputFormat::Json)));
}
_ => panic!("expected the consult subcommand"),
}
}
fn init_static_workspace() -> (tempfile::TempDir, std::path::PathBuf) {
let tmp = tempfile::tempdir().unwrap();
let cwd = dunce::canonicalize(tmp.path()).unwrap();
crate::system::workspace::init(&cwd).expect("init .magi/");
std::fs::write(cwd.join(".magi/magi.toml"), "provider = \"anthropic\"\n").unwrap();
(tmp, cwd)
}
fn init_default_workspace() -> (tempfile::TempDir, std::path::PathBuf) {
let tmp = tempfile::tempdir().unwrap();
let cwd = dunce::canonicalize(tmp.path()).unwrap();
crate::system::workspace::init(&cwd).expect("init .magi/");
(tmp, cwd)
}
fn write_envelope(cwd: &Path, name: &str, body: &str) -> std::path::PathBuf {
let path = cwd.join(name);
std::fs::write(&path, body).unwrap();
path
}
#[test]
#[serial_test::serial]
fn test_prepare_headless_provider_only_envelope_resolves_matching_default_model() {
with_var("MAGI_PROVIDER", None, || {
with_var("ANTHROPIC_MODEL", None, || {
with_var("OPENAI_MODEL", None, || {
let (_tmp, cwd) = init_default_workspace();
let input = write_envelope(
&cwd,
"env.json",
r#"{"prompt":"hi","provider":"anthropic"}"#,
);
let mut h = base_hargs();
h.input = Some(input);
h.workdir = Some(cwd.clone());
h.no_memory = true;
let rt = tokio::runtime::Runtime::new().unwrap();
let ctx = rt
.block_on(prepare_headless(&h, None, &cwd, None, None))
.expect("prepare_headless must succeed");
assert_eq!(ctx.resolved.provider, "anthropic");
assert_eq!(
ctx.resolved.model,
crate::defaults::DEFAULT_ANTHROPIC_MODEL,
"provider-without-model must resolve the default MODEL for the \
EFFECTIVE (envelope-overridden) provider, not the config-default \
provider's model"
);
});
});
});
}
#[test]
#[serial_test::serial]
fn test_prepare_headless_explicit_envelope_model_still_wins_over_provider_default() {
with_var("MAGI_PROVIDER", None, || {
with_var("ANTHROPIC_MODEL", None, || {
with_var("OPENAI_MODEL", None, || {
let (_tmp, cwd) = init_default_workspace();
let input = write_envelope(
&cwd,
"env.json",
r#"{"prompt":"hi","provider":"anthropic","model":"custom-model"}"#,
);
let mut h = base_hargs();
h.input = Some(input);
h.workdir = Some(cwd.clone());
h.no_memory = true;
let rt = tokio::runtime::Runtime::new().unwrap();
let ctx = rt
.block_on(prepare_headless(&h, None, &cwd, None, None))
.expect("prepare_headless must succeed");
assert_eq!(ctx.resolved.provider, "anthropic");
assert_eq!(
ctx.resolved.model, "custom-model",
"an explicit envelope model must still win"
);
});
});
});
}
#[test]
#[serial_test::serial]
fn test_prepare_headless_neither_provider_nor_model_set_uses_config_default() {
with_var("MAGI_PROVIDER", None, || {
with_var("ANTHROPIC_MODEL", None, || {
with_var("OPENAI_MODEL", None, || {
let (_tmp, cwd) = init_default_workspace();
let input = write_envelope(&cwd, "env.json", r#"{"prompt":"hi"}"#);
let mut h = base_hargs();
h.input = Some(input);
h.workdir = Some(cwd.clone());
h.no_memory = true;
let rt = tokio::runtime::Runtime::new().unwrap();
let ctx = rt
.block_on(prepare_headless(&h, None, &cwd, None, None))
.expect("prepare_headless must succeed");
assert_eq!(ctx.resolved.provider, "openai");
assert_eq!(ctx.resolved.model, crate::defaults::DEFAULT_OPENAI_MODEL);
});
});
});
}
#[test]
#[serial_test::serial]
fn test_headless_query_static_provider_returns_response_exit_0() {
with_var("MAGI_PROVIDER", None, || {
let (_tmp, cwd) = init_static_workspace();
let prompt = cwd.join("prompt.txt");
std::fs::write(&prompt, b"hello").unwrap();
let out = cwd.join("out.txt");
let mut h = base_hargs();
h.input = Some(prompt);
h.output = Some(out.clone());
h.workdir = Some(cwd.clone());
h.no_memory = true;
let rt = tokio::runtime::Runtime::new().unwrap();
let code = rt.block_on(run_query_subcommand(h, None, &cwd, None, None));
assert_eq!(code, 0, "a static-provider query must succeed");
let body = std::fs::read_to_string(&out).unwrap();
assert!(!body.is_empty(), "the response must be written to -o");
});
}
#[test]
#[serial_test::serial]
fn test_headless_query_json_output_has_schema_version_and_stop_reason() {
with_var("MAGI_PROVIDER", None, || {
let (_tmp, cwd) = init_static_workspace();
let prompt = cwd.join("q.txt");
std::fs::write(&prompt, b"hello json").unwrap();
let out = cwd.join("out.json");
let mut h = base_hargs();
h.input = Some(prompt);
h.output = Some(out.clone());
h.workdir = Some(cwd.clone());
h.no_memory = true;
h.output_format = Some(CliOutputFormat::Json);
let rt = tokio::runtime::Runtime::new().unwrap();
let code = rt.block_on(run_query_subcommand(h, None, &cwd, None, None));
assert_eq!(code, 0);
let v: serde_json::Value =
serde_json::from_slice(&std::fs::read(&out).unwrap()).unwrap();
assert_eq!(v["schema_version"], 1);
assert!(v.get("response").is_some());
assert!(v.get("stop_reason").is_some());
assert!(v.get("tool_calls").is_some());
});
}
#[test]
#[serial_test::serial]
fn test_headless_query_no_clobber_on_existing_output_exits_2() {
with_var("MAGI_PROVIDER", None, || {
let (_tmp, cwd) = init_static_workspace();
let prompt = cwd.join("q.txt");
std::fs::write(&prompt, b"hi").unwrap();
let out = cwd.join("exists.txt");
std::fs::write(&out, b"PRESERVE").unwrap();
let mut h = base_hargs();
h.input = Some(prompt);
h.output = Some(out.clone());
h.workdir = Some(cwd.clone());
h.no_memory = true;
h.no_clobber = true;
let rt = tokio::runtime::Runtime::new().unwrap();
let code = rt.block_on(run_query_subcommand(h, None, &cwd, None, None));
assert_eq!(code, 2, "--no-clobber on an existing -o file ⇒ exit 2");
assert_eq!(
std::fs::read(&out).unwrap(),
b"PRESERVE",
"the existing file must be untouched"
);
});
}
#[test]
#[serial_test::serial]
fn test_headless_consult_over_max_query_len_exits_2() {
with_var("MAGI_PROVIDER", None, || {
let (_tmp, cwd) = init_static_workspace();
let prompt = cwd.join("big.txt");
std::fs::write(&prompt, "x".repeat(9000)).unwrap();
let out = cwd.join("out.txt");
let mut h = base_hargs();
h.input = Some(prompt);
h.output = Some(out);
h.workdir = Some(cwd.clone());
h.no_memory = true;
let rt = tokio::runtime::Runtime::new().unwrap();
let code = rt.block_on(run_consult_subcommand(h, None, &cwd, None, None));
assert_eq!(
code, 2,
"an over-cap consult prompt ⇒ exit 2 (rejected, not truncated)"
);
});
}
}