use std::cell::RefCell;
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use super::environment_policy::environment_names_equal;
use serde::{Deserialize, Serialize};
thread_local! {
static SESSION_ENVIRONMENT_CONTEXT: RefCell<Option<SessionEnvironment>> =
const { RefCell::new(None) };
}
pub fn set_session_environment(environment: Option<SessionEnvironment>) {
SESSION_ENVIRONMENT_CONTEXT.with(|current| *current.borrow_mut() = environment);
}
pub fn current_session_environment() -> Option<SessionEnvironment> {
SESSION_ENVIRONMENT_CONTEXT.with(|current| current.borrow().clone())
}
#[must_use = "the declaration lasts only while the guard is held"]
pub fn declare_session_environment_if_absent(
environment: SessionEnvironment,
) -> SessionEnvironmentGuard {
let previous = current_session_environment();
if previous.is_none() {
set_session_environment(Some(environment));
}
SessionEnvironmentGuard { previous }
}
pub struct SessionEnvironmentGuard {
previous: Option<SessionEnvironment>,
}
impl Drop for SessionEnvironmentGuard {
fn drop(&mut self) {
set_session_environment(self.previous.take());
}
}
impl SessionEnvironmentGuard {
pub fn with_host_inference_boundary(
self,
boundary: Option<crate::llm::api::InferenceBoundary>,
) -> Self {
if let Some(environment) = current_session_environment() {
set_session_environment(Some(environment.with_host_inference_boundary(boundary)));
}
self
}
}
pub(crate) fn swap_session_environment(
next: Option<SessionEnvironment>,
) -> Option<SessionEnvironment> {
SESSION_ENVIRONMENT_CONTEXT.with(|current| std::mem::replace(&mut *current.borrow_mut(), next))
}
#[derive(Clone, PartialEq, Eq)]
pub struct LauncherEnvironment {
snapshot: BTreeMap<String, String>,
}
impl fmt::Debug for LauncherEnvironment {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("LauncherEnvironment")
.field("names", &self.snapshot.keys().collect::<Vec<_>>())
.finish()
}
}
impl LauncherEnvironment {
pub(crate) fn captured_value(&self, name: &str) -> Option<&str> {
self.snapshot
.iter()
.find(|(candidate, _)| environment_names_equal(candidate, name))
.map(|(_, value)| value.as_str())
}
pub fn capture() -> Self {
Self::from_snapshot(capture_process_environment())
}
pub fn from_snapshot(snapshot: BTreeMap<String, String>) -> Self {
let mut normalized = BTreeMap::new();
for (name, value) in snapshot {
insert_env_value(&mut normalized, &name, value);
}
Self {
snapshot: normalized,
}
}
pub fn with_overlay(mut self, overlay: BTreeMap<String, String>) -> Self {
for (name, value) in overlay {
insert_env_value(&mut self.snapshot, &name, value);
}
self
}
pub fn launch(
&self,
kind: EnvironmentPolicyKind,
specs: Vec<GrantSpec>,
) -> Result<SessionEnvironment, EnvironmentPolicyError> {
SessionEnvironment::launch_from_snapshot(kind, specs, self.snapshot.clone(), &|name| {
self.snapshot
.iter()
.find(|(candidate, _)| environment_names_equal(candidate, name))
.map(|(_, value)| value.clone())
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GrantSource {
Env,
SecretStore,
Literal,
}
impl GrantSource {
pub fn as_str(self) -> &'static str {
match self {
GrantSource::Env => "env",
GrantSource::SecretStore => "secret_store",
GrantSource::Literal => "literal",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GrantSourceSpec {
Env { var: String },
SecretStore { account: String, key: String },
Literal { value: String },
}
impl GrantSourceSpec {
fn kind(&self) -> GrantSource {
match self {
GrantSourceSpec::Env { .. } => GrantSource::Env,
GrantSourceSpec::SecretStore { .. } => GrantSource::SecretStore,
GrantSourceSpec::Literal { .. } => GrantSource::Literal,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum GrantAudience {
#[default]
Session,
InProcess,
}
impl GrantAudience {
pub fn is_session(&self) -> bool {
matches!(self, GrantAudience::Session)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct GrantSpec {
pub name: String,
pub source: GrantSourceSpec,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expose_as_env: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub for_command: Option<String>,
#[serde(default, skip_serializing_if = "GrantAudience::is_session")]
pub expose_to: GrantAudience,
}
impl GrantSpec {
fn resolve(
self,
env_lookup: &dyn Fn(&str) -> Option<String>,
) -> Result<SessionGrant, EnvironmentPolicyError> {
let name = self.name.trim();
if name.is_empty() {
return Err(EnvironmentPolicyError::EmptyName);
}
if let Some(var) = self.expose_as_env.as_deref() {
if var.trim().is_empty() {
return Err(EnvironmentPolicyError::EmptyExposeVar {
name: name.to_string(),
});
}
}
let for_command = match self.for_command.as_deref().map(str::trim) {
None | Some("") if self.for_command.is_some() => {
return Err(EnvironmentPolicyError::EmptyForCommand {
name: name.to_string(),
});
}
None => None,
Some(command) => {
if self
.expose_as_env
.as_deref()
.map(str::trim)
.is_none_or(|v| v.is_empty())
{
return Err(EnvironmentPolicyError::ForWithoutExpose {
name: name.to_string(),
});
}
if command.contains('/') || command.contains('\\') {
return Err(EnvironmentPolicyError::InvalidForCommand {
name: name.to_string(),
command: command.to_string(),
});
}
Some(command.to_string())
}
};
if self.expose_to == GrantAudience::InProcess {
if self
.expose_as_env
.as_deref()
.map(str::trim)
.is_none_or(|v| v.is_empty())
{
return Err(EnvironmentPolicyError::InProcessWithoutExpose {
name: name.to_string(),
});
}
if for_command.is_some() {
return Err(EnvironmentPolicyError::InProcessWithCommand {
name: name.to_string(),
});
}
}
let source_kind = self.source.kind();
let source_spec = self.source.clone();
let resolved_ref = match self.source {
GrantSourceSpec::Env { var } => {
let var = var.trim();
if var.is_empty() {
return Err(EnvironmentPolicyError::EmptyEnvVar {
name: name.to_string(),
});
}
let value = env_lookup(var).ok_or_else(|| EnvironmentPolicyError::MissingEnv {
name: name.to_string(),
var: var.to_string(),
})?;
ResolvedRef::EnvSnapshot(value)
}
GrantSourceSpec::SecretStore { account, key } => {
let (account, key) = (account.trim(), key.trim());
if account.is_empty() || key.is_empty() {
return Err(EnvironmentPolicyError::EmptySecretRef {
name: name.to_string(),
});
}
ResolvedRef::SecretStore {
account: account.to_string(),
key: key.to_string(),
}
}
GrantSourceSpec::Literal { value } => {
if value
.trim_start()
.starts_with(crate::secrets::SECRET_REF_SCHEME)
{
return Err(EnvironmentPolicyError::LiteralSecretReference {
name: name.to_string(),
});
}
ResolvedRef::Literal(value)
}
};
Ok(SessionGrant {
name: name.to_string(),
source_kind,
source_spec,
expose_as_env: self.expose_as_env.map(|var| var.trim().to_string()),
for_command,
audience: self.expose_to,
resolved_ref,
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum ResolvedRef {
EnvSnapshot(String),
SecretStore { account: String, key: String },
Literal(String),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SessionGrant {
name: String,
source_kind: GrantSource,
source_spec: GrantSourceSpec,
expose_as_env: Option<String>,
for_command: Option<String>,
audience: GrantAudience,
resolved_ref: ResolvedRef,
}
impl SessionGrant {
fn matches_spec(&self, spec: &GrantSpec) -> bool {
self.name == spec.name.trim()
&& self.source_spec == spec.source
&& self.expose_as_env.as_deref() == spec.expose_as_env.as_deref().map(str::trim)
&& self.for_command.as_deref() == spec.for_command.as_deref().map(str::trim)
&& self.audience == spec.expose_to
}
pub fn name(&self) -> &str {
&self.name
}
pub fn source_kind(&self) -> GrantSource {
self.source_kind
}
pub fn exposed_env_var(&self) -> Option<&str> {
self.expose_as_env.as_deref()
}
pub fn for_command(&self) -> Option<&str> {
self.for_command.as_deref()
}
pub fn audience(&self) -> GrantAudience {
self.audience
}
fn reaches_in_process(&self) -> bool {
self.for_command.is_none()
}
pub fn reaches_spawn(&self, program: Option<&str>) -> bool {
if self.audience == GrantAudience::InProcess {
return false;
}
match (self.for_command.as_deref(), program) {
(None, _) => true,
(Some(expected), Some(program)) => command_basename(program) == expected,
(Some(_), None) => false,
}
}
fn exposure(
&self,
resolve_secret: &dyn Fn(&str, &str) -> Option<String>,
) -> Option<Result<(String, String), EnvironmentPolicyError>> {
let var = self.expose_as_env.as_ref()?;
let value = match &self.resolved_ref {
ResolvedRef::EnvSnapshot(value) | ResolvedRef::Literal(value) => value.clone(),
ResolvedRef::SecretStore { account, key } => match resolve_secret(account, key) {
Some(value) => value,
None => {
return Some(Err(EnvironmentPolicyError::MissingSecret {
name: self.name.clone(),
}))
}
},
};
Some(Ok((var.clone(), value)))
}
pub fn receipt(&self) -> GrantReceipt {
GrantReceipt {
name: self.name.clone(),
source_kind: self.source_kind.as_str().to_string(),
exposed_as_env: self.expose_as_env.clone(),
for_command: self.for_command.clone(),
exposed_to: self.audience,
}
}
}
pub fn command_basename(program: &str) -> &str {
let name = program
.rsplit(['/', '\\'])
.next()
.filter(|name| !name.is_empty())
.unwrap_or(program);
strip_windows_executable_suffix(name)
}
fn strip_windows_executable_suffix(name: &str) -> &str {
const SUFFIXES: &[&str] = &[".exe", ".bat", ".cmd", ".com"];
for suffix in SUFFIXES {
let Some(stem_len) = name.len().checked_sub(suffix.len()).filter(|len| *len > 0) else {
continue;
};
if let Some((stem, tail)) = name.split_at_checked(stem_len) {
if tail.eq_ignore_ascii_case(suffix) {
return stem;
}
}
}
name
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EnvironmentPolicyKind {
#[default]
Inherited,
Isolated,
Granted,
}
impl EnvironmentPolicyKind {
pub fn as_str(self) -> &'static str {
match self {
EnvironmentPolicyKind::Inherited => "inherited",
EnvironmentPolicyKind::Isolated => "isolated",
EnvironmentPolicyKind::Granted => "granted",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SessionEnvironment {
kind: EnvironmentPolicyKind,
launcher_snapshot: BTreeMap<String, String>,
grants: Vec<SessionGrant>,
host_inference_boundary: Option<crate::llm::api::InferenceBoundary>,
}
impl SessionEnvironment {
pub fn inherited() -> Self {
Self::launch(EnvironmentPolicyKind::Inherited, Vec::new(), &|name| {
std::env::var(name).ok()
})
.expect("the inherited policy has no fallible grant configuration")
}
pub fn launch(
kind: EnvironmentPolicyKind,
specs: Vec<GrantSpec>,
env_lookup: &dyn Fn(&str) -> Option<String>,
) -> Result<Self, EnvironmentPolicyError> {
let mut launcher_snapshot = capture_process_environment();
for name in super::environment_policy::ENV_ALLOWLIST {
if let Some(value) = env_lookup(name) {
insert_env_value(&mut launcher_snapshot, name, value);
}
}
Self::launch_from_snapshot(kind, specs, launcher_snapshot, env_lookup)
}
pub fn launch_from_snapshot(
kind: EnvironmentPolicyKind,
specs: Vec<GrantSpec>,
launcher_snapshot: BTreeMap<String, String>,
env_lookup: &dyn Fn(&str) -> Option<String>,
) -> Result<Self, EnvironmentPolicyError> {
if !matches!(kind, EnvironmentPolicyKind::Granted) && !specs.is_empty() {
return Err(EnvironmentPolicyError::PolicyForbidsGrants {
policy: kind,
attempted: specs.len(),
});
}
validate_unique_specs(&specs)?;
let grants = specs
.into_iter()
.map(|spec| spec.resolve(env_lookup))
.collect::<Result<Vec<_>, _>>()?;
let launcher_snapshot = if matches!(kind, EnvironmentPolicyKind::Inherited) {
launcher_snapshot
} else {
launcher_snapshot
.into_iter()
.filter(|(name, _)| super::environment_policy::allowlist_admits(name))
.collect()
};
Ok(SessionEnvironment {
kind,
launcher_snapshot,
grants,
host_inference_boundary: None,
})
}
pub fn with_host_inference_boundary(
mut self,
boundary: Option<crate::llm::api::InferenceBoundary>,
) -> Self {
self.host_inference_boundary =
crate::llm::api::inference_boundary::meet(self.host_inference_boundary, boundary);
if self.host_inference_boundary.is_some() {
self.launcher_snapshot.retain(|name, _| {
!environment_names_equal(
name,
crate::llm::api::inference_boundary::HOST_BOUNDARY_ENV,
)
});
}
self
}
pub fn isolated() -> Self {
Self::launch(EnvironmentPolicyKind::Isolated, Vec::new(), &|name| {
std::env::var(name).ok()
})
.expect("the isolated policy has no fallible grant configuration")
}
pub fn kind(&self) -> EnvironmentPolicyKind {
self.kind
}
pub fn is_isolated(&self) -> bool {
matches!(self.kind, EnvironmentPolicyKind::Isolated)
}
pub fn allows_implicit_discovery(&self) -> bool {
matches!(self.kind, EnvironmentPolicyKind::Inherited)
}
pub fn narrow(
&self,
requested: EnvironmentPolicyKind,
specs: Vec<GrantSpec>,
) -> Result<Self, EnvironmentPolicyError> {
let inherited_boundary = if self.host_inference_boundary.is_some() {
self.env_exposure_for(
crate::llm::api::inference_boundary::HOST_BOUNDARY_ENV,
&|_, _| None,
)?
.map(|raw| crate::llm::api::inference_boundary::parse_host_boundary(&raw))
.transpose()
.map_err(|_| EnvironmentPolicyError::MalformedHostInferenceBoundary)?
} else {
None
};
let child = match (self.kind, requested) {
(EnvironmentPolicyKind::Inherited, EnvironmentPolicyKind::Inherited)
if specs.is_empty() =>
{
Ok(self.clone())
}
(EnvironmentPolicyKind::Inherited, EnvironmentPolicyKind::Isolated)
if specs.is_empty() =>
{
Ok(Self {
kind: requested,
launcher_snapshot: self.launcher_snapshot.clone(),
grants: Vec::new(),
host_inference_boundary: self.host_inference_boundary,
})
}
(EnvironmentPolicyKind::Inherited, EnvironmentPolicyKind::Granted) => {
if let Some(spec) = specs
.iter()
.find(|spec| matches!(spec.source, GrantSourceSpec::SecretStore { .. }))
{
return Err(EnvironmentPolicyError::ChildPolicyExceedsParent {
parent: self.kind,
requested,
offending_grant: Some(spec.name.trim().to_string()),
detail: "an inherited parent can grant only values in its launch-time environment snapshot; secret-store authority must be granted to the parent first".to_string(),
});
}
let snapshot = self.launcher_snapshot.clone();
Self::launch_from_snapshot(requested, specs, snapshot.clone(), &|name| {
snapshot.get(name).cloned()
})
}
(
EnvironmentPolicyKind::Granted | EnvironmentPolicyKind::Isolated,
EnvironmentPolicyKind::Isolated,
) if specs.is_empty() => Ok(Self {
kind: requested,
launcher_snapshot: self.launcher_snapshot.clone(),
grants: Vec::new(),
host_inference_boundary: self.host_inference_boundary,
}),
(EnvironmentPolicyKind::Granted, EnvironmentPolicyKind::Granted) => {
validate_unique_specs(&specs)?;
let mut grants = Vec::with_capacity(specs.len());
for spec in &specs {
let Some(grant) = self.grants.iter().find(|grant| grant.matches_spec(spec))
else {
return Err(EnvironmentPolicyError::ChildPolicyExceedsParent {
parent: self.kind,
requested,
offending_grant: Some(spec.name.trim().to_string()),
detail: format!(
"grant '{}' is not an unchanged subset of the parent grants",
spec.name.trim()
),
});
};
grants.push(grant.clone());
}
Ok(Self {
kind: requested,
launcher_snapshot: self.launcher_snapshot.clone(),
grants,
host_inference_boundary: self.host_inference_boundary,
})
}
_ => Err(EnvironmentPolicyError::ChildPolicyExceedsParent {
parent: self.kind,
requested,
offending_grant: specs.first().map(|spec| spec.name.trim().to_string()),
detail:
"a child may keep or reduce its parent's environment access, never widen it"
.to_string(),
}),
}?;
Ok(child.with_host_inference_boundary(inherited_boundary))
}
pub(crate) fn launcher_value(&self, name: &str) -> Option<&str> {
self.launcher_value_for_platform(name, cfg!(windows))
}
pub(crate) fn launcher_value_for_platform(&self, name: &str, windows: bool) -> Option<&str> {
if let Some(value) = self.launcher_snapshot.get(name) {
return Some(value.as_str());
}
if windows {
return self
.launcher_snapshot
.iter()
.find(|(key, _)| {
super::environment_policy::environment_names_equal_for_platform(
key, name, windows,
)
})
.map(|(_, value)| value.as_str());
}
None
}
pub(crate) fn launcher_snapshot(&self) -> &BTreeMap<String, String> {
&self.launcher_snapshot
}
pub fn grants(&self) -> &[SessionGrant] {
&self.grants
}
pub(crate) fn is_host_inference_target(&self, name: &str) -> bool {
self.host_inference_boundary.is_some()
&& environment_names_equal(name, crate::llm::api::inference_boundary::HOST_BOUNDARY_ENV)
}
pub fn receipts(&self) -> Vec<GrantReceipt> {
let mut receipts: Vec<_> = self
.grants
.iter()
.filter(|grant| {
!grant
.expose_as_env
.as_deref()
.is_some_and(|name| self.is_host_inference_target(name))
})
.map(SessionGrant::receipt)
.collect();
if self.host_inference_boundary.is_some() {
receipts.push(GrantReceipt {
name: "host-inference-boundary".into(),
source_kind: GrantSource::Literal.as_str().into(),
exposed_as_env: Some(crate::llm::api::inference_boundary::HOST_BOUNDARY_ENV.into()),
for_command: None,
exposed_to: GrantAudience::InProcess,
});
}
receipts
}
pub fn admitted_environment_names(&self) -> Vec<String> {
let mut names: Vec<String> = self.launcher_snapshot.keys().cloned().collect();
for grant in &self.grants {
if grant.audience == GrantAudience::InProcess
|| grant
.expose_as_env
.as_deref()
.is_some_and(|name| self.is_host_inference_target(name))
{
continue;
}
if let Some(var) = grant.expose_as_env.clone() {
names.push(var);
}
}
names.sort();
names.dedup();
names
}
pub fn env_exposure(
&self,
resolve_secret: &dyn Fn(&str, &str) -> Option<String>,
) -> Result<Vec<(String, String)>, EnvironmentPolicyError> {
self.child_exposure(None, resolve_secret)
}
pub fn env_exposure_for_command(
&self,
program: &str,
resolve_secret: &dyn Fn(&str, &str) -> Option<String>,
) -> Result<Vec<(String, String)>, EnvironmentPolicyError> {
self.child_exposure(Some(program), resolve_secret)
}
fn child_exposure(
&self,
program: Option<&str>,
resolve_secret: &dyn Fn(&str, &str) -> Option<String>,
) -> Result<Vec<(String, String)>, EnvironmentPolicyError> {
self.grants
.iter()
.filter(|grant| {
self.host_inference_boundary.is_none()
|| !grant
.expose_as_env
.as_deref()
.is_some_and(|name| self.is_host_inference_target(name))
})
.filter(|grant| grant.reaches_spawn(program))
.filter_map(|grant| grant.exposure(resolve_secret))
.collect()
}
pub fn env_exposure_for(
&self,
var: &str,
resolve_secret: &dyn Fn(&str, &str) -> Option<String>,
) -> Result<Option<String>, EnvironmentPolicyError> {
if environment_names_equal(var, crate::llm::api::inference_boundary::HOST_BOUNDARY_ENV) {
if let Some(host) = self.host_inference_boundary {
let requested = self.grants.iter().find(|grant| {
grant.reaches_in_process()
&& grant
.expose_as_env
.as_deref()
.is_some_and(|name| environment_names_equal(name, var))
});
let requested = requested
.and_then(|grant| grant.exposure(resolve_secret))
.transpose()?
.map(|(_, value)| {
crate::llm::api::inference_boundary::parse_host_boundary(&value)
})
.transpose()
.map_err(|_| EnvironmentPolicyError::MalformedHostInferenceBoundary)?;
let effective = crate::llm::api::inference_boundary::meet(Some(host), requested)
.expect("the host ceiling is present");
return Ok(Some(
serde_json::to_string(&effective)
.expect("the closed inference ceiling is serializable"),
));
}
}
let Some(grant) = self.grants.iter().find(|grant| {
grant.reaches_in_process() && grant.expose_as_env.as_deref() == Some(var)
}) else {
return Ok(None);
};
grant
.exposure(resolve_secret)
.transpose()
.map(|pair| pair.map(|(_, value)| value))
}
}
fn capture_process_environment() -> BTreeMap<String, String> {
std::env::vars_os()
.filter_map(|(name, value)| Some((name.into_string().ok()?, value.into_string().ok()?)))
.collect()
}
fn insert_env_value_case_insensitive(
map: &mut BTreeMap<String, String>,
name: &str,
value: String,
) {
let existing_key = map
.keys()
.find(|key| {
super::environment_policy::environment_names_equal_for_platform(key, name, true)
})
.cloned();
map.insert(existing_key.unwrap_or_else(|| name.to_string()), value);
}
pub(crate) fn insert_env_value(map: &mut BTreeMap<String, String>, name: &str, value: String) {
if cfg!(windows) {
insert_env_value_case_insensitive(map, name, value);
} else {
map.insert(name.to_string(), value);
}
}
fn validate_unique_specs(specs: &[GrantSpec]) -> Result<(), EnvironmentPolicyError> {
let mut names = BTreeSet::new();
let mut targets = BTreeSet::new();
for spec in specs {
let name = spec.name.trim();
if !name.is_empty() && !names.insert(name) {
return Err(EnvironmentPolicyError::DuplicateGrant {
name: name.to_string(),
});
}
if let Some(target) = spec.expose_as_env.as_deref().map(str::trim) {
if !target.is_empty() && !targets.insert(target) {
return Err(EnvironmentPolicyError::DuplicateExposureTarget {
target: target.to_string(),
});
}
}
}
Ok(())
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct GrantReceipt {
pub name: String,
pub source_kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exposed_as_env: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub for_command: Option<String>,
#[serde(default, skip_serializing_if = "GrantAudience::is_session")]
pub exposed_to: GrantAudience,
}
impl GrantReceipt {
pub fn child_visible_env(&self) -> Option<&str> {
match self.exposed_to {
GrantAudience::Session => self.exposed_as_env.as_deref(),
GrantAudience::InProcess => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum EnvironmentPolicyError {
MalformedHostInferenceBoundary,
EmptyName,
EmptyEnvVar { name: String },
LiteralSecretReference { name: String },
EmptySecretRef { name: String },
EmptyExposeVar { name: String },
EmptyForCommand { name: String },
ForWithoutExpose { name: String },
InvalidForCommand { name: String, command: String },
InProcessWithoutExpose { name: String },
InProcessWithCommand { name: String },
MissingEnv { name: String, var: String },
PolicyForbidsGrants {
policy: EnvironmentPolicyKind,
attempted: usize,
},
DuplicateGrant { name: String },
DuplicateExposureTarget { target: String },
ChildPolicyExceedsParent {
parent: EnvironmentPolicyKind,
requested: EnvironmentPolicyKind,
offending_grant: Option<String>,
detail: String,
},
MissingSecret { name: String },
UnsafeTrustedSetupVariable { variable: String },
}
impl fmt::Display for EnvironmentPolicyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MalformedHostInferenceBoundary => {
write!(f, "[inference_boundary.host_boundary_malformed] invalid host inference ceiling")
}
EnvironmentPolicyError::UnsafeTrustedSetupVariable { variable } => write!(
f, "trusted sandbox setup cannot preserve loader control {variable}; remove it from this launch environment"
),
EnvironmentPolicyError::EmptyName => write!(
f,
"[environment_policy.empty_grant_name] grant spec has an empty name"
),
EnvironmentPolicyError::LiteralSecretReference { name } => {
write!(
f,
"[environment_policy.literal_secret_reference] grant '{name}' literal source states a secret reference; declare it as a secret_store source instead"
)
}
EnvironmentPolicyError::EmptyEnvVar { name } => {
write!(
f,
"[environment_policy.empty_source_variable] grant '{name}' env source names an empty variable"
)
}
EnvironmentPolicyError::EmptySecretRef { name } => {
write!(
f,
"[environment_policy.empty_secret_reference] grant '{name}' secret source names an empty account/key"
)
}
EnvironmentPolicyError::EmptyExposeVar { name } => {
write!(
f,
"[environment_policy.empty_exposure_target] grant '{name}' expose target is an empty variable"
)
}
EnvironmentPolicyError::EmptyForCommand { name } => {
write!(
f,
"[environment_policy.empty_for_command] grant '{name}' for_command binding is empty; name the command basename (for example 'gh')"
)
}
EnvironmentPolicyError::ForWithoutExpose { name } => {
write!(
f,
"[environment_policy.for_without_expose] grant '{name}' declares for_command without expose_as_env; command binding only applies to an exposed environment variable"
)
}
EnvironmentPolicyError::InvalidForCommand { name, command } => {
write!(
f,
"[environment_policy.invalid_for_command] grant '{name}' for_command '{command}' must be a command basename, not a path"
)
}
EnvironmentPolicyError::InProcessWithoutExpose { name } => write!(
f,
"[environment_policy.in_process_without_expose] grant '{name}' declares expose_to in_process without expose_as_env; name the variable Harn's own readers should find it under"
),
EnvironmentPolicyError::InProcessWithCommand { name } => write!(
f,
"[environment_policy.in_process_with_command] grant '{name}' declares expose_to in_process with for_command; an in-process grant reaches no spawned command, so remove one of the two"
),
EnvironmentPolicyError::MissingEnv { name, var } => write!(
f,
"[environment_policy.source_variable_missing] grant '{name}' env source variable '{var}' is not set in the launcher environment; set it before launch or choose another source"
),
EnvironmentPolicyError::PolicyForbidsGrants { policy, attempted } => write!(
f,
"[environment_policy.grants_forbidden] environment policy '{}' forbids grants, but {attempted} were declared; use 'granted' or remove the grants",
policy.as_str()
),
EnvironmentPolicyError::DuplicateGrant { name } => write!(
f,
"[environment_policy.duplicate_grant] grant name '{name}' is declared more than once; give every grant a unique name"
),
EnvironmentPolicyError::DuplicateExposureTarget { target } => write!(
f,
"[environment_policy.duplicate_exposure_target] environment target '{target}' is exposed by more than one grant; choose one grant for each target"
),
EnvironmentPolicyError::ChildPolicyExceedsParent {
parent,
requested,
offending_grant: _,
detail,
} => write!(
f,
"[environment_policy.child_exceeds_parent] child policy '{}' exceeds parent policy '{}': {detail}",
requested.as_str(),
parent.as_str()
),
EnvironmentPolicyError::MissingSecret { name } => {
write!(
f,
"[environment_policy.secret_unavailable] grant '{name}' is unavailable from the secret store; restore access, rotate the reference, or remove the grant"
)
}
}
}
}
impl std::error::Error for EnvironmentPolicyError {}
impl EnvironmentPolicyError {
pub fn code(&self) -> &'static str {
match self {
Self::MalformedHostInferenceBoundary => "inference_boundary.host_boundary_malformed",
Self::UnsafeTrustedSetupVariable { .. } => {
"environment_policy.unsafe_trusted_setup_variable"
}
Self::EmptyName => "environment_policy.empty_grant_name",
Self::EmptyEnvVar { .. } => "environment_policy.empty_source_variable",
Self::LiteralSecretReference { .. } => "environment_policy.literal_secret_reference",
Self::EmptySecretRef { .. } => "environment_policy.empty_secret_reference",
Self::EmptyExposeVar { .. } => "environment_policy.empty_exposure_target",
Self::EmptyForCommand { .. } => "environment_policy.empty_for_command",
Self::ForWithoutExpose { .. } => "environment_policy.for_without_expose",
Self::InvalidForCommand { .. } => "environment_policy.invalid_for_command",
Self::InProcessWithoutExpose { .. } => "environment_policy.in_process_without_expose",
Self::InProcessWithCommand { .. } => "environment_policy.in_process_with_command",
Self::MissingEnv { .. } => "environment_policy.source_variable_missing",
Self::PolicyForbidsGrants { .. } => "environment_policy.grants_forbidden",
Self::DuplicateGrant { .. } => "environment_policy.duplicate_grant",
Self::DuplicateExposureTarget { .. } => "environment_policy.duplicate_exposure_target",
Self::ChildPolicyExceedsParent { .. } => "environment_policy.child_exceeds_parent",
Self::MissingSecret { .. } => "environment_policy.secret_unavailable",
}
}
pub fn to_json(&self) -> serde_json::Value {
let mut value = serde_json::json!({
"code": self.code(),
"message": self.to_string(),
});
let object = value
.as_object_mut()
.expect("environment policy diagnostic is an object");
match self {
Self::UnsafeTrustedSetupVariable { variable } => {
object.insert("sourceVariable".to_string(), serde_json::json!(variable));
}
Self::LiteralSecretReference { name }
| Self::EmptyEnvVar { name }
| Self::EmptySecretRef { name }
| Self::EmptyExposeVar { name }
| Self::EmptyForCommand { name }
| Self::ForWithoutExpose { name }
| Self::InProcessWithoutExpose { name }
| Self::InProcessWithCommand { name }
| Self::MissingSecret { name } => {
object.insert("grant".to_string(), serde_json::json!(name));
}
Self::InvalidForCommand { name, command } => {
object.insert("grant".to_string(), serde_json::json!(name));
object.insert("forCommand".to_string(), serde_json::json!(command));
}
Self::MissingEnv { name, var } => {
object.insert("grant".to_string(), serde_json::json!(name));
object.insert("sourceVariable".to_string(), serde_json::json!(var));
}
Self::PolicyForbidsGrants { policy, attempted } => {
object.insert("policy".to_string(), serde_json::json!(policy.as_str()));
object.insert("attemptedGrants".to_string(), serde_json::json!(attempted));
}
Self::DuplicateGrant { name } => {
object.insert("grant".to_string(), serde_json::json!(name));
}
Self::DuplicateExposureTarget { target } => {
object.insert("target".to_string(), serde_json::json!(target));
}
Self::ChildPolicyExceedsParent {
parent,
requested,
offending_grant,
detail,
} => {
object.insert(
"parentPolicy".to_string(),
serde_json::json!(parent.as_str()),
);
object.insert(
"requestedPolicy".to_string(),
serde_json::json!(requested.as_str()),
);
object.insert("detail".to_string(), serde_json::json!(detail));
if let Some(grant) = offending_grant {
object.insert("grant".to_string(), serde_json::json!(grant));
}
}
Self::EmptyName | Self::MalformedHostInferenceBoundary => {}
}
value
}
}
#[cfg(test)]
#[path = "session_environment_tests.rs"]
mod tests;