use crate::cli::PolicyFormat;
use crate::engine::error::Error;
use anyhow::{Context, Result};
use serde::{Deserialize, Deserializer};
use serde_json::{Map, Value};
use std::error::Error as StdError;
use std::ffi::OsStr;
use std::fs;
use std::io::{self, Read};
use std::path::{Path, PathBuf};
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
pub(crate) struct Settings {
pub(crate) filesystem: SandboxFilesystem,
pub(crate) network: SandboxNetwork,
pub(crate) windows: SandboxWindows,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub(crate) struct SandboxFilesystem {
#[serde(deserialize_with = "deserialize_paths")]
pub(crate) allow_write: Vec<String>,
#[serde(deserialize_with = "deserialize_paths")]
pub(crate) deny_write: Vec<String>,
#[serde(deserialize_with = "deserialize_paths")]
pub(crate) allow_read: Vec<String>,
#[serde(deserialize_with = "deserialize_paths")]
pub(crate) deny_read: Vec<String>,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub(crate) struct SandboxNetwork {
pub(crate) allow_network: bool,
pub(crate) allow_local_binding: bool,
pub(crate) allow_all_unix_sockets: bool,
#[serde(deserialize_with = "deserialize_paths")]
pub(crate) allow_unix_sockets: Vec<String>,
pub(crate) http_proxy_port: Option<u16>,
pub(crate) socks_proxy_port: Option<u16>,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) enum AppContainerMode {
#[default]
Lpac,
Standard,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Hash, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) enum WindowsBackend {
#[default]
AppContainer,
RestrictedUser,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub(crate) struct SandboxWindows {
pub(crate) backend: WindowsBackend,
pub(crate) app_container_mode: AppContainerMode,
pub(crate) allow_loopback: bool,
}
pub(crate) fn load_settings(
policy_paths: &[PathBuf],
format: PolicyFormat,
tool: Option<&OsStr>,
cwd: &Path,
) -> Result<Settings> {
let mut merged = Value::Object(Map::new());
let mut paths_to_load = Vec::from(policy_paths);
if let Some(tool_name) = tool {
let exe_policy_paths = find_executable_policy_paths(tool_name, format, cwd);
paths_to_load.extend(exe_policy_paths);
}
if paths_to_load.is_empty() {
let mut document = String::new();
io::stdin()
.read_to_string(&mut document)
.map_err(|source| Error::PolicyIoFailed { source })
.context("policy stdin")?;
let value = parse_policy_document(&document, format).context("policy stdin")?;
merge_json(&mut merged, value);
parse_settings(merged).context("policy stdin")
} else {
let mut last_path = &paths_to_load[0];
for path in &paths_to_load {
log::debug!("config: {}", path.display());
let document = fs::read_to_string(path)
.map_err(|source| Error::PolicyIoFailed { source })
.with_context(|| format!("policy file {}", path.display()))?;
let value = parse_policy_document(&document, format)
.with_context(|| format!("policy file {}", path.display()))?;
merge_json(&mut merged, value);
last_path = path;
}
parse_settings(merged).with_context(|| format!("policy file {}", last_path.display()))
}
}
pub(crate) fn find_executable_policy_paths(
tool: &OsStr,
format: PolicyFormat,
cwd: &Path,
) -> Vec<PathBuf> {
let tool_path = Path::new(tool);
let Some(tool_file_name) = tool_path.file_name().and_then(OsStr::to_str) else {
return Vec::new();
};
let name = Path::new(tool_file_name)
.file_stem()
.and_then(OsStr::to_str)
.unwrap_or(tool_file_name);
let ext = match format {
PolicyFormat::Json => "json",
PolicyFormat::Yaml => "yaml",
};
let filename = format!("sandbox.{name}.{ext}");
let mut candidates = Vec::new();
if let Some(parent) = tool_path.parent()
&& !parent.as_os_str().is_empty()
{
candidates.push(parent.join(".pi").join(&filename));
candidates.push(parent.join(".opencode").join(&filename));
}
candidates.push(PathBuf::from("/etc/landstrip").join(&filename));
candidates.push(cwd.join(".pi").join(&filename));
candidates.push(cwd.join(".opencode").join(&filename));
candidates.push(cwd.join(&filename));
if let Ok(home) = std::env::var("HOME") {
let home_path = PathBuf::from(home);
candidates.push(home_path.join(".pi").join("agent").join(&filename));
candidates.push(home_path.join(".config").join("landstrip").join(&filename));
}
if matches!(format, PolicyFormat::Yaml) {
let yml_filename = format!("sandbox.{name}.yml");
if let Some(parent) = tool_path.parent()
&& !parent.as_os_str().is_empty()
{
candidates.push(parent.join(".pi").join(&yml_filename));
candidates.push(parent.join(".opencode").join(&yml_filename));
}
candidates.push(PathBuf::from("/etc/landstrip").join(&yml_filename));
candidates.push(cwd.join(".pi").join(&yml_filename));
candidates.push(cwd.join(".opencode").join(&yml_filename));
candidates.push(cwd.join(&yml_filename));
if let Ok(home) = std::env::var("HOME") {
let home_path = PathBuf::from(home);
candidates.push(home_path.join(".pi").join("agent").join(&yml_filename));
candidates.push(
home_path
.join(".config")
.join("landstrip")
.join(&yml_filename),
);
}
}
let mut result = Vec::new();
for path in candidates {
if path.is_file() && !result.contains(&path) {
result.push(path);
}
}
result
}
fn parse_policy_document(
document: &str,
format: PolicyFormat,
) -> std::result::Result<Value, Error> {
match format {
PolicyFormat::Json => serde_json::from_str(document).map_err(parse_failed),
PolicyFormat::Yaml => serde_yml::from_str(document).map_err(parse_failed),
}
}
fn parse_settings(document: Value) -> std::result::Result<Settings, Error> {
serde_json::from_value(document).map_err(parse_failed)
}
fn parse_failed(source: impl StdError + Send + Sync + 'static) -> Error {
Error::PolicyParseFailed {
source: Box::new(source),
}
}
fn deserialize_paths<'de, D>(deserializer: D) -> std::result::Result<Vec<String>, D::Error>
where
D: Deserializer<'de>,
{
let input = Option::<PathInput>::deserialize(deserializer)?;
Ok(match input {
Some(PathInput::List(paths)) => paths,
Some(PathInput::Lines(lines)) => lines
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.map(ToOwned::to_owned)
.collect(),
None => Vec::new(),
})
}
#[derive(Deserialize)]
#[serde(untagged)]
enum PathInput {
List(Vec<String>),
Lines(String),
}
fn merge_json(base: &mut Value, overlay: Value) {
match (base, overlay) {
(Value::Object(base), Value::Object(overlay)) => {
for (key, value) in overlay {
merge_json(base.entry(key).or_insert(Value::Null), value);
}
}
(Value::Array(base), Value::Array(overlay)) => {
for value in overlay {
if !base.contains(&value) {
base.push(value);
}
}
}
(base, overlay) => {
*base = overlay;
}
}
}