use crate::coordinate::{Coordinate, EnvSegment};
use crate::scope::{AgentScope, Operation, Origin, Surface};
use crate::sensitivity::Sensitivity;
pub const PROD: &str = "prod";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Decision {
Allow,
RequireConfirmation,
Deny(DenyReason),
Unaddressable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DenyReason {
InjectOnlyNeverRevealed,
McpCriticalForbidden,
WebUiCriticalMasked,
ProdRevealIntoAgentContext,
NotRevealable,
}
#[derive(Debug, Clone)]
pub struct AccessRequest<'a> {
pub coordinate: &'a Coordinate,
pub project: Option<&'a str>,
pub sensitivity: Sensitivity,
pub revealable: bool,
pub operation: Operation,
pub surface: Surface,
pub origin: Origin,
}
impl AccessRequest<'_> {
fn is_prod(&self) -> bool {
matches!(&self.coordinate.environment, EnvSegment::Literal(e) if e == PROD)
}
}
pub fn decide(req: &AccessRequest, scope: &AgentScope) -> Decision {
if !scope.addresses(req.coordinate, req.project) || !scope.permits(req.operation) {
return Decision::Unaddressable;
}
let prod = req.is_prod();
let high = req.sensitivity == Sensitivity::High;
let inject_only = req.sensitivity == Sensitivity::InjectOnly;
match req.operation {
Operation::Metadata => Decision::Allow,
Operation::Write => Decision::Allow,
Operation::Inject => {
if inject_requires_confirmation(req.sensitivity) {
Decision::RequireConfirmation
} else {
Decision::Allow
}
}
Operation::Reveal => {
if inject_only {
return Decision::Deny(DenyReason::InjectOnlyNeverRevealed);
}
match req.surface {
Surface::Mcp => {
if prod || high {
Decision::Deny(DenyReason::McpCriticalForbidden)
} else if !req.revealable {
Decision::Deny(DenyReason::NotRevealable)
} else {
Decision::Allow
}
}
Surface::WebUi => {
if high {
Decision::Deny(DenyReason::WebUiCriticalMasked)
} else {
Decision::Allow
}
}
Surface::Cli => {
if prod {
match req.origin {
Origin::Agent => Decision::Deny(DenyReason::ProdRevealIntoAgentContext),
Origin::Human => Decision::RequireConfirmation,
}
} else if high {
Decision::RequireConfirmation
} else {
Decision::Allow
}
}
}
}
}
}
pub fn birth_sensitivity(environment: &str, non_prod_default: Sensitivity) -> Sensitivity {
if environment == PROD {
Sensitivity::High
} else {
non_prod_default
}
}
pub fn is_downgrade(from: Sensitivity, to: Sensitivity) -> bool {
fn rank(s: Sensitivity) -> u8 {
match s {
Sensitivity::Low => 0,
Sensitivity::Medium => 1,
Sensitivity::High => 2,
Sensitivity::InjectOnly => 3,
}
}
rank(to) < rank(from)
}
pub fn downgrade_requires_confirmation(from: Sensitivity, to: Sensitivity) -> bool {
is_downgrade(from, to) && matches!(from, Sensitivity::High | Sensitivity::InjectOnly)
}
pub fn delete_requires_confirmation(sensitivity: Sensitivity) -> bool {
matches!(sensitivity, Sensitivity::High | Sensitivity::InjectOnly)
}
pub fn prod_not_packageable(environment: &str) -> bool {
environment == PROD
}
pub fn prod_blocks_unattended(environment: &str) -> bool {
environment == PROD
}
pub fn prod_forbids_fallback(environment: &str) -> bool {
environment == PROD
}
pub fn inject_requires_confirmation(sensitivity: Sensitivity) -> bool {
sensitivity == Sensitivity::High
}
pub fn inject_requires_allowlist(sensitivity: Sensitivity, is_prod: bool) -> bool {
sensitivity == Sensitivity::High || is_prod
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
fn coord(s: &str) -> Coordinate {
Coordinate::from_str(s).unwrap()
}
fn req<'a>(
c: &'a Coordinate,
sensitivity: Sensitivity,
operation: Operation,
surface: Surface,
origin: Origin,
) -> AccessRequest<'a> {
AccessRequest {
coordinate: c,
project: None,
sensitivity,
revealable: false,
operation,
surface,
origin,
}
}
#[test]
fn metadata_is_allowed_when_addressable() {
let c = coord("secret:prod/db/password");
let d = decide(
&req(
&c,
Sensitivity::High,
Operation::Metadata,
Surface::Mcp,
Origin::Agent,
),
&AgentScope::full(),
);
assert_eq!(d, Decision::Allow);
}
#[test]
fn out_of_scope_is_unaddressable_not_denied() {
let c = coord("secret:prod/db/password");
let scope = AgentScope::metadata_only(); let d = decide(
&req(
&c,
Sensitivity::Low,
Operation::Reveal,
Surface::Cli,
Origin::Human,
),
&scope,
);
assert_eq!(d, Decision::Unaddressable);
}
#[test]
fn inject_only_is_never_revealed_on_any_surface() {
let c = coord("secret:dev/app/key");
for surface in [Surface::Cli, Surface::WebUi, Surface::Mcp] {
let d = decide(
&req(
&c,
Sensitivity::InjectOnly,
Operation::Reveal,
surface,
Origin::Human,
),
&AgentScope::full(),
);
assert_eq!(d, Decision::Deny(DenyReason::InjectOnlyNeverRevealed));
}
}
#[test]
fn high_inject_requires_confirmation_low_does_not() {
let c = coord("secret:dev/app/key");
assert_eq!(
decide(
&req(
&c,
Sensitivity::High,
Operation::Inject,
Surface::Cli,
Origin::Human
),
&AgentScope::full()
),
Decision::RequireConfirmation
);
assert_eq!(
decide(
&req(
&c,
Sensitivity::Low,
Operation::Inject,
Surface::Cli,
Origin::Human
),
&AgentScope::full()
),
Decision::Allow
);
}
#[test]
fn mcp_never_reveals_critical() {
let prod = coord("secret:prod/db/password");
let dev = coord("secret:dev/app/key");
assert_eq!(
decide(
&req(
&prod,
Sensitivity::Medium,
Operation::Reveal,
Surface::Mcp,
Origin::Agent
),
&AgentScope::full()
),
Decision::Deny(DenyReason::McpCriticalForbidden)
);
assert_eq!(
decide(
&req(
&dev,
Sensitivity::High,
Operation::Reveal,
Surface::Mcp,
Origin::Agent
),
&AgentScope::full()
),
Decision::Deny(DenyReason::McpCriticalForbidden)
);
assert_eq!(
decide(
&req(
&dev,
Sensitivity::Medium,
Operation::Reveal,
Surface::Mcp,
Origin::Agent
),
&AgentScope::full()
),
Decision::Deny(DenyReason::NotRevealable)
);
let mut r = req(
&dev,
Sensitivity::Medium,
Operation::Reveal,
Surface::Mcp,
Origin::Agent,
);
r.revealable = true;
assert_eq!(decide(&r, &AgentScope::full()), Decision::Allow);
}
#[test]
fn prod_reveal_into_agent_context_is_denied_human_requires_confirmation() {
let c = coord("secret:prod/db/password");
assert_eq!(
decide(
&req(
&c,
Sensitivity::Medium,
Operation::Reveal,
Surface::Cli,
Origin::Agent
),
&AgentScope::full()
),
Decision::Deny(DenyReason::ProdRevealIntoAgentContext)
);
assert_eq!(
decide(
&req(
&c,
Sensitivity::High,
Operation::Reveal,
Surface::Cli,
Origin::Human
),
&AgentScope::full()
),
Decision::RequireConfirmation
);
}
#[test]
fn webui_masks_high_reveals_low_medium() {
let c = coord("secret:dev/app/key");
assert_eq!(
decide(
&req(
&c,
Sensitivity::High,
Operation::Reveal,
Surface::WebUi,
Origin::Human
),
&AgentScope::full()
),
Decision::Deny(DenyReason::WebUiCriticalMasked)
);
assert_eq!(
decide(
&req(
&c,
Sensitivity::Medium,
Operation::Reveal,
Surface::WebUi,
Origin::Human
),
&AgentScope::full()
),
Decision::Allow
);
}
#[test]
fn cli_high_reveal_requires_confirmation() {
let c = coord("secret:dev/app/key");
assert_eq!(
decide(
&req(
&c,
Sensitivity::High,
Operation::Reveal,
Surface::Cli,
Origin::Human
),
&AgentScope::full()
),
Decision::RequireConfirmation
);
}
#[test]
fn birth_sensitivity_prod_is_high() {
assert_eq!(birth_sensitivity(PROD, Sensitivity::Low), Sensitivity::High);
assert_eq!(birth_sensitivity("dev", Sensitivity::Low), Sensitivity::Low);
assert_eq!(
birth_sensitivity("staging", Sensitivity::Medium),
Sensitivity::Medium
);
}
#[test]
fn prod_structural_predicates() {
assert!(prod_not_packageable(PROD));
assert!(prod_blocks_unattended(PROD));
assert!(prod_forbids_fallback(PROD));
assert!(!prod_forbids_fallback("dev"));
}
#[test]
fn downgrade_from_critical_requires_confirmation() {
assert!(downgrade_requires_confirmation(
Sensitivity::High,
Sensitivity::Medium
));
assert!(downgrade_requires_confirmation(
Sensitivity::High,
Sensitivity::Low
));
assert!(downgrade_requires_confirmation(
Sensitivity::InjectOnly,
Sensitivity::High
));
assert!(downgrade_requires_confirmation(
Sensitivity::InjectOnly,
Sensitivity::Low
));
assert!(!downgrade_requires_confirmation(
Sensitivity::Medium,
Sensitivity::Low
));
assert!(!downgrade_requires_confirmation(
Sensitivity::Low,
Sensitivity::High
));
assert!(!downgrade_requires_confirmation(
Sensitivity::High,
Sensitivity::High
));
}
#[test]
fn delete_requires_confirmation_for_critical_only() {
assert!(delete_requires_confirmation(Sensitivity::High));
assert!(delete_requires_confirmation(Sensitivity::InjectOnly));
assert!(!delete_requires_confirmation(Sensitivity::Medium));
assert!(!delete_requires_confirmation(Sensitivity::Low));
}
#[test]
fn downgrade_detection_follows_reveal_strictness() {
assert!(is_downgrade(Sensitivity::High, Sensitivity::Medium));
assert!(is_downgrade(Sensitivity::High, Sensitivity::Low));
assert!(is_downgrade(Sensitivity::Medium, Sensitivity::Low));
assert!(!is_downgrade(Sensitivity::Low, Sensitivity::High));
assert!(!is_downgrade(Sensitivity::High, Sensitivity::High));
assert!(!is_downgrade(Sensitivity::High, Sensitivity::InjectOnly));
assert!(is_downgrade(Sensitivity::InjectOnly, Sensitivity::High));
}
#[test]
fn inject_confirmation_is_sensitivity_only() {
assert!(inject_requires_confirmation(Sensitivity::High));
assert!(!inject_requires_confirmation(Sensitivity::Medium));
assert!(!inject_requires_confirmation(Sensitivity::Low));
assert!(!inject_requires_confirmation(Sensitivity::InjectOnly));
}
#[test]
fn inject_allowlist_is_high_or_prod() {
assert!(inject_requires_allowlist(Sensitivity::High, false));
assert!(inject_requires_allowlist(Sensitivity::Medium, true));
assert!(inject_requires_allowlist(Sensitivity::Low, true)); assert!(inject_requires_allowlist(Sensitivity::InjectOnly, true));
assert!(!inject_requires_allowlist(Sensitivity::Low, false));
assert!(!inject_requires_allowlist(Sensitivity::Medium, false));
}
#[test]
fn downgraded_prod_inject_is_allowed_without_confirmation() {
let c = coord("secret:prod/db/password");
assert_eq!(
decide(
&req(
&c,
Sensitivity::Low,
Operation::Inject,
Surface::Cli,
Origin::Human
),
&AgentScope::full()
),
Decision::Allow
);
assert_eq!(
decide(
&req(
&c,
Sensitivity::High,
Operation::Inject,
Surface::Cli,
Origin::Human
),
&AgentScope::full()
),
Decision::RequireConfirmation
);
}
}