use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum DenyReason {
TargetNotAllowed,
TargetMissing,
RequiredSecretNotFound,
DangerousEnvVariable,
VaultProfileNotFound,
NamespaceMissing,
AccessProfileViolation,
AllowedSecretNotFound,
NetworkPolicyViolation,
InsufficientAuthority,
MissingContract,
LockedVault,
MissingAgent,
BadWorkdir,
PathEscape,
BlankScope,
ProfileOverride,
ContractOverride,
RequestTimeWidening,
NetworkUnenforced,
AuditUnavailable,
OutputCap,
Timeout,
HostSchemaUnstable,
ConfigStale,
ProofUnavailable,
ParseError,
InternalError,
}
impl DenyReason {
pub fn message(&self) -> &'static str {
match self {
Self::TargetNotAllowed => "command not in contract target allowlist",
Self::TargetMissing => "command required by contract but not provided",
Self::RequiredSecretNotFound => "required vault secret does not exist",
Self::DangerousEnvVariable => {
"environment variable is high-risk; use --allow-dangerous-env to override"
}
Self::VaultProfileNotFound => "vault profile not found or cannot be opened",
Self::NamespaceMissing => "namespace does not exist in vault",
Self::AccessProfileViolation => {
"operation violates access profile (read-only) constraint"
}
Self::AllowedSecretNotFound => "secret in contract allowlist does not exist in vault",
Self::NetworkPolicyViolation => "operation violates network policy",
Self::InsufficientAuthority => {
"insufficient authority: add --keys or --ns or use a contract"
}
Self::MissingContract => "bound contract not found or could not be parsed",
Self::LockedVault => "vault is locked and secret injection is required",
Self::MissingAgent => "tsafe-agent is absent or unreachable",
Self::BadWorkdir => "workdir is missing, invalid, unsafe, or not canonicalizable",
Self::PathEscape => "path escapes the authorized boundary",
Self::BlankScope => "request or contract would create an empty authority scope",
Self::ProfileOverride => "request attempts to override the bound profile",
Self::ContractOverride => "request attempts to override the bound contract",
Self::RequestTimeWidening => "request attempts to widen authority at call time",
Self::NetworkUnenforced => "network restriction cannot be enforced on this platform",
Self::AuditUnavailable => "required audit recording is unavailable",
Self::OutputCap => "output exceeded the configured cap",
Self::Timeout => "execution exceeded the configured timeout",
Self::HostSchemaUnstable => "host config schema is not stable for this alias",
Self::ConfigStale => "host config no longer matches the authority envelope",
Self::ProofUnavailable => "required proof evidence is unavailable",
Self::ParseError => "request, contract, or command data failed validation",
Self::InternalError => "internal error after failing closed",
}
}
pub fn code(&self) -> &'static str {
match self {
Self::TargetNotAllowed => "TARGET_NOT_ALLOWED",
Self::TargetMissing => "TARGET_MISSING",
Self::RequiredSecretNotFound => "REQUIRED_SECRET_NOT_FOUND",
Self::DangerousEnvVariable => "DANGEROUS_ENV_VARIABLE",
Self::VaultProfileNotFound => "VAULT_PROFILE_NOT_FOUND",
Self::NamespaceMissing => "NAMESPACE_MISSING",
Self::AccessProfileViolation => "ACCESS_PROFILE_VIOLATION",
Self::AllowedSecretNotFound => "ALLOWED_SECRET_NOT_FOUND",
Self::NetworkPolicyViolation => "NETWORK_POLICY_VIOLATION",
Self::InsufficientAuthority => "INSUFFICIENT_AUTHORITY",
Self::MissingContract => "MISSING_CONTRACT",
Self::LockedVault => "LOCKED_VAULT",
Self::MissingAgent => "MISSING_AGENT",
Self::BadWorkdir => "BAD_WORKDIR",
Self::PathEscape => "PATH_ESCAPE",
Self::BlankScope => "BLANK_SCOPE",
Self::ProfileOverride => "PROFILE_OVERRIDE",
Self::ContractOverride => "CONTRACT_OVERRIDE",
Self::RequestTimeWidening => "REQUEST_TIME_WIDENING",
Self::NetworkUnenforced => "NETWORK_UNENFORCED",
Self::AuditUnavailable => "AUDIT_UNAVAILABLE",
Self::OutputCap => "OUTPUT_CAP",
Self::Timeout => "TIMEOUT",
Self::HostSchemaUnstable => "HOST_SCHEMA_UNSTABLE",
Self::ConfigStale => "CONFIG_STALE",
Self::ProofUnavailable => "PROOF_UNAVAILABLE",
Self::ParseError => "PARSE_ERROR",
Self::InternalError => "INTERNAL_ERROR",
}
}
}
impl fmt::Display for DenyReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.code(), self.message())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deny_reasons_have_unique_codes() {
let codes = [
DenyReason::TargetNotAllowed.code(),
DenyReason::TargetMissing.code(),
DenyReason::RequiredSecretNotFound.code(),
DenyReason::DangerousEnvVariable.code(),
DenyReason::VaultProfileNotFound.code(),
DenyReason::NamespaceMissing.code(),
DenyReason::AccessProfileViolation.code(),
DenyReason::AllowedSecretNotFound.code(),
DenyReason::NetworkPolicyViolation.code(),
DenyReason::InsufficientAuthority.code(),
DenyReason::MissingContract.code(),
DenyReason::LockedVault.code(),
DenyReason::MissingAgent.code(),
DenyReason::BadWorkdir.code(),
DenyReason::PathEscape.code(),
DenyReason::BlankScope.code(),
DenyReason::ProfileOverride.code(),
DenyReason::ContractOverride.code(),
DenyReason::RequestTimeWidening.code(),
DenyReason::NetworkUnenforced.code(),
DenyReason::AuditUnavailable.code(),
DenyReason::OutputCap.code(),
DenyReason::Timeout.code(),
DenyReason::HostSchemaUnstable.code(),
DenyReason::ConfigStale.code(),
DenyReason::ProofUnavailable.code(),
DenyReason::ParseError.code(),
DenyReason::InternalError.code(),
];
assert_eq!(
codes.len(),
codes.iter().collect::<std::collections::HashSet<_>>().len()
);
}
#[test]
fn deny_reason_display_is_deterministic() {
let reason = DenyReason::TargetNotAllowed;
let display = format!("{reason}");
assert_eq!(
display,
"TARGET_NOT_ALLOWED: command not in contract target allowlist"
);
}
#[test]
fn legacy_values_still_serialize_and_display_exactly() {
assert_eq!(
serde_json::to_string(&DenyReason::TargetNotAllowed).unwrap(),
"\"TARGET_NOT_ALLOWED\""
);
assert_eq!(
format!("{}", DenyReason::TargetNotAllowed),
"TARGET_NOT_ALLOWED: command not in contract target allowlist"
);
assert_eq!(
serde_json::to_string(&DenyReason::DangerousEnvVariable).unwrap(),
"\"DANGEROUS_ENV_VARIABLE\""
);
assert_eq!(
format!("{}", DenyReason::DangerousEnvVariable),
"DANGEROUS_ENV_VARIABLE: environment variable is high-risk; use --allow-dangerous-env to override"
);
}
#[test]
fn new_variants_have_stable_codes() {
let codes = [
(DenyReason::MissingContract, "MISSING_CONTRACT"),
(DenyReason::LockedVault, "LOCKED_VAULT"),
(DenyReason::MissingAgent, "MISSING_AGENT"),
(DenyReason::BadWorkdir, "BAD_WORKDIR"),
(DenyReason::PathEscape, "PATH_ESCAPE"),
(DenyReason::BlankScope, "BLANK_SCOPE"),
(DenyReason::ProfileOverride, "PROFILE_OVERRIDE"),
(DenyReason::ContractOverride, "CONTRACT_OVERRIDE"),
(DenyReason::RequestTimeWidening, "REQUEST_TIME_WIDENING"),
(DenyReason::NetworkUnenforced, "NETWORK_UNENFORCED"),
(DenyReason::AuditUnavailable, "AUDIT_UNAVAILABLE"),
(DenyReason::OutputCap, "OUTPUT_CAP"),
(DenyReason::Timeout, "TIMEOUT"),
(DenyReason::HostSchemaUnstable, "HOST_SCHEMA_UNSTABLE"),
(DenyReason::ConfigStale, "CONFIG_STALE"),
(DenyReason::ProofUnavailable, "PROOF_UNAVAILABLE"),
(DenyReason::ParseError, "PARSE_ERROR"),
(DenyReason::InternalError, "INTERNAL_ERROR"),
];
for (reason, code) in codes {
assert_eq!(reason.code(), code);
}
}
}