diff --git a/src/cli/mod.rs b/src/cli/mod.rs index d8bf334..fd3d76e 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -30,7 +30,7 @@ use std::io::{Read, stdin}; ", group( ArgGroup::new("sbx-mode") - .args(["sandbox"]) + .args(["sandbox", "fresh"]) .multiple(true) .conflicts_with_all([ "model", "prompt", "role", "session", "agent", "rag", "rebuild_rag", @@ -227,6 +227,9 @@ pub struct Cli { /// Launch Coyote inside a Docker sandbox (via `sbx`); name defaults to current directory basename #[arg(long, value_name = "NAME", help_heading = "Sandbox")] pub sandbox: Option>, + /// Start the sandbox with a clean slate — no copied config or tokens; LLM credentials injected via sbx proxy + #[arg(long, requires = "sandbox", help_heading = "Sandbox")] + pub fresh: bool, /// Display information #[arg(long, help_heading = "Diagnostics & Tools")] pub info: bool, diff --git a/src/config/mod.rs b/src/config/mod.rs index 50a3448..4b1fd74 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -45,7 +45,7 @@ pub use self::skill_registry::SkillRegistry; pub use self::update::run_self_update; use crate::client::{ ClientConfig, MessageContentToolCalls, Model, ModelType, OPENAI_COMPATIBLE_PROVIDERS, - ProviderModels, create_client_config, list_client_types, oauth, + ProviderModels, create_client_config, list_client_types, oauth, set_client_models_config, }; use crate::function::{FunctionDeclaration, Functions}; use crate::rag::Rag; @@ -758,6 +758,10 @@ pub async fn create_config_file(config_path: &Path) -> Result<()> { process::exit(0); } + if env::var_os(SANDBOX_ENV_FLAG).is_some() { + return create_config_file_sandbox(config_path).await; + } + let provider_choice = prompt_provider_choice()?; let mut vault = match &provider_choice { None => Vault::default_local(), @@ -817,6 +821,62 @@ pub async fn create_config_file(config_path: &Path) -> Result<()> { Ok(()) } +async fn create_config_file_sandbox(config_path: &Path) -> Result<()> { + let client = Select::new("API Provider (required):", list_client_types()).prompt()?; + + println!( + "Running in sandbox mode — your API provider credentials are managed by your host Coyote configuration if configured." + ); + + let mut client_config = serde_json::json!({ "type": client }); + + if matches!(client, "claude" | "openai" | "gemini") { + let use_oauth = Confirm::new("Use OAuth authentication instead?") + .with_default(false) + .prompt()?; + if use_oauth { + client_config["auth"] = "oauth".into(); + } + } + + let model = set_client_models_config(&mut client_config, client).await?; + + let mut config = serde_json::json!({}); + config["model"] = model.into(); + config["stream"] = serde_json::json!(true); + config["save"] = serde_json::json!(true); + config["keybindings"] = serde_json::json!("vi"); + config["wrap"] = serde_json::json!("auto"); + config["wrap_code"] = serde_json::json!(false); + config["function_calling_support"] = serde_json::json!(true); + config["enabled_tools"] = serde_json::json!(null); + config["visible_tools"] = serde_json::json!(DEFAULT_VISIBLE_TOOLS); + config["mcp_server_support"] = serde_json::json!(true); + config["enabled_mcp_servers"] = serde_json::json!(null); + config["highlight"] = serde_json::json!(true); + config["light_theme"] = serde_json::json!(false); + config[CLIENTS_FIELD] = serde_json::json!(vec![client_config]); + + let config_data = serde_yaml::to_string(&config).with_context(|| "Failed to create config")?; + let config_data = format!( + "# see https://github.com/Dark-Alex-17/coyote/blob/main/config.example.yaml\n\n{config_data}" + ); + + ensure_parent_exists(config_path)?; + std::fs::write(config_path, config_data) + .with_context(|| format!("Failed to write to '{}'", config_path.display()))?; + #[cfg(unix)] + { + use std::os::unix::prelude::PermissionsExt; + let perms = std::fs::Permissions::from_mode(0o600); + std::fs::set_permissions(config_path, perms)?; + } + + println!("✓ Saved the config file to '{}'.\n", config_path.display()); + + Ok(()) +} + pub(crate) fn ensure_parent_exists(path: &Path) -> Result<()> { if path.exists() { return Ok(()); diff --git a/src/main.rs b/src/main.rs index 3df48e2..09b3048 100644 --- a/src/main.rs +++ b/src/main.rs @@ -106,7 +106,7 @@ async fn main() -> Result<()> { } if let Some(name) = &cli.sandbox { - return sandbox::launch(name.clone()); + return sandbox::launch(name.clone(), cli.fresh); } install_builtins()?; diff --git a/src/sandbox/mod.rs b/src/sandbox/mod.rs index 94aa8a0..19a9fc4 100644 --- a/src/sandbox/mod.rs +++ b/src/sandbox/mod.rs @@ -29,7 +29,7 @@ const SANDBOX_AGENT: &str = "coyote"; #[folder = "assets/sbx-kit/"] struct EmbeddedKit; -pub fn launch(name: Option) -> Result<()> { +pub fn launch(name: Option, fresh: bool) -> Result<()> { ensure_sbx_installed()?; bail_if_nested()?; @@ -49,7 +49,9 @@ pub fn launch(name: Option) -> Result<()> { let vault = Vault::init(&bootstrap)?; let registered = sbx_registered_services()?; inject_llm_secret(&config_content, &vault, ®istered)?; - inject_mcp_secrets(&vault, ®istered)?; + if !fresh { + inject_mcp_secrets(&vault, ®istered)?; + } let discovered = mixins::discover()?; @@ -58,7 +60,9 @@ pub fn launch(name: Option) -> Result<()> { } else { mixins::log_discovery(&discovered, false); create_sandbox(&name, &kit_path, &discovered)?; - copy_host_files(&name)?; + if !fresh { + copy_host_files(&name)?; + } } exec_run(&name, &kit_path)