feat: Added sbx mixins for the secrets providers so users can also bootstrap those as well.
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
schemaVersion: "1"
|
||||
kind: mixin
|
||||
name: vault-aws-secrets-manager
|
||||
description: >
|
||||
Installs the AWS CLI v2 so the Coyote vault can read secrets from AWS
|
||||
Secrets Manager inside the sandbox. The AWS Rust SDK does not strictly
|
||||
require the CLI, but most users authenticate via `aws sso login` or
|
||||
`aws configure`, which need the CLI to be installed. After install, run
|
||||
the appropriate auth command in the sandbox; cached credentials persist
|
||||
for the lifetime of the sandbox.
|
||||
|
||||
network:
|
||||
allowedDomains:
|
||||
- "awscli.amazonaws.com:443"
|
||||
- "sts.amazonaws.com:443"
|
||||
- "*.sts.amazonaws.com:443"
|
||||
- "*.secretsmanager.amazonaws.com:443"
|
||||
- "*.amazonaws.com:443"
|
||||
- "*.awsapps.com:443"
|
||||
|
||||
commands:
|
||||
install:
|
||||
- command: |
|
||||
set -euo pipefail
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y unzip
|
||||
ARCH=$(uname -m)
|
||||
curl -sSL "https://awscli.amazonaws.com/awscli-exe-linux-${ARCH}.zip" -o /tmp/awscliv2.zip
|
||||
unzip -q /tmp/awscliv2.zip -d /tmp
|
||||
sudo /tmp/aws/install
|
||||
rm -rf /tmp/awscliv2.zip /tmp/aws
|
||||
user: "1000"
|
||||
description: Install AWS CLI v2 from the official installer
|
||||
@@ -0,0 +1,24 @@
|
||||
schemaVersion: "1"
|
||||
kind: mixin
|
||||
name: vault-azure-key-vault
|
||||
description: >
|
||||
Installs the Azure CLI (`az`) so the Coyote vault can read secrets from
|
||||
Azure Key Vault inside the sandbox. After install, run `az login` in the
|
||||
sandbox to authenticate; the session token persists for the lifetime of
|
||||
the sandbox.
|
||||
|
||||
network:
|
||||
allowedDomains:
|
||||
- "aka.ms:443"
|
||||
- "packages.microsoft.com:443"
|
||||
- "azurecliprod.blob.core.windows.net:443"
|
||||
- "login.microsoftonline.com:443"
|
||||
- "graph.microsoft.com:443"
|
||||
- "management.azure.com:443"
|
||||
- "*.vault.azure.net:443"
|
||||
|
||||
commands:
|
||||
install:
|
||||
- command: "curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash"
|
||||
user: "1000"
|
||||
description: Install Azure CLI via Microsoft's official install script
|
||||
@@ -0,0 +1,34 @@
|
||||
schemaVersion: "1"
|
||||
kind: mixin
|
||||
name: vault-gcp-secret-manager
|
||||
description: >
|
||||
Installs the Google Cloud CLI (`gcloud`) so the Coyote vault can read
|
||||
secrets from GCP Secret Manager inside the sandbox. The GCP Rust SDK does
|
||||
not strictly require the CLI, but most users authenticate via
|
||||
`gcloud auth application-default login`, which needs the CLI to be
|
||||
installed. After install, run that command in the sandbox; the ADC file
|
||||
persists for the lifetime of the sandbox.
|
||||
|
||||
network:
|
||||
allowedDomains:
|
||||
- "packages.cloud.google.com:443"
|
||||
- "accounts.google.com:443"
|
||||
- "oauth2.googleapis.com:443"
|
||||
- "secretmanager.googleapis.com:443"
|
||||
- "cloudresourcemanager.googleapis.com:443"
|
||||
- "*.googleapis.com:443"
|
||||
|
||||
commands:
|
||||
install:
|
||||
- command: |
|
||||
set -euo pipefail
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y apt-transport-https ca-certificates gnupg
|
||||
echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" \
|
||||
| sudo tee /etc/apt/sources.list.d/google-cloud-sdk.list >/dev/null
|
||||
curl -sSL https://packages.cloud.google.com/apt/doc/apt-key.gpg \
|
||||
| sudo gpg --dearmor -o /usr/share/keyrings/cloud.google.gpg
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y google-cloud-cli
|
||||
user: "1000"
|
||||
description: Install gcloud CLI from Google's official apt repository
|
||||
@@ -0,0 +1,30 @@
|
||||
schemaVersion: "1"
|
||||
kind: mixin
|
||||
name: vault-gopass
|
||||
description: >
|
||||
Installs `gopass` and `gpg` so the Coyote vault can read secrets from a
|
||||
gopass store inside the sandbox. The store must be cloned manually
|
||||
(gopass walks a user-specific git remote, so v1 only allowlists github.com
|
||||
and gitlab.com; add other hosts via a user mixin if needed). After install,
|
||||
run `gopass setup` or `gopass clone <remote>` in the sandbox.
|
||||
|
||||
network:
|
||||
allowedDomains:
|
||||
- "github.com:443"
|
||||
- "api.github.com:443"
|
||||
- "objects.githubusercontent.com:443"
|
||||
- "gitlab.com:443"
|
||||
|
||||
commands:
|
||||
install:
|
||||
- command: |
|
||||
set -euo pipefail
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gnupg2 git
|
||||
GOPASS_VERSION="1.15.13"
|
||||
ARCH=$(dpkg --print-architecture)
|
||||
curl -sSL "https://github.com/gopasspw/gopass/releases/download/v${GOPASS_VERSION}/gopass_${GOPASS_VERSION}_linux_${ARCH}.deb" -o /tmp/gopass.deb
|
||||
sudo dpkg -i /tmp/gopass.deb
|
||||
rm -f /tmp/gopass.deb
|
||||
user: "1000"
|
||||
description: Install gnupg2, git, and gopass from the official .deb release
|
||||
@@ -0,0 +1,31 @@
|
||||
schemaVersion: "1"
|
||||
kind: mixin
|
||||
name: vault-one-password
|
||||
description: >
|
||||
Installs the 1Password CLI (`op`) so the Coyote vault can decrypt secrets
|
||||
inside the sandbox. After install, run `op signin` in the sandbox to
|
||||
authenticate; credentials persist for the lifetime of the sandbox.
|
||||
|
||||
network:
|
||||
allowedDomains:
|
||||
- "downloads.1password.com:443"
|
||||
- "cache.agilebits.com:443"
|
||||
- "my.1password.com:443"
|
||||
- "my.1password.eu:443"
|
||||
- "my.1password.ca:443"
|
||||
- "events.1password.com:443"
|
||||
|
||||
commands:
|
||||
install:
|
||||
- command: |
|
||||
set -euo pipefail
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y unzip
|
||||
OP_VERSION="v2.30.3"
|
||||
ARCH=$(dpkg --print-architecture)
|
||||
curl -sSL "https://cache.agilebits.com/dist/1P/op2/pkg/${OP_VERSION}/op_linux_${ARCH}_${OP_VERSION}.zip" -o /tmp/op.zip
|
||||
sudo unzip -od /usr/local/bin /tmp/op.zip op
|
||||
sudo chmod +x /usr/local/bin/op
|
||||
rm -f /tmp/op.zip
|
||||
user: "1000"
|
||||
description: Install 1Password CLI from the official archive
|
||||
@@ -146,6 +146,7 @@ const WORKSPACE_MEMORY_DIR_NAME: &str = ".coyote";
|
||||
const SBX_KIT_DIR_NAME: &str = "sbx-kit";
|
||||
const SBX_KIT_HASH_FILE: &str = "kit.sha256";
|
||||
const SBX_MIXIN_FILE_NAME: &str = "sbx-mixin.yaml";
|
||||
const SBX_VAULT_MIXINS_DIR_NAME: &str = "sbx-vault-mixins";
|
||||
const GIT_DIR_NAME: &str = ".git";
|
||||
const GITIGNORE_FILE_NAME: &str = ".gitignore";
|
||||
const DEFAULT_VISIBLE_TOOLS: [&str; 18] = [
|
||||
|
||||
+10
-1
@@ -4,7 +4,8 @@ use super::{
|
||||
ENV_FILE_NAME, FUNCTIONS_BIN_DIR_NAME, FUNCTIONS_DIR_NAME, GLOBAL_TOOLS_DIR_NAME,
|
||||
GLOBAL_TOOLS_UTILS_DIR_NAME, MACROS_DIR_NAME, MCP_FILE_NAME, MEMORY_DIR_NAME,
|
||||
MEMORY_INDEX_FILE_NAME, ModelsOverride, RAGS_DIR_NAME, ROLES_DIR_NAME, SBX_KIT_DIR_NAME,
|
||||
SBX_KIT_HASH_FILE, SBX_MIXIN_FILE_NAME, SKILLS_DIR_NAME, WORKSPACE_MEMORY_DIR_NAME,
|
||||
SBX_KIT_HASH_FILE, SBX_MIXIN_FILE_NAME, SBX_VAULT_MIXINS_DIR_NAME, SKILLS_DIR_NAME,
|
||||
WORKSPACE_MEMORY_DIR_NAME,
|
||||
};
|
||||
use crate::client::ProviderModels;
|
||||
use crate::utils::{get_env_name, list_file_names, normalize_env_name};
|
||||
@@ -81,6 +82,14 @@ pub fn sbx_kit_hash_file() -> PathBuf {
|
||||
sbx_kit_dir().join(SBX_KIT_HASH_FILE)
|
||||
}
|
||||
|
||||
pub fn sbx_vault_mixins_dir() -> PathBuf {
|
||||
cache_path().join(SBX_VAULT_MIXINS_DIR_NAME)
|
||||
}
|
||||
|
||||
pub fn sbx_vault_mixins_hash_file() -> PathBuf {
|
||||
sbx_vault_mixins_dir().join(SBX_KIT_HASH_FILE)
|
||||
}
|
||||
|
||||
pub fn config_file() -> PathBuf {
|
||||
match env::var(get_env_name("config_file")) {
|
||||
Ok(value) => PathBuf::from(value),
|
||||
|
||||
+206
-1
@@ -9,6 +9,8 @@ use which::which;
|
||||
|
||||
mod mixins;
|
||||
|
||||
use gman::providers::SupportedProvider;
|
||||
|
||||
use crate::config::paths;
|
||||
use crate::sandbox::mixins::DiscoveredMixin;
|
||||
use crate::utils::run_command_with_output;
|
||||
@@ -22,6 +24,10 @@ const SANDBOX_AGENT: &str = "coyote";
|
||||
#[folder = "assets/sbx-kit/"]
|
||||
struct EmbeddedKit;
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "assets/sbx-vault-mixins/"]
|
||||
struct EmbeddedVaultMixins;
|
||||
|
||||
pub fn launch(name: Option<String>, fresh: bool, no_mixins: bool) -> Result<()> {
|
||||
ensure_sbx_installed()?;
|
||||
bail_if_nested()?;
|
||||
@@ -32,7 +38,13 @@ pub fn launch(name: Option<String>, fresh: bool, no_mixins: bool) -> Result<()>
|
||||
let discovered = if no_mixins {
|
||||
Vec::new()
|
||||
} else {
|
||||
mixins::discover()?
|
||||
let mut all = mixins::discover()?;
|
||||
if let Ok(vault) = Vault::init_bare()
|
||||
&& let Some(vault_mixin) = extract_vault_mixin(&vault.provider)?
|
||||
{
|
||||
all.insert(0, vault_mixin);
|
||||
}
|
||||
all
|
||||
};
|
||||
|
||||
if sandbox_exists(&name)? {
|
||||
@@ -195,6 +207,99 @@ fn compute_kit_hash() -> Result<String> {
|
||||
Ok(format!("{:x}", hasher.finalize()))
|
||||
}
|
||||
|
||||
fn extract_vault_mixin(provider: &SupportedProvider) -> Result<Option<DiscoveredMixin>> {
|
||||
let provider_dir = match provider {
|
||||
SupportedProvider::Local { .. } => return Ok(None),
|
||||
SupportedProvider::AwsSecretsManager { .. } => "aws_secrets_manager",
|
||||
SupportedProvider::GcpSecretManager { .. } => "gcp_secret_manager",
|
||||
SupportedProvider::AzureKeyVault { .. } => "azure_key_vault",
|
||||
SupportedProvider::Gopass { .. } => "gopass",
|
||||
SupportedProvider::OnePassword { .. } => "one_password",
|
||||
};
|
||||
|
||||
let cache_root = extract_vault_mixins_cache()?;
|
||||
let provider_root = cache_root.join(provider_dir);
|
||||
let spec_path = provider_root.join("spec.yaml");
|
||||
|
||||
if !spec_path.exists() {
|
||||
bail!(
|
||||
"Embedded vault mixin for '{provider_dir}' is missing spec.yaml at {}",
|
||||
spec_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
let label = format!("<built-in: vault-{provider_dir}>");
|
||||
let (install_count, domain_count) = mixins::summarize(&spec_path)?;
|
||||
|
||||
Ok(Some(DiscoveredMixin {
|
||||
path: provider_root,
|
||||
label,
|
||||
install_count,
|
||||
domain_count,
|
||||
}))
|
||||
}
|
||||
|
||||
fn extract_vault_mixins_cache() -> Result<PathBuf> {
|
||||
let cache_root = paths::sbx_vault_mixins_dir();
|
||||
let new_hash = compute_vault_mixins_hash()?;
|
||||
let hash_file = paths::sbx_vault_mixins_hash_file();
|
||||
if let Ok(existing) = fs::read_to_string(&hash_file)
|
||||
&& existing == new_hash
|
||||
{
|
||||
return Ok(cache_root);
|
||||
}
|
||||
|
||||
if cache_root.exists() {
|
||||
fs::remove_dir_all(&cache_root).with_context(|| {
|
||||
format!(
|
||||
"Failed to clear stale vault mixins at {}",
|
||||
cache_root.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
fs::create_dir_all(&cache_root)
|
||||
.with_context(|| format!("Failed to create {}", cache_root.display()))?;
|
||||
|
||||
for entry in EmbeddedVaultMixins::iter() {
|
||||
let file = EmbeddedVaultMixins::get(&entry).ok_or_else(|| {
|
||||
anyhow!("Embedded vault mixin file missing during extraction: {entry}")
|
||||
})?;
|
||||
let dest = cache_root.join(entry.as_ref());
|
||||
if let Some(parent) = dest.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Failed to create {}", parent.display()))?;
|
||||
}
|
||||
|
||||
fs::write(&dest, &file.data)
|
||||
.with_context(|| format!("Failed to write {}", dest.display()))?;
|
||||
}
|
||||
|
||||
fs::write(&hash_file, &new_hash)
|
||||
.with_context(|| format!("Failed to write {}", hash_file.display()))?;
|
||||
debug!(
|
||||
"Extracted embedded sbx-vault-mixins to {}",
|
||||
cache_root.display()
|
||||
);
|
||||
|
||||
Ok(cache_root)
|
||||
}
|
||||
|
||||
fn compute_vault_mixins_hash() -> Result<String> {
|
||||
let mut hasher = Sha256::new();
|
||||
let mut entries: Vec<_> = EmbeddedVaultMixins::iter().collect();
|
||||
entries.sort();
|
||||
|
||||
for entry in &entries {
|
||||
let file = EmbeddedVaultMixins::get(entry)
|
||||
.ok_or_else(|| anyhow!("Embedded vault mixin file missing during hash: {entry}"))?;
|
||||
hasher.update(entry.as_bytes());
|
||||
hasher.update(b"\0");
|
||||
hasher.update(&file.data);
|
||||
}
|
||||
|
||||
Ok(format!("{:x}", hasher.finalize()))
|
||||
}
|
||||
|
||||
fn sandbox_exists(name: &str) -> Result<bool> {
|
||||
let (success, stdout, stderr) =
|
||||
run_command_with_output(SBX_BINARY, &["ls"], None).context("Failed to run `sbx ls`")?;
|
||||
@@ -487,4 +592,104 @@ mod tests {
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
mod vault_mixins {
|
||||
use super::*;
|
||||
use gman::providers::aws_secrets_manager::AwsSecretsManagerProvider;
|
||||
use gman::providers::azure_key_vault::AzureKeyVaultProvider;
|
||||
use gman::providers::gcp_secret_manager::GcpSecretManagerProvider;
|
||||
use gman::providers::gopass::GopassProvider;
|
||||
use gman::providers::local::LocalProvider;
|
||||
use gman::providers::one_password::OnePasswordProvider;
|
||||
use serial_test::serial;
|
||||
|
||||
#[test]
|
||||
fn returns_none_for_local() {
|
||||
let p = SupportedProvider::Local {
|
||||
provider_def: LocalProvider::default(),
|
||||
};
|
||||
assert!(extract_vault_mixin(&p).unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn returns_some_for_aws() {
|
||||
let p = SupportedProvider::AwsSecretsManager {
|
||||
provider_def: AwsSecretsManagerProvider {
|
||||
aws_profile: None,
|
||||
aws_region: None,
|
||||
},
|
||||
};
|
||||
let m = extract_vault_mixin(&p)
|
||||
.unwrap()
|
||||
.expect("expected vault mixin");
|
||||
assert!(m.path.join("spec.yaml").exists());
|
||||
assert!(m.label.contains("aws_secrets_manager"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn returns_some_for_gcp() {
|
||||
let p = SupportedProvider::GcpSecretManager {
|
||||
provider_def: GcpSecretManagerProvider {
|
||||
gcp_project_id: None,
|
||||
},
|
||||
};
|
||||
let m = extract_vault_mixin(&p)
|
||||
.unwrap()
|
||||
.expect("expected vault mixin");
|
||||
assert!(m.path.join("spec.yaml").exists());
|
||||
assert!(m.label.contains("gcp_secret_manager"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn returns_some_for_one_password() {
|
||||
let p = SupportedProvider::OnePassword {
|
||||
provider_def: OnePasswordProvider {
|
||||
vault: None,
|
||||
account: None,
|
||||
},
|
||||
};
|
||||
let m = extract_vault_mixin(&p)
|
||||
.unwrap()
|
||||
.expect("expected vault mixin");
|
||||
assert!(m.path.join("spec.yaml").exists());
|
||||
assert!(m.label.contains("one_password"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn returns_some_for_azure() {
|
||||
let p = SupportedProvider::AzureKeyVault {
|
||||
provider_def: AzureKeyVaultProvider { vault_name: None },
|
||||
};
|
||||
let m = extract_vault_mixin(&p)
|
||||
.unwrap()
|
||||
.expect("expected vault mixin");
|
||||
assert!(m.path.join("spec.yaml").exists());
|
||||
assert!(m.label.contains("azure_key_vault"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn returns_some_for_gopass() {
|
||||
let p = SupportedProvider::Gopass {
|
||||
provider_def: GopassProvider { store: None },
|
||||
};
|
||||
let m = extract_vault_mixin(&p)
|
||||
.unwrap()
|
||||
.expect("expected vault mixin");
|
||||
assert!(m.path.join("spec.yaml").exists());
|
||||
assert!(m.label.contains("gopass"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hash_is_deterministic() {
|
||||
let h1 = compute_vault_mixins_hash().unwrap();
|
||||
let h2 = compute_vault_mixins_hash().unwrap();
|
||||
assert_eq!(h1, h2);
|
||||
assert_eq!(h1.len(), 64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user