Compare commits
2
Commits
df909325a7
...
5a1bb569b4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a1bb569b4
|
||
|
|
0c12580836
|
+4
-1
@@ -30,7 +30,7 @@ use std::io::{Read, stdin};
|
|||||||
",
|
",
|
||||||
group(
|
group(
|
||||||
ArgGroup::new("sbx-mode")
|
ArgGroup::new("sbx-mode")
|
||||||
.args(["sandbox"])
|
.args(["sandbox", "fresh"])
|
||||||
.multiple(true)
|
.multiple(true)
|
||||||
.conflicts_with_all([
|
.conflicts_with_all([
|
||||||
"model", "prompt", "role", "session", "agent", "rag", "rebuild_rag",
|
"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
|
/// Launch Coyote inside a Docker sandbox (via `sbx`); name defaults to current directory basename
|
||||||
#[arg(long, value_name = "NAME", help_heading = "Sandbox")]
|
#[arg(long, value_name = "NAME", help_heading = "Sandbox")]
|
||||||
pub sandbox: Option<Option<String>>,
|
pub sandbox: Option<Option<String>>,
|
||||||
|
/// 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
|
/// Display information
|
||||||
#[arg(long, help_heading = "Diagnostics & Tools")]
|
#[arg(long, help_heading = "Diagnostics & Tools")]
|
||||||
pub info: bool,
|
pub info: bool,
|
||||||
|
|||||||
+61
-1
@@ -45,7 +45,7 @@ pub use self::skill_registry::SkillRegistry;
|
|||||||
pub use self::update::run_self_update;
|
pub use self::update::run_self_update;
|
||||||
use crate::client::{
|
use crate::client::{
|
||||||
ClientConfig, MessageContentToolCalls, Model, ModelType, OPENAI_COMPATIBLE_PROVIDERS,
|
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::function::{FunctionDeclaration, Functions};
|
||||||
use crate::rag::Rag;
|
use crate::rag::Rag;
|
||||||
@@ -758,6 +758,10 @@ pub async fn create_config_file(config_path: &Path) -> Result<()> {
|
|||||||
process::exit(0);
|
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 provider_choice = prompt_provider_choice()?;
|
||||||
let mut vault = match &provider_choice {
|
let mut vault = match &provider_choice {
|
||||||
None => Vault::default_local(),
|
None => Vault::default_local(),
|
||||||
@@ -817,6 +821,62 @@ pub async fn create_config_file(config_path: &Path) -> Result<()> {
|
|||||||
Ok(())
|
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<()> {
|
pub(crate) fn ensure_parent_exists(path: &Path) -> Result<()> {
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|||||||
+1
-1
@@ -106,7 +106,7 @@ async fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(name) = &cli.sandbox {
|
if let Some(name) = &cli.sandbox {
|
||||||
return sandbox::launch(name.clone());
|
return sandbox::launch(name.clone(), cli.fresh);
|
||||||
}
|
}
|
||||||
|
|
||||||
install_builtins()?;
|
install_builtins()?;
|
||||||
|
|||||||
+60
-9
@@ -2,6 +2,7 @@ use anyhow::{Context, Result, anyhow, bail};
|
|||||||
use rust_embed::RustEmbed;
|
use rust_embed::RustEmbed;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::collections::HashSet;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
@@ -28,7 +29,7 @@ const SANDBOX_AGENT: &str = "coyote";
|
|||||||
#[folder = "assets/sbx-kit/"]
|
#[folder = "assets/sbx-kit/"]
|
||||||
struct EmbeddedKit;
|
struct EmbeddedKit;
|
||||||
|
|
||||||
pub fn launch(name: Option<String>) -> Result<()> {
|
pub fn launch(name: Option<String>, fresh: bool) -> Result<()> {
|
||||||
ensure_sbx_installed()?;
|
ensure_sbx_installed()?;
|
||||||
bail_if_nested()?;
|
bail_if_nested()?;
|
||||||
|
|
||||||
@@ -46,8 +47,11 @@ pub fn launch(name: Option<String>) -> Result<()> {
|
|||||||
..AppConfig::default()
|
..AppConfig::default()
|
||||||
};
|
};
|
||||||
let vault = Vault::init(&bootstrap)?;
|
let vault = Vault::init(&bootstrap)?;
|
||||||
inject_llm_secret(&config_content, &vault)?;
|
let registered = sbx_registered_services()?;
|
||||||
inject_mcp_secrets(&vault)?;
|
inject_llm_secret(&config_content, &vault, ®istered)?;
|
||||||
|
if !fresh {
|
||||||
|
inject_mcp_secrets(&vault, ®istered)?;
|
||||||
|
}
|
||||||
|
|
||||||
let discovered = mixins::discover()?;
|
let discovered = mixins::discover()?;
|
||||||
|
|
||||||
@@ -56,7 +60,9 @@ pub fn launch(name: Option<String>) -> Result<()> {
|
|||||||
} else {
|
} else {
|
||||||
mixins::log_discovery(&discovered, false);
|
mixins::log_discovery(&discovered, false);
|
||||||
create_sandbox(&name, &kit_path, &discovered)?;
|
create_sandbox(&name, &kit_path, &discovered)?;
|
||||||
copy_host_files(&name)?;
|
if !fresh {
|
||||||
|
copy_host_files(&name)?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exec_run(&name, &kit_path)
|
exec_run(&name, &kit_path)
|
||||||
@@ -197,7 +203,11 @@ fn compute_kit_hash() -> Result<String> {
|
|||||||
Ok(format!("{:x}", hasher.finalize()))
|
Ok(format!("{:x}", hasher.finalize()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inject_llm_secret(config_content: &str, vault: &Vault) -> Result<()> {
|
fn inject_llm_secret(
|
||||||
|
config_content: &str,
|
||||||
|
vault: &Vault,
|
||||||
|
registered: &HashSet<String>,
|
||||||
|
) -> Result<()> {
|
||||||
let value: serde_yaml::Value = serde_yaml::from_str(config_content)
|
let value: serde_yaml::Value = serde_yaml::from_str(config_content)
|
||||||
.context("Failed to parse config for LLM secret injection")?;
|
.context("Failed to parse config for LLM secret injection")?;
|
||||||
|
|
||||||
@@ -219,6 +229,14 @@ fn inject_llm_secret(config_content: &str, vault: &Vault) -> Result<()> {
|
|||||||
let client_name = client.get("name").and_then(|v| v.as_str());
|
let client_name = client.get("name").and_then(|v| v.as_str());
|
||||||
let service = provider_to_sbx_service(client_type, client_name);
|
let service = provider_to_sbx_service(client_type, client_name);
|
||||||
|
|
||||||
|
if registered.contains(&service) {
|
||||||
|
eprintln!(
|
||||||
|
"Secret for '{service}' already registered with sbx. \
|
||||||
|
To update it, run: sbx secret set -g --force {service}"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let secret_value = vault
|
let secret_value = vault
|
||||||
.get_secret(&secret_name, false)
|
.get_secret(&secret_name, false)
|
||||||
.with_context(|| format!("Failed to decrypt LLM api_key secret '{secret_name}'"))?;
|
.with_context(|| format!("Failed to decrypt LLM api_key secret '{secret_name}'"))?;
|
||||||
@@ -242,7 +260,7 @@ fn find_secret_placeholder(value: &Value) -> Option<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inject_mcp_secrets(vault: &Vault) -> Result<()> {
|
fn inject_mcp_secrets(vault: &Vault, registered: &HashSet<String>) -> Result<()> {
|
||||||
let mcp_path = paths::mcp_config_file();
|
let mcp_path = paths::mcp_config_file();
|
||||||
if !mcp_path.exists() {
|
if !mcp_path.exists() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -262,6 +280,14 @@ fn inject_mcp_secrets(vault: &Vault) -> Result<()> {
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if registered.contains(server_name.as_str()) {
|
||||||
|
eprintln!(
|
||||||
|
"Secret for '{server_name}' already registered with sbx. \
|
||||||
|
To update it, run: sbx secret set -g --force {server_name}"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let secret_value = vault.get_secret(&secret_name, false).with_context(|| {
|
let secret_value = vault.get_secret(&secret_name, false).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"Secret '{secret_name}' referenced by MCP server '{server_name}' not found \
|
"Secret '{secret_name}' referenced by MCP server '{server_name}' not found \
|
||||||
@@ -285,17 +311,42 @@ fn provider_to_sbx_service(provider_type: &str, client_name: Option<&str>) -> St
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sbx_registered_services() -> Result<HashSet<String>> {
|
||||||
|
let (success, stdout, _) = run_command_with_output(SBX_BINARY, &["secret", "ls"], None)
|
||||||
|
.context("Failed to run `sbx secret ls`")?;
|
||||||
|
|
||||||
|
if !success {
|
||||||
|
return Ok(HashSet::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(stdout
|
||||||
|
.lines()
|
||||||
|
.skip(1)
|
||||||
|
.filter_map(|line| {
|
||||||
|
let mut parts = line.split_whitespace();
|
||||||
|
let scope = parts.next()?;
|
||||||
|
let _kind = parts.next()?;
|
||||||
|
let name = parts.next()?;
|
||||||
|
if scope == "(global)" {
|
||||||
|
Some(name.to_string())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
fn sbx_secret_set(service: &str, secret_value: &str) -> Result<()> {
|
fn sbx_secret_set(service: &str, secret_value: &str) -> Result<()> {
|
||||||
let mut child = Command::new(SBX_BINARY)
|
let mut child = Command::new(SBX_BINARY)
|
||||||
.args(["secret", "set", "-g", "--force", service])
|
.args(["secret", "set", "-g", service])
|
||||||
.stdin(Stdio::piped())
|
.stdin(Stdio::piped())
|
||||||
.stdout(Stdio::inherit())
|
.stdout(Stdio::inherit())
|
||||||
.stderr(Stdio::inherit())
|
.stderr(Stdio::inherit())
|
||||||
.spawn()
|
.spawn()
|
||||||
.context("Failed to spawn `sbx secret set -g`")?;
|
.context("Failed to spawn `sbx secret set -g`")?;
|
||||||
|
|
||||||
if let Some(ref mut stdin) = child.stdin {
|
if let Some(mut stdin_handle) = child.stdin.take() {
|
||||||
stdin
|
stdin_handle
|
||||||
.write_all(secret_value.as_bytes())
|
.write_all(secret_value.as_bytes())
|
||||||
.context("Failed to write secret to `sbx secret set -g` stdin")?;
|
.context("Failed to write secret to `sbx secret set -g` stdin")?;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user