use crate::error::{Error, Result};
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct WebAccess {
pub search: bool,
pub fetch: bool,
pub max_uses: Option<u32>,
pub allowed_domains: Vec<String>,
pub blocked_domains: Vec<String>,
}
impl WebAccess {
#[must_use]
pub fn search() -> Self {
Self {
search: true,
..Self::default()
}
}
#[must_use]
pub fn with_fetch(mut self) -> Self {
self.fetch = true;
self
}
#[must_use]
pub fn max_uses(mut self, uses: u32) -> Self {
self.max_uses = Some(uses);
self
}
#[must_use]
pub fn allow(mut self, domain: impl Into<String>) -> Self {
self.allowed_domains.push(domain.into());
self
}
#[must_use]
pub fn block(mut self, domain: impl Into<String>) -> Self {
self.blocked_domains.push(domain.into());
self
}
#[must_use]
pub fn enabled(&self) -> bool {
self.search || self.fetch
}
#[must_use]
pub fn vendor_filter(&self) -> (Vec<String>, Vec<String>) {
if self.allowed_domains.is_empty() {
return (Vec::new(), self.blocked_domains.clone());
}
let allowed = self
.allowed_domains
.iter()
.filter(|d| !self.blocked_domains.contains(d))
.cloned()
.collect();
(allowed, Vec::new())
}
pub fn from_policy(mut self, policy: &crate::policy::Policy) -> Result<Self> {
use crate::policy::{Act, Effect};
let mut allowed = Vec::new();
let mut blocked = Vec::new();
let mut allow_everything = false;
for layer in &policy.layers {
for rule in &layer.rules {
if rule.act != Act::Net {
continue;
}
let host = host_of(&rule.pattern)?;
match rule.effect {
Effect::Allow if host == "*" => allow_everything = true,
Effect::Allow => allowed.push(host),
Effect::Deny => blocked.push(host),
Effect::Ask => {
return Err(Error::Config(format!(
"network rule {:?} asks for approval, which a \
provider-executed fetch cannot offer: the provider \
dials the URL, so there is no connection for this \
process to pause. Allow or deny the host instead.",
rule.pattern
)))
}
}
}
}
self.allowed_domains = if allow_everything {
Vec::new()
} else {
allowed
};
self.blocked_domains = blocked;
Ok(self)
}
}
fn host_of(pattern: &str) -> Result<String> {
if pattern.contains("://") || pattern.contains('/') {
return Err(Error::Config(format!(
"network rule {pattern:?} is not a bare host, so it cannot be projected \
onto a provider's domain filter; write the host alone (`docs.rs`) or \
`host:port`"
)));
}
Ok(match pattern.rsplit_once(':') {
Some((host, port)) if !port.is_empty() && port.chars().all(|c| c.is_ascii_digit()) => {
host.to_string()
}
_ => pattern.to_string(),
})
}
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Citation {
pub url: String,
pub title: Option<String>,
pub cited_text: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct ServerToolCall {
pub provider: String,
pub tool: String,
pub error: Option<String>,
}
impl ServerToolCall {
pub fn ok(provider: impl Into<String>, tool: impl Into<String>) -> Self {
Self {
provider: provider.into(),
tool: tool.into(),
error: None,
}
}
pub fn failed(
provider: impl Into<String>,
tool: impl Into<String>,
error: impl Into<String>,
) -> Self {
Self {
provider: provider.into(),
tool: tool.into(),
error: Some(error.into()),
}
}
#[must_use]
pub fn succeeded(&self) -> bool {
self.error.is_none()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::policy::Policy;
#[test]
fn a_policy_projects_onto_the_vendor_filter() {
let policy = Policy::default()
.layer("app")
.allow_net("docs.rs:443")
.allow_net("crates.io:443")
.deny_net("*.example.com");
let web = WebAccess::search().from_policy(&policy).unwrap();
assert_eq!(web.allowed_domains, ["docs.rs", "crates.io"]);
assert_eq!(web.blocked_domains, ["*.example.com"]);
}
#[test]
fn allow_everything_projects_to_no_filter_at_all() {
let open = Policy::default().layer("app").allow_net("*");
let web = WebAccess::search().from_policy(&open).unwrap();
assert!(web.allowed_domains.is_empty());
assert!(web.blocked_domains.is_empty());
}
#[test]
fn a_pattern_the_filter_cannot_express_is_an_error_naming_it() {
let policy = Policy::default()
.layer("app")
.allow_net("https://docs.rs/std");
let err = WebAccess::search().from_policy(&policy).unwrap_err();
assert!(
err.to_string().contains("https://docs.rs/std"),
"the error must name the rule, got {err}"
);
let asking = Policy::default().layer("app").ask_net("docs.rs");
let err = WebAccess::search().from_policy(&asking).unwrap_err();
assert!(err.to_string().contains("docs.rs"), "got {err}");
}
#[test]
fn a_declaration_with_neither_switch_is_not_enabled() {
assert!(!WebAccess::default().enabled());
assert!(!WebAccess::default().allow("docs.rs").max_uses(3).enabled());
assert!(WebAccess::search().enabled());
assert!(WebAccess::default().with_fetch().enabled());
}
}