feat: add web search and fetch tools
This commit is contained in:
@@ -18,7 +18,7 @@ use crate::model::{AuthRef, ModelManifest, ReasoningControl};
|
||||
use crate::{
|
||||
CompactionConfig, FileUploadLimits, MemoryConfig, PodManifest, PodMeta, ScopeConfig,
|
||||
SessionConfig, SkillsConfig, ToolOutputLimits, ToolPermissionConfig, ToolPermissionRule,
|
||||
WorkerManifest,
|
||||
WebConfig, WorkerManifest,
|
||||
};
|
||||
|
||||
/// Partial-form Pod manifest. Every field is optional; one or more
|
||||
@@ -46,6 +46,9 @@ pub struct PodManifestConfig {
|
||||
pub permissions: Option<PermissionConfigPartial>,
|
||||
#[serde(default)]
|
||||
pub compaction: Option<CompactionConfigPartial>,
|
||||
/// First-class web tool opt-in. See [`WebConfig`].
|
||||
#[serde(default)]
|
||||
pub web: Option<WebConfig>,
|
||||
/// Memory subsystem opt-in. See [`MemoryConfig`].
|
||||
#[serde(default)]
|
||||
pub memory: Option<MemoryConfig>,
|
||||
@@ -296,6 +299,7 @@ impl PodManifestConfig {
|
||||
upper.compaction,
|
||||
CompactionConfigPartial::merge,
|
||||
),
|
||||
web: merge_option(self.web, upper.web, WebConfig::merge),
|
||||
memory: merge_option(self.memory, upper.memory, MemoryConfig::merge),
|
||||
skills: merge_option(self.skills, upper.skills, SkillsConfig::merge),
|
||||
}
|
||||
@@ -309,6 +313,49 @@ impl SkillsConfig {
|
||||
}
|
||||
}
|
||||
|
||||
impl WebConfig {
|
||||
fn merge(self, upper: Self) -> Self {
|
||||
Self {
|
||||
enabled: upper.enabled.or(self.enabled),
|
||||
allow_private_addresses: upper
|
||||
.allow_private_addresses
|
||||
.or(self.allow_private_addresses),
|
||||
search: merge_option(self.search, upper.search, crate::WebSearchConfig::merge),
|
||||
fetch: merge_option(self.fetch, upper.fetch, crate::WebFetchConfig::merge),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::WebSearchConfig {
|
||||
fn merge(self, upper: Self) -> Self {
|
||||
Self {
|
||||
enabled: upper.enabled.or(self.enabled),
|
||||
provider: upper.provider.or(self.provider),
|
||||
api_key_env: upper.api_key_env.or(self.api_key_env),
|
||||
base_url: upper.base_url.or(self.base_url),
|
||||
country: upper.country.or(self.country),
|
||||
search_lang: upper.search_lang.or(self.search_lang),
|
||||
ui_lang: upper.ui_lang.or(self.ui_lang),
|
||||
safesearch: upper.safesearch.or(self.safesearch),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::WebFetchConfig {
|
||||
fn merge(self, upper: Self) -> Self {
|
||||
Self {
|
||||
enabled: upper.enabled.or(self.enabled),
|
||||
timeout_secs: upper.timeout_secs.or(self.timeout_secs),
|
||||
redirect_limit: upper.redirect_limit.or(self.redirect_limit),
|
||||
max_response_bytes: upper.max_response_bytes.or(self.max_response_bytes),
|
||||
max_output_bytes: upper.max_output_bytes.or(self.max_output_bytes),
|
||||
allow_private_addresses: upper
|
||||
.allow_private_addresses
|
||||
.or(self.allow_private_addresses),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MemoryConfig {
|
||||
fn merge(self, upper: Self) -> Self {
|
||||
Self {
|
||||
@@ -625,6 +672,7 @@ impl TryFrom<PodManifestConfig> for PodManifest {
|
||||
session,
|
||||
permissions,
|
||||
compaction,
|
||||
web: cfg.web,
|
||||
memory: cfg.memory,
|
||||
skills: cfg.skills,
|
||||
})
|
||||
@@ -671,6 +719,7 @@ mod tests {
|
||||
permissions: None,
|
||||
session: None,
|
||||
compaction: None,
|
||||
web: None,
|
||||
memory: None,
|
||||
skills: None,
|
||||
}
|
||||
|
||||
@@ -53,6 +53,11 @@ pub struct PodManifest {
|
||||
/// memory tools registered.
|
||||
#[serde(default)]
|
||||
pub memory: Option<MemoryConfig>,
|
||||
/// First-class web tools configuration. Absent or `enabled = false` keeps
|
||||
/// WebSearch/WebFetch registered but disabled, so no network access occurs
|
||||
/// unless a manifest explicitly opts in.
|
||||
#[serde(default)]
|
||||
pub web: Option<WebConfig>,
|
||||
/// External Agent Skills (`SKILL.md`) directories to ingest as
|
||||
/// Workflows. Each entry is a path to a skills *root* (i.e. a
|
||||
/// directory whose children are individual `<name>/SKILL.md` skill
|
||||
@@ -79,6 +84,75 @@ pub struct SkillsConfig {
|
||||
pub directories: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
/// Configuration for WebSearch and WebFetch built-in tools.
|
||||
///
|
||||
/// Network tools are fail-closed: absent config or `enabled = false` disables
|
||||
/// both tools. Per-tool `enabled = false` can disable a tool under an enabled
|
||||
/// global section.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct WebConfig {
|
||||
/// Global opt-in for web tools. Defaults to false when omitted.
|
||||
#[serde(default)]
|
||||
pub enabled: Option<bool>,
|
||||
/// Escape hatch for tests / trusted local deployments. Defaults to false.
|
||||
#[serde(default)]
|
||||
pub allow_private_addresses: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub search: Option<WebSearchConfig>,
|
||||
#[serde(default)]
|
||||
pub fetch: Option<WebFetchConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WebSearchProvider {
|
||||
Brave,
|
||||
}
|
||||
|
||||
/// WebSearch provider configuration.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct WebSearchConfig {
|
||||
#[serde(default)]
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub provider: Option<WebSearchProvider>,
|
||||
/// Environment variable that stores the provider API key. Raw secrets do
|
||||
/// not belong in manifest files.
|
||||
#[serde(default)]
|
||||
pub api_key_env: Option<String>,
|
||||
/// Optional provider endpoint override for tests/proxies. Defaults to the
|
||||
/// Brave web search endpoint for the Brave provider.
|
||||
#[serde(default)]
|
||||
pub base_url: Option<String>,
|
||||
#[serde(default)]
|
||||
pub country: Option<String>,
|
||||
#[serde(default)]
|
||||
pub search_lang: Option<String>,
|
||||
#[serde(default)]
|
||||
pub ui_lang: Option<String>,
|
||||
#[serde(default)]
|
||||
pub safesearch: Option<String>,
|
||||
}
|
||||
|
||||
/// WebFetch HTTP client limits and policy.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct WebFetchConfig {
|
||||
#[serde(default)]
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub timeout_secs: Option<u64>,
|
||||
#[serde(default)]
|
||||
pub redirect_limit: Option<usize>,
|
||||
#[serde(default)]
|
||||
pub max_response_bytes: Option<usize>,
|
||||
#[serde(default)]
|
||||
pub max_output_bytes: Option<usize>,
|
||||
/// Per-fetch escape hatch; when absent falls back to `[web]`
|
||||
/// `allow_private_addresses`, then false.
|
||||
#[serde(default)]
|
||||
pub allow_private_addresses: Option<bool>,
|
||||
}
|
||||
|
||||
/// Memory subsystem configuration. Presence in the manifest enables
|
||||
/// memory; the workspace root defaults to the Pod's pwd unless an
|
||||
/// explicit override is given.
|
||||
@@ -560,6 +634,24 @@ permission = "write"
|
||||
assert!(manifest.worker.top_p.is_none());
|
||||
assert!(manifest.worker.top_k.is_none());
|
||||
assert!(manifest.worker.stop_sequences.is_empty());
|
||||
assert!(manifest.web.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_web_config() {
|
||||
let toml = format!(
|
||||
"{}\n[web]\nenabled = true\n\n[web.search]\nprovider = \"brave\"\napi_key_env = \"BRAVE_SEARCH_API_KEY\"\n\n[web.fetch]\ntimeout_secs = 7\nredirect_limit = 3\nmax_response_bytes = 12345\nmax_output_bytes = 2048\n",
|
||||
MINIMAL_REQUIRED
|
||||
);
|
||||
let manifest = PodManifest::from_toml(&toml).unwrap();
|
||||
let web = manifest.web.unwrap();
|
||||
assert_eq!(web.enabled, Some(true));
|
||||
assert_eq!(web.search.unwrap().provider, Some(WebSearchProvider::Brave));
|
||||
let fetch = web.fetch.unwrap();
|
||||
assert_eq!(fetch.timeout_secs, Some(7));
|
||||
assert_eq!(fetch.redirect_limit, Some(3));
|
||||
assert_eq!(fetch.max_response_bytes, Some(12345));
|
||||
assert_eq!(fetch.max_output_bytes, Some(2048));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user