feat: add web search and fetch tools

This commit is contained in:
2026-05-29 17:58:11 +09:00
parent 7a6d9c33f3
commit 2be3a5bd36
12 changed files with 1232 additions and 5 deletions
+50 -1
View File
@@ -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,
}
+92
View File
@@ -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]