use crate::backend::{FileInjection, Sandbox};
use anyhow::{Result, bail};
use rand::RngCore;
use std::collections::HashMap;
pub const DEFAULT_SECRETS_PATH: &str = "/run/agentkernel/secrets";
pub const PLACEHOLDER_PREFIX: &str = "AGENTKERNEL_PLACEHOLDER_";
#[derive(Debug, Clone, Default)]
pub struct PlaceholderMap {
pub mappings: HashMap<String, String>,
}
impl PlaceholderMap {
pub fn new() -> Self {
Self::default()
}
pub fn insert_secret(&mut self, real_value: &str) -> String {
let mut bytes = [0u8; 16];
rand::rngs::OsRng.fill_bytes(&mut bytes);
let token = format!("{}{}", PLACEHOLDER_PREFIX, hex::encode(bytes));
self.mappings.insert(token.clone(), real_value.to_string());
token
}
pub fn substitute(&self, input: &str) -> (String, bool) {
let mut result = input.to_string();
let mut replaced = false;
for (placeholder, real_value) in &self.mappings {
if result.contains(placeholder.as_str()) {
result = result.replace(placeholder.as_str(), real_value);
replaced = true;
}
}
(result, replaced)
}
pub fn substitute_bytes(&self, input: &[u8]) -> (Vec<u8>, bool) {
if !contains_bytes(input, PLACEHOLDER_PREFIX.as_bytes()) {
return (input.to_vec(), false);
}
match std::str::from_utf8(input) {
Ok(s) => {
let (result, replaced) = self.substitute(s);
(result.into_bytes(), replaced)
}
Err(_) => (input.to_vec(), false),
}
}
pub fn is_empty(&self) -> bool {
self.mappings.is_empty()
}
}
fn contains_bytes(haystack: &[u8], needle: &[u8]) -> bool {
haystack
.windows(needle.len())
.any(|window| window == needle)
}
pub async fn inject_secrets_as_files(
sandbox: &mut dyn Sandbox,
mount_path: &str,
resolved_secrets: &HashMap<String, String>,
) -> Result<Vec<String>> {
if resolved_secrets.is_empty() {
return Ok(Vec::new());
}
for key in resolved_secrets.keys() {
validate_secret_key(key)?;
}
sandbox.exec(&["mkdir", "-p", mount_path]).await?;
let files: Vec<FileInjection> = resolved_secrets
.iter()
.map(|(key, value)| FileInjection {
dest: format!("{}/{}", mount_path, key),
content: value.as_bytes().to_vec(),
})
.collect();
let injected: Vec<String> = resolved_secrets.keys().cloned().collect();
sandbox.inject_files(&files).await?;
let chmod_cmd = format!("chmod 400 {}/*", mount_path);
let _ = sandbox.exec(&["sh", "-c", &chmod_cmd]).await;
Ok(injected)
}
pub async fn inject_secrets_as_placeholders(
sandbox: &mut dyn Sandbox,
mount_path: &str,
resolved_secrets: &HashMap<String, String>,
) -> Result<(Vec<String>, PlaceholderMap)> {
if resolved_secrets.is_empty() {
return Ok((Vec::new(), PlaceholderMap::new()));
}
for key in resolved_secrets.keys() {
validate_secret_key(key)?;
}
let mut placeholder_map = PlaceholderMap::new();
sandbox.exec(&["mkdir", "-p", mount_path]).await?;
let files: Vec<FileInjection> = resolved_secrets
.iter()
.map(|(key, value)| {
let placeholder = placeholder_map.insert_secret(value);
FileInjection {
dest: format!("{}/{}", mount_path, key),
content: placeholder.into_bytes(),
}
})
.collect();
let injected: Vec<String> = resolved_secrets.keys().cloned().collect();
sandbox.inject_files(&files).await?;
let chmod_cmd = format!("chmod 400 {}/*", mount_path);
let _ = sandbox.exec(&["sh", "-c", &chmod_cmd]).await;
Ok((injected, placeholder_map))
}
pub fn validate_secret_key(key: &str) -> Result<()> {
if key.is_empty() {
bail!("Secret key cannot be empty");
}
if !key
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
{
bail!(
"Secret key '{}' contains invalid characters. Use alphanumeric, underscore, or hyphen only.",
key
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_secret_key_valid() {
assert!(validate_secret_key("OPENAI_API_KEY").is_ok());
assert!(validate_secret_key("my-secret").is_ok());
assert!(validate_secret_key("SECRET123").is_ok());
assert!(validate_secret_key("a").is_ok());
}
#[test]
fn test_validate_secret_key_invalid() {
assert!(validate_secret_key("").is_err());
assert!(validate_secret_key("secret/key").is_err());
assert!(validate_secret_key("key with spaces").is_err());
}
#[test]
fn test_default_secrets_path() {
assert_eq!(DEFAULT_SECRETS_PATH, "/run/agentkernel/secrets");
}
#[test]
fn test_placeholder_map_insert_and_substitute() {
let mut map = PlaceholderMap::new();
let placeholder = map.insert_secret("sk-real-api-key-12345");
assert!(placeholder.starts_with(PLACEHOLDER_PREFIX));
assert_eq!(placeholder.len(), PLACEHOLDER_PREFIX.len() + 32);
let input = format!("Authorization: Bearer {}", placeholder);
let (result, replaced) = map.substitute(&input);
assert!(replaced);
assert_eq!(result, "Authorization: Bearer sk-real-api-key-12345");
}
#[test]
fn test_placeholder_map_no_match() {
let map = PlaceholderMap::new();
let (result, replaced) = map.substitute("no placeholders here");
assert!(!replaced);
assert_eq!(result, "no placeholders here");
}
#[test]
fn test_placeholder_map_multiple_secrets() {
let mut map = PlaceholderMap::new();
let p1 = map.insert_secret("secret-one");
let p2 = map.insert_secret("secret-two");
assert_ne!(p1, p2);
let input = format!("key1={} key2={}", p1, p2);
let (result, replaced) = map.substitute(&input);
assert!(replaced);
assert_eq!(result, "key1=secret-one key2=secret-two");
}
#[test]
fn test_placeholder_map_substitute_bytes() {
let mut map = PlaceholderMap::new();
let placeholder = map.insert_secret("real-value");
let input = format!("{{\"api_key\":\"{}\"}}", placeholder);
let (result, replaced) = map.substitute_bytes(input.as_bytes());
assert!(replaced);
assert_eq!(
String::from_utf8(result).unwrap(),
"{\"api_key\":\"real-value\"}"
);
}
#[test]
fn test_placeholder_map_substitute_bytes_no_match() {
let map = PlaceholderMap::new();
let input = b"no placeholders here";
let (result, replaced) = map.substitute_bytes(input);
assert!(!replaced);
assert_eq!(result, input);
}
#[test]
fn test_placeholder_map_substitute_bytes_invalid_utf8() {
let mut map = PlaceholderMap::new();
map.insert_secret("value");
let input: &[u8] = &[0xFF, 0xFE, 0xFD];
let (result, replaced) = map.substitute_bytes(input);
assert!(!replaced);
assert_eq!(result, input);
}
#[test]
fn test_contains_bytes() {
assert!(contains_bytes(b"hello world", b"world"));
assert!(contains_bytes(b"hello world", b"hello"));
assert!(!contains_bytes(b"hello world", b"xyz"));
assert!(!contains_bytes(b"hi", b"hello"));
}
}