From 53efde3930817819c99e158ced97b612f6b7b61b Mon Sep 17 00:00:00 2001 From: Jesse Proudman Date: Fri, 21 Aug 2026 12:31:03 -0700 Subject: [PATCH 1/3] feat(search): add Venice backend for web_search Brave stays the default. Shops that already vault VENICE_API_KEY can drop BRAVE_SEARCH_API_KEY by setting [server.integrations.search] provider = "venice". Co-authored-by: Cursor --- Cargo.lock | 1 + .../administration/server-configuration.mdx | 22 +- .../public/administration/troubleshooting.mdx | 2 +- docs/public/agents/tools.mdx | 22 +- docs/public/api-reference/fabro-api.yaml | 21 +- docs/public/changelog/2026-08-21.mdx | 10 + docs/public/docs.json | 10 +- docs/public/integrations/brave-search.mdx | 4 +- docs/public/integrations/venice-search.mdx | 84 +++ lib/apps/fabro-server/src/diagnostics.rs | 391 ++++++----- lib/components/fabro-agent/Cargo.toml | 1 + lib/components/fabro-agent/src/cli.rs | 6 +- lib/components/fabro-agent/src/config.rs | 27 +- lib/components/fabro-agent/src/lib.rs | 2 + .../fabro-agent/src/profiles/claude5.rs | 30 +- .../fabro-agent/src/profiles/claude5_tools.rs | 116 ++-- .../fabro-agent/src/profiles/mod.rs | 1 + lib/components/fabro-agent/src/tools.rs | 556 +++++++--------- lib/components/fabro-agent/src/web_search.rs | 627 ++++++++++++++++++ .../fabro-agent/tests/it/parity_matrix.rs | 20 +- .../fabro-workflow/src/handler/llm/api.rs | 1 + .../fabro-workflow/src/pipeline/initialize.rs | 10 +- lib/foundation/fabro-api/build.rs | 22 +- lib/foundation/fabro-api/src/lib.rs | 11 +- .../fabro-config/src/layers/combine.rs | 6 +- lib/foundation/fabro-config/src/layers/mod.rs | 4 +- .../fabro-config/src/layers/server.rs | 82 ++- lib/foundation/fabro-config/src/lib.rs | 11 +- .../fabro-config/src/resolve/server.rs | 49 +- .../fabro-config/src/tests/resolve_server.rs | 45 +- lib/foundation/fabro-static/src/env_vars.rs | 2 + .../fabro-static/src/secret_registry.rs | 2 + .../fabro-types/src/settings/mod.rs | 9 +- .../fabro-types/src/settings/server.rs | 156 +++-- .../src/.openapi-generator/FILES | 3 + .../fabro-api-client/src/models/index.ts | 3 + .../src/models/search-integration-settings.ts | 26 + .../src/models/search-provider.ts | 23 + .../models/server-integrations-settings.ts | 4 + .../src/models/venice-search-engine.ts | 23 + 40 files changed, 1761 insertions(+), 684 deletions(-) create mode 100644 docs/public/changelog/2026-08-21.mdx create mode 100644 docs/public/integrations/venice-search.mdx create mode 100644 lib/components/fabro-agent/src/web_search.rs create mode 100644 lib/packages/fabro-api-client/src/models/search-integration-settings.ts create mode 100644 lib/packages/fabro-api-client/src/models/search-provider.ts create mode 100644 lib/packages/fabro-api-client/src/models/venice-search-engine.ts diff --git a/Cargo.lock b/Cargo.lock index 800846e58..c09095c1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2298,6 +2298,7 @@ dependencies = [ "futures", "glob", "htmd", + "httpmock", "insta", "jsonschema", "libc", diff --git a/docs/public/administration/server-configuration.mdx b/docs/public/administration/server-configuration.mdx index b6fa8d2b7..5ed65df8c 100644 --- a/docs/public/administration/server-configuration.mdx +++ b/docs/public/administration/server-configuration.mdx @@ -334,6 +334,22 @@ Tailscale Services and Tailscale Funnel are different ingress features. Services Incoming webhooks are authenticated only by GitHub's `X-Hub-Signature-256` HMAC signature, not by Fabro's bearer/session auth. +### `[server.integrations.search]` section + +Select the HTTP backend for the built-in [`web_search`](/agents/tools#web_search) tool. Brave remains the default when this table is absent. + +```toml title="settings.toml" +[server.integrations.search] +provider = "brave" # "brave" (default) | "venice" +venice_engine = "brave" # venice-only: "brave" | "google" +``` + +- `provider = "brave"`: direct Brave Search. Requires vault `BRAVE_SEARCH_API_KEY`. See [Brave Search](/integrations/brave-search). +- `provider = "venice"`: Venice `POST /api/v1/augment/search`. Requires vault `VENICE_API_KEY` (the same key as the Venice LLM provider). See [Venice Search](/integrations/venice-search). +- `venice_engine = "brave"` is Brave **through Venice** (Firecrawl ZDR, billed as Venice credits). Direct Brave remains `provider = "brave"`. + +The tool is registered only when the selected provider is configured. Failed calls do not fall back between backends. + ### `[run.checkpoint]` section Configure checkpoint behavior for all runs. @@ -360,7 +376,7 @@ Fabro splits server-runtime secrets into two scopes: - `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` / `AWS_SESSION_TOKEN` when a manual config uses static S3 object-store credentials -`server.env` is not used for Slack, Daytona, Brave Search, LLM provider keys, `GITHUB_TOKEN`, or GitHub App private key/client secret/webhook secret. Configure those optional integrations with `fabro secret set`, `fabro provider login`, or `fabro install`. +`server.env` is not used for Slack, Daytona, Brave Search, Venice Search, LLM provider keys, `GITHUB_TOKEN`, or GitHub App private key/client secret/webhook secret. Configure those optional integrations with `fabro secret set`, `fabro provider login`, or `fabro install`. During startup, Fabro temporarily migrates recognized legacy optional integration secrets from process env or `server.env` into the vault. When a matching `server.env` entry can be safely removed, Fabro writes a hidden backup beside `server.env` first. Process env values cannot be cleaned up automatically, so remove those from your deployment environment after the vault contains the secret. @@ -402,12 +418,14 @@ These optional server integrations are vault-only: ```bash fabro secret set DAYTONA_API_KEY dtn_... fabro secret set BRAVE_SEARCH_API_KEY BSA... +fabro secret set VENICE_API_KEY venice-... ``` | Variable | Description | |---|---| | `DAYTONA_API_KEY` | Daytona cloud sandbox API key | -| `BRAVE_SEARCH_API_KEY` | Brave Search API key (for the `web_search` tool) | +| `BRAVE_SEARCH_API_KEY` | Brave Search API key (`web_search` when `provider = "brave"`) | +| `VENICE_API_KEY` | Venice API key (LLM provider, and `web_search` when `provider = "venice"`) | ### Server authentication diff --git a/docs/public/administration/troubleshooting.mdx b/docs/public/administration/troubleshooting.mdx index e6da40fa5..d3a59f056 100644 --- a/docs/public/administration/troubleshooting.mdx +++ b/docs/public/administration/troubleshooting.mdx @@ -17,7 +17,7 @@ It checks: - Local user config and storage directory health - Server-reported LLM provider connectivity, with configured providers probed concurrently -- GitHub App, sandbox, and Brave Search credentials, plus Docker daemon reachability when the Docker sandbox provider is enabled +- GitHub App, sandbox, and web search credentials (Brave or Venice), plus Docker daemon reachability when the Docker sandbox provider is enabled - Server authentication and crypto configuration LLM provider probe failures are reported as errors. Use `--verbose` to see the underlying provider error chain when a key, network route, or model endpoint fails. diff --git a/docs/public/agents/tools.mdx b/docs/public/agents/tools.mdx index 62d4e9f98..9b17b8957 100644 --- a/docs/public/agents/tools.mdx +++ b/docs/public/agents/tools.mdx @@ -20,7 +20,7 @@ These tools are registered for every provider profile: | `write_file` | write | Create or overwrite a file | | `grep` | read | Search file contents with regex patterns | | `glob` | read | Find files by name pattern | -| `web_search` | shell | Search the web via Brave Search | +| `web_search` | shell | Search the web via Brave or Venice | | `web_fetch` | shell | Fetch and optionally summarize a URL | ## Provider-specific tools @@ -123,14 +123,28 @@ Patterns are case-sensitive and relative to `path`: `*` and `?` stay within one ### web_search -Searches the web using the Brave Search API. +Searches the web using Brave Search (default) or Venice Search. | Parameter | Type | Required | Description | |---|---|---|---| -| `query` | string | yes | Search query | +| `query` | string | yes | Search query. Venice rejects queries longer than 400 characters before the HTTP call. | | `max_results` | integer | no | Maximum results (default: 5, max: 20) | +| `engine` | `"brave"` \| `"google"` | no | Venice backend only. Overrides `[server.integrations.search].venice_engine`. Brave ignores this parameter. | -Requires `BRAVE_SEARCH_API_KEY` to be configured for the current runtime. Runs read it from the server vault (`fabro secret set BRAVE_SEARCH_API_KEY `) — workers start from a cleared environment and this key is not inherited, so exporting it in the server's shell has no effect. The standalone agent CLI reads it from the invoking shell instead. Returns numbered results with title, URL, and description. +Select the backend in server settings. Brave remains the default when the table is absent: + +```toml +[server.integrations.search] +provider = "venice" # "brave" (default) | "venice" +venice_engine = "brave" # venice-only: "brave" (ZDR) | "google" (anon proxy) +``` + +Runs read the selected provider's key from the server vault — workers start from a cleared environment, so exporting the key in the server's shell has no effect. The standalone agent CLI reads keys from the invoking shell instead. + +- Brave (`provider = "brave"`): `fabro secret set BRAVE_SEARCH_API_KEY `. See [Brave Search](/integrations/brave-search). +- Venice (`provider = "venice"`): reuse `VENICE_API_KEY` (`fabro provider login --provider venice` or `fabro secret set VENICE_API_KEY `). See [Venice Search](/integrations/venice-search). + +The tool is registered only when the **selected** provider is configured. Fabro does not fall back Brave ↔ Venice on a failed call. Returns numbered results with title, URL, and description; Venice includes `date` on a fourth line when present. ### web_fetch diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index 546b9d106..e9b019329 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -14231,12 +14231,14 @@ components: ServerIntegrationsSettings: type: object - required: [github, slack] + required: [github, slack, search] properties: github: $ref: "#/components/schemas/GithubIntegrationSettings" slack: $ref: "#/components/schemas/SlackIntegrationSettings" + search: + $ref: "#/components/schemas/SearchIntegrationSettings" GithubIntegrationSettings: type: object @@ -14276,6 +14278,23 @@ components: default_channel: type: ["string", "null"] + SearchIntegrationSettings: + type: object + required: [provider, venice_engine] + properties: + provider: + $ref: "#/components/schemas/SearchProvider" + venice_engine: + $ref: "#/components/schemas/VeniceSearchEngine" + + SearchProvider: + type: string + enum: [brave, venice] + + VeniceSearchEngine: + type: string + enum: [brave, google] + IntegrationWebhooksSettings: type: object required: [strategy] diff --git a/docs/public/changelog/2026-08-21.mdx b/docs/public/changelog/2026-08-21.mdx new file mode 100644 index 000000000..d264b5aad --- /dev/null +++ b/docs/public/changelog/2026-08-21.mdx @@ -0,0 +1,10 @@ +--- +title: "Venice search backend" +date: "2026-08-21" +--- + +## Venice search backend for `web_search` + +The built-in `web_search` tool now has a second HTTP backend. Brave Search remains the default. Set `[server.integrations.search].provider = "venice"` to send queries to Venice `POST /api/v1/augment/search`, reusing vault `VENICE_API_KEY`. Failed calls do not fall back between providers. + +See [Venice Search](/integrations/venice-search) and [Brave Search](/integrations/brave-search). diff --git a/docs/public/docs.json b/docs/public/docs.json index faf0d0f84..9faa178a8 100644 --- a/docs/public/docs.json +++ b/docs/public/docs.json @@ -102,7 +102,8 @@ "integrations/modal", "integrations/fireworks", "integrations/slack", - "integrations/brave-search" + "integrations/brave-search", + "integrations/venice-search" ] }, { @@ -293,6 +294,13 @@ "tab": "Changelog", "icon": "clock-rotate-left", "groups": [ + { + "group": "August 2026", + "icon": "clock-rotate-left", + "pages": [ + "changelog/2026-08-21" + ] + }, { "group": "July 2026", "icon": "clock-rotate-left", diff --git a/docs/public/integrations/brave-search.mdx b/docs/public/integrations/brave-search.mdx index 4ce0cfc9e..2c46f25dc 100644 --- a/docs/public/integrations/brave-search.mdx +++ b/docs/public/integrations/brave-search.mdx @@ -5,6 +5,8 @@ description: "Give Fabro agents web search capabilities via the Brave Search API Fabro's [`web_search`](/agents/tools#web_search) tool lets agents search the web during workflow execution. It uses the [Brave Search API](https://brave.com/search/api/) to return titles, URLs, and descriptions for any query. Setting the API key is the only configuration needed — the tool is then registered for all provider profiles (Anthropic, OpenAI, Gemini). Without a key the tool is not registered at all, so agents are never offered a search tool they cannot use. +Brave is the default `web_search` backend. To use Venice instead, see [Venice Search](/integrations/venice-search). + ## Setup 1. Get a Brave Search API key from the [Brave Search API dashboard](https://brave.com/search/api/) @@ -21,7 +23,7 @@ fabro secret set BRAVE_SEARCH_API_KEY BSA... fabro doctor ``` -The doctor output should show **Brave Search** as "connected". If the key is missing, web search is reported as a warning — workflows still run, but the `web_search` tool is omitted from the agent's tool set and its system prompt, so agents fall back to other tools. +The doctor output should show **Web Search** as `brave: configured and reachable`. If the key is missing, web search is reported as a warning — workflows still run, but the `web_search` tool is omitted from the agent's tool set and its system prompt, so agents fall back to other tools. The Fabro server reads this key from the vault only. It does not read `BRAVE_SEARCH_API_KEY` from process env or `server.env`. diff --git a/docs/public/integrations/venice-search.mdx b/docs/public/integrations/venice-search.mdx new file mode 100644 index 000000000..2e23cbad0 --- /dev/null +++ b/docs/public/integrations/venice-search.mdx @@ -0,0 +1,84 @@ +--- +title: "Venice Search" +description: "Give Fabro agents web search capabilities via Venice's augment/search API" +--- + +Fabro's [`web_search`](/agents/tools#web_search) tool lets agents search the web during workflow execution. The default backend is [Brave Search](/integrations/brave-search). Set `[server.integrations.search].provider = "venice"` to use [Venice Search](https://docs.venice.ai/api-reference/endpoint/augment/search) instead, reusing the same `VENICE_API_KEY` already used for the Venice LLM provider. + +Agents keep calling `web_search`. Only the HTTP backend changes. + +## Setup + +1. Store a Venice API key on the Fabro server (skip this if the Venice LLM provider is already logged in): + +```bash +fabro provider login --provider venice +# or +fabro secret set VENICE_API_KEY venice-... +``` + +2. Select Venice as the search backend: + +```toml title="settings.toml" +[server.integrations.search] +provider = "venice" +# venice_engine = "brave" # default: Firecrawl ZDR, billed as Venice credits +# venice_engine = "google" # anonymized proxy +``` + +`provider = "brave"` (the default) talks to Brave Search directly and still needs `BRAVE_SEARCH_API_KEY`. `venice_engine = "brave"` is Brave **through Venice**, not the direct Brave backend. + +3. Verify the key is working: + +```bash +fabro doctor +``` + +The doctor output should show **Web Search** as `venice: configured and reachable`. If `VENICE_API_KEY` is missing, web search is reported as a warning — workflows still run, but the `web_search` tool is omitted from the agent's tool set. + +The Fabro server reads this key from the vault only. It does not read `VENICE_API_KEY` from process env or `server.env`. + +## How it works + +Agents call the `web_search` tool with a query string. Fabro `POST`s to Venice `https://api.venice.ai/api/v1/augment/search` and returns numbered results with title, URL, description, and date when Venice includes one: + +``` +1. Rust Lang + https://rust-lang.org + A systems language + 2026-01-02 +``` + +Venice Search is billed by Venice at $0.01 per request and is rate-limited to 20 requests per minute on the Venice side. Queries longer than 400 characters are rejected before the HTTP call. + +If `VENICE_API_KEY` is not configured in the vault, the tool is not registered. Failed calls return an error; Fabro does not fall back to Brave. + +See the [`web_search` tool reference](/agents/tools#web_search) for parameters and details. + +## Permissions + +`web_search` is classified as a `shell` category tool, requiring the `full` [permission level](/agents/permissions) for auto-approval. At lower permission levels: + +- **Interactive mode** — the user is prompted to approve each call +- **Non-interactive mode** (`--auto-approve`) — calls are denied + +## Troubleshooting + +**"VENICE_API_KEY is not configured"** — Add the key with `fabro secret set VENICE_API_KEY ` or `fabro provider login --provider venice`. Run `fabro doctor` to verify. + +**"Venice Search API returned status 401"** — The API key is invalid or expired. Create a new key at [venice.ai](https://venice.ai). + +**"Venice Search API returned status 402"** — The Venice account is out of credits. The error may include a remaining-balance hint. + +**"Venice Search API returned status 429"** — Rate limit exceeded (20 requests per minute on Venice Search). Reduce the frequency of `web_search` calls. + +## Further reading + + + + Full `web_search` tool reference — parameters, output format, and error handling. + + + Direct Brave Search backend (the default when `provider` is unset). + + diff --git a/lib/apps/fabro-server/src/diagnostics.rs b/lib/apps/fabro-server/src/diagnostics.rs index 9373cc81e..ac161a1b0 100644 --- a/lib/apps/fabro-server/src/diagnostics.rs +++ b/lib/apps/fabro-server/src/diagnostics.rs @@ -5,12 +5,14 @@ use std::time::Duration; use base64::Engine as _; use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use fabro_auth::auth_issue_message; +use fabro_http::Response; use fabro_llm::client::Client as LlmClient; use fabro_llm::model_test::{ModelTestStatus, run_basic_model_probe_with_timeout}; use fabro_model::{Catalog, ProviderId}; use fabro_redact::redact_string; use fabro_sandbox::{DockerSandboxProvider, daytona}; use fabro_static::EnvVars; +use fabro_types::settings::SearchProvider; use fabro_types::settings::ServerAuthMethod; use fabro_types::settings::server::GithubIntegrationStrategy; use fabro_util::check_report::{CheckDetail, CheckResult, CheckSection, CheckStatus}; @@ -19,6 +21,7 @@ use fabro_util::session_secret; use fabro_util::version::FABRO_VERSION; use futures_util::future::join_all; use serde::Serialize; +use tokio::time::error::Elapsed; use tokio::time::timeout; use crate::server::AppState; @@ -41,21 +44,21 @@ fn http_client_or_check( #[derive(Debug, Serialize)] pub struct DiagnosticsReport { - pub version: String, + pub version: String, pub sections: Vec, } #[derive(Debug, Clone, Serialize)] pub(crate) struct ProviderProbeReport { - pub data: Vec, + pub data: Vec, pub summary: ProviderProbeSummary, } #[derive(Debug, Clone, Serialize)] pub(crate) struct ProviderProbeResult { - pub provider: ProviderId, - pub model_id: Option, - pub status: ProviderProbeStatus, + pub provider: ProviderId, + pub model_id: Option, + pub status: ProviderProbeStatus, pub error_message: Option, #[serde(skip)] diagnostic_detail: Option, @@ -64,7 +67,7 @@ pub(crate) struct ProviderProbeResult { #[derive(Debug, Clone, Serialize)] pub(crate) struct ProviderProbeSummary { pub status: ProviderProbeStatus, - pub total: u32, + pub total: u32, pub passed: u32, pub failed: u32, } @@ -92,24 +95,24 @@ fn validate_session_secret(value: &str) -> Result<(), String> { } pub async fn run_all(state: &AppState) -> DiagnosticsReport { - let (llm, github, docker_sandbox, cloud_sandbox, brave, crypto) = tokio::join!( + let (llm, github, docker_sandbox, cloud_sandbox, web_search, crypto) = tokio::join!( check_llm_providers(state), check_github_app(state), check_docker_sandbox(state), check_cloud_sandbox(state), - check_brave_search(state), + check_web_search(state), check_crypto(state), ); DiagnosticsReport { - version: FABRO_VERSION.to_string(), + version: FABRO_VERSION.to_string(), sections: vec![ CheckSection { - title: "Credentials".to_string(), - checks: vec![llm, github, docker_sandbox, cloud_sandbox, brave], + title: "Credentials".to_string(), + checks: vec![llm, github, docker_sandbox, cloud_sandbox, web_search], }, CheckSection { - title: "Configuration".to_string(), + title: "Configuration".to_string(), checks: vec![crypto, check_storage_dir(state)], }, ], @@ -121,20 +124,20 @@ async fn check_llm_providers(state: &AppState) -> CheckResult { Ok(report) => report, Err(err) => { return CheckResult { - name: "LLM Providers".to_string(), - status: CheckStatus::Error, - summary: "failed to initialize".to_string(), - details: vec![CheckDetail::new(format!("{err:#}"))], + name: "LLM Providers".to_string(), + status: CheckStatus::Error, + summary: "failed to initialize".to_string(), + details: vec![CheckDetail::new(format!("{err:#}"))], remediation: Some("Check configured provider credentials".to_string()), }; } }; if report.data.is_empty() { return CheckResult { - name: "LLM Providers".to_string(), - status: CheckStatus::Error, - summary: "none configured".to_string(), - details: Vec::new(), + name: "LLM Providers".to_string(), + status: CheckStatus::Error, + summary: "none configured".to_string(), + details: Vec::new(), remediation: Some("Set at least one provider API key".to_string()), }; } @@ -156,7 +159,7 @@ async fn check_llm_providers(state: &AppState) -> CheckResult { .clone() .unwrap_or_else(|| format!("{}: {message}", result.provider)); failures.push(ProviderFailure { - provider: result.provider.to_string(), + provider: result.provider.to_string(), summary_line: short_error_line(message), }); details.push(CheckDetail::new(detail)); @@ -195,7 +198,7 @@ async fn check_llm_providers(state: &AppState) -> CheckResult { } struct ProviderFailure { - provider: String, + provider: String, summary_line: String, } @@ -352,10 +355,10 @@ async fn check_github_app(state: &AppState) -> CheckResult { Ok(token) => token.to_string(), Err(err) => { return CheckResult { - name: "GitHub Token".to_string(), - status: CheckStatus::Error, - summary: "token expired".to_string(), - details: vec![CheckDetail::new(err.to_string())], + name: "GitHub Token".to_string(), + status: CheckStatus::Error, + summary: "token expired".to_string(), + details: vec![CheckDetail::new(err.to_string())], remediation: Some( "Run fabro install or run `fabro secret set GITHUB_TOKEN`" .to_string(), @@ -367,10 +370,10 @@ async fn check_github_app(state: &AppState) -> CheckResult { Ok(Some(_)) => unreachable!("token strategy should not return app credentials"), Ok(None) => { return CheckResult { - name: "GitHub Token".to_string(), - status: CheckStatus::Warning, - summary: "not configured".to_string(), - details: Vec::new(), + name: "GitHub Token".to_string(), + status: CheckStatus::Warning, + summary: "not configured".to_string(), + details: Vec::new(), remediation: Some( "Run fabro install or run `fabro secret set GITHUB_TOKEN`".to_string(), ), @@ -379,10 +382,10 @@ async fn check_github_app(state: &AppState) -> CheckResult { Err(err) => { let rendered = format!("{err:#}"); return CheckResult { - name: "GitHub Token".to_string(), - status: CheckStatus::Error, - summary: "missing token".to_string(), - details: vec![CheckDetail::new(rendered.clone())], + name: "GitHub Token".to_string(), + status: CheckStatus::Error, + summary: "missing token".to_string(), + details: vec![CheckDetail::new(rendered.clone())], remediation: Some(rendered), }; } @@ -404,18 +407,18 @@ async fn check_github_app(state: &AppState) -> CheckResult { return match probe { Ok(Ok(response)) if response.status().is_success() => CheckResult { - name: "GitHub Token".to_string(), - status: CheckStatus::Pass, - summary: "configured".to_string(), - details: Vec::new(), + name: "GitHub Token".to_string(), + status: CheckStatus::Pass, + summary: "configured".to_string(), + details: Vec::new(), remediation: None, }, Ok(Ok(response)) if response.status() == fabro_http::StatusCode::UNAUTHORIZED => { CheckResult { - name: "GitHub Token".to_string(), - status: CheckStatus::Error, - summary: "token invalid".to_string(), - details: vec![CheckDetail::new(format!( + name: "GitHub Token".to_string(), + status: CheckStatus::Error, + summary: "token invalid".to_string(), + details: vec![CheckDetail::new(format!( "GitHub returned {}", response.status() ))], @@ -425,10 +428,10 @@ async fn check_github_app(state: &AppState) -> CheckResult { } } Ok(Ok(response)) => CheckResult { - name: "GitHub Token".to_string(), - status: CheckStatus::Error, - summary: "connectivity error".to_string(), - details: vec![CheckDetail::new(format!( + name: "GitHub Token".to_string(), + status: CheckStatus::Error, + summary: "connectivity error".to_string(), + details: vec![CheckDetail::new(format!( "GitHub returned {}", response.status() ))], @@ -437,19 +440,19 @@ async fn check_github_app(state: &AppState) -> CheckResult { ), }, Ok(Err(err)) => CheckResult { - name: "GitHub Token".to_string(), - status: CheckStatus::Error, - summary: "connectivity error".to_string(), - details: vec![CheckDetail::new(err.to_string())], + name: "GitHub Token".to_string(), + status: CheckStatus::Error, + summary: "connectivity error".to_string(), + details: vec![CheckDetail::new(err.to_string())], remediation: Some( "Check GitHub connectivity and the vault GITHUB_TOKEN".to_string(), ), }, Err(_) => CheckResult { - name: "GitHub Token".to_string(), - status: CheckStatus::Error, - summary: "timeout".to_string(), - details: vec![CheckDetail::new("GitHub probe timed out".to_string())], + name: "GitHub Token".to_string(), + status: CheckStatus::Error, + summary: "timeout".to_string(), + details: vec![CheckDetail::new("GitHub probe timed out".to_string())], remediation: Some( "Check GitHub connectivity and the vault GITHUB_TOKEN".to_string(), ), @@ -483,20 +486,20 @@ async fn check_github_app(state: &AppState) -> CheckResult { && !webhook_secret { return CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Warning, - summary: "not configured".to_string(), - details: Vec::new(), + name: "GitHub App".to_string(), + status: CheckStatus::Warning, + summary: "not configured".to_string(), + details: Vec::new(), remediation: Some("Configure GitHub App settings and secrets".to_string()), }; } let Some(app_id) = app_id else { return CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Error, - summary: "missing app_id".to_string(), - details: Vec::new(), + name: "GitHub App".to_string(), + status: CheckStatus::Error, + summary: "missing app_id".to_string(), + details: Vec::new(), remediation: Some( "Set [server.integrations.github].app_id in settings.toml".to_string(), ), @@ -504,10 +507,10 @@ async fn check_github_app(state: &AppState) -> CheckResult { }; let Some(private_key_raw) = private_key_raw else { return CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Error, - summary: "missing private key".to_string(), - details: Vec::new(), + name: "GitHub App".to_string(), + status: CheckStatus::Error, + summary: "missing private key".to_string(), + details: Vec::new(), remediation: Some("Run `fabro secret set GITHUB_APP_PRIVATE_KEY`".to_string()), }; }; @@ -516,10 +519,10 @@ async fn check_github_app(state: &AppState) -> CheckResult { Ok(value) => value, Err(err) => { return CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Error, - summary: "private key invalid".to_string(), - details: vec![CheckDetail::new(err.clone())], + name: "GitHub App".to_string(), + status: CheckStatus::Error, + summary: "private key invalid".to_string(), + details: vec![CheckDetail::new(err.clone())], remediation: Some(err), }; } @@ -529,10 +532,10 @@ async fn check_github_app(state: &AppState) -> CheckResult { Ok(jwt) => jwt, Err(err) => { return CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Error, - summary: "JWT signing failed".to_string(), - details: vec![CheckDetail::new(format!("{err:#}"))], + name: "GitHub App".to_string(), + status: CheckStatus::Error, + summary: "JWT signing failed".to_string(), + details: vec![CheckDetail::new(format!("{err:#}"))], remediation: Some(err.to_string()), }; } @@ -549,24 +552,24 @@ async fn check_github_app(state: &AppState) -> CheckResult { .await; match auth_result { Ok(Ok(_app)) => CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Pass, - summary: slug.unwrap_or_else(|| "configured".to_string()), - details: Vec::new(), + name: "GitHub App".to_string(), + status: CheckStatus::Pass, + summary: slug.unwrap_or_else(|| "configured".to_string()), + details: Vec::new(), remediation: None, }, Ok(Err(err)) => CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Error, - summary: "connectivity error".to_string(), - details: vec![CheckDetail::new(format!("{err:#}"))], + name: "GitHub App".to_string(), + status: CheckStatus::Error, + summary: "connectivity error".to_string(), + details: vec![CheckDetail::new(format!("{err:#}"))], remediation: Some("Check GitHub App credentials and network connectivity".to_string()), }, Err(_) => CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Error, - summary: "timeout".to_string(), - details: vec![CheckDetail::new("GitHub probe timed out".to_string())], + name: "GitHub App".to_string(), + status: CheckStatus::Error, + summary: "timeout".to_string(), + details: vec![CheckDetail::new("GitHub probe timed out".to_string())], remediation: Some("Check GitHub connectivity and credentials".to_string()), }, } @@ -602,10 +605,10 @@ where { if !enabled { return CheckResult { - name: "Docker Sandbox".to_string(), - status: CheckStatus::Pass, - summary: "disabled".to_string(), - details: vec![CheckDetail::new( + name: "Docker Sandbox".to_string(), + status: CheckStatus::Pass, + summary: "disabled".to_string(), + details: vec![CheckDetail::new( "server.sandbox.providers.docker.enabled = false".to_string(), )], remediation: None, @@ -650,10 +653,10 @@ async fn check_cloud_sandbox(state: &AppState) -> CheckResult { }; let Some(api_key) = api_key else { return CheckResult { - name: "Cloud Sandbox".to_string(), - status: CheckStatus::Warning, - summary: "recommended, not configured".to_string(), - details: Vec::new(), + name: "Cloud Sandbox".to_string(), + status: CheckStatus::Warning, + summary: "recommended, not configured".to_string(), + details: Vec::new(), remediation: Some( "Run `fabro secret set DAYTONA_API_KEY` to enable cloud sandbox execution" .to_string(), @@ -670,17 +673,17 @@ async fn check_cloud_sandbox(state: &AppState) -> CheckResult { fn cloud_sandbox_probe_check(probe: anyhow::Result) -> CheckResult { match probe { Ok(check) if check.ok() => CheckResult { - name: "Cloud Sandbox".to_string(), - status: CheckStatus::Pass, - summary: format!("Daytona configured ({})", check.key_name), - details: Vec::new(), + name: "Cloud Sandbox".to_string(), + status: CheckStatus::Pass, + summary: format!("Daytona configured ({})", check.key_name), + details: Vec::new(), remediation: None, }, Ok(check) => CheckResult { - name: "Cloud Sandbox".to_string(), - status: CheckStatus::Error, - summary: "Daytona API key is missing required scopes".to_string(), - details: vec![CheckDetail::new(format!( + name: "Cloud Sandbox".to_string(), + status: CheckStatus::Error, + summary: "Daytona API key is missing required scopes".to_string(), + details: vec![CheckDetail::new(format!( "missing: {}", check.missing_display() ))], @@ -693,10 +696,10 @@ fn cloud_sandbox_probe_check(probe: anyhow::Result) -> Err(err) => { if let Some(timeout) = err.downcast_ref::() { return CheckResult { - name: "Cloud Sandbox".to_string(), - status: CheckStatus::Error, - summary: format!("timeout ({:?})", timeout.timeout()), - details: vec![CheckDetail::new("Daytona probe timed out".to_string())], + name: "Cloud Sandbox".to_string(), + status: CheckStatus::Error, + summary: format!("timeout ({:?})", timeout.timeout()), + details: vec![CheckDetail::new("Daytona probe timed out".to_string())], remediation: Some( "Verify DAYTONA_API_KEY value and Daytona reachability".to_string(), ), @@ -704,10 +707,10 @@ fn cloud_sandbox_probe_check(probe: anyhow::Result) -> } CheckResult { - name: "Cloud Sandbox".to_string(), - status: CheckStatus::Error, - summary: "Daytona credential rejected".to_string(), - details: vec![CheckDetail::new(format!("{err:#}"))], + name: "Cloud Sandbox".to_string(), + status: CheckStatus::Error, + summary: "Daytona credential rejected".to_string(), + details: vec![CheckDetail::new(format!("{err:#}"))], remediation: Some( "Verify DAYTONA_API_KEY value and Daytona reachability".to_string(), ), @@ -755,25 +758,40 @@ fn check_storage_dir_path(path: &std::path::Path) -> CheckResult { } } +async fn check_web_search(state: &AppState) -> CheckResult { + let search = state.server_settings().server.integrations.search; + match search.provider { + SearchProvider::Brave => check_brave_search(state).await, + SearchProvider::Venice => check_venice_search(state).await, + } +} + +const WEB_SEARCH_CHECK_NAME: &str = "Web Search"; + async fn check_brave_search(state: &AppState) -> CheckResult { - let api_key = - match diagnostic_secret(state, "Web Search (Brave)", EnvVars::BRAVE_SEARCH_API_KEY).await { - Ok(value) => value, - Err(result) => return result, - }; + let api_key = match diagnostic_secret( + state, + WEB_SEARCH_CHECK_NAME, + EnvVars::BRAVE_SEARCH_API_KEY, + ) + .await + { + Ok(value) => value, + Err(result) => return result, + }; let Some(api_key) = api_key else { return CheckResult { - name: "Web Search (Brave)".to_string(), - status: CheckStatus::Warning, - summary: "optional, not configured".to_string(), - details: Vec::new(), + name: WEB_SEARCH_CHECK_NAME.to_string(), + status: CheckStatus::Warning, + summary: "brave: optional, not configured".to_string(), + details: Vec::new(), remediation: Some( "Run `fabro secret set BRAVE_SEARCH_API_KEY` to enable web search".to_string(), ), }; }; - let http = match http_client_or_check("Web Search (Brave)", CheckStatus::Warning) { + let http = match http_client_or_check(WEB_SEARCH_CHECK_NAME, CheckStatus::Warning) { Ok(http) => http, Err(result) => return result, }; @@ -787,36 +805,80 @@ async fn check_brave_search(state: &AppState) -> CheckResult { }) .await; + match_web_search_probe(probe, "brave", "BRAVE_SEARCH_API_KEY") +} + +async fn check_venice_search(state: &AppState) -> CheckResult { + let api_key = + match diagnostic_secret(state, WEB_SEARCH_CHECK_NAME, EnvVars::VENICE_API_KEY).await { + Ok(value) => value, + Err(result) => return result, + }; + let Some(api_key) = api_key else { + return CheckResult { + name: WEB_SEARCH_CHECK_NAME.to_string(), + status: CheckStatus::Warning, + summary: "venice: optional, not configured".to_string(), + details: Vec::new(), + remediation: Some( + "Run `fabro secret set VENICE_API_KEY` to enable web search".to_string(), + ), + }; + }; + + let http = match http_client_or_check(WEB_SEARCH_CHECK_NAME, CheckStatus::Warning) { + Ok(http) => http, + Err(result) => return result, + }; + + let probe = timeout(EXTERNAL_SERVICE_PROBE_TIMEOUT, async move { + http.post("https://api.venice.ai/api/v1/augment/search") + .bearer_auth(api_key) + .json(&serde_json::json!({ "query": "test", "limit": 1 })) + .send() + .await + .map_err(anyhow::Error::new) + }) + .await; + + match_web_search_probe(probe, "venice", "VENICE_API_KEY") +} + +fn match_web_search_probe( + probe: Result, Elapsed>, + provider: &str, + secret_name: &str, +) -> CheckResult { match probe { Ok(Ok(response)) if response.status().is_success() => CheckResult { - name: "Web Search (Brave)".to_string(), - status: CheckStatus::Pass, - summary: "configured and reachable".to_string(), - details: Vec::new(), + name: WEB_SEARCH_CHECK_NAME.to_string(), + status: CheckStatus::Pass, + summary: format!("{provider}: configured and reachable"), + details: Vec::new(), remediation: None, }, Ok(Ok(response)) => CheckResult { - name: "Web Search (Brave)".to_string(), - status: CheckStatus::Warning, - summary: format!("HTTP {}", response.status()), - details: Vec::new(), - remediation: Some("Check BRAVE_SEARCH_API_KEY and network connectivity".to_string()), + name: WEB_SEARCH_CHECK_NAME.to_string(), + status: CheckStatus::Warning, + summary: format!("{provider}: HTTP {}", response.status()), + details: Vec::new(), + remediation: Some(format!("Check {secret_name} and network connectivity")), }, Ok(Err(err)) => CheckResult { - name: "Web Search (Brave)".to_string(), - status: CheckStatus::Warning, - summary: "connectivity error".to_string(), - details: vec![CheckDetail::new(format!("{err:#}"))], - remediation: Some("Check BRAVE_SEARCH_API_KEY and network connectivity".to_string()), + name: WEB_SEARCH_CHECK_NAME.to_string(), + status: CheckStatus::Warning, + summary: format!("{provider}: connectivity error"), + details: vec![CheckDetail::new(format!("{err:#}"))], + remediation: Some(format!("Check {secret_name} and network connectivity")), }, Err(_) => CheckResult { - name: "Web Search (Brave)".to_string(), - status: CheckStatus::Warning, - summary: "timeout".to_string(), - details: vec![CheckDetail::new( - "Web Search (Brave) probe timed out".to_string(), - )], - remediation: Some("Check BRAVE_SEARCH_API_KEY and network connectivity".to_string()), + name: WEB_SEARCH_CHECK_NAME.to_string(), + status: CheckStatus::Warning, + summary: format!("{provider}: timeout"), + details: vec![CheckDetail::new(format!( + "Web Search ({provider}) probe timed out" + ))], + remediation: Some(format!("Check {secret_name} and network connectivity")), }, } } @@ -894,10 +956,10 @@ async fn diagnostic_secret( name: &str, ) -> Result, CheckResult> { state.vault_secret(name).await.map_err(|err| CheckResult { - name: check_name.to_string(), - status: CheckStatus::Error, - summary: "secret store unavailable".to_string(), - details: vec![CheckDetail::new(err.to_string())], + name: check_name.to_string(), + status: CheckStatus::Error, + summary: "secret store unavailable".to_string(), + details: vec![CheckDetail::new(err.to_string())], remediation: Some("Check the Fabro database and retry".to_string()), }) } @@ -1195,23 +1257,56 @@ enabled = false } #[tokio::test] - async fn check_brave_search_ignores_env_backed_api_key() { + async fn check_web_search_ignores_env_backed_api_key() { let state = TestAppStateBuilder::new() .env_lookup(|name| { (name == EnvVars::BRAVE_SEARCH_API_KEY).then(|| "brave-from-env".to_string()) }) .build(); - let result = check_brave_search(&state).await; + let result = check_web_search(&state).await; + assert_eq!(result.name, "Web Search"); assert_eq!(result.status, CheckStatus::Warning); - assert_eq!(result.summary, "optional, not configured"); + assert_eq!(result.summary, "brave: optional, not configured"); assert_eq!( result.remediation.as_deref(), Some("Run `fabro secret set BRAVE_SEARCH_API_KEY` to enable web search") ); } + #[tokio::test] + async fn check_web_search_venice_ignores_env_backed_api_key() { + let settings = fabro_config::ServerSettingsBuilder::from_toml( + r#" +_version = 1 + +[server.auth] +methods = ["dev-token"] + +[server.integrations.search] +provider = "venice" +"#, + ) + .expect("venice search settings should parse"); + let state = TestAppStateBuilder::new() + .runtime_settings(settings, RunLayer::default()) + .env_lookup(|name| { + (name == EnvVars::VENICE_API_KEY).then(|| "venice-from-env".to_string()) + }) + .build(); + + let result = check_web_search(&state).await; + + assert_eq!(result.name, "Web Search"); + assert_eq!(result.status, CheckStatus::Warning); + assert_eq!(result.summary, "venice: optional, not configured"); + assert_eq!( + result.remediation.as_deref(), + Some("Run `fabro secret set VENICE_API_KEY` to enable web search") + ); + } + #[tokio::test] async fn check_crypto_requires_github_client_secret_from_vault() { let settings = fabro_config::ServerSettingsBuilder::from_toml( diff --git a/lib/components/fabro-agent/Cargo.toml b/lib/components/fabro-agent/Cargo.toml index 2e9a299b6..91f2e0e98 100644 --- a/lib/components/fabro-agent/Cargo.toml +++ b/lib/components/fabro-agent/Cargo.toml @@ -67,5 +67,6 @@ paste = "1" shlex = "1" fabro-sandbox = { path = "../fabro-sandbox", features = ["test-support"] } fabro-macros = { path = "../../foundation/fabro-macros" } +httpmock = "0.8" fabro-test = { workspace = true } tracing-subscriber.workspace = true diff --git a/lib/components/fabro-agent/src/cli.rs b/lib/components/fabro-agent/src/cli.rs index 720b8b0af..e16d8fa2f 100644 --- a/lib/components/fabro-agent/src/cli.rs +++ b/lib/components/fabro-agent/src/cli.rs @@ -34,16 +34,18 @@ use crate::tool_permissions::{is_auto_approved, tool_category}; use crate::tools::WebFetchSummarizer; use crate::{ AgentEvent, AgentProfile, AgentProfileBuilder, LocalSandbox, Message, Sandbox, Session, - SessionOptions, SessionShutdownReason, + SessionOptions, SessionShutdownReason, search_settings_from_disk, }; #[expect( clippy::disallowed_methods, - reason = "Standalone agent CLI explicitly passes the Brave Search process-env credential into tool configuration." + reason = "Standalone agent CLI explicitly passes search process-env credentials into tool configuration." )] fn cli_tool_secrets() -> ToolSecrets { ToolSecrets { brave_search_api_key: std::env::var(EnvVars::BRAVE_SEARCH_API_KEY).ok(), + venice_api_key: std::env::var(EnvVars::VENICE_API_KEY).ok(), + search: search_settings_from_disk(), } } diff --git a/lib/components/fabro-agent/src/config.rs b/lib/components/fabro-agent/src/config.rs index a6c8b5c39..44dce5af0 100644 --- a/lib/components/fabro-agent/src/config.rs +++ b/lib/components/fabro-agent/src/config.rs @@ -6,6 +6,7 @@ use fabro_llm::types::{ReasoningEffort, Speed}; use fabro_mcp::config::McpServerSettings; use fabro_model::AgentProfileKind; use fabro_types::PermissionLevel; +use fabro_types::settings::SearchIntegrationSettings; /// Callback invoked before each tool execution. Return `Ok(())` to allow, /// `Err(message)` to deny with the given message. @@ -103,6 +104,8 @@ impl ToolHookCallback for ToolApprovalAdapter { #[derive(Clone, Default, PartialEq, Eq)] pub struct ToolSecrets { pub brave_search_api_key: Option, + pub venice_api_key: Option, + pub search: SearchIntegrationSettings, } impl std::fmt::Debug for ToolSecrets { @@ -112,6 +115,8 @@ impl std::fmt::Debug for ToolSecrets { "brave_search_configured", &self.brave_search_api_key.is_some(), ) + .field("venice_search_configured", &self.venice_api_key.is_some()) + .field("search_provider", &self.search.provider.as_str()) .finish() } } @@ -120,8 +125,8 @@ impl std::fmt::Debug for ToolSecrets { #[derive(Clone, Debug, PartialEq, Eq)] pub struct NativeToolOptions { pub default_command_timeout_ms: u64, - pub max_command_timeout_ms: u64, - pub secrets: ToolSecrets, + pub max_command_timeout_ms: u64, + pub secrets: ToolSecrets, } impl NativeToolOptions { @@ -152,8 +157,8 @@ impl Default for NativeToolOptions { fn default() -> Self { Self { default_command_timeout_ms: 10_000, - max_command_timeout_ms: 600_000, - secrets: ToolSecrets::default(), + max_command_timeout_ms: 600_000, + secrets: ToolSecrets::default(), } } } @@ -350,12 +355,17 @@ mod tests { fn tool_secrets_debug_redacts_values() { let secrets = ToolSecrets { brave_search_api_key: Some("brave-secret-value".to_string()), + venice_api_key: Some("venice-secret-value".to_string()), + ..ToolSecrets::default() }; let debug = format!("{secrets:?}"); assert!(debug.contains("brave_search_configured: true")); + assert!(debug.contains("venice_search_configured: true")); + assert!(debug.contains("search_provider: \"brave\"")); assert!(!debug.contains("brave-secret-value")); + assert!(!debug.contains("venice-secret-value")); } #[test] @@ -437,9 +447,12 @@ mod tests { let approval: ToolApprovalFn = Arc::new(|_name, _args| Err("denied".to_string())); let adapter = ToolApprovalAdapter(approval); let decision = adapter.pre_tool_use("shell", &serde_json::json!({})).await; - assert_eq!(decision, ToolHookDecision::Block { - reason: "denied".to_string(), - }); + assert_eq!( + decision, + ToolHookDecision::Block { + reason: "denied".to_string(), + } + ); } #[tokio::test] diff --git a/lib/components/fabro-agent/src/lib.rs b/lib/components/fabro-agent/src/lib.rs index e9549a716..f7600a7f6 100644 --- a/lib/components/fabro-agent/src/lib.rs +++ b/lib/components/fabro-agent/src/lib.rs @@ -31,6 +31,7 @@ pub mod tool_registry; pub mod tools; pub mod truncation; pub mod types; +pub(crate) mod web_search; pub use agent_profile::AgentProfile; pub use config::{ @@ -85,6 +86,7 @@ pub use types::{ AgentEvent, McpToolSummary, MemoryFileSummary, Message, SessionEvent, SessionState, SkillActivationSource, SkillSummary, }; +pub use web_search::search_settings_from_disk; #[cfg(test)] #[allow( diff --git a/lib/components/fabro-agent/src/profiles/claude5.rs b/lib/components/fabro-agent/src/profiles/claude5.rs index ffa4b6ea1..97d35a187 100644 --- a/lib/components/fabro-agent/src/profiles/claude5.rs +++ b/lib/components/fabro-agent/src/profiles/claude5.rs @@ -18,6 +18,7 @@ use crate::todo_tools::{ make_task_create_tool, make_task_get_tool, make_task_list_tool, make_task_update_tool, }; use crate::tool_registry::ToolRegistry; +use crate::web_search::SearchBackend; const CORE_PROMPT: &str = include_str!("prompts/claude5.md.j2"); @@ -43,8 +44,8 @@ impl Claude5Profile { registry.register(claude5_tools::make_edit_tool()); registry.register(claude5_tools::make_bash_tool(options)); registry.register(claude5_tools::make_web_fetch_tool(summarizer)); - if let Some(api_key) = &options.secrets.brave_search_api_key { - registry.register(claude5_tools::make_web_search_tool(api_key.clone())); + if let Some(backend) = SearchBackend::from_secrets(&options.secrets) { + registry.register(claude5_tools::make_web_search_tool(backend)); } registry.register(claude5_tools::strict_object_tool(make_task_create_tool( @@ -173,17 +174,20 @@ mod tests { let profile = Claude5Profile::new("claude-sonnet-5"); let mut names = profile.tool_registry().names(); names.sort(); - assert_eq!(names, vec![ - "Bash", - "Edit", - "Read", - "TaskCreate", - "TaskGet", - "TaskList", - "TaskUpdate", - "WebFetch", - "Write", - ]); + assert_eq!( + names, + vec![ + "Bash", + "Edit", + "Read", + "TaskCreate", + "TaskGet", + "TaskList", + "TaskUpdate", + "WebFetch", + "Write", + ] + ); assert!(!names.iter().any(|name| name == "Grep" || name == "Glob")); } diff --git a/lib/components/fabro-agent/src/profiles/claude5_tools.rs b/lib/components/fabro-agent/src/profiles/claude5_tools.rs index d9dd53aa6..9366194cd 100644 --- a/lib/components/fabro-agent/src/profiles/claude5_tools.rs +++ b/lib/components/fabro-agent/src/profiles/claude5_tools.rs @@ -19,6 +19,7 @@ use crate::session::Session; use crate::subagent::{SessionFactory, SubAgentResult, SubAgentStatus, SubAgentSupervisor}; use crate::tool_registry::{RegisteredTool, ToolContext, ToolSource}; use crate::tools::{self, WebFetchSummarizer}; +use crate::web_search::{self, SearchBackend}; fn definition( tool: NativeTool, @@ -98,7 +99,7 @@ pub(crate) fn make_bash_tool(options: &NativeToolOptions) -> RegisteredTool { "additionalProperties": false }), ), - executor: Arc::new(move |args, ctx| { + executor: Arc::new(move |args, ctx| { Box::pin(async move { let command = tools::required_str(&args, "command")?; let timeout_ms = args @@ -109,25 +110,34 @@ pub(crate) fn make_bash_tool(options: &NativeToolOptions) -> RegisteredTool { tools::run_shell_command(&ctx, command, timeout_ms, None).await }) }), - source: ToolSource::Native, + source: ToolSource::Native, } } #[must_use] -pub(crate) fn make_web_search_tool(api_key: String) -> RegisteredTool { - let mut tool = tools::make_web_search_tool_with_api_key(api_key); +pub(crate) fn make_web_search_tool(backend: SearchBackend) -> RegisteredTool { + let includes_engine = backend.includes_engine_param(); + let mut tool = web_search::make_web_search_tool(backend); + let mut properties = serde_json::json!({ + "query": { + "type": "string", + "description": "The web search query." + } + }); + if includes_engine { + properties["engine"] = serde_json::json!({ + "type": "string", + "enum": ["brave", "google"], + "description": "Venice search engine. brave is ZDR (default); google is an anonymized proxy." + }); + } tool.definition = definition( NativeTool::WebSearch, "Search the web when current external information is needed. Returns result titles, URLs, \ and descriptions; use WebFetch to inspect a specific URL.", serde_json::json!({ "type": "object", - "properties": { - "query": { - "type": "string", - "description": "The web search query." - } - }, + "properties": properties, "required": ["query"], "additionalProperties": false }), @@ -211,7 +221,7 @@ pub(crate) fn make_agent_tool( "additionalProperties": false }), ), - executor: Arc::new(move |args, ctx| { + executor: Arc::new(move |args, ctx| { let supervisor = supervisor.clone(); let session_factory = session_factory.clone(); Box::pin(async move { @@ -249,7 +259,7 @@ pub(crate) fn make_agent_tool( } }) }), - source: ToolSource::Native, + source: ToolSource::Native, } } @@ -320,7 +330,7 @@ pub(crate) fn make_task_output_tool(supervisor: SubAgentSupervisor) -> Registere "additionalProperties": false }), ), - executor: Arc::new(move |args, ctx| { + executor: Arc::new(move |args, ctx| { let supervisor = supervisor.clone(); Box::pin(async move { let task_id = tools::required_str(&args, "task_id")?; @@ -374,7 +384,7 @@ pub(crate) fn make_task_output_tool(supervisor: SubAgentSupervisor) -> Registere } }) }), - source: ToolSource::Native, + source: ToolSource::Native, } } @@ -396,7 +406,7 @@ pub(crate) fn make_task_stop_tool(supervisor: SubAgentSupervisor) -> RegisteredT "additionalProperties": false }), ), - executor: Arc::new(move |args, _ctx| { + executor: Arc::new(move |args, _ctx| { let supervisor = supervisor.clone(); Box::pin(async move { let task_id = tools::required_str(&args, "task_id")?; @@ -407,7 +417,7 @@ pub(crate) fn make_task_stop_tool(supervisor: SubAgentSupervisor) -> RegisteredT Ok(format!("Agent {task_id} stopped.")) }) }), - source: ToolSource::Native, + source: ToolSource::Native, } } @@ -438,7 +448,7 @@ pub(crate) fn make_send_message_tool(supervisor: SubAgentSupervisor) -> Register "additionalProperties": false }), ), - executor: Arc::new(move |args, _ctx| { + executor: Arc::new(move |args, _ctx| { let supervisor = supervisor.clone(); Box::pin(async move { let recipient = tools::required_str(&args, "to")?; @@ -449,7 +459,7 @@ pub(crate) fn make_send_message_tool(supervisor: SubAgentSupervisor) -> Register Ok(format!("Message sent to agent {recipient}.")) }) }), - source: ToolSource::Native, + source: ToolSource::Native, } } @@ -468,6 +478,7 @@ mod tests { use crate::todo_tools::{ make_task_create_tool, make_task_get_tool, make_task_list_tool, make_task_update_tool, }; + use fabro_types::settings::VeniceSearchEngine; fn property_names(tool: &RegisteredTool) -> BTreeSet<&str> { tool.definition.parameters["properties"] @@ -502,12 +513,12 @@ mod tests { fn context() -> ToolContext { ToolContext { - env: Arc::new(MockSandbox::default()) as Arc, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: Some("root".to_string()), - root_session_id: Some("root".to_string()), - tool_call_id: Some("call".to_string()), + env: Arc::new(MockSandbox::default()) as Arc, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: Some("root".to_string()), + root_session_id: Some("root".to_string()), + tool_call_id: Some("call".to_string()), agent_event_emitter: None, } } @@ -515,13 +526,16 @@ mod tests { #[test] fn core_adapter_schemas_match_the_claude5_contract() { let options = NativeToolOptions::for_profile(fabro_model::AgentProfileKind::Claude5); - assert_schema(&make_read_tool(), &["file_path", "limit", "offset"], &[ - "file_path", - ]); - assert_schema(&make_write_tool(), &["content", "file_path"], &[ - "content", - "file_path", - ]); + assert_schema( + &make_read_tool(), + &["file_path", "limit", "offset"], + &["file_path"], + ); + assert_schema( + &make_write_tool(), + &["content", "file_path"], + &["content", "file_path"], + ); assert_schema( &make_edit_tool(), &["file_path", "new_string", "old_string", "replace_all"], @@ -533,12 +547,24 @@ mod tests { bash.definition.parameters["properties"]["timeout"]["maximum"], 600_000 ); - assert_schema(&make_web_fetch_tool(None), &["prompt", "url"], &[ - "prompt", "url", - ]); - assert_schema(&make_web_search_tool("key".to_string()), &["query"], &[ - "query", - ]); + assert_schema( + &make_web_fetch_tool(None), + &["prompt", "url"], + &["prompt", "url"], + ); + assert_schema( + &make_web_search_tool(SearchBackend::brave("key".to_string())), + &["query"], + &["query"], + ); + assert_schema( + &make_web_search_tool(SearchBackend::venice( + "key".to_string(), + VeniceSearchEngine::Brave, + )), + &["engine", "query"], + &["query"], + ); let todo_runtime = Arc::new(TodoRuntime::new()); assert_schema( @@ -587,13 +613,17 @@ mod tests { &["block", "task_id", "timeout"], &["block", "task_id", "timeout"], ); - assert_schema(&make_task_stop_tool(supervisor.clone()), &["task_id"], &[ - "task_id", - ]); + assert_schema( + &make_task_stop_tool(supervisor.clone()), + &["task_id"], + &["task_id"], + ); let send_message = make_send_message_tool(supervisor); - assert_schema(&send_message, &["message", "summary", "to"], &[ - "message", "to", - ]); + assert_schema( + &send_message, + &["message", "summary", "to"], + &["message", "to"], + ); assert!( send_message .definition diff --git a/lib/components/fabro-agent/src/profiles/mod.rs b/lib/components/fabro-agent/src/profiles/mod.rs index dbc0d0c54..2ecc237bb 100644 --- a/lib/components/fabro-agent/src/profiles/mod.rs +++ b/lib/components/fabro-agent/src/profiles/mod.rs @@ -746,6 +746,7 @@ mod tests { ) .with_tool_secrets(ToolSecrets { brave_search_api_key: Some("configured-key".to_string()), + ..ToolSecrets::default() }); // Built twice: one configured builder must outfit both a root // session and the child sessions it spawns. diff --git a/lib/components/fabro-agent/src/tools.rs b/lib/components/fabro-agent/src/tools.rs index aad8e4e3d..b6515a4a5 100644 --- a/lib/components/fabro-agent/src/tools.rs +++ b/lib/components/fabro-agent/src/tools.rs @@ -12,6 +12,7 @@ use tokio::task; use crate::config::NativeToolOptions; use crate::sandbox::{ExecStreamingResult, GrepOptions}; +use crate::web_search::{SearchBackend, make_web_search_tool}; use crate::tool_registry::{RegisteredTool, ToolContext, ToolRegistry, ToolSource}; use crate::types::AgentEvent; @@ -22,7 +23,7 @@ pub(crate) const DEFAULT_READ_LINES: usize = 2000; /// Configuration for the optional LLM-based summarizer used by `web_fetch`. #[derive(Clone)] pub struct WebFetchSummarizer { - pub client: Client, + pub client: Client, pub model_id: ModelHandle, } @@ -82,13 +83,13 @@ pub(crate) fn register_discovery_and_web_tools( registry.register(make_web_fetch_tool(summarizer)); } -/// Register `web_search` when a Brave Search key is configured. +/// Register `web_search` when the selected search provider is configured. /// /// Separate from [`register_discovery_and_web_tools`] for profiles that offer /// search without fabro's discovery tools. pub(crate) fn register_web_search_tool(registry: &mut ToolRegistry, options: &NativeToolOptions) { - if let Some(api_key) = &options.secrets.brave_search_api_key { - registry.register(make_web_search_tool_with_api_key(api_key.clone())); + if let Some(backend) = SearchBackend::from_secrets(&options.secrets) { + registry.register(make_web_search_tool(backend)); } } @@ -511,9 +512,9 @@ pub fn make_glob_tool() -> RegisteredTool { pub(crate) fn make_read_many_files_tool() -> RegisteredTool { RegisteredTool { definition: ToolDefinition { - name: "read_many_files".into(), + name: "read_many_files".into(), description: "Read multiple files at once".into(), - parameters: serde_json::json!({ + parameters: serde_json::json!({ "type": "object", "properties": { "paths": { @@ -525,7 +526,7 @@ pub(crate) fn make_read_many_files_tool() -> RegisteredTool { "required": ["paths"] }), }, - executor: Arc::new(|args, ctx| { + executor: Arc::new(|args, ctx| { Box::pin(async move { let paths: Vec = args["paths"] .as_array() @@ -564,7 +565,7 @@ pub(crate) fn make_read_many_files_tool() -> RegisteredTool { Ok(output) }) }), - source: ToolSource::Native, + source: ToolSource::Native, } } @@ -572,9 +573,9 @@ pub(crate) fn make_read_many_files_tool() -> RegisteredTool { pub(crate) fn make_list_dir_tool() -> RegisteredTool { RegisteredTool { definition: ToolDefinition { - name: "list_dir".into(), + name: "list_dir".into(), description: "List directory contents with depth control".into(), - parameters: serde_json::json!({ + parameters: serde_json::json!({ "type": "object", "properties": { "path": {"type": "string", "description": "Directory path to list"}, @@ -583,7 +584,7 @@ pub(crate) fn make_list_dir_tool() -> RegisteredTool { "required": ["path"] }), }, - executor: Arc::new(|args, ctx| { + executor: Arc::new(|args, ctx| { Box::pin(async move { let path = required_str(&args, "path")?; let depth = optional_usize_arg(&args, "depth")?; @@ -606,103 +607,7 @@ pub(crate) fn make_list_dir_tool() -> RegisteredTool { Ok(lines.join("\n")) }) }), - source: ToolSource::Native, - } -} - -fn format_brave_results(body: &serde_json::Value) -> String { - let results = body - .get("web") - .and_then(|w| w.get("results")) - .and_then(serde_json::Value::as_array); - - let Some(results) = results else { - return "No results found.".to_string(); - }; - - let mut output = String::new(); - for (i, result) in results.iter().enumerate() { - let title = result - .get("title") - .and_then(serde_json::Value::as_str) - .unwrap_or("(no title)"); - let url = result - .get("url") - .and_then(serde_json::Value::as_str) - .unwrap_or("(no url)"); - let description = result - .get("description") - .and_then(serde_json::Value::as_str) - .unwrap_or(""); - let _ = write!( - output, - "{}. {}\n {}\n {}\n\n", - i + 1, - title, - url, - description - ); - } - output -} - -pub(crate) fn make_web_search_tool_with_api_key(api_key: String) -> RegisteredTool { - use std::sync::OnceLock; - static CLIENT: OnceLock = OnceLock::new(); - - RegisteredTool { - definition: ToolDefinition { - name: WEB_SEARCH_TOOL_NAME.into(), - description: "Search the web using Brave Search when current external information is needed. Returns result titles, URLs, and descriptions; use web_fetch for a specific URL.".into(), - parameters: serde_json::json!({ - "type": "object", - "properties": { - "query": {"type": "string", "description": "Search query"}, - "max_results": {"type": "integer", "description": "Maximum number of results (default 5, max 20)"} - }, - "required": ["query"] - }), - }, - executor: Arc::new(move |args, _ctx| { - let api_key = api_key.clone(); - Box::pin(async move { - let query = required_str(&args, "query")?; - let client = CLIENT - .get_or_init(|| { - fabro_http::http_client().expect("Brave Search HTTP client should build") - }) - .clone(); - let count = args - .get("max_results") - .and_then(serde_json::Value::as_u64) - .unwrap_or(5) - .min(20); - - let resp = client - .get("https://api.search.brave.com/res/v1/web/search") - .header("X-Subscription-Token", &api_key) - .header("Accept", "application/json") - .query(&[("q", query), ("count", &count.to_string())]) - .send() - .await - .map_err(|e| format!("HTTP request failed: {e}"))?; - - if !resp.status().is_success() { - return Err(format!( - "Brave Search API returned status {}", - resp.status() - )); - } - - let body: serde_json::Value = resp - .json() - .await - .map_err(|e| format!("Failed to parse response: {e}"))?; - - Ok(format_brave_results(&body)) - }) - }), - source: ToolSource::Native, + source: ToolSource::Native, } } @@ -820,6 +725,7 @@ mod tests { use super::*; use crate::config::{NativeToolOptions, SessionOptions, ToolSecrets}; + use crate::web_search::make_web_search_tool_with_api_key; use crate::event::{Emitter, SessionBoundEmitter}; use crate::local_sandbox::LocalSandbox; use crate::sandbox::*; @@ -917,15 +823,18 @@ mod tests { files, ..Default::default() }); - let result = (tool.executor)(serde_json::json!({"file_path": "/test.txt"}), ToolContext { - env, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: None, - root_session_id: None, - tool_call_id: None, - agent_event_emitter: None, - }) + let result = (tool.executor)( + serde_json::json!({"file_path": "/test.txt"}), + ToolContext { + env, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, + agent_event_emitter: None, + }, + ) .await; assert_eq!(result.unwrap(), "1 | hello\n2 | world\n"); } @@ -942,15 +851,18 @@ mod tests { ..Default::default() }); - let result = (tool.executor)(serde_json::json!({"file_path": "/test.txt"}), ToolContext { - env, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: None, - root_session_id: None, - tool_call_id: None, - agent_event_emitter: None, - }) + let result = (tool.executor)( + serde_json::json!({"file_path": "/test.txt"}), + ToolContext { + env, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, + agent_event_emitter: None, + }, + ) .await .unwrap(); @@ -991,12 +903,12 @@ mod tests { let result = (tool.executor)( serde_json::json!({"file_path": "/out.txt", "content": "hello"}), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: None, - root_session_id: None, - tool_call_id: None, + env: env_clone, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, agent_event_emitter: None, }, ) @@ -1026,12 +938,12 @@ mod tests { "new_string": "goodbye" }), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: None, - root_session_id: None, - tool_call_id: None, + env: env_clone, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, agent_event_emitter: None, }, ) @@ -1121,12 +1033,12 @@ mod tests { "replace_all": true }), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: None, - root_session_id: None, - tool_call_id: None, + env: env_clone, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, agent_event_emitter: None, }, ) @@ -1154,12 +1066,12 @@ mod tests { "new_string": "goodbye" }), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: None, - root_session_id: None, - tool_call_id: None, + env: env_clone, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, agent_event_emitter: None, }, ) @@ -1188,8 +1100,8 @@ mod tests { root_session_id: Some("test-session".to_string()), tool_call_id: Some("call_1".to_string()), agent_event_emitter: Some(Arc::new(SessionBoundEmitter { - emitter: emitter.clone(), - session_id: "test-session".to_string(), + emitter: emitter.clone(), + session_id: "test-session".to_string(), tool_call_id: Some("call_1".to_string()), })), ..shell_context(env) @@ -1218,9 +1130,9 @@ mod tests { async fn shell_success_returns_ok_with_metadata_and_separate_streams() { let tool = make_shell_tool(); let env: Arc = mock_sandbox_with(ExecResult { - stdout: "hello".into(), - stderr: "a warning".into(), - exit_code: Some(0), + stdout: "hello".into(), + stderr: "a warning".into(), + exit_code: Some(0), termination: CommandTermination::Exited, duration_ms: 10, }); @@ -1242,9 +1154,9 @@ mod tests { async fn shell_forwards_command_without_stream_redirection_wrapper() { let tool = make_shell_tool(); let env = mock_sandbox_with(ExecResult { - stdout: String::new(), - stderr: String::new(), - exit_code: Some(0), + stdout: String::new(), + stderr: String::new(), + exit_code: Some(0), termination: CommandTermination::Exited, duration_ms: 1, }); @@ -1270,12 +1182,12 @@ mod tests { let _result = (tool.executor)( serde_json::json!({"command": "sleep 1", "timeout_ms": 5000}), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: None, - root_session_id: None, - tool_call_id: None, + env: env_clone, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, agent_event_emitter: None, }, ) @@ -1288,9 +1200,9 @@ mod tests { let tool = make_shell_tool(); let env: Arc = Arc::new(MockSandbox { exec_result: ExecResult { - stdout: "error".into(), - stderr: String::new(), - exit_code: Some(1), + stdout: "error".into(), + stderr: String::new(), + exit_code: Some(1), termination: CommandTermination::Exited, duration_ms: 10, }, @@ -1309,9 +1221,9 @@ mod tests { async fn shell_timeout_returns_error_with_partial_output() { let tool = make_shell_tool(); let env: Arc = mock_sandbox_with(ExecResult { - stdout: "partial".into(), - stderr: String::new(), - exit_code: None, + stdout: "partial".into(), + stderr: String::new(), + exit_code: None, termination: CommandTermination::TimedOut, duration_ms: 10000, }); @@ -1331,9 +1243,9 @@ mod tests { async fn shell_cancellation_returns_error_with_partial_output() { let tool = make_shell_tool(); let env: Arc = mock_sandbox_with(ExecResult { - stdout: "partial".into(), - stderr: String::new(), - exit_code: None, + stdout: "partial".into(), + stderr: String::new(), + exit_code: None, termination: CommandTermination::Cancelled, duration_ms: 42, }); @@ -1385,9 +1297,9 @@ mod tests { async fn shell_emits_process_event_with_typed_outcome_and_redacted_tails() { let tool = make_shell_tool(); let env: Arc = mock_sandbox_with(ExecResult { - stdout: "out".into(), - stderr: "boom key=AKIAYRWQG5EJLPZLBYNP".into(), - exit_code: Some(7), + stdout: "out".into(), + stderr: "boom key=AKIAYRWQG5EJLPZLBYNP".into(), + exit_code: Some(7), termination: CommandTermination::Exited, duration_ms: 12, }); @@ -1427,9 +1339,9 @@ mod tests { let tool = make_shell_tool(); let env: Arc = Arc::new(MockSandbox { exec_result: ExecResult { - stdout: "interleaved".into(), - stderr: String::new(), - exit_code: Some(0), + stdout: "interleaved".into(), + stderr: String::new(), + exit_code: Some(0), termination: CommandTermination::Exited, duration_ms: 5, }, @@ -1545,12 +1457,12 @@ mod tests { let _result = (tool.executor)( serde_json::json!({"command": "echo $MY_KEY"}), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), - tool_env_provider: Some(Arc::new(crate::StaticEnvProvider(tool_env.clone()))), - session_id: None, - root_session_id: None, - tool_call_id: None, + env: env_clone, + cancel: CancellationToken::new(), + tool_env_provider: Some(Arc::new(crate::StaticEnvProvider(tool_env.clone()))), + session_id: None, + root_session_id: None, + tool_call_id: None, agent_event_emitter: None, }, ) @@ -1593,12 +1505,12 @@ mod tests { let _result = (tool.executor)( serde_json::json!({"command": "echo $GITHUB_TOKEN"}), ToolContext { - env: env.clone(), - cancel: CancellationToken::new(), - tool_env_provider: Some(provider.clone()), - session_id: None, - root_session_id: None, - tool_call_id: None, + env: env.clone(), + cancel: CancellationToken::new(), + tool_env_provider: Some(provider.clone()), + session_id: None, + root_session_id: None, + tool_call_id: None, agent_event_emitter: None, }, ) @@ -1614,12 +1526,12 @@ mod tests { let _result = (tool.executor)( serde_json::json!({"command": "echo $GITHUB_TOKEN"}), ToolContext { - env: env.clone(), - cancel: CancellationToken::new(), - tool_env_provider: Some(provider), - session_id: None, - root_session_id: None, - tool_call_id: None, + env: env.clone(), + cancel: CancellationToken::new(), + tool_env_provider: Some(provider), + session_id: None, + root_session_id: None, + tool_call_id: None, agent_event_emitter: None, }, ) @@ -1669,15 +1581,18 @@ mod tests { ..Default::default() }); - let result = (tool.executor)(serde_json::json!({"file_path": "/test.txt"}), ToolContext { - env, - cancel: CancellationToken::new(), - tool_env_provider: Some(Arc::new(FailingToolEnvProvider)), - session_id: None, - root_session_id: None, - tool_call_id: None, - agent_event_emitter: None, - }) + let result = (tool.executor)( + serde_json::json!({"file_path": "/test.txt"}), + ToolContext { + env, + cancel: CancellationToken::new(), + tool_env_provider: Some(Arc::new(FailingToolEnvProvider)), + session_id: None, + root_session_id: None, + tool_call_id: None, + agent_event_emitter: None, + }, + ) .await; assert_eq!(result.unwrap(), "1 | hello\n"); @@ -1688,15 +1603,18 @@ mod tests { let tool = make_shell_tool(); let env = Arc::new(MockSandbox::default()); let env_clone: Arc = env.clone(); - let _result = (tool.executor)(serde_json::json!({"command": "echo hello"}), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: None, - root_session_id: None, - tool_call_id: None, - agent_event_emitter: None, - }) + let _result = (tool.executor)( + serde_json::json!({"command": "echo hello"}), + ToolContext { + env: env_clone, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, + agent_event_emitter: None, + }, + ) .await; let captured = env.captured_env_vars.lock().unwrap().clone(); assert_eq!(captured, None); @@ -1707,9 +1625,9 @@ mod tests { let tool = make_web_fetch_tool(None); let env = Arc::new(MockSandbox { exec_result: ExecResult { - stdout: "fetched content".into(), - stderr: String::new(), - exit_code: Some(0), + stdout: "fetched content".into(), + stderr: String::new(), + exit_code: Some(0), termination: CommandTermination::Exited, duration_ms: 100, }, @@ -1721,12 +1639,12 @@ mod tests { let _result = (tool.executor)( serde_json::json!({"url": "https://example.com"}), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), - tool_env_provider: Some(Arc::new(crate::StaticEnvProvider(tool_env.clone()))), - session_id: None, - root_session_id: None, - tool_call_id: None, + env: env_clone, + cancel: CancellationToken::new(), + tool_env_provider: Some(Arc::new(crate::StaticEnvProvider(tool_env.clone()))), + session_id: None, + root_session_id: None, + tool_call_id: None, agent_event_emitter: None, }, ) @@ -1745,15 +1663,18 @@ mod tests { ], ..Default::default() }); - let result = (tool.executor)(serde_json::json!({"pattern": "fn"}), ToolContext { - env, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: None, - root_session_id: None, - tool_call_id: None, - agent_event_emitter: None, - }) + let result = (tool.executor)( + serde_json::json!({"pattern": "fn"}), + ToolContext { + env, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, + agent_event_emitter: None, + }, + ) .await; let output = result.unwrap(); assert!(output.contains("src/main.rs:10:fn main()")); @@ -1767,15 +1688,18 @@ mod tests { glob_results: vec!["src/main.rs".into(), "src/lib.rs".into()], ..Default::default() }); - let result = (tool.executor)(serde_json::json!({"pattern": "src/**/*.rs"}), ToolContext { - env, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: None, - root_session_id: None, - tool_call_id: None, - agent_event_emitter: None, - }) + let result = (tool.executor)( + serde_json::json!({"pattern": "src/**/*.rs"}), + ToolContext { + env, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, + agent_event_emitter: None, + }, + ) .await; let output = result.unwrap(); assert!(output.contains("src/main.rs")); @@ -1795,15 +1719,18 @@ mod tests { async fn web_search_missing_query_returns_error() { let tool = make_web_search_tool_with_api_key("fake-key".into()); let env: Arc = Arc::new(MockSandbox::default()); - let result = (tool.executor)(serde_json::json!({}), ToolContext { - env, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: None, - root_session_id: None, - tool_call_id: None, - agent_event_emitter: None, - }) + let result = (tool.executor)( + serde_json::json!({}), + ToolContext { + env, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, + agent_event_emitter: None, + }, + ) .await; let err = result.unwrap_err(); assert!( @@ -1818,6 +1745,7 @@ mod tests { let options = NativeToolOptions { secrets: ToolSecrets { brave_search_api_key: Some("fake-key".to_string()), + ..ToolSecrets::default() }, ..NativeToolOptions::default() }; @@ -1828,15 +1756,18 @@ mod tests { .get("web_search") .expect("web_search should be registered"); let env: Arc = Arc::new(MockSandbox::default()); - let result = (tool.executor)(serde_json::json!({}), ToolContext { - env, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: None, - root_session_id: None, - tool_call_id: None, - agent_event_emitter: None, - }) + let result = (tool.executor)( + serde_json::json!({}), + ToolContext { + env, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, + agent_event_emitter: None, + }, + ) .await; let err = result.unwrap_err(); @@ -1846,37 +1777,14 @@ mod tests { ); } - #[test] - fn format_brave_results_formats_results() { - let body = serde_json::json!({ - "web": { - "results": [ - {"title": "Rust Lang", "url": "https://rust-lang.org", "description": "A systems language"}, - {"title": "Rust Book", "url": "https://doc.rust-lang.org/book", "description": "The Rust book"} - ] - } - }); - let output = format_brave_results(&body); - assert!(output.contains("1. Rust Lang")); - assert!(output.contains("https://rust-lang.org")); - assert!(output.contains("A systems language")); - assert!(output.contains("2. Rust Book")); - } - - #[test] - fn format_brave_results_no_results() { - let body = serde_json::json!({"web": {}}); - assert_eq!(format_brave_results(&body), "No results found."); - } - #[tokio::test] async fn web_fetch_builds_curl_command() { let tool = make_web_fetch_tool(None); let env = Arc::new(MockSandbox { exec_result: ExecResult { - stdout: "

hello

".into(), - stderr: String::new(), - exit_code: Some(0), + stdout: "

hello

".into(), + stderr: String::new(), + exit_code: Some(0), termination: CommandTermination::Exited, duration_ms: 100, }, @@ -1886,12 +1794,12 @@ mod tests { let result = (tool.executor)( serde_json::json!({"url": "https://example.com"}), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: None, - root_session_id: None, - tool_call_id: None, + env: env_clone, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, agent_event_emitter: None, }, ) @@ -1952,12 +1860,12 @@ mod tests { let _result = (tool.executor)( serde_json::json!({"url": "https://example.com", "timeout_ms": 15000}), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: None, - root_session_id: None, - tool_call_id: None, + env: env_clone, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, agent_event_emitter: None, }, ) @@ -1978,12 +1886,12 @@ mod tests { let _result = (tool.executor)( serde_json::json!({"url": "https://example.com", "timeout_ms": 120_000}), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: None, - root_session_id: None, - tool_call_id: None, + env: env_clone, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, agent_event_emitter: None, }, ) @@ -2002,9 +1910,9 @@ mod tests { let tool = make_web_fetch_tool(None); let env: Arc = Arc::new(MockSandbox { exec_result: ExecResult { - stdout: large_content, - stderr: String::new(), - exit_code: Some(0), + stdout: large_content, + stderr: String::new(), + exit_code: Some(0), termination: CommandTermination::Exited, duration_ms: 100, }, @@ -2033,9 +1941,9 @@ mod tests { let tool = make_web_fetch_tool(None); let env: Arc = Arc::new(MockSandbox { exec_result: ExecResult { - stdout: String::new(), - stderr: "curl: (6) Could not resolve host".into(), - exit_code: Some(6), + stdout: String::new(), + stderr: "curl: (6) Could not resolve host".into(), + exit_code: Some(6), termination: CommandTermination::Exited, duration_ms: 100, }, @@ -2077,17 +1985,16 @@ mod tests { client, model_id: ModelHandle::ByName { provider: ProviderId::anthropic(), - model: "mock-model".to_string(), + model: "mock-model".to_string(), }, }; let tool = make_web_fetch_tool(Some(summarizer)); let env: Arc = Arc::new(MockSandbox { exec_result: ExecResult { - stdout: "

Lots of content about Rust...

" - .into(), - stderr: String::new(), - exit_code: Some(0), + stdout: "

Lots of content about Rust...

".into(), + stderr: String::new(), + exit_code: Some(0), termination: CommandTermination::Exited, duration_ms: 100, }, @@ -2118,11 +2025,10 @@ mod tests { let tool = make_web_fetch_tool(None); let env: Arc = Arc::new(MockSandbox { exec_result: ExecResult { - stdout: - "

Rust is a systems programming language.

" - .into(), - stderr: String::new(), - exit_code: Some(0), + stdout: "

Rust is a systems programming language.

" + .into(), + stderr: String::new(), + exit_code: Some(0), termination: CommandTermination::Exited, duration_ms: 100, }, @@ -2162,7 +2068,7 @@ mod tests { // "other_provider" is the default — it rejects all requests. let default_provider: Arc = Arc::new(MockErrorProvider { error: LlmError::Provider { - kind: ProviderErrorKind::NotFound, + kind: ProviderErrorKind::NotFound, detail: Box::new(ProviderErrorDetail::new( "model not found", "other_provider", @@ -2186,16 +2092,16 @@ mod tests { client, model_id: ModelHandle::ByName { provider: ProviderId::anthropic(), - model: "target-model".to_string(), + model: "target-model".to_string(), }, }; let tool = make_web_fetch_tool(Some(summarizer)); let env: Arc = Arc::new(MockSandbox { exec_result: ExecResult { - stdout: "

Page content

".into(), - stderr: String::new(), - exit_code: Some(0), + stdout: "

Page content

".into(), + stderr: String::new(), + exit_code: Some(0), termination: CommandTermination::Exited, duration_ms: 100, }, diff --git a/lib/components/fabro-agent/src/web_search.rs b/lib/components/fabro-agent/src/web_search.rs new file mode 100644 index 000000000..9be79c811 --- /dev/null +++ b/lib/components/fabro-agent/src/web_search.rs @@ -0,0 +1,627 @@ +//! Built-in `web_search` backends. +//! +//! Agents always call the same tool. The HTTP backend is selected by +//! `[server.integrations.search].provider`. + +use std::fmt::Write; +use std::sync::OnceLock; +use std::time::Duration; + +use fabro_llm::types::ToolDefinition; +use fabro_types::settings::{SearchIntegrationSettings, SearchProvider, VeniceSearchEngine}; + +use crate::config::ToolSecrets; +use crate::tool_registry::{RegisteredTool, ToolSource}; +use crate::tools::{WEB_SEARCH_TOOL_NAME, required_str}; + +const BRAVE_SEARCH_URL: &str = "https://api.search.brave.com/res/v1/web/search"; +const VENICE_SEARCH_URL: &str = "https://api.venice.ai/api/v1/augment/search"; +const VENICE_QUERY_MAX_CHARS: usize = 400; +const VENICE_REQUEST_TIMEOUT: Duration = Duration::from_mins(1); +const DEFAULT_MAX_RESULTS: u64 = 5; +const MAX_RESULTS: u64 = 20; + +#[derive(Clone, Debug)] +pub(crate) enum SearchBackend { + Brave { + api_key: String, + search_url: String, + }, + Venice { + api_key: String, + engine: VeniceSearchEngine, + search_url: String, + }, +} + +impl SearchBackend { + #[must_use] + pub(crate) fn from_secrets(secrets: &ToolSecrets) -> Option { + match secrets.search.provider { + SearchProvider::Brave => secrets + .brave_search_api_key + .as_ref() + .map(|api_key| Self::brave(api_key.clone())), + SearchProvider::Venice => secrets + .venice_api_key + .as_ref() + .map(|api_key| Self::venice(api_key.clone(), secrets.search.venice_engine)), + } + } + + #[must_use] + pub(crate) fn brave(api_key: String) -> Self { + Self::Brave { + api_key, + search_url: BRAVE_SEARCH_URL.to_string(), + } + } + + #[must_use] + pub(crate) fn venice(api_key: String, engine: VeniceSearchEngine) -> Self { + Self::Venice { + api_key, + engine, + search_url: VENICE_SEARCH_URL.to_string(), + } + } + + #[must_use] + pub(crate) fn includes_engine_param(&self) -> bool { + matches!(self, Self::Venice { .. }) + } + + async fn search( + &self, + query: &str, + max_results: u64, + engine_override: Option, + ) -> Result { + match self { + Self::Brave { + api_key, + search_url, + } => search_brave(api_key, search_url, query, max_results).await, + Self::Venice { + api_key, + engine, + search_url, + } => { + if query.chars().count() > VENICE_QUERY_MAX_CHARS { + return Err(format!( + "query exceeds Venice Search maximum of {VENICE_QUERY_MAX_CHARS} characters" + )); + } + let engine = engine_override.unwrap_or(*engine); + search_venice(api_key, search_url, query, max_results, engine).await + } + } + } +} + +fn search_http_client() -> fabro_http::HttpClient { + static CLIENT: OnceLock = OnceLock::new(); + CLIENT + .get_or_init(|| { + #[cfg(test)] + { + fabro_http::test_http_client().expect("Search HTTP client should build") + } + #[cfg(not(test))] + { + fabro_http::http_client().expect("Search HTTP client should build") + } + }) + .clone() +} + +async fn search_brave( + api_key: &str, + search_url: &str, + query: &str, + max_results: u64, +) -> Result { + let count = max_results.min(MAX_RESULTS); + let resp = search_http_client() + .get(search_url) + .header("X-Subscription-Token", api_key) + .header("Accept", "application/json") + .query(&[("q", query), ("count", &count.to_string())]) + .send() + .await + .map_err(|e| format!("HTTP request failed: {e}"))?; + + if !resp.status().is_success() { + return Err(format!( + "Brave Search API returned status {}", + resp.status() + )); + } + + let body: serde_json::Value = resp + .json() + .await + .map_err(|e| format!("Failed to parse response: {e}"))?; + Ok(format_brave_results(&body)) +} + +async fn search_venice( + api_key: &str, + search_url: &str, + query: &str, + max_results: u64, + engine: VeniceSearchEngine, +) -> Result { + let limit = max_results.clamp(1, MAX_RESULTS); + let resp = search_http_client() + .post(search_url) + .timeout(VENICE_REQUEST_TIMEOUT) + .bearer_auth(api_key) + .header("Accept", "application/json") + .json(&serde_json::json!({ + "query": query, + "limit": limit, + "search_provider": engine.as_str(), + })) + .send() + .await + .map_err(|e| format!("HTTP request failed: {e}"))?; + + let status = resp.status(); + if !status.is_success() { + return Err(venice_status_error(status.as_u16(), &resp)); + } + + let body: serde_json::Value = resp + .json() + .await + .map_err(|e| format!("Failed to parse response: {e}"))?; + Ok(format_venice_results(&body)) +} + +fn venice_status_error(status: u16, resp: &fabro_http::Response) -> String { + let mut message = format!("Venice Search API returned status {status}"); + if status == 402 { + if let Some(balance) = header_str(resp, "x-venice-balance-usd") { + let _ = write!(message, " (balance USD {balance})"); + } else if let Some(balance) = header_str(resp, "x-venice-balance-diem") { + let _ = write!(message, " (balance DIEM {balance})"); + } + } + message +} + +fn header_str(resp: &fabro_http::Response, name: &str) -> Option { + resp.headers() + .get(name) + .and_then(|value| value.to_str().ok()) + .map(str::to_owned) +} + +fn format_brave_results(body: &serde_json::Value) -> String { + let results = body + .get("web") + .and_then(|w| w.get("results")) + .and_then(serde_json::Value::as_array); + format_hits(results.map(|results| { + results + .iter() + .map(|result| SearchHit { + title: json_str(result, "title"), + url: json_str(result, "url"), + description: json_str(result, "description"), + date: None, + }) + .collect() + })) +} + +fn format_venice_results(body: &serde_json::Value) -> String { + let results = body.get("results").and_then(serde_json::Value::as_array); + format_hits(results.map(|results| { + results + .iter() + .map(|result| SearchHit { + title: json_str(result, "title"), + url: json_str(result, "url"), + description: json_str(result, "content"), + date: optional_json_str(result, "date"), + }) + .collect() + })) +} + +struct SearchHit { + title: String, + url: String, + description: String, + date: Option, +} + +fn format_hits(hits: Option>) -> String { + let Some(hits) = hits.filter(|hits| !hits.is_empty()) else { + return "No results found.".to_string(); + }; + + let mut output = String::new(); + for (i, hit) in hits.iter().enumerate() { + let _ = write!( + output, + "{}. {}\n {}\n {}\n", + i + 1, + hit.title, + hit.url, + hit.description + ); + if let Some(date) = &hit.date { + let _ = writeln!(output, " {date}"); + } + output.push('\n'); + } + output +} + +fn json_str(value: &serde_json::Value, key: &str) -> String { + optional_json_str(value, key).unwrap_or_else(|| match key { + "title" => "(no title)".to_string(), + "url" => "(no url)".to_string(), + _ => String::new(), + }) +} + +fn optional_json_str(value: &serde_json::Value, key: &str) -> Option { + value + .get(key) + .and_then(serde_json::Value::as_str) + .filter(|s| !s.is_empty()) + .map(str::to_owned) +} + +fn parse_engine_arg(args: &serde_json::Value) -> Result, String> { + let Some(value) = args.get("engine").and_then(serde_json::Value::as_str) else { + return Ok(None); + }; + value + .parse() + .map(Some) + .map_err(|_| format!("Invalid engine `{value}`; expected `brave` or `google`")) +} + +fn max_results_arg(args: &serde_json::Value) -> u64 { + args.get("max_results") + .and_then(serde_json::Value::as_u64) + .unwrap_or(DEFAULT_MAX_RESULTS) + .min(MAX_RESULTS) +} + +#[must_use] +pub(crate) fn make_web_search_tool(backend: SearchBackend) -> RegisteredTool { + let mut properties = serde_json::json!({ + "query": {"type": "string", "description": "Search query"}, + "max_results": {"type": "integer", "description": "Maximum number of results (default 5, max 20)"} + }); + if backend.includes_engine_param() { + properties["engine"] = serde_json::json!({ + "type": "string", + "enum": ["brave", "google"], + "description": "Venice search engine. `brave` is ZDR (default); `google` is an anonymized proxy." + }); + } + + RegisteredTool { + definition: ToolDefinition { + name: WEB_SEARCH_TOOL_NAME.into(), + description: "Search the web when current external information is needed. Returns result titles, URLs, and descriptions; use web_fetch for a specific URL.".into(), + parameters: serde_json::json!({ + "type": "object", + "properties": properties, + "required": ["query"] + }), + }, + executor: std::sync::Arc::new(move |args, _ctx| { + let backend = backend.clone(); + Box::pin(async move { + let query = required_str(&args, "query")?; + let engine = parse_engine_arg(&args)?; + backend + .search(query, max_results_arg(&args), engine) + .await + }) + }), + source: ToolSource::Native, + } +} + +#[cfg(test)] +#[must_use] +pub(crate) fn make_web_search_tool_with_api_key(api_key: String) -> RegisteredTool { + make_web_search_tool(SearchBackend::brave(api_key)) +} + +#[must_use] +pub fn search_settings_from_disk() -> SearchIntegrationSettings { + fabro_config::ServerSettingsBuilder::load_default() + .ok() + .map(|settings| settings.server.integrations.search) + .unwrap_or_default() +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use fabro_types::settings::{SearchIntegrationSettings, SearchProvider, VeniceSearchEngine}; + use httpmock::Method::{GET, POST}; + use httpmock::MockServer; + use tokio_util::sync::CancellationToken; + + use super::*; + use crate::config::ToolSecrets; + use crate::sandbox::Sandbox; + use crate::test_support::MockSandbox; + use crate::tool_registry::ToolContext; + + fn secrets(brave: Option<&str>, venice: Option<&str>, provider: SearchProvider) -> ToolSecrets { + ToolSecrets { + brave_search_api_key: brave.map(str::to_string), + venice_api_key: venice.map(str::to_string), + search: SearchIntegrationSettings { + provider, + venice_engine: VeniceSearchEngine::Brave, + }, + } + } + + async fn execute(tool: &RegisteredTool, args: serde_json::Value) -> Result { + let env: Arc = Arc::new(MockSandbox::default()); + (tool.executor)( + args, + ToolContext { + env, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, + agent_event_emitter: None, + }, + ) + .await + } + + #[test] + fn from_secrets_registers_brave_by_default_when_brave_key_is_present() { + let backend = SearchBackend::from_secrets(&secrets( + Some("brave-key"), + Some("venice-key"), + SearchProvider::Brave, + )); + assert!(matches!(backend, Some(SearchBackend::Brave { .. }))); + } + + #[test] + fn from_secrets_omits_brave_when_key_missing() { + assert!( + SearchBackend::from_secrets(&secrets(None, Some("venice-key"), SearchProvider::Brave)) + .is_none() + ); + } + + #[test] + fn from_secrets_registers_venice_when_selected_and_key_present() { + let backend = SearchBackend::from_secrets(&secrets( + Some("brave-key"), + Some("venice-key"), + SearchProvider::Venice, + )); + assert!(matches!(backend, Some(SearchBackend::Venice { .. }))); + } + + #[test] + fn from_secrets_omits_venice_when_key_missing() { + assert!( + SearchBackend::from_secrets(&secrets(Some("brave-key"), None, SearchProvider::Venice)) + .is_none() + ); + } + + #[test] + fn format_brave_results_formats_results() { + let body = serde_json::json!({ + "web": { + "results": [ + {"title": "Rust Lang", "url": "https://rust-lang.org", "description": "A systems language"}, + {"title": "Rust Book", "url": "https://doc.rust-lang.org/book", "description": "The Rust book"} + ] + } + }); + let output = format_brave_results(&body); + assert!(output.contains("1. Rust Lang")); + assert!(output.contains("https://rust-lang.org")); + assert!(output.contains("A systems language")); + assert!(output.contains("2. Rust Book")); + } + + #[test] + fn format_brave_results_no_results() { + let body = serde_json::json!({"web": {}}); + assert_eq!(format_brave_results(&body), "No results found."); + } + + #[test] + fn format_venice_results_includes_date_when_present() { + let body = serde_json::json!({ + "query": "rust", + "results": [ + { + "title": "Rust Lang", + "url": "https://rust-lang.org", + "content": "A systems language", + "date": "2026-01-02" + } + ] + }); + let output = format_venice_results(&body); + assert!(output.contains("1. Rust Lang")); + assert!(output.contains("https://rust-lang.org")); + assert!(output.contains("A systems language")); + assert!(output.contains("2026-01-02")); + } + + #[test] + fn venice_schema_includes_engine_and_brave_schema_does_not() { + let brave = make_web_search_tool(SearchBackend::brave("key".into())); + let venice = make_web_search_tool(SearchBackend::venice( + "key".into(), + VeniceSearchEngine::Brave, + )); + assert!( + brave.definition.parameters["properties"] + .get("engine") + .is_none() + ); + assert!( + venice.definition.parameters["properties"] + .get("engine") + .is_some() + ); + } + + #[tokio::test] + async fn venice_search_posts_augment_search_and_maps_engine() { + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(POST) + .path("/api/v1/augment/search") + .header("authorization", "Bearer venice-key") + .json_body(serde_json::json!({ + "query": "fabro", + "limit": 3, + "search_provider": "google" + })); + then.status(200).json_body(serde_json::json!({ + "query": "fabro", + "results": [{ + "title": "Fabro", + "url": "https://docs.fabro.sh", + "content": "Agent runtime", + "date": "2026-08-21" + }] + })); + }); + + let mut backend = SearchBackend::venice("venice-key".into(), VeniceSearchEngine::Brave); + if let SearchBackend::Venice { search_url, .. } = &mut backend { + *search_url = format!("{}/api/v1/augment/search", server.base_url()); + } + let tool = make_web_search_tool(backend); + let output = execute( + &tool, + serde_json::json!({ + "query": "fabro", + "max_results": 3, + "engine": "google" + }), + ) + .await + .expect("venice search should succeed"); + + mock.assert(); + assert!(output.contains("1. Fabro")); + assert!(output.contains("https://docs.fabro.sh")); + assert!(output.contains("Agent runtime")); + assert!(output.contains("2026-08-21")); + } + + #[tokio::test] + async fn venice_rejects_query_over_400_chars_before_http() { + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(POST).path("/api/v1/augment/search"); + then.status(200) + .json_body(serde_json::json!({"results": []})); + }); + + let mut backend = SearchBackend::venice("venice-key".into(), VeniceSearchEngine::Brave); + if let SearchBackend::Venice { search_url, .. } = &mut backend { + *search_url = format!("{}/api/v1/augment/search", server.base_url()); + } + let tool = make_web_search_tool(backend); + let query = "a".repeat(401); + let err = execute(&tool, serde_json::json!({ "query": query })) + .await + .expect_err("overlong query should fail before HTTP"); + + mock.assert_calls(0); + assert!(err.contains("400")); + } + + #[tokio::test] + async fn venice_maps_401_402_and_429_to_tool_errors() { + async fn assert_status(status: u16, header: Option<(&str, &str)>, expected: &str) { + let server = MockServer::start(); + let mock = match header { + Some((name, value)) => server.mock(|when, then| { + when.method(POST).path("/api/v1/augment/search"); + then.status(status).header(name, value).body("error"); + }), + None => server.mock(|when, then| { + when.method(POST).path("/api/v1/augment/search"); + then.status(status).body("error"); + }), + }; + let mut backend = SearchBackend::venice("venice-key".into(), VeniceSearchEngine::Brave); + if let SearchBackend::Venice { search_url, .. } = &mut backend { + *search_url = format!("{}/api/v1/augment/search", server.base_url()); + } + let tool = make_web_search_tool(backend); + let err = execute(&tool, serde_json::json!({ "query": "fabro" })) + .await + .expect_err("status should become a tool error"); + assert_eq!(err, expected); + mock.assert(); + } + + assert_status(401, None, "Venice Search API returned status 401").await; + assert_status( + 402, + Some(("x-venice-balance-usd", "0.12")), + "Venice Search API returned status 402 (balance USD 0.12)", + ) + .await; + assert_status(429, None, "Venice Search API returned status 429").await; + } + + #[tokio::test] + async fn brave_search_still_uses_get_and_subscription_token() { + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(GET) + .path("/res/v1/web/search") + .header("x-subscription-token", "brave-key") + .query_param("q", "rust") + .query_param("count", "5"); + then.status(200).json_body(serde_json::json!({ + "web": { + "results": [{ + "title": "Rust", + "url": "https://rust-lang.org", + "description": "A language" + }] + } + })); + }); + + let mut backend = SearchBackend::brave("brave-key".into()); + if let SearchBackend::Brave { search_url, .. } = &mut backend { + *search_url = format!("{}/res/v1/web/search", server.base_url()); + } + let tool = make_web_search_tool(backend); + let output = execute(&tool, serde_json::json!({ "query": "rust" })) + .await + .expect("brave search should succeed"); + mock.assert(); + assert!(output.contains("1. Rust")); + assert!(output.contains("A language")); + } +} diff --git a/lib/components/fabro-agent/tests/it/parity_matrix.rs b/lib/components/fabro-agent/tests/it/parity_matrix.rs index 7f64a29fb..486526f07 100644 --- a/lib/components/fabro-agent/tests/it/parity_matrix.rs +++ b/lib/components/fabro-agent/tests/it/parity_matrix.rs @@ -26,22 +26,22 @@ type Provider = ProviderId; #[derive(Clone)] struct OpenAiTwinOptions { base_url: String, - api_key: String, + api_key: String, } fn summarizer_model_id(provider: &Provider) -> ModelHandle { match provider.as_str() { ProviderId::OPENAI | "moonshot" | "zai" | "minimax" | "inception" => ModelHandle::ByName { provider: ProviderId::openai(), - model: "gpt-5.4-mini".to_string(), + model: "gpt-5.4-mini".to_string(), }, ProviderId::GEMINI => ModelHandle::ByName { provider: ProviderId::gemini(), - model: "gemini-3-flash-preview".to_string(), + model: "gemini-3-flash-preview".to_string(), }, ProviderId::ANTHROPIC => ModelHandle::ByName { provider: ProviderId::anthropic(), - model: "claude-haiku-4-5".to_string(), + model: "claude-haiku-4-5".to_string(), }, other => panic!("unexpected provider {other}"), } @@ -49,7 +49,7 @@ fn summarizer_model_id(provider: &Provider) -> ModelHandle { fn build_summarizer(provider: &Provider, client: &Client) -> WebFetchSummarizer { WebFetchSummarizer { - client: client.clone(), + client: client.clone(), model_id: summarizer_model_id(provider), } } @@ -170,12 +170,13 @@ fn make_openai_compatible_twin_session( // twin fixture so the profile can resolve the same OpenAI-compatible // codec that the manually registered adapter uses. let mut settings = LlmCatalogSettings::default(); - settings - .providers - .insert(provider.to_string(), ProviderCatalogSettings { + settings.providers.insert( + provider.to_string(), + ProviderCatalogSettings { enabled: Some(true), ..ProviderCatalogSettings::default() - }); + }, + ); let catalog = Arc::new( Catalog::from_builtin_with_overrides(&settings) .expect("OpenAI-compatible twin catalog should build"), @@ -230,6 +231,7 @@ macro_rules! web_search_provider_test { "BRAVE_SEARCH_API_KEY must be set for web-search tests", ), ), + ..ToolSecrets::default() } ); }; diff --git a/lib/components/fabro-workflow/src/handler/llm/api.rs b/lib/components/fabro-workflow/src/handler/llm/api.rs index 02be6fa2a..d7e44bf96 100644 --- a/lib/components/fabro-workflow/src/handler/llm/api.rs +++ b/lib/components/fabro-workflow/src/handler/llm/api.rs @@ -3921,6 +3921,7 @@ enabled = true // An invalid header value makes a correctly configured executor // fail locally before any request can leave the test process. brave_search_api_key: Some("\n".to_string()), + ..ToolSecrets::default() }); let node = Node::new("search"); let context = Context::new(); diff --git a/lib/components/fabro-workflow/src/pipeline/initialize.rs b/lib/components/fabro-workflow/src/pipeline/initialize.rs index 1c4711b7f..cdb8a6520 100644 --- a/lib/components/fabro-workflow/src/pipeline/initialize.rs +++ b/lib/components/fabro-workflow/src/pipeline/initialize.rs @@ -235,13 +235,11 @@ async fn build_registry( } async fn tool_secrets_from_configured_sources(vault: &Arc>) -> ToolSecrets { - let brave_search_api_key = vault - .read() - .await - .get(EnvVars::BRAVE_SEARCH_API_KEY) - .map(str::to_string); + let vault = vault.read().await; ToolSecrets { - brave_search_api_key, + brave_search_api_key: vault.get(EnvVars::BRAVE_SEARCH_API_KEY).map(str::to_string), + venice_api_key: vault.get(EnvVars::VENICE_API_KEY).map(str::to_string), + search: fabro_agent::search_settings_from_disk(), } } diff --git a/lib/foundation/fabro-api/build.rs b/lib/foundation/fabro-api/build.rs index 0fb3cccd0..5a3ff06ae 100644 --- a/lib/foundation/fabro-api/build.rs +++ b/lib/foundation/fabro-api/build.rs @@ -324,6 +324,21 @@ fn main() { "fabro_types::settings::server::SlackIntegrationSettings", &[], ), + ( + "SearchIntegrationSettings", + "fabro_types::settings::server::SearchIntegrationSettings", + &[], + ), + ( + "SearchProvider", + "fabro_types::settings::server::SearchProvider", + &[], + ), + ( + "VeniceSearchEngine", + "fabro_types::settings::server::VeniceSearchEngine", + &[], + ), ( "IntegrationWebhooksSettings", "fabro_types::settings::server::IntegrationWebhooksSettings", @@ -676,8 +691,11 @@ fn main() { ("AskFabro", "fabro_types::AskFabro", &[]), ("Automation", "fabro_automation::Automation", &[]), ("AutomationRef", "fabro_types::AutomationRef", &[]), - ("AutomationTarget", "fabro_automation::AutomationTarget", &[ - ]), + ( + "AutomationTarget", + "fabro_automation::AutomationTarget", + &[], + ), ( "AutomationTrigger", "fabro_automation::AutomationTrigger", diff --git a/lib/foundation/fabro-api/src/lib.rs b/lib/foundation/fabro-api/src/lib.rs index c53e682c7..97eb3c35b 100644 --- a/lib/foundation/fabro-api/src/lib.rs +++ b/lib/foundation/fabro-api/src/lib.rs @@ -28,11 +28,12 @@ pub mod types { pub use fabro_types::settings::run::{McpHttpProtocol, RunModelControls, RunModelSettings}; pub use fabro_types::settings::server::{ GithubIntegrationSettings, GithubIntegrationStrategy, IntegrationWebhooksSettings, - LogDestination, ObjectStoreSettings, ServerApiSettings, ServerArtifactsSettings, - ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings, ServerIntegrationsSettings, - ServerListenSettings, ServerLoggingSettings, ServerSandboxProviderSettings, - ServerSandboxProvidersSettings, ServerSandboxSettings, ServerSchedulerSettings, - ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings, + LogDestination, ObjectStoreSettings, SearchIntegrationSettings, SearchProvider, + ServerApiSettings, ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod, + ServerAuthSettings, ServerIntegrationsSettings, ServerListenSettings, + ServerLoggingSettings, ServerSandboxProviderSettings, ServerSandboxProvidersSettings, + ServerSandboxSettings, ServerSchedulerSettings, ServerSlateDbSettings, + ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings, VeniceSearchEngine, WebhookStrategy, }; pub use fabro_types::settings::{McpTransport, ServerNamespace}; diff --git a/lib/foundation/fabro-config/src/layers/combine.rs b/lib/foundation/fabro-config/src/layers/combine.rs index aec393777..f3ef27232 100644 --- a/lib/foundation/fabro-config/src/layers/combine.rs +++ b/lib/foundation/fabro-config/src/layers/combine.rs @@ -7,8 +7,8 @@ use fabro_types::settings::run::{ ApprovalMode, EnvironmentNetworkMode, EnvironmentProvider, MergeStrategy, RunMode, }; use fabro_types::settings::server::{ - GithubIntegrationStrategy, LogDestination, ObjectStoreProvider, ServerAuthMethod, - WebhookStrategy, + GithubIntegrationStrategy, LogDestination, ObjectStoreProvider, SearchProvider, + ServerAuthMethod, VeniceSearchEngine, WebhookStrategy, }; use fabro_types::settings::{Duration, InterpString, Size}; @@ -82,7 +82,9 @@ impl_combine_or_option!( GithubIntegrationStrategy, LogDestination, ObjectStoreProvider, + SearchProvider, ServerAuthMethod, + VeniceSearchEngine, WebhookStrategy, LogFilter, AgentProfileKind, diff --git a/lib/foundation/fabro-config/src/layers/mod.rs b/lib/foundation/fabro-config/src/layers/mod.rs index c3fa1632c..253345227 100644 --- a/lib/foundation/fabro-config/src/layers/mod.rs +++ b/lib/foundation/fabro-config/src/layers/mod.rs @@ -39,8 +39,8 @@ pub use run::{ }; pub use server::{ GithubIntegrationLayer, IntegrationWebhooksLayer, ObjectStoreLocalLayer, ObjectStoreS3Layer, - ServerApiLayer, ServerArtifactsLayer, ServerAuthGithubLayer, ServerAuthLayer, - ServerIntegrationsLayer, ServerLayer, ServerListenLayer, ServerLoggingLayer, + SearchIntegrationLayer, ServerApiLayer, ServerArtifactsLayer, ServerAuthGithubLayer, + ServerAuthLayer, ServerIntegrationsLayer, ServerLayer, ServerListenLayer, ServerLoggingLayer, ServerSandboxLayer, ServerSandboxProviderLayer, ServerSandboxProvidersLayer, ServerSchedulerLayer, ServerSlateDbLayer, ServerStorageLayer, ServerWebLayer, SlackIntegrationLayer, diff --git a/lib/foundation/fabro-config/src/layers/server.rs b/lib/foundation/fabro-config/src/layers/server.rs index 351d4da68..7d4a96f5b 100644 --- a/lib/foundation/fabro-config/src/layers/server.rs +++ b/lib/foundation/fabro-config/src/layers/server.rs @@ -1,8 +1,8 @@ //! Sparse `[server]` settings layer definitions. use fabro_types::settings::server::{ - GithubIntegrationStrategy, LogDestination, ObjectStoreProvider, ServerAuthMethod, - WebhookStrategy, + GithubIntegrationStrategy, LogDestination, ObjectStoreProvider, SearchProvider, + ServerAuthMethod, VeniceSearchEngine, WebhookStrategy, }; use fabro_types::settings::{Duration, InterpString}; use serde::{Deserialize, Serialize}; @@ -13,25 +13,25 @@ use super::LogFilter; #[serde(deny_unknown_fields)] pub struct ServerLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub listen: Option, + pub listen: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub api: Option, + pub api: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub web: Option, + pub web: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub auth: Option, + pub auth: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub sandbox: Option, + pub sandbox: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub storage: Option, + pub storage: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub artifacts: Option, + pub artifacts: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub slatedb: Option, + pub slatedb: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub scheduler: Option, + pub scheduler: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub logging: Option, + pub logging: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub integrations: Option, } @@ -67,7 +67,7 @@ pub struct ServerWebLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub enabled: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub url: Option, + pub url: Option, } /// `[server.auth]` — cohesive server auth surface. @@ -81,7 +81,7 @@ pub struct ServerAuthLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub methods: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] - pub github: Option, + pub github: Option, } #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] @@ -103,9 +103,9 @@ pub struct ServerSandboxLayer { #[serde(deny_unknown_fields)] pub struct ServerSandboxProvidersLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub local: Option, + pub local: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub docker: Option, + pub docker: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub daytona: Option, } @@ -132,11 +132,11 @@ pub struct ServerArtifactsLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub prefix: Option, + pub prefix: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub local: Option, + pub local: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub s3: Option, + pub s3: Option, } /// `[server.slatedb]` — SlateDB bottomless storage plus tunables. @@ -144,17 +144,17 @@ pub struct ServerArtifactsLayer { #[serde(deny_unknown_fields)] pub struct ServerSlateDbLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub provider: Option, + pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub prefix: Option, + pub prefix: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub flush_interval: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub local: Option, + pub local: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub s3: Option, + pub s3: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub disk_cache: Option, + pub disk_cache: Option, } #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] @@ -170,11 +170,11 @@ pub struct ObjectStoreLocalLayer { #[serde(deny_unknown_fields)] pub struct ObjectStoreS3Layer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub bucket: Option, + pub bucket: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub region: Option, + pub region: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub endpoint: Option, + pub endpoint: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub path_style: Option, } @@ -192,7 +192,7 @@ pub struct ServerSchedulerLayer { #[serde(deny_unknown_fields)] pub struct ServerLoggingLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub level: Option, + pub level: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub destination: Option, } @@ -205,7 +205,9 @@ pub struct ServerIntegrationsLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub github: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub slack: Option, + pub slack: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub search: Option, } /// `[server.integrations.github]` — GitHub App, credentials, and inbound @@ -214,17 +216,17 @@ pub struct ServerIntegrationsLayer { #[serde(deny_unknown_fields)] pub struct GithubIntegrationLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub enabled: Option, + pub enabled: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub strategy: Option, + pub strategy: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub app_id: Option, + pub app_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub client_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub slug: Option, + pub slug: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub webhooks: Option, + pub webhooks: Option, } /// `[server.integrations.slack]` — Slack workspace credentials and defaults. @@ -232,11 +234,21 @@ pub struct GithubIntegrationLayer { #[serde(deny_unknown_fields)] pub struct SlackIntegrationLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub enabled: Option, + pub enabled: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub default_channel: Option, } +/// `[server.integrations.search]` — backend for the built-in `web_search` tool. +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)] +#[serde(deny_unknown_fields)] +pub struct SearchIntegrationLayer { + #[serde(default, skip_serializing_if = "Option::is_none")] + pub provider: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub venice_engine: Option, +} + #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)] #[serde(deny_unknown_fields)] pub struct IntegrationWebhooksLayer { diff --git a/lib/foundation/fabro-config/src/lib.rs b/lib/foundation/fabro-config/src/lib.rs index f6097b9be..43bc1c5ab 100644 --- a/lib/foundation/fabro-config/src/lib.rs +++ b/lib/foundation/fabro-config/src/lib.rs @@ -53,11 +53,12 @@ pub use layers::{ RunArtifactsLayer, RunCheckpointLayer, RunCloneLayer, RunEnvironmentLayer, RunExecutionLayer, RunGitLayer, RunGoalLayer, RunIntegrationsGithubLayer, RunIntegrationsLayer, RunLayer, RunMetaBranchLayer, RunModelControlsLayer, RunModelLayer, RunPrepareLayer, RunPullRequestLayer, - RunRunBranchLayer, RunScmLayer, ScmGitHubLayer, ServerApiLayer, ServerArtifactsLayer, - ServerAuthGithubLayer, ServerAuthLayer, ServerIntegrationsLayer, ServerLayer, - ServerListenLayer, ServerLoggingLayer, ServerSandboxLayer, ServerSandboxProviderLayer, - ServerSandboxProvidersLayer, ServerSchedulerLayer, ServerSlateDbLayer, ServerStorageLayer, - ServerWebLayer, SettingsLayer, SlackIntegrationLayer, StickyMap, StringOrSplice, WorkflowLayer, + RunRunBranchLayer, RunScmLayer, ScmGitHubLayer, SearchIntegrationLayer, ServerApiLayer, + ServerArtifactsLayer, ServerAuthGithubLayer, ServerAuthLayer, ServerIntegrationsLayer, + ServerLayer, ServerListenLayer, ServerLoggingLayer, ServerSandboxLayer, + ServerSandboxProviderLayer, ServerSandboxProvidersLayer, ServerSchedulerLayer, + ServerSlateDbLayer, ServerStorageLayer, ServerWebLayer, SettingsLayer, SlackIntegrationLayer, + StickyMap, StringOrSplice, WorkflowLayer, }; pub use logging::{resolve_log_destination, resolve_log_destination_with_env}; pub use parse::ParseError; diff --git a/lib/foundation/fabro-config/src/resolve/server.rs b/lib/foundation/fabro-config/src/resolve/server.rs index 39b41eaf9..df6f1b76b 100644 --- a/lib/foundation/fabro-config/src/resolve/server.rs +++ b/lib/foundation/fabro-config/src/resolve/server.rs @@ -2,12 +2,12 @@ use std::path::Path; use fabro_types::settings::server::{ GithubIntegrationSettings, GithubIntegrationStrategy, IntegrationWebhooksSettings, - ObjectStoreProvider, ObjectStoreSettings, ServerApiSettings, ServerArtifactsSettings, - ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings, ServerIntegrationsSettings, - ServerListenSettings, ServerLoggingSettings, ServerNamespace, ServerSandboxProviderSettings, - ServerSandboxProvidersSettings, ServerSandboxSettings, ServerSchedulerSettings, - ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings, - WebhookStrategy, + ObjectStoreProvider, ObjectStoreSettings, SearchIntegrationSettings, ServerApiSettings, + ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings, + ServerIntegrationsSettings, ServerListenSettings, ServerLoggingSettings, ServerNamespace, + ServerSandboxProviderSettings, ServerSandboxProvidersSettings, ServerSandboxSettings, + ServerSchedulerSettings, ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings, + SlackIntegrationSettings, WebhookStrategy, }; use fabro_util::Home; @@ -51,7 +51,7 @@ pub fn resolve_server(layer: &ServerLayer, errors: &mut Vec) -> Se .expect("defaults.toml should provide server.scheduler.max_concurrent_runs"), }, logging: ServerLoggingSettings { - level: layer + level: layer .logging .as_ref() .and_then(|logging| logging.level.as_ref()) @@ -70,10 +70,10 @@ fn resolve_sandbox(layer: Option<&ServerSandboxLayer>) -> ServerSandboxSettings let providers = layer.and_then(|sandbox| sandbox.providers.as_ref()); ServerSandboxSettings { providers: ServerSandboxProvidersSettings { - local: resolve_sandbox_provider( + local: resolve_sandbox_provider( providers.and_then(|providers| providers.local.as_ref()), ), - docker: resolve_sandbox_provider( + docker: resolve_sandbox_provider( providers.and_then(|providers| providers.docker.as_ref()), ), daytona: resolve_sandbox_provider( @@ -150,7 +150,7 @@ fn resolve_auth( let methods = if let Some(mut methods) = layer.and_then(|auth| auth.methods.clone()) { if methods.is_empty() { errors.push(ResolveError::Invalid { - path: "server.auth.methods".to_string(), + path: "server.auth.methods".to_string(), reason: "must not be empty".to_string(), }); } @@ -169,7 +169,7 @@ fn resolve_auth( .unwrap_or_default(); if methods.contains(&ServerAuthMethod::Github) && github.allowed_usernames.is_empty() { errors.push(ResolveError::Invalid { - path: "server.auth.github.allowed_usernames".to_string(), + path: "server.auth.github.allowed_usernames".to_string(), reason: "must not be empty when github auth is enabled".to_string(), }); } @@ -198,7 +198,7 @@ fn validate_github_webhook_strategy( && github.app_id.is_none() { errors.push(ResolveError::Invalid { - path: "server.integrations.github.app_id".to_string(), + path: "server.integrations.github.app_id".to_string(), reason: "must be set when server.integrations.github.webhooks.strategy is configured" .to_string(), }); @@ -208,7 +208,7 @@ fn validate_github_webhook_strategy( && api_layer.and_then(|api| api.url.as_ref()).is_none() { errors.push(ResolveError::Invalid { - path: "server.api.url".to_string(), + path: "server.api.url".to_string(), reason: "must be set when server.integrations.github.webhooks.strategy = \"server_url\"" .to_string(), @@ -348,20 +348,20 @@ fn resolve_integrations(layer: Option<&ServerIntegrationsLayer>) -> ServerIntegr ); warn_if_demoted_template("server.integrations.github.slug", github.slug.as_deref()); GithubIntegrationSettings { - enabled: github.enabled.unwrap_or(true), - strategy: github.strategy.unwrap_or_default(), - app_id: github.app_id.clone(), + enabled: github.enabled.unwrap_or(true), + strategy: github.strategy.unwrap_or_default(), + app_id: github.app_id.clone(), client_id: github.client_id.clone(), - slug: github.slug.clone(), - webhooks: github.webhooks.as_ref().map(resolve_github_webhooks), + slug: github.slug.clone(), + webhooks: github.webhooks.as_ref().map(resolve_github_webhooks), } }) .unwrap_or_default(), - slack: layer + slack: layer .and_then(|integrations| integrations.slack.as_ref()) .map_or( SlackIntegrationSettings { - enabled: false, + enabled: false, default_channel: None, }, |slack| { @@ -370,11 +370,18 @@ fn resolve_integrations(layer: Option<&ServerIntegrationsLayer>) -> ServerIntegr slack.default_channel.as_deref(), ); SlackIntegrationSettings { - enabled: slack.enabled.unwrap_or(true), + enabled: slack.enabled.unwrap_or(true), default_channel: slack.default_channel.clone(), } }, ), + search: layer + .and_then(|integrations| integrations.search.as_ref()) + .map(|search| SearchIntegrationSettings { + provider: search.provider.unwrap_or_default(), + venice_engine: search.venice_engine.unwrap_or_default(), + }) + .unwrap_or_default(), } } diff --git a/lib/foundation/fabro-config/src/tests/resolve_server.rs b/lib/foundation/fabro-config/src/tests/resolve_server.rs index e4eecb353..0e058dd12 100644 --- a/lib/foundation/fabro-config/src/tests/resolve_server.rs +++ b/lib/foundation/fabro-config/src/tests/resolve_server.rs @@ -4,8 +4,8 @@ )] use fabro_types::settings::server::{ - GithubIntegrationStrategy, LogDestination, ObjectStoreSettings, ServerAuthMethod, - ServerListenSettings, ServerNamespace, + GithubIntegrationStrategy, LogDestination, ObjectStoreSettings, SearchProvider, + ServerAuthMethod, ServerListenSettings, ServerNamespace, VeniceSearchEngine, }; use fabro_util::Home; use temp_env::with_var; @@ -127,6 +127,10 @@ fn resolved_server_integrations_disable_slack_when_config_is_absent() { "enabled": false, "default_channel": null, }, + "search": { + "provider": "brave", + "venice_engine": "brave", + }, }) ); } @@ -173,6 +177,43 @@ default_channel = "#releases" ); } +#[test] +fn resolve_search_defaults_to_brave_when_config_is_absent() { + let settings = resolve_server(&parse( + r" +_version = 1 +", + )); + + assert_eq!(settings.integrations.search.provider, SearchProvider::Brave); + assert_eq!( + settings.integrations.search.venice_engine, + VeniceSearchEngine::Brave + ); +} + +#[test] +fn resolve_search_provider_venice_and_google_engine() { + let settings = resolve_server(&parse( + r#" +_version = 1 + +[server.integrations.search] +provider = "venice" +venice_engine = "google" +"#, + )); + + assert_eq!( + settings.integrations.search.provider, + SearchProvider::Venice + ); + assert_eq!( + settings.integrations.search.venice_engine, + VeniceSearchEngine::Google + ); +} + #[test] fn resolve_slack_default_channel_keeps_template_token_literal() { // `server.integrations.slack.default_channel` is a plain literal now: a diff --git a/lib/foundation/fabro-static/src/env_vars.rs b/lib/foundation/fabro-static/src/env_vars.rs index bb8c5d078..477f5c26e 100644 --- a/lib/foundation/fabro-static/src/env_vars.rs +++ b/lib/foundation/fabro-static/src/env_vars.rs @@ -47,6 +47,7 @@ impl EnvVars { pub const ANTHROPIC_BASE_URL: &'static str = "ANTHROPIC_BASE_URL"; pub const BEDROCK_API_KEY: &'static str = "BEDROCK_API_KEY"; pub const BRAVE_SEARCH_API_KEY: &'static str = "BRAVE_SEARCH_API_KEY"; + pub const VENICE_API_KEY: &'static str = "VENICE_API_KEY"; pub const CHATGPT_ACCOUNT_ID: &'static str = "CHATGPT_ACCOUNT_ID"; pub const DEEPSEEK_API_KEY: &'static str = "DEEPSEEK_API_KEY"; pub const FIREWORKS_API_KEY: &'static str = "FIREWORKS_API_KEY"; @@ -199,6 +200,7 @@ mod tests { EnvVars::AWS_BEARER_TOKEN_BEDROCK, EnvVars::BEDROCK_API_KEY, EnvVars::BRAVE_SEARCH_API_KEY, + EnvVars::VENICE_API_KEY, EnvVars::CHATGPT_ACCOUNT_ID, EnvVars::DEEPSEEK_API_KEY, EnvVars::FIREWORKS_API_KEY, diff --git a/lib/foundation/fabro-static/src/secret_registry.rs b/lib/foundation/fabro-static/src/secret_registry.rs index 22cf26177..4fb715b6f 100644 --- a/lib/foundation/fabro-static/src/secret_registry.rs +++ b/lib/foundation/fabro-static/src/secret_registry.rs @@ -19,6 +19,7 @@ const OPTIONAL_VAULT_SECRETS: &[&str] = &[ EnvVars::AWS_BEARER_TOKEN_BEDROCK, EnvVars::BEDROCK_API_KEY, EnvVars::BRAVE_SEARCH_API_KEY, + EnvVars::VENICE_API_KEY, EnvVars::DEEPSEEK_API_KEY, EnvVars::FABRO_SLACK_APP_TOKEN, EnvVars::FABRO_SLACK_BOT_TOKEN, @@ -91,6 +92,7 @@ mod tests { EnvVars::FABRO_SLACK_BOT_TOKEN, EnvVars::DAYTONA_API_KEY, EnvVars::BRAVE_SEARCH_API_KEY, + EnvVars::VENICE_API_KEY, EnvVars::ANTHROPIC_API_KEY, EnvVars::AWS_BEARER_TOKEN_BEDROCK, EnvVars::BEDROCK_API_KEY, diff --git a/lib/foundation/fabro-types/src/settings/mod.rs b/lib/foundation/fabro-types/src/settings/mod.rs index 5bf91e4d9..d7694c6fa 100644 --- a/lib/foundation/fabro-types/src/settings/mod.rs +++ b/lib/foundation/fabro-types/src/settings/mod.rs @@ -46,10 +46,11 @@ pub use run::{ }; pub use server::{ GithubIntegrationSettings, IntegrationWebhooksSettings, LogDestination, ObjectStoreSettings, - ServerApiSettings, ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod, - ServerAuthSettings, ServerIntegrationsSettings, ServerListenSettings, ServerLoggingSettings, - ServerNamespace, ServerSchedulerSettings, ServerSlateDbSettings, ServerStorageSettings, - ServerWebSettings, SlackIntegrationSettings, + SearchIntegrationSettings, SearchProvider, ServerApiSettings, ServerArtifactsSettings, + ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings, ServerIntegrationsSettings, + ServerListenSettings, ServerLoggingSettings, ServerNamespace, ServerSchedulerSettings, + ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings, + VeniceSearchEngine, }; pub use size::{ParseSizeError, Size}; pub use workflow::WorkflowNamespace; diff --git a/lib/foundation/fabro-types/src/settings/server.rs b/lib/foundation/fabro-types/src/settings/server.rs index ef8da68b8..044c1776b 100644 --- a/lib/foundation/fabro-types/src/settings/server.rs +++ b/lib/foundation/fabro-types/src/settings/server.rs @@ -22,16 +22,16 @@ use super::duration::Duration; /// (tests). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerNamespace { - pub listen: ServerListenSettings, - pub api: ServerApiSettings, - pub web: ServerWebSettings, - pub auth: ServerAuthSettings, - pub sandbox: ServerSandboxSettings, - pub storage: ServerStorageSettings, - pub artifacts: ServerArtifactsSettings, - pub slatedb: ServerSlateDbSettings, - pub scheduler: ServerSchedulerSettings, - pub logging: ServerLoggingSettings, + pub listen: ServerListenSettings, + pub api: ServerApiSettings, + pub web: ServerWebSettings, + pub auth: ServerAuthSettings, + pub sandbox: ServerSandboxSettings, + pub storage: ServerStorageSettings, + pub artifacts: ServerArtifactsSettings, + pub slatedb: ServerSlateDbSettings, + pub scheduler: ServerSchedulerSettings, + pub logging: ServerLoggingSettings, pub integrations: ServerIntegrationsSettings, } @@ -43,16 +43,16 @@ impl ServerNamespace { #[must_use] pub fn test_default() -> Self { Self { - listen: ServerListenSettings::default(), - api: ServerApiSettings::default(), - web: ServerWebSettings::default(), - auth: ServerAuthSettings::default(), - sandbox: ServerSandboxSettings::default(), - storage: ServerStorageSettings::default(), - artifacts: ServerArtifactsSettings::default(), - slatedb: ServerSlateDbSettings::default(), - scheduler: ServerSchedulerSettings::default(), - logging: ServerLoggingSettings::default(), + listen: ServerListenSettings::default(), + api: ServerApiSettings::default(), + web: ServerWebSettings::default(), + auth: ServerAuthSettings::default(), + sandbox: ServerSandboxSettings::default(), + storage: ServerStorageSettings::default(), + artifacts: ServerArtifactsSettings::default(), + slatedb: ServerSlateDbSettings::default(), + scheduler: ServerSchedulerSettings::default(), + logging: ServerLoggingSettings::default(), integrations: ServerIntegrationsSettings::default(), } } @@ -89,13 +89,13 @@ pub struct ServerApiSettings { #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerWebSettings { pub enabled: bool, - pub url: String, + pub url: String, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerAuthSettings { pub methods: Vec, - pub github: ServerAuthGithubSettings, + pub github: ServerAuthGithubSettings, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -117,8 +117,8 @@ pub struct ServerSandboxSettings { #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerSandboxProvidersSettings { - pub local: ServerSandboxProviderSettings, - pub docker: ServerSandboxProviderSettings, + pub local: ServerSandboxProviderSettings, + pub docker: ServerSandboxProviderSettings, pub daytona: ServerSandboxProviderSettings, } @@ -158,28 +158,28 @@ pub struct ServerStorageSettings { #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerArtifactsSettings { pub prefix: String, - pub store: ObjectStoreSettings, + pub store: ObjectStoreSettings, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerSlateDbSettings { - pub prefix: String, - pub store: ObjectStoreSettings, + pub prefix: String, + pub store: ObjectStoreSettings, #[serde( serialize_with = "serialize_std_duration", deserialize_with = "deserialize_std_duration" )] pub flush_interval: StdDuration, - pub disk_cache: bool, + pub disk_cache: bool, } impl Default for ServerSlateDbSettings { fn default() -> Self { Self { - prefix: String::new(), - store: ObjectStoreSettings::default(), + prefix: String::new(), + store: ObjectStoreSettings::default(), flush_interval: StdDuration::ZERO, - disk_cache: false, + disk_cache: false, } } } @@ -191,9 +191,9 @@ pub enum ObjectStoreSettings { root: String, }, S3 { - bucket: String, - region: String, - endpoint: Option, + bucket: String, + region: String, + endpoint: Option, path_style: bool, }, } @@ -233,7 +233,7 @@ pub enum LogDestination { #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerLoggingSettings { - pub level: Option, + pub level: Option, #[serde(default)] pub destination: LogDestination, } @@ -241,34 +241,104 @@ pub struct ServerLoggingSettings { #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerIntegrationsSettings { pub github: GithubIntegrationSettings, - pub slack: SlackIntegrationSettings, + pub slack: SlackIntegrationSettings, + pub search: SearchIntegrationSettings, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct GithubIntegrationSettings { - pub enabled: bool, - pub strategy: GithubIntegrationStrategy, - pub app_id: Option, + pub enabled: bool, + pub strategy: GithubIntegrationStrategy, + pub app_id: Option, pub client_id: Option, - pub slug: Option, - pub webhooks: Option, + pub slug: Option, + pub webhooks: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SlackIntegrationSettings { - pub enabled: bool, + pub enabled: bool, pub default_channel: Option, } impl Default for SlackIntegrationSettings { fn default() -> Self { Self { - enabled: true, + enabled: true, default_channel: None, } } } +/// Backend used by the built-in `web_search` tool. +#[derive( + Debug, + Clone, + Copy, + Default, + PartialEq, + Eq, + Serialize, + Deserialize, + strum::EnumString, + strum::IntoStaticStr, +)] +#[serde(rename_all = "lowercase")] +#[strum(serialize_all = "lowercase")] +pub enum SearchProvider { + #[default] + Brave, + Venice, +} + +impl SearchProvider { + #[must_use] + pub const fn as_str(self) -> &'static str { + match self { + Self::Brave => "brave", + Self::Venice => "venice", + } + } +} + +/// Venice `/augment/search` engine. `brave` is Firecrawl ZDR; `google` is an +/// anonymized proxy. Direct Brave Search remains [`SearchProvider::Brave`]. +#[derive( + Debug, + Clone, + Copy, + Default, + PartialEq, + Eq, + Serialize, + Deserialize, + strum::EnumString, + strum::IntoStaticStr, +)] +#[serde(rename_all = "lowercase")] +#[strum(serialize_all = "lowercase")] +pub enum VeniceSearchEngine { + #[default] + Brave, + Google, +} + +impl VeniceSearchEngine { + #[must_use] + pub const fn as_str(self) -> &'static str { + match self { + Self::Brave => "brave", + Self::Google => "google", + } + } +} + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct SearchIntegrationSettings { + pub provider: SearchProvider, + pub venice_engine: VeniceSearchEngine, +} + #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct IntegrationWebhooksSettings { pub strategy: Option, diff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES index 4f8d5f871..42ca0bf8f 100644 --- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES +++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES @@ -441,6 +441,8 @@ models/saved-query.ts models/secret-list-response.ts models/secret-metadata.ts models/secret-type.ts +models/search-integration-settings.ts +models/search-provider.ts models/server-api-settings.ts models/server-artifacts-settings.ts models/server-auth-github-settings.ts @@ -532,6 +534,7 @@ models/user-response.ts models/validate-response.ts models/variable-list-response.ts models/variable.ts +models/venice-search-engine.ts models/vnc-preview-response.ts models/webhook-strategy.ts models/workflow-detail-response.ts diff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts index 04df237be..12d3fa017 100644 --- a/lib/packages/fabro-api-client/src/models/index.ts +++ b/lib/packages/fabro-api-client/src/models/index.ts @@ -410,6 +410,8 @@ export * from './saved-query'; export * from './secret-list-response'; export * from './secret-metadata'; export * from './secret-type'; +export * from './search-integration-settings'; +export * from './search-provider'; export * from './server-api-settings'; export * from './server-artifacts-settings'; export * from './server-auth-github-settings'; @@ -501,6 +503,7 @@ export * from './user-response'; export * from './validate-response'; export * from './variable'; export * from './variable-list-response'; +export * from './venice-search-engine'; export * from './vnc-preview-response'; export * from './webhook-strategy'; export * from './workflow-detail-response'; diff --git a/lib/packages/fabro-api-client/src/models/search-integration-settings.ts b/lib/packages/fabro-api-client/src/models/search-integration-settings.ts new file mode 100644 index 000000000..65a6902fe --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/search-integration-settings.ts @@ -0,0 +1,26 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.2.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { SearchProvider } from './search-provider'; +// May contain unused imports in some cases +// @ts-ignore +import type { VeniceSearchEngine } from './venice-search-engine'; + +export interface SearchIntegrationSettings { + 'provider': SearchProvider; + 'venice_engine': VeniceSearchEngine; +} diff --git a/lib/packages/fabro-api-client/src/models/search-provider.ts b/lib/packages/fabro-api-client/src/models/search-provider.ts new file mode 100644 index 000000000..552ccbe54 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/search-provider.ts @@ -0,0 +1,23 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.2.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + + +export const SearchProvider = { + BRAVE: 'brave', + VENICE: 'venice' +} as const; + +export type SearchProvider = typeof SearchProvider[keyof typeof SearchProvider]; diff --git a/lib/packages/fabro-api-client/src/models/server-integrations-settings.ts b/lib/packages/fabro-api-client/src/models/server-integrations-settings.ts index 88b8fdd8d..3525f9c5a 100644 --- a/lib/packages/fabro-api-client/src/models/server-integrations-settings.ts +++ b/lib/packages/fabro-api-client/src/models/server-integrations-settings.ts @@ -18,9 +18,13 @@ import type { GithubIntegrationSettings } from './github-integration-settings'; // May contain unused imports in some cases // @ts-ignore +import type { SearchIntegrationSettings } from './search-integration-settings'; +// May contain unused imports in some cases +// @ts-ignore import type { SlackIntegrationSettings } from './slack-integration-settings'; export interface ServerIntegrationsSettings { 'github': GithubIntegrationSettings; 'slack': SlackIntegrationSettings; + 'search': SearchIntegrationSettings; } diff --git a/lib/packages/fabro-api-client/src/models/venice-search-engine.ts b/lib/packages/fabro-api-client/src/models/venice-search-engine.ts new file mode 100644 index 000000000..29e7494cf --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/venice-search-engine.ts @@ -0,0 +1,23 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.2.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + + +export const VeniceSearchEngine = { + BRAVE: 'brave', + GOOGLE: 'google' +} as const; + +export type VeniceSearchEngine = typeof VeniceSearchEngine[keyof typeof VeniceSearchEngine]; From cfa8ae92c0e9c5493ccfa8d46f0ce8bce32111cc Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 21 Aug 2026 19:34:38 -0400 Subject: [PATCH 2/3] style: apply pinned rustfmt --- lib/apps/fabro-server/src/diagnostics.rs | 273 ++++++----- lib/components/fabro-agent/src/cli.rs | 4 +- lib/components/fabro-agent/src/config.rs | 21 +- .../fabro-agent/src/profiles/claude5.rs | 25 +- .../fabro-agent/src/profiles/claude5_tools.rs | 75 ++- lib/components/fabro-agent/src/tools.rs | 432 +++++++++--------- lib/components/fabro-agent/src/web_search.rs | 49 +- .../fabro-agent/tests/it/parity_matrix.rs | 19 +- .../fabro-workflow/src/pipeline/initialize.rs | 4 +- lib/foundation/fabro-api/build.rs | 7 +- .../fabro-config/src/layers/server.rs | 68 +-- .../fabro-config/src/resolve/server.rs | 32 +- .../fabro-types/src/settings/server.rs | 88 ++-- 13 files changed, 526 insertions(+), 571 deletions(-) diff --git a/lib/apps/fabro-server/src/diagnostics.rs b/lib/apps/fabro-server/src/diagnostics.rs index ac161a1b0..ab1a87bd0 100644 --- a/lib/apps/fabro-server/src/diagnostics.rs +++ b/lib/apps/fabro-server/src/diagnostics.rs @@ -12,9 +12,8 @@ use fabro_model::{Catalog, ProviderId}; use fabro_redact::redact_string; use fabro_sandbox::{DockerSandboxProvider, daytona}; use fabro_static::EnvVars; -use fabro_types::settings::SearchProvider; -use fabro_types::settings::ServerAuthMethod; use fabro_types::settings::server::GithubIntegrationStrategy; +use fabro_types::settings::{SearchProvider, ServerAuthMethod}; use fabro_util::check_report::{CheckDetail, CheckResult, CheckSection, CheckStatus}; use fabro_util::dev_token::validate_dev_token_format; use fabro_util::session_secret; @@ -44,21 +43,21 @@ fn http_client_or_check( #[derive(Debug, Serialize)] pub struct DiagnosticsReport { - pub version: String, + pub version: String, pub sections: Vec, } #[derive(Debug, Clone, Serialize)] pub(crate) struct ProviderProbeReport { - pub data: Vec, + pub data: Vec, pub summary: ProviderProbeSummary, } #[derive(Debug, Clone, Serialize)] pub(crate) struct ProviderProbeResult { - pub provider: ProviderId, - pub model_id: Option, - pub status: ProviderProbeStatus, + pub provider: ProviderId, + pub model_id: Option, + pub status: ProviderProbeStatus, pub error_message: Option, #[serde(skip)] diagnostic_detail: Option, @@ -67,7 +66,7 @@ pub(crate) struct ProviderProbeResult { #[derive(Debug, Clone, Serialize)] pub(crate) struct ProviderProbeSummary { pub status: ProviderProbeStatus, - pub total: u32, + pub total: u32, pub passed: u32, pub failed: u32, } @@ -105,14 +104,14 @@ pub async fn run_all(state: &AppState) -> DiagnosticsReport { ); DiagnosticsReport { - version: FABRO_VERSION.to_string(), + version: FABRO_VERSION.to_string(), sections: vec![ CheckSection { - title: "Credentials".to_string(), + title: "Credentials".to_string(), checks: vec![llm, github, docker_sandbox, cloud_sandbox, web_search], }, CheckSection { - title: "Configuration".to_string(), + title: "Configuration".to_string(), checks: vec![crypto, check_storage_dir(state)], }, ], @@ -124,20 +123,20 @@ async fn check_llm_providers(state: &AppState) -> CheckResult { Ok(report) => report, Err(err) => { return CheckResult { - name: "LLM Providers".to_string(), - status: CheckStatus::Error, - summary: "failed to initialize".to_string(), - details: vec![CheckDetail::new(format!("{err:#}"))], + name: "LLM Providers".to_string(), + status: CheckStatus::Error, + summary: "failed to initialize".to_string(), + details: vec![CheckDetail::new(format!("{err:#}"))], remediation: Some("Check configured provider credentials".to_string()), }; } }; if report.data.is_empty() { return CheckResult { - name: "LLM Providers".to_string(), - status: CheckStatus::Error, - summary: "none configured".to_string(), - details: Vec::new(), + name: "LLM Providers".to_string(), + status: CheckStatus::Error, + summary: "none configured".to_string(), + details: Vec::new(), remediation: Some("Set at least one provider API key".to_string()), }; } @@ -159,7 +158,7 @@ async fn check_llm_providers(state: &AppState) -> CheckResult { .clone() .unwrap_or_else(|| format!("{}: {message}", result.provider)); failures.push(ProviderFailure { - provider: result.provider.to_string(), + provider: result.provider.to_string(), summary_line: short_error_line(message), }); details.push(CheckDetail::new(detail)); @@ -198,7 +197,7 @@ async fn check_llm_providers(state: &AppState) -> CheckResult { } struct ProviderFailure { - provider: String, + provider: String, summary_line: String, } @@ -355,10 +354,10 @@ async fn check_github_app(state: &AppState) -> CheckResult { Ok(token) => token.to_string(), Err(err) => { return CheckResult { - name: "GitHub Token".to_string(), - status: CheckStatus::Error, - summary: "token expired".to_string(), - details: vec![CheckDetail::new(err.to_string())], + name: "GitHub Token".to_string(), + status: CheckStatus::Error, + summary: "token expired".to_string(), + details: vec![CheckDetail::new(err.to_string())], remediation: Some( "Run fabro install or run `fabro secret set GITHUB_TOKEN`" .to_string(), @@ -370,10 +369,10 @@ async fn check_github_app(state: &AppState) -> CheckResult { Ok(Some(_)) => unreachable!("token strategy should not return app credentials"), Ok(None) => { return CheckResult { - name: "GitHub Token".to_string(), - status: CheckStatus::Warning, - summary: "not configured".to_string(), - details: Vec::new(), + name: "GitHub Token".to_string(), + status: CheckStatus::Warning, + summary: "not configured".to_string(), + details: Vec::new(), remediation: Some( "Run fabro install or run `fabro secret set GITHUB_TOKEN`".to_string(), ), @@ -382,10 +381,10 @@ async fn check_github_app(state: &AppState) -> CheckResult { Err(err) => { let rendered = format!("{err:#}"); return CheckResult { - name: "GitHub Token".to_string(), - status: CheckStatus::Error, - summary: "missing token".to_string(), - details: vec![CheckDetail::new(rendered.clone())], + name: "GitHub Token".to_string(), + status: CheckStatus::Error, + summary: "missing token".to_string(), + details: vec![CheckDetail::new(rendered.clone())], remediation: Some(rendered), }; } @@ -407,18 +406,18 @@ async fn check_github_app(state: &AppState) -> CheckResult { return match probe { Ok(Ok(response)) if response.status().is_success() => CheckResult { - name: "GitHub Token".to_string(), - status: CheckStatus::Pass, - summary: "configured".to_string(), - details: Vec::new(), + name: "GitHub Token".to_string(), + status: CheckStatus::Pass, + summary: "configured".to_string(), + details: Vec::new(), remediation: None, }, Ok(Ok(response)) if response.status() == fabro_http::StatusCode::UNAUTHORIZED => { CheckResult { - name: "GitHub Token".to_string(), - status: CheckStatus::Error, - summary: "token invalid".to_string(), - details: vec![CheckDetail::new(format!( + name: "GitHub Token".to_string(), + status: CheckStatus::Error, + summary: "token invalid".to_string(), + details: vec![CheckDetail::new(format!( "GitHub returned {}", response.status() ))], @@ -428,10 +427,10 @@ async fn check_github_app(state: &AppState) -> CheckResult { } } Ok(Ok(response)) => CheckResult { - name: "GitHub Token".to_string(), - status: CheckStatus::Error, - summary: "connectivity error".to_string(), - details: vec![CheckDetail::new(format!( + name: "GitHub Token".to_string(), + status: CheckStatus::Error, + summary: "connectivity error".to_string(), + details: vec![CheckDetail::new(format!( "GitHub returned {}", response.status() ))], @@ -440,19 +439,19 @@ async fn check_github_app(state: &AppState) -> CheckResult { ), }, Ok(Err(err)) => CheckResult { - name: "GitHub Token".to_string(), - status: CheckStatus::Error, - summary: "connectivity error".to_string(), - details: vec![CheckDetail::new(err.to_string())], + name: "GitHub Token".to_string(), + status: CheckStatus::Error, + summary: "connectivity error".to_string(), + details: vec![CheckDetail::new(err.to_string())], remediation: Some( "Check GitHub connectivity and the vault GITHUB_TOKEN".to_string(), ), }, Err(_) => CheckResult { - name: "GitHub Token".to_string(), - status: CheckStatus::Error, - summary: "timeout".to_string(), - details: vec![CheckDetail::new("GitHub probe timed out".to_string())], + name: "GitHub Token".to_string(), + status: CheckStatus::Error, + summary: "timeout".to_string(), + details: vec![CheckDetail::new("GitHub probe timed out".to_string())], remediation: Some( "Check GitHub connectivity and the vault GITHUB_TOKEN".to_string(), ), @@ -486,20 +485,20 @@ async fn check_github_app(state: &AppState) -> CheckResult { && !webhook_secret { return CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Warning, - summary: "not configured".to_string(), - details: Vec::new(), + name: "GitHub App".to_string(), + status: CheckStatus::Warning, + summary: "not configured".to_string(), + details: Vec::new(), remediation: Some("Configure GitHub App settings and secrets".to_string()), }; } let Some(app_id) = app_id else { return CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Error, - summary: "missing app_id".to_string(), - details: Vec::new(), + name: "GitHub App".to_string(), + status: CheckStatus::Error, + summary: "missing app_id".to_string(), + details: Vec::new(), remediation: Some( "Set [server.integrations.github].app_id in settings.toml".to_string(), ), @@ -507,10 +506,10 @@ async fn check_github_app(state: &AppState) -> CheckResult { }; let Some(private_key_raw) = private_key_raw else { return CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Error, - summary: "missing private key".to_string(), - details: Vec::new(), + name: "GitHub App".to_string(), + status: CheckStatus::Error, + summary: "missing private key".to_string(), + details: Vec::new(), remediation: Some("Run `fabro secret set GITHUB_APP_PRIVATE_KEY`".to_string()), }; }; @@ -519,10 +518,10 @@ async fn check_github_app(state: &AppState) -> CheckResult { Ok(value) => value, Err(err) => { return CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Error, - summary: "private key invalid".to_string(), - details: vec![CheckDetail::new(err.clone())], + name: "GitHub App".to_string(), + status: CheckStatus::Error, + summary: "private key invalid".to_string(), + details: vec![CheckDetail::new(err.clone())], remediation: Some(err), }; } @@ -532,10 +531,10 @@ async fn check_github_app(state: &AppState) -> CheckResult { Ok(jwt) => jwt, Err(err) => { return CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Error, - summary: "JWT signing failed".to_string(), - details: vec![CheckDetail::new(format!("{err:#}"))], + name: "GitHub App".to_string(), + status: CheckStatus::Error, + summary: "JWT signing failed".to_string(), + details: vec![CheckDetail::new(format!("{err:#}"))], remediation: Some(err.to_string()), }; } @@ -552,24 +551,24 @@ async fn check_github_app(state: &AppState) -> CheckResult { .await; match auth_result { Ok(Ok(_app)) => CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Pass, - summary: slug.unwrap_or_else(|| "configured".to_string()), - details: Vec::new(), + name: "GitHub App".to_string(), + status: CheckStatus::Pass, + summary: slug.unwrap_or_else(|| "configured".to_string()), + details: Vec::new(), remediation: None, }, Ok(Err(err)) => CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Error, - summary: "connectivity error".to_string(), - details: vec![CheckDetail::new(format!("{err:#}"))], + name: "GitHub App".to_string(), + status: CheckStatus::Error, + summary: "connectivity error".to_string(), + details: vec![CheckDetail::new(format!("{err:#}"))], remediation: Some("Check GitHub App credentials and network connectivity".to_string()), }, Err(_) => CheckResult { - name: "GitHub App".to_string(), - status: CheckStatus::Error, - summary: "timeout".to_string(), - details: vec![CheckDetail::new("GitHub probe timed out".to_string())], + name: "GitHub App".to_string(), + status: CheckStatus::Error, + summary: "timeout".to_string(), + details: vec![CheckDetail::new("GitHub probe timed out".to_string())], remediation: Some("Check GitHub connectivity and credentials".to_string()), }, } @@ -605,10 +604,10 @@ where { if !enabled { return CheckResult { - name: "Docker Sandbox".to_string(), - status: CheckStatus::Pass, - summary: "disabled".to_string(), - details: vec![CheckDetail::new( + name: "Docker Sandbox".to_string(), + status: CheckStatus::Pass, + summary: "disabled".to_string(), + details: vec![CheckDetail::new( "server.sandbox.providers.docker.enabled = false".to_string(), )], remediation: None, @@ -653,10 +652,10 @@ async fn check_cloud_sandbox(state: &AppState) -> CheckResult { }; let Some(api_key) = api_key else { return CheckResult { - name: "Cloud Sandbox".to_string(), - status: CheckStatus::Warning, - summary: "recommended, not configured".to_string(), - details: Vec::new(), + name: "Cloud Sandbox".to_string(), + status: CheckStatus::Warning, + summary: "recommended, not configured".to_string(), + details: Vec::new(), remediation: Some( "Run `fabro secret set DAYTONA_API_KEY` to enable cloud sandbox execution" .to_string(), @@ -673,17 +672,17 @@ async fn check_cloud_sandbox(state: &AppState) -> CheckResult { fn cloud_sandbox_probe_check(probe: anyhow::Result) -> CheckResult { match probe { Ok(check) if check.ok() => CheckResult { - name: "Cloud Sandbox".to_string(), - status: CheckStatus::Pass, - summary: format!("Daytona configured ({})", check.key_name), - details: Vec::new(), + name: "Cloud Sandbox".to_string(), + status: CheckStatus::Pass, + summary: format!("Daytona configured ({})", check.key_name), + details: Vec::new(), remediation: None, }, Ok(check) => CheckResult { - name: "Cloud Sandbox".to_string(), - status: CheckStatus::Error, - summary: "Daytona API key is missing required scopes".to_string(), - details: vec![CheckDetail::new(format!( + name: "Cloud Sandbox".to_string(), + status: CheckStatus::Error, + summary: "Daytona API key is missing required scopes".to_string(), + details: vec![CheckDetail::new(format!( "missing: {}", check.missing_display() ))], @@ -696,10 +695,10 @@ fn cloud_sandbox_probe_check(probe: anyhow::Result) -> Err(err) => { if let Some(timeout) = err.downcast_ref::() { return CheckResult { - name: "Cloud Sandbox".to_string(), - status: CheckStatus::Error, - summary: format!("timeout ({:?})", timeout.timeout()), - details: vec![CheckDetail::new("Daytona probe timed out".to_string())], + name: "Cloud Sandbox".to_string(), + status: CheckStatus::Error, + summary: format!("timeout ({:?})", timeout.timeout()), + details: vec![CheckDetail::new("Daytona probe timed out".to_string())], remediation: Some( "Verify DAYTONA_API_KEY value and Daytona reachability".to_string(), ), @@ -707,10 +706,10 @@ fn cloud_sandbox_probe_check(probe: anyhow::Result) -> } CheckResult { - name: "Cloud Sandbox".to_string(), - status: CheckStatus::Error, - summary: "Daytona credential rejected".to_string(), - details: vec![CheckDetail::new(format!("{err:#}"))], + name: "Cloud Sandbox".to_string(), + status: CheckStatus::Error, + summary: "Daytona credential rejected".to_string(), + details: vec![CheckDetail::new(format!("{err:#}"))], remediation: Some( "Verify DAYTONA_API_KEY value and Daytona reachability".to_string(), ), @@ -781,10 +780,10 @@ async fn check_brave_search(state: &AppState) -> CheckResult { }; let Some(api_key) = api_key else { return CheckResult { - name: WEB_SEARCH_CHECK_NAME.to_string(), - status: CheckStatus::Warning, - summary: "brave: optional, not configured".to_string(), - details: Vec::new(), + name: WEB_SEARCH_CHECK_NAME.to_string(), + status: CheckStatus::Warning, + summary: "brave: optional, not configured".to_string(), + details: Vec::new(), remediation: Some( "Run `fabro secret set BRAVE_SEARCH_API_KEY` to enable web search".to_string(), ), @@ -816,10 +815,10 @@ async fn check_venice_search(state: &AppState) -> CheckResult { }; let Some(api_key) = api_key else { return CheckResult { - name: WEB_SEARCH_CHECK_NAME.to_string(), - status: CheckStatus::Warning, - summary: "venice: optional, not configured".to_string(), - details: Vec::new(), + name: WEB_SEARCH_CHECK_NAME.to_string(), + status: CheckStatus::Warning, + summary: "venice: optional, not configured".to_string(), + details: Vec::new(), remediation: Some( "Run `fabro secret set VENICE_API_KEY` to enable web search".to_string(), ), @@ -851,31 +850,31 @@ fn match_web_search_probe( ) -> CheckResult { match probe { Ok(Ok(response)) if response.status().is_success() => CheckResult { - name: WEB_SEARCH_CHECK_NAME.to_string(), - status: CheckStatus::Pass, - summary: format!("{provider}: configured and reachable"), - details: Vec::new(), + name: WEB_SEARCH_CHECK_NAME.to_string(), + status: CheckStatus::Pass, + summary: format!("{provider}: configured and reachable"), + details: Vec::new(), remediation: None, }, Ok(Ok(response)) => CheckResult { - name: WEB_SEARCH_CHECK_NAME.to_string(), - status: CheckStatus::Warning, - summary: format!("{provider}: HTTP {}", response.status()), - details: Vec::new(), + name: WEB_SEARCH_CHECK_NAME.to_string(), + status: CheckStatus::Warning, + summary: format!("{provider}: HTTP {}", response.status()), + details: Vec::new(), remediation: Some(format!("Check {secret_name} and network connectivity")), }, Ok(Err(err)) => CheckResult { - name: WEB_SEARCH_CHECK_NAME.to_string(), - status: CheckStatus::Warning, - summary: format!("{provider}: connectivity error"), - details: vec![CheckDetail::new(format!("{err:#}"))], + name: WEB_SEARCH_CHECK_NAME.to_string(), + status: CheckStatus::Warning, + summary: format!("{provider}: connectivity error"), + details: vec![CheckDetail::new(format!("{err:#}"))], remediation: Some(format!("Check {secret_name} and network connectivity")), }, Err(_) => CheckResult { - name: WEB_SEARCH_CHECK_NAME.to_string(), - status: CheckStatus::Warning, - summary: format!("{provider}: timeout"), - details: vec![CheckDetail::new(format!( + name: WEB_SEARCH_CHECK_NAME.to_string(), + status: CheckStatus::Warning, + summary: format!("{provider}: timeout"), + details: vec![CheckDetail::new(format!( "Web Search ({provider}) probe timed out" ))], remediation: Some(format!("Check {secret_name} and network connectivity")), @@ -956,10 +955,10 @@ async fn diagnostic_secret( name: &str, ) -> Result, CheckResult> { state.vault_secret(name).await.map_err(|err| CheckResult { - name: check_name.to_string(), - status: CheckStatus::Error, - summary: "secret store unavailable".to_string(), - details: vec![CheckDetail::new(err.to_string())], + name: check_name.to_string(), + status: CheckStatus::Error, + summary: "secret store unavailable".to_string(), + details: vec![CheckDetail::new(err.to_string())], remediation: Some("Check the Fabro database and retry".to_string()), }) } diff --git a/lib/components/fabro-agent/src/cli.rs b/lib/components/fabro-agent/src/cli.rs index e16d8fa2f..63c485f20 100644 --- a/lib/components/fabro-agent/src/cli.rs +++ b/lib/components/fabro-agent/src/cli.rs @@ -44,8 +44,8 @@ use crate::{ fn cli_tool_secrets() -> ToolSecrets { ToolSecrets { brave_search_api_key: std::env::var(EnvVars::BRAVE_SEARCH_API_KEY).ok(), - venice_api_key: std::env::var(EnvVars::VENICE_API_KEY).ok(), - search: search_settings_from_disk(), + venice_api_key: std::env::var(EnvVars::VENICE_API_KEY).ok(), + search: search_settings_from_disk(), } } diff --git a/lib/components/fabro-agent/src/config.rs b/lib/components/fabro-agent/src/config.rs index 44dce5af0..44a9db968 100644 --- a/lib/components/fabro-agent/src/config.rs +++ b/lib/components/fabro-agent/src/config.rs @@ -104,8 +104,8 @@ impl ToolHookCallback for ToolApprovalAdapter { #[derive(Clone, Default, PartialEq, Eq)] pub struct ToolSecrets { pub brave_search_api_key: Option, - pub venice_api_key: Option, - pub search: SearchIntegrationSettings, + pub venice_api_key: Option, + pub search: SearchIntegrationSettings, } impl std::fmt::Debug for ToolSecrets { @@ -125,8 +125,8 @@ impl std::fmt::Debug for ToolSecrets { #[derive(Clone, Debug, PartialEq, Eq)] pub struct NativeToolOptions { pub default_command_timeout_ms: u64, - pub max_command_timeout_ms: u64, - pub secrets: ToolSecrets, + pub max_command_timeout_ms: u64, + pub secrets: ToolSecrets, } impl NativeToolOptions { @@ -157,8 +157,8 @@ impl Default for NativeToolOptions { fn default() -> Self { Self { default_command_timeout_ms: 10_000, - max_command_timeout_ms: 600_000, - secrets: ToolSecrets::default(), + max_command_timeout_ms: 600_000, + secrets: ToolSecrets::default(), } } } @@ -447,12 +447,9 @@ mod tests { let approval: ToolApprovalFn = Arc::new(|_name, _args| Err("denied".to_string())); let adapter = ToolApprovalAdapter(approval); let decision = adapter.pre_tool_use("shell", &serde_json::json!({})).await; - assert_eq!( - decision, - ToolHookDecision::Block { - reason: "denied".to_string(), - } - ); + assert_eq!(decision, ToolHookDecision::Block { + reason: "denied".to_string(), + }); } #[tokio::test] diff --git a/lib/components/fabro-agent/src/profiles/claude5.rs b/lib/components/fabro-agent/src/profiles/claude5.rs index 97d35a187..97cdeff1d 100644 --- a/lib/components/fabro-agent/src/profiles/claude5.rs +++ b/lib/components/fabro-agent/src/profiles/claude5.rs @@ -174,20 +174,17 @@ mod tests { let profile = Claude5Profile::new("claude-sonnet-5"); let mut names = profile.tool_registry().names(); names.sort(); - assert_eq!( - names, - vec![ - "Bash", - "Edit", - "Read", - "TaskCreate", - "TaskGet", - "TaskList", - "TaskUpdate", - "WebFetch", - "Write", - ] - ); + assert_eq!(names, vec![ + "Bash", + "Edit", + "Read", + "TaskCreate", + "TaskGet", + "TaskList", + "TaskUpdate", + "WebFetch", + "Write", + ]); assert!(!names.iter().any(|name| name == "Grep" || name == "Glob")); } diff --git a/lib/components/fabro-agent/src/profiles/claude5_tools.rs b/lib/components/fabro-agent/src/profiles/claude5_tools.rs index 9366194cd..140e8502a 100644 --- a/lib/components/fabro-agent/src/profiles/claude5_tools.rs +++ b/lib/components/fabro-agent/src/profiles/claude5_tools.rs @@ -99,7 +99,7 @@ pub(crate) fn make_bash_tool(options: &NativeToolOptions) -> RegisteredTool { "additionalProperties": false }), ), - executor: Arc::new(move |args, ctx| { + executor: Arc::new(move |args, ctx| { Box::pin(async move { let command = tools::required_str(&args, "command")?; let timeout_ms = args @@ -110,7 +110,7 @@ pub(crate) fn make_bash_tool(options: &NativeToolOptions) -> RegisteredTool { tools::run_shell_command(&ctx, command, timeout_ms, None).await }) }), - source: ToolSource::Native, + source: ToolSource::Native, } } @@ -221,7 +221,7 @@ pub(crate) fn make_agent_tool( "additionalProperties": false }), ), - executor: Arc::new(move |args, ctx| { + executor: Arc::new(move |args, ctx| { let supervisor = supervisor.clone(); let session_factory = session_factory.clone(); Box::pin(async move { @@ -259,7 +259,7 @@ pub(crate) fn make_agent_tool( } }) }), - source: ToolSource::Native, + source: ToolSource::Native, } } @@ -330,7 +330,7 @@ pub(crate) fn make_task_output_tool(supervisor: SubAgentSupervisor) -> Registere "additionalProperties": false }), ), - executor: Arc::new(move |args, ctx| { + executor: Arc::new(move |args, ctx| { let supervisor = supervisor.clone(); Box::pin(async move { let task_id = tools::required_str(&args, "task_id")?; @@ -384,7 +384,7 @@ pub(crate) fn make_task_output_tool(supervisor: SubAgentSupervisor) -> Registere } }) }), - source: ToolSource::Native, + source: ToolSource::Native, } } @@ -406,7 +406,7 @@ pub(crate) fn make_task_stop_tool(supervisor: SubAgentSupervisor) -> RegisteredT "additionalProperties": false }), ), - executor: Arc::new(move |args, _ctx| { + executor: Arc::new(move |args, _ctx| { let supervisor = supervisor.clone(); Box::pin(async move { let task_id = tools::required_str(&args, "task_id")?; @@ -417,7 +417,7 @@ pub(crate) fn make_task_stop_tool(supervisor: SubAgentSupervisor) -> RegisteredT Ok(format!("Agent {task_id} stopped.")) }) }), - source: ToolSource::Native, + source: ToolSource::Native, } } @@ -448,7 +448,7 @@ pub(crate) fn make_send_message_tool(supervisor: SubAgentSupervisor) -> Register "additionalProperties": false }), ), - executor: Arc::new(move |args, _ctx| { + executor: Arc::new(move |args, _ctx| { let supervisor = supervisor.clone(); Box::pin(async move { let recipient = tools::required_str(&args, "to")?; @@ -459,7 +459,7 @@ pub(crate) fn make_send_message_tool(supervisor: SubAgentSupervisor) -> Register Ok(format!("Message sent to agent {recipient}.")) }) }), - source: ToolSource::Native, + source: ToolSource::Native, } } @@ -468,6 +468,7 @@ mod tests { use std::collections::BTreeSet; use std::sync::Mutex; + use fabro_types::settings::VeniceSearchEngine; use serde_json::json; use tokio_util::sync::CancellationToken; @@ -478,7 +479,6 @@ mod tests { use crate::todo_tools::{ make_task_create_tool, make_task_get_tool, make_task_list_tool, make_task_update_tool, }; - use fabro_types::settings::VeniceSearchEngine; fn property_names(tool: &RegisteredTool) -> BTreeSet<&str> { tool.definition.parameters["properties"] @@ -513,12 +513,12 @@ mod tests { fn context() -> ToolContext { ToolContext { - env: Arc::new(MockSandbox::default()) as Arc, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: Some("root".to_string()), - root_session_id: Some("root".to_string()), - tool_call_id: Some("call".to_string()), + env: Arc::new(MockSandbox::default()) as Arc, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: Some("root".to_string()), + root_session_id: Some("root".to_string()), + tool_call_id: Some("call".to_string()), agent_event_emitter: None, } } @@ -526,16 +526,13 @@ mod tests { #[test] fn core_adapter_schemas_match_the_claude5_contract() { let options = NativeToolOptions::for_profile(fabro_model::AgentProfileKind::Claude5); - assert_schema( - &make_read_tool(), - &["file_path", "limit", "offset"], - &["file_path"], - ); - assert_schema( - &make_write_tool(), - &["content", "file_path"], - &["content", "file_path"], - ); + assert_schema(&make_read_tool(), &["file_path", "limit", "offset"], &[ + "file_path", + ]); + assert_schema(&make_write_tool(), &["content", "file_path"], &[ + "content", + "file_path", + ]); assert_schema( &make_edit_tool(), &["file_path", "new_string", "old_string", "replace_all"], @@ -547,11 +544,9 @@ mod tests { bash.definition.parameters["properties"]["timeout"]["maximum"], 600_000 ); - assert_schema( - &make_web_fetch_tool(None), - &["prompt", "url"], - &["prompt", "url"], - ); + assert_schema(&make_web_fetch_tool(None), &["prompt", "url"], &[ + "prompt", "url", + ]); assert_schema( &make_web_search_tool(SearchBackend::brave("key".to_string())), &["query"], @@ -613,17 +608,13 @@ mod tests { &["block", "task_id", "timeout"], &["block", "task_id", "timeout"], ); - assert_schema( - &make_task_stop_tool(supervisor.clone()), - &["task_id"], - &["task_id"], - ); + assert_schema(&make_task_stop_tool(supervisor.clone()), &["task_id"], &[ + "task_id", + ]); let send_message = make_send_message_tool(supervisor); - assert_schema( - &send_message, - &["message", "summary", "to"], - &["message", "to"], - ); + assert_schema(&send_message, &["message", "summary", "to"], &[ + "message", "to", + ]); assert!( send_message .definition diff --git a/lib/components/fabro-agent/src/tools.rs b/lib/components/fabro-agent/src/tools.rs index b6515a4a5..9cbeb571b 100644 --- a/lib/components/fabro-agent/src/tools.rs +++ b/lib/components/fabro-agent/src/tools.rs @@ -12,9 +12,9 @@ use tokio::task; use crate::config::NativeToolOptions; use crate::sandbox::{ExecStreamingResult, GrepOptions}; -use crate::web_search::{SearchBackend, make_web_search_tool}; use crate::tool_registry::{RegisteredTool, ToolContext, ToolRegistry, ToolSource}; use crate::types::AgentEvent; +use crate::web_search::{SearchBackend, make_web_search_tool}; const MAX_WEB_FETCH_BYTES: usize = 100 * 1024; const MAX_READ_MANY_FILES_CONCURRENCY: usize = 8; @@ -23,7 +23,7 @@ pub(crate) const DEFAULT_READ_LINES: usize = 2000; /// Configuration for the optional LLM-based summarizer used by `web_fetch`. #[derive(Clone)] pub struct WebFetchSummarizer { - pub client: Client, + pub client: Client, pub model_id: ModelHandle, } @@ -512,9 +512,9 @@ pub fn make_glob_tool() -> RegisteredTool { pub(crate) fn make_read_many_files_tool() -> RegisteredTool { RegisteredTool { definition: ToolDefinition { - name: "read_many_files".into(), + name: "read_many_files".into(), description: "Read multiple files at once".into(), - parameters: serde_json::json!({ + parameters: serde_json::json!({ "type": "object", "properties": { "paths": { @@ -526,7 +526,7 @@ pub(crate) fn make_read_many_files_tool() -> RegisteredTool { "required": ["paths"] }), }, - executor: Arc::new(|args, ctx| { + executor: Arc::new(|args, ctx| { Box::pin(async move { let paths: Vec = args["paths"] .as_array() @@ -565,7 +565,7 @@ pub(crate) fn make_read_many_files_tool() -> RegisteredTool { Ok(output) }) }), - source: ToolSource::Native, + source: ToolSource::Native, } } @@ -573,9 +573,9 @@ pub(crate) fn make_read_many_files_tool() -> RegisteredTool { pub(crate) fn make_list_dir_tool() -> RegisteredTool { RegisteredTool { definition: ToolDefinition { - name: "list_dir".into(), + name: "list_dir".into(), description: "List directory contents with depth control".into(), - parameters: serde_json::json!({ + parameters: serde_json::json!({ "type": "object", "properties": { "path": {"type": "string", "description": "Directory path to list"}, @@ -584,7 +584,7 @@ pub(crate) fn make_list_dir_tool() -> RegisteredTool { "required": ["path"] }), }, - executor: Arc::new(|args, ctx| { + executor: Arc::new(|args, ctx| { Box::pin(async move { let path = required_str(&args, "path")?; let depth = optional_usize_arg(&args, "depth")?; @@ -607,7 +607,7 @@ pub(crate) fn make_list_dir_tool() -> RegisteredTool { Ok(lines.join("\n")) }) }), - source: ToolSource::Native, + source: ToolSource::Native, } } @@ -725,7 +725,6 @@ mod tests { use super::*; use crate::config::{NativeToolOptions, SessionOptions, ToolSecrets}; - use crate::web_search::make_web_search_tool_with_api_key; use crate::event::{Emitter, SessionBoundEmitter}; use crate::local_sandbox::LocalSandbox; use crate::sandbox::*; @@ -733,6 +732,7 @@ mod tests { use crate::tool_registry::ToolContext; use crate::truncation; use crate::types::SessionEvent; + use crate::web_search::make_web_search_tool_with_api_key; #[test] fn core_tool_descriptions_include_actionable_guidance() { @@ -823,18 +823,15 @@ mod tests { files, ..Default::default() }); - let result = (tool.executor)( - serde_json::json!({"file_path": "/test.txt"}), - ToolContext { - env, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: None, - root_session_id: None, - tool_call_id: None, - agent_event_emitter: None, - }, - ) + let result = (tool.executor)(serde_json::json!({"file_path": "/test.txt"}), ToolContext { + env, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, + agent_event_emitter: None, + }) .await; assert_eq!(result.unwrap(), "1 | hello\n2 | world\n"); } @@ -851,18 +848,15 @@ mod tests { ..Default::default() }); - let result = (tool.executor)( - serde_json::json!({"file_path": "/test.txt"}), - ToolContext { - env, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: None, - root_session_id: None, - tool_call_id: None, - agent_event_emitter: None, - }, - ) + let result = (tool.executor)(serde_json::json!({"file_path": "/test.txt"}), ToolContext { + env, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, + agent_event_emitter: None, + }) .await .unwrap(); @@ -903,12 +897,12 @@ mod tests { let result = (tool.executor)( serde_json::json!({"file_path": "/out.txt", "content": "hello"}), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: None, - root_session_id: None, - tool_call_id: None, + env: env_clone, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, agent_event_emitter: None, }, ) @@ -938,12 +932,12 @@ mod tests { "new_string": "goodbye" }), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: None, - root_session_id: None, - tool_call_id: None, + env: env_clone, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, agent_event_emitter: None, }, ) @@ -1033,12 +1027,12 @@ mod tests { "replace_all": true }), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: None, - root_session_id: None, - tool_call_id: None, + env: env_clone, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, agent_event_emitter: None, }, ) @@ -1066,12 +1060,12 @@ mod tests { "new_string": "goodbye" }), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: None, - root_session_id: None, - tool_call_id: None, + env: env_clone, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, agent_event_emitter: None, }, ) @@ -1100,8 +1094,8 @@ mod tests { root_session_id: Some("test-session".to_string()), tool_call_id: Some("call_1".to_string()), agent_event_emitter: Some(Arc::new(SessionBoundEmitter { - emitter: emitter.clone(), - session_id: "test-session".to_string(), + emitter: emitter.clone(), + session_id: "test-session".to_string(), tool_call_id: Some("call_1".to_string()), })), ..shell_context(env) @@ -1130,9 +1124,9 @@ mod tests { async fn shell_success_returns_ok_with_metadata_and_separate_streams() { let tool = make_shell_tool(); let env: Arc = mock_sandbox_with(ExecResult { - stdout: "hello".into(), - stderr: "a warning".into(), - exit_code: Some(0), + stdout: "hello".into(), + stderr: "a warning".into(), + exit_code: Some(0), termination: CommandTermination::Exited, duration_ms: 10, }); @@ -1154,9 +1148,9 @@ mod tests { async fn shell_forwards_command_without_stream_redirection_wrapper() { let tool = make_shell_tool(); let env = mock_sandbox_with(ExecResult { - stdout: String::new(), - stderr: String::new(), - exit_code: Some(0), + stdout: String::new(), + stderr: String::new(), + exit_code: Some(0), termination: CommandTermination::Exited, duration_ms: 1, }); @@ -1182,12 +1176,12 @@ mod tests { let _result = (tool.executor)( serde_json::json!({"command": "sleep 1", "timeout_ms": 5000}), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: None, - root_session_id: None, - tool_call_id: None, + env: env_clone, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, agent_event_emitter: None, }, ) @@ -1200,9 +1194,9 @@ mod tests { let tool = make_shell_tool(); let env: Arc = Arc::new(MockSandbox { exec_result: ExecResult { - stdout: "error".into(), - stderr: String::new(), - exit_code: Some(1), + stdout: "error".into(), + stderr: String::new(), + exit_code: Some(1), termination: CommandTermination::Exited, duration_ms: 10, }, @@ -1221,9 +1215,9 @@ mod tests { async fn shell_timeout_returns_error_with_partial_output() { let tool = make_shell_tool(); let env: Arc = mock_sandbox_with(ExecResult { - stdout: "partial".into(), - stderr: String::new(), - exit_code: None, + stdout: "partial".into(), + stderr: String::new(), + exit_code: None, termination: CommandTermination::TimedOut, duration_ms: 10000, }); @@ -1243,9 +1237,9 @@ mod tests { async fn shell_cancellation_returns_error_with_partial_output() { let tool = make_shell_tool(); let env: Arc = mock_sandbox_with(ExecResult { - stdout: "partial".into(), - stderr: String::new(), - exit_code: None, + stdout: "partial".into(), + stderr: String::new(), + exit_code: None, termination: CommandTermination::Cancelled, duration_ms: 42, }); @@ -1297,9 +1291,9 @@ mod tests { async fn shell_emits_process_event_with_typed_outcome_and_redacted_tails() { let tool = make_shell_tool(); let env: Arc = mock_sandbox_with(ExecResult { - stdout: "out".into(), - stderr: "boom key=AKIAYRWQG5EJLPZLBYNP".into(), - exit_code: Some(7), + stdout: "out".into(), + stderr: "boom key=AKIAYRWQG5EJLPZLBYNP".into(), + exit_code: Some(7), termination: CommandTermination::Exited, duration_ms: 12, }); @@ -1339,9 +1333,9 @@ mod tests { let tool = make_shell_tool(); let env: Arc = Arc::new(MockSandbox { exec_result: ExecResult { - stdout: "interleaved".into(), - stderr: String::new(), - exit_code: Some(0), + stdout: "interleaved".into(), + stderr: String::new(), + exit_code: Some(0), termination: CommandTermination::Exited, duration_ms: 5, }, @@ -1457,12 +1451,12 @@ mod tests { let _result = (tool.executor)( serde_json::json!({"command": "echo $MY_KEY"}), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), - tool_env_provider: Some(Arc::new(crate::StaticEnvProvider(tool_env.clone()))), - session_id: None, - root_session_id: None, - tool_call_id: None, + env: env_clone, + cancel: CancellationToken::new(), + tool_env_provider: Some(Arc::new(crate::StaticEnvProvider(tool_env.clone()))), + session_id: None, + root_session_id: None, + tool_call_id: None, agent_event_emitter: None, }, ) @@ -1505,12 +1499,12 @@ mod tests { let _result = (tool.executor)( serde_json::json!({"command": "echo $GITHUB_TOKEN"}), ToolContext { - env: env.clone(), - cancel: CancellationToken::new(), - tool_env_provider: Some(provider.clone()), - session_id: None, - root_session_id: None, - tool_call_id: None, + env: env.clone(), + cancel: CancellationToken::new(), + tool_env_provider: Some(provider.clone()), + session_id: None, + root_session_id: None, + tool_call_id: None, agent_event_emitter: None, }, ) @@ -1526,12 +1520,12 @@ mod tests { let _result = (tool.executor)( serde_json::json!({"command": "echo $GITHUB_TOKEN"}), ToolContext { - env: env.clone(), - cancel: CancellationToken::new(), - tool_env_provider: Some(provider), - session_id: None, - root_session_id: None, - tool_call_id: None, + env: env.clone(), + cancel: CancellationToken::new(), + tool_env_provider: Some(provider), + session_id: None, + root_session_id: None, + tool_call_id: None, agent_event_emitter: None, }, ) @@ -1581,18 +1575,15 @@ mod tests { ..Default::default() }); - let result = (tool.executor)( - serde_json::json!({"file_path": "/test.txt"}), - ToolContext { - env, - cancel: CancellationToken::new(), - tool_env_provider: Some(Arc::new(FailingToolEnvProvider)), - session_id: None, - root_session_id: None, - tool_call_id: None, - agent_event_emitter: None, - }, - ) + let result = (tool.executor)(serde_json::json!({"file_path": "/test.txt"}), ToolContext { + env, + cancel: CancellationToken::new(), + tool_env_provider: Some(Arc::new(FailingToolEnvProvider)), + session_id: None, + root_session_id: None, + tool_call_id: None, + agent_event_emitter: None, + }) .await; assert_eq!(result.unwrap(), "1 | hello\n"); @@ -1603,18 +1594,15 @@ mod tests { let tool = make_shell_tool(); let env = Arc::new(MockSandbox::default()); let env_clone: Arc = env.clone(); - let _result = (tool.executor)( - serde_json::json!({"command": "echo hello"}), - ToolContext { - env: env_clone, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: None, - root_session_id: None, - tool_call_id: None, - agent_event_emitter: None, - }, - ) + let _result = (tool.executor)(serde_json::json!({"command": "echo hello"}), ToolContext { + env: env_clone, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, + agent_event_emitter: None, + }) .await; let captured = env.captured_env_vars.lock().unwrap().clone(); assert_eq!(captured, None); @@ -1625,9 +1613,9 @@ mod tests { let tool = make_web_fetch_tool(None); let env = Arc::new(MockSandbox { exec_result: ExecResult { - stdout: "fetched content".into(), - stderr: String::new(), - exit_code: Some(0), + stdout: "fetched content".into(), + stderr: String::new(), + exit_code: Some(0), termination: CommandTermination::Exited, duration_ms: 100, }, @@ -1639,12 +1627,12 @@ mod tests { let _result = (tool.executor)( serde_json::json!({"url": "https://example.com"}), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), - tool_env_provider: Some(Arc::new(crate::StaticEnvProvider(tool_env.clone()))), - session_id: None, - root_session_id: None, - tool_call_id: None, + env: env_clone, + cancel: CancellationToken::new(), + tool_env_provider: Some(Arc::new(crate::StaticEnvProvider(tool_env.clone()))), + session_id: None, + root_session_id: None, + tool_call_id: None, agent_event_emitter: None, }, ) @@ -1663,18 +1651,15 @@ mod tests { ], ..Default::default() }); - let result = (tool.executor)( - serde_json::json!({"pattern": "fn"}), - ToolContext { - env, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: None, - root_session_id: None, - tool_call_id: None, - agent_event_emitter: None, - }, - ) + let result = (tool.executor)(serde_json::json!({"pattern": "fn"}), ToolContext { + env, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, + agent_event_emitter: None, + }) .await; let output = result.unwrap(); assert!(output.contains("src/main.rs:10:fn main()")); @@ -1688,18 +1673,15 @@ mod tests { glob_results: vec!["src/main.rs".into(), "src/lib.rs".into()], ..Default::default() }); - let result = (tool.executor)( - serde_json::json!({"pattern": "src/**/*.rs"}), - ToolContext { - env, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: None, - root_session_id: None, - tool_call_id: None, - agent_event_emitter: None, - }, - ) + let result = (tool.executor)(serde_json::json!({"pattern": "src/**/*.rs"}), ToolContext { + env, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, + agent_event_emitter: None, + }) .await; let output = result.unwrap(); assert!(output.contains("src/main.rs")); @@ -1719,18 +1701,15 @@ mod tests { async fn web_search_missing_query_returns_error() { let tool = make_web_search_tool_with_api_key("fake-key".into()); let env: Arc = Arc::new(MockSandbox::default()); - let result = (tool.executor)( - serde_json::json!({}), - ToolContext { - env, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: None, - root_session_id: None, - tool_call_id: None, - agent_event_emitter: None, - }, - ) + let result = (tool.executor)(serde_json::json!({}), ToolContext { + env, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, + agent_event_emitter: None, + }) .await; let err = result.unwrap_err(); assert!( @@ -1756,18 +1735,15 @@ mod tests { .get("web_search") .expect("web_search should be registered"); let env: Arc = Arc::new(MockSandbox::default()); - let result = (tool.executor)( - serde_json::json!({}), - ToolContext { - env, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: None, - root_session_id: None, - tool_call_id: None, - agent_event_emitter: None, - }, - ) + let result = (tool.executor)(serde_json::json!({}), ToolContext { + env, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, + agent_event_emitter: None, + }) .await; let err = result.unwrap_err(); @@ -1782,9 +1758,9 @@ mod tests { let tool = make_web_fetch_tool(None); let env = Arc::new(MockSandbox { exec_result: ExecResult { - stdout: "

hello

".into(), - stderr: String::new(), - exit_code: Some(0), + stdout: "

hello

".into(), + stderr: String::new(), + exit_code: Some(0), termination: CommandTermination::Exited, duration_ms: 100, }, @@ -1794,12 +1770,12 @@ mod tests { let result = (tool.executor)( serde_json::json!({"url": "https://example.com"}), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: None, - root_session_id: None, - tool_call_id: None, + env: env_clone, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, agent_event_emitter: None, }, ) @@ -1860,12 +1836,12 @@ mod tests { let _result = (tool.executor)( serde_json::json!({"url": "https://example.com", "timeout_ms": 15000}), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: None, - root_session_id: None, - tool_call_id: None, + env: env_clone, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, agent_event_emitter: None, }, ) @@ -1886,12 +1862,12 @@ mod tests { let _result = (tool.executor)( serde_json::json!({"url": "https://example.com", "timeout_ms": 120_000}), ToolContext { - env: env_clone, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: None, - root_session_id: None, - tool_call_id: None, + env: env_clone, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, agent_event_emitter: None, }, ) @@ -1910,9 +1886,9 @@ mod tests { let tool = make_web_fetch_tool(None); let env: Arc = Arc::new(MockSandbox { exec_result: ExecResult { - stdout: large_content, - stderr: String::new(), - exit_code: Some(0), + stdout: large_content, + stderr: String::new(), + exit_code: Some(0), termination: CommandTermination::Exited, duration_ms: 100, }, @@ -1941,9 +1917,9 @@ mod tests { let tool = make_web_fetch_tool(None); let env: Arc = Arc::new(MockSandbox { exec_result: ExecResult { - stdout: String::new(), - stderr: "curl: (6) Could not resolve host".into(), - exit_code: Some(6), + stdout: String::new(), + stderr: "curl: (6) Could not resolve host".into(), + exit_code: Some(6), termination: CommandTermination::Exited, duration_ms: 100, }, @@ -1985,16 +1961,17 @@ mod tests { client, model_id: ModelHandle::ByName { provider: ProviderId::anthropic(), - model: "mock-model".to_string(), + model: "mock-model".to_string(), }, }; let tool = make_web_fetch_tool(Some(summarizer)); let env: Arc = Arc::new(MockSandbox { exec_result: ExecResult { - stdout: "

Lots of content about Rust...

".into(), - stderr: String::new(), - exit_code: Some(0), + stdout: "

Lots of content about Rust...

" + .into(), + stderr: String::new(), + exit_code: Some(0), termination: CommandTermination::Exited, duration_ms: 100, }, @@ -2025,10 +2002,11 @@ mod tests { let tool = make_web_fetch_tool(None); let env: Arc = Arc::new(MockSandbox { exec_result: ExecResult { - stdout: "

Rust is a systems programming language.

" - .into(), - stderr: String::new(), - exit_code: Some(0), + stdout: + "

Rust is a systems programming language.

" + .into(), + stderr: String::new(), + exit_code: Some(0), termination: CommandTermination::Exited, duration_ms: 100, }, @@ -2068,7 +2046,7 @@ mod tests { // "other_provider" is the default — it rejects all requests. let default_provider: Arc = Arc::new(MockErrorProvider { error: LlmError::Provider { - kind: ProviderErrorKind::NotFound, + kind: ProviderErrorKind::NotFound, detail: Box::new(ProviderErrorDetail::new( "model not found", "other_provider", @@ -2092,16 +2070,16 @@ mod tests { client, model_id: ModelHandle::ByName { provider: ProviderId::anthropic(), - model: "target-model".to_string(), + model: "target-model".to_string(), }, }; let tool = make_web_fetch_tool(Some(summarizer)); let env: Arc = Arc::new(MockSandbox { exec_result: ExecResult { - stdout: "

Page content

".into(), - stderr: String::new(), - exit_code: Some(0), + stdout: "

Page content

".into(), + stderr: String::new(), + exit_code: Some(0), termination: CommandTermination::Exited, duration_ms: 100, }, diff --git a/lib/components/fabro-agent/src/web_search.rs b/lib/components/fabro-agent/src/web_search.rs index 9be79c811..11fe4c7bf 100644 --- a/lib/components/fabro-agent/src/web_search.rs +++ b/lib/components/fabro-agent/src/web_search.rs @@ -24,12 +24,12 @@ const MAX_RESULTS: u64 = 20; #[derive(Clone, Debug)] pub(crate) enum SearchBackend { Brave { - api_key: String, + api_key: String, search_url: String, }, Venice { - api_key: String, - engine: VeniceSearchEngine, + api_key: String, + engine: VeniceSearchEngine, search_url: String, }, } @@ -207,10 +207,10 @@ fn format_brave_results(body: &serde_json::Value) -> String { results .iter() .map(|result| SearchHit { - title: json_str(result, "title"), - url: json_str(result, "url"), + title: json_str(result, "title"), + url: json_str(result, "url"), description: json_str(result, "description"), - date: None, + date: None, }) .collect() })) @@ -222,20 +222,20 @@ fn format_venice_results(body: &serde_json::Value) -> String { results .iter() .map(|result| SearchHit { - title: json_str(result, "title"), - url: json_str(result, "url"), + title: json_str(result, "title"), + url: json_str(result, "url"), description: json_str(result, "content"), - date: optional_json_str(result, "date"), + date: optional_json_str(result, "date"), }) .collect() })) } struct SearchHit { - title: String, - url: String, + title: String, + url: String, description: String, - date: Option, + date: Option, } fn format_hits(hits: Option>) -> String { @@ -364,8 +364,8 @@ mod tests { fn secrets(brave: Option<&str>, venice: Option<&str>, provider: SearchProvider) -> ToolSecrets { ToolSecrets { brave_search_api_key: brave.map(str::to_string), - venice_api_key: venice.map(str::to_string), - search: SearchIntegrationSettings { + venice_api_key: venice.map(str::to_string), + search: SearchIntegrationSettings { provider, venice_engine: VeniceSearchEngine::Brave, }, @@ -374,18 +374,15 @@ mod tests { async fn execute(tool: &RegisteredTool, args: serde_json::Value) -> Result { let env: Arc = Arc::new(MockSandbox::default()); - (tool.executor)( - args, - ToolContext { - env, - cancel: CancellationToken::new(), - tool_env_provider: None, - session_id: None, - root_session_id: None, - tool_call_id: None, - agent_event_emitter: None, - }, - ) + (tool.executor)(args, ToolContext { + env, + cancel: CancellationToken::new(), + tool_env_provider: None, + session_id: None, + root_session_id: None, + tool_call_id: None, + agent_event_emitter: None, + }) .await } diff --git a/lib/components/fabro-agent/tests/it/parity_matrix.rs b/lib/components/fabro-agent/tests/it/parity_matrix.rs index 486526f07..3f8f9e55e 100644 --- a/lib/components/fabro-agent/tests/it/parity_matrix.rs +++ b/lib/components/fabro-agent/tests/it/parity_matrix.rs @@ -26,22 +26,22 @@ type Provider = ProviderId; #[derive(Clone)] struct OpenAiTwinOptions { base_url: String, - api_key: String, + api_key: String, } fn summarizer_model_id(provider: &Provider) -> ModelHandle { match provider.as_str() { ProviderId::OPENAI | "moonshot" | "zai" | "minimax" | "inception" => ModelHandle::ByName { provider: ProviderId::openai(), - model: "gpt-5.4-mini".to_string(), + model: "gpt-5.4-mini".to_string(), }, ProviderId::GEMINI => ModelHandle::ByName { provider: ProviderId::gemini(), - model: "gemini-3-flash-preview".to_string(), + model: "gemini-3-flash-preview".to_string(), }, ProviderId::ANTHROPIC => ModelHandle::ByName { provider: ProviderId::anthropic(), - model: "claude-haiku-4-5".to_string(), + model: "claude-haiku-4-5".to_string(), }, other => panic!("unexpected provider {other}"), } @@ -49,7 +49,7 @@ fn summarizer_model_id(provider: &Provider) -> ModelHandle { fn build_summarizer(provider: &Provider, client: &Client) -> WebFetchSummarizer { WebFetchSummarizer { - client: client.clone(), + client: client.clone(), model_id: summarizer_model_id(provider), } } @@ -170,13 +170,12 @@ fn make_openai_compatible_twin_session( // twin fixture so the profile can resolve the same OpenAI-compatible // codec that the manually registered adapter uses. let mut settings = LlmCatalogSettings::default(); - settings.providers.insert( - provider.to_string(), - ProviderCatalogSettings { + settings + .providers + .insert(provider.to_string(), ProviderCatalogSettings { enabled: Some(true), ..ProviderCatalogSettings::default() - }, - ); + }); let catalog = Arc::new( Catalog::from_builtin_with_overrides(&settings) .expect("OpenAI-compatible twin catalog should build"), diff --git a/lib/components/fabro-workflow/src/pipeline/initialize.rs b/lib/components/fabro-workflow/src/pipeline/initialize.rs index cdb8a6520..275e20527 100644 --- a/lib/components/fabro-workflow/src/pipeline/initialize.rs +++ b/lib/components/fabro-workflow/src/pipeline/initialize.rs @@ -238,8 +238,8 @@ async fn tool_secrets_from_configured_sources(vault: &Arc>) - let vault = vault.read().await; ToolSecrets { brave_search_api_key: vault.get(EnvVars::BRAVE_SEARCH_API_KEY).map(str::to_string), - venice_api_key: vault.get(EnvVars::VENICE_API_KEY).map(str::to_string), - search: fabro_agent::search_settings_from_disk(), + venice_api_key: vault.get(EnvVars::VENICE_API_KEY).map(str::to_string), + search: fabro_agent::search_settings_from_disk(), } } diff --git a/lib/foundation/fabro-api/build.rs b/lib/foundation/fabro-api/build.rs index 5a3ff06ae..577746fa4 100644 --- a/lib/foundation/fabro-api/build.rs +++ b/lib/foundation/fabro-api/build.rs @@ -691,11 +691,8 @@ fn main() { ("AskFabro", "fabro_types::AskFabro", &[]), ("Automation", "fabro_automation::Automation", &[]), ("AutomationRef", "fabro_types::AutomationRef", &[]), - ( - "AutomationTarget", - "fabro_automation::AutomationTarget", - &[], - ), + ("AutomationTarget", "fabro_automation::AutomationTarget", &[ + ]), ( "AutomationTrigger", "fabro_automation::AutomationTrigger", diff --git a/lib/foundation/fabro-config/src/layers/server.rs b/lib/foundation/fabro-config/src/layers/server.rs index 7d4a96f5b..867a8da22 100644 --- a/lib/foundation/fabro-config/src/layers/server.rs +++ b/lib/foundation/fabro-config/src/layers/server.rs @@ -13,25 +13,25 @@ use super::LogFilter; #[serde(deny_unknown_fields)] pub struct ServerLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub listen: Option, + pub listen: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub api: Option, + pub api: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub web: Option, + pub web: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub auth: Option, + pub auth: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub sandbox: Option, + pub sandbox: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub storage: Option, + pub storage: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub artifacts: Option, + pub artifacts: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub slatedb: Option, + pub slatedb: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub scheduler: Option, + pub scheduler: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub logging: Option, + pub logging: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub integrations: Option, } @@ -67,7 +67,7 @@ pub struct ServerWebLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub enabled: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub url: Option, + pub url: Option, } /// `[server.auth]` — cohesive server auth surface. @@ -81,7 +81,7 @@ pub struct ServerAuthLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub methods: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] - pub github: Option, + pub github: Option, } #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] @@ -103,9 +103,9 @@ pub struct ServerSandboxLayer { #[serde(deny_unknown_fields)] pub struct ServerSandboxProvidersLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub local: Option, + pub local: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub docker: Option, + pub docker: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub daytona: Option, } @@ -132,11 +132,11 @@ pub struct ServerArtifactsLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub prefix: Option, + pub prefix: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub local: Option, + pub local: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub s3: Option, + pub s3: Option, } /// `[server.slatedb]` — SlateDB bottomless storage plus tunables. @@ -144,17 +144,17 @@ pub struct ServerArtifactsLayer { #[serde(deny_unknown_fields)] pub struct ServerSlateDbLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub provider: Option, + pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub prefix: Option, + pub prefix: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub flush_interval: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub local: Option, + pub local: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub s3: Option, + pub s3: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub disk_cache: Option, + pub disk_cache: Option, } #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] @@ -170,11 +170,11 @@ pub struct ObjectStoreLocalLayer { #[serde(deny_unknown_fields)] pub struct ObjectStoreS3Layer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub bucket: Option, + pub bucket: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub region: Option, + pub region: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub endpoint: Option, + pub endpoint: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub path_style: Option, } @@ -192,7 +192,7 @@ pub struct ServerSchedulerLayer { #[serde(deny_unknown_fields)] pub struct ServerLoggingLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub level: Option, + pub level: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub destination: Option, } @@ -205,7 +205,7 @@ pub struct ServerIntegrationsLayer { #[serde(default, skip_serializing_if = "Option::is_none")] pub github: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub slack: Option, + pub slack: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub search: Option, } @@ -216,17 +216,17 @@ pub struct ServerIntegrationsLayer { #[serde(deny_unknown_fields)] pub struct GithubIntegrationLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub enabled: Option, + pub enabled: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub strategy: Option, + pub strategy: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub app_id: Option, + pub app_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub client_id: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub slug: Option, + pub slug: Option, #[serde(default, skip_serializing_if = "Option::is_none")] - pub webhooks: Option, + pub webhooks: Option, } /// `[server.integrations.slack]` — Slack workspace credentials and defaults. @@ -234,7 +234,7 @@ pub struct GithubIntegrationLayer { #[serde(deny_unknown_fields)] pub struct SlackIntegrationLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub enabled: Option, + pub enabled: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub default_channel: Option, } @@ -244,7 +244,7 @@ pub struct SlackIntegrationLayer { #[serde(deny_unknown_fields)] pub struct SearchIntegrationLayer { #[serde(default, skip_serializing_if = "Option::is_none")] - pub provider: Option, + pub provider: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub venice_engine: Option, } diff --git a/lib/foundation/fabro-config/src/resolve/server.rs b/lib/foundation/fabro-config/src/resolve/server.rs index df6f1b76b..08a5900db 100644 --- a/lib/foundation/fabro-config/src/resolve/server.rs +++ b/lib/foundation/fabro-config/src/resolve/server.rs @@ -51,7 +51,7 @@ pub fn resolve_server(layer: &ServerLayer, errors: &mut Vec) -> Se .expect("defaults.toml should provide server.scheduler.max_concurrent_runs"), }, logging: ServerLoggingSettings { - level: layer + level: layer .logging .as_ref() .and_then(|logging| logging.level.as_ref()) @@ -70,10 +70,10 @@ fn resolve_sandbox(layer: Option<&ServerSandboxLayer>) -> ServerSandboxSettings let providers = layer.and_then(|sandbox| sandbox.providers.as_ref()); ServerSandboxSettings { providers: ServerSandboxProvidersSettings { - local: resolve_sandbox_provider( + local: resolve_sandbox_provider( providers.and_then(|providers| providers.local.as_ref()), ), - docker: resolve_sandbox_provider( + docker: resolve_sandbox_provider( providers.and_then(|providers| providers.docker.as_ref()), ), daytona: resolve_sandbox_provider( @@ -150,7 +150,7 @@ fn resolve_auth( let methods = if let Some(mut methods) = layer.and_then(|auth| auth.methods.clone()) { if methods.is_empty() { errors.push(ResolveError::Invalid { - path: "server.auth.methods".to_string(), + path: "server.auth.methods".to_string(), reason: "must not be empty".to_string(), }); } @@ -169,7 +169,7 @@ fn resolve_auth( .unwrap_or_default(); if methods.contains(&ServerAuthMethod::Github) && github.allowed_usernames.is_empty() { errors.push(ResolveError::Invalid { - path: "server.auth.github.allowed_usernames".to_string(), + path: "server.auth.github.allowed_usernames".to_string(), reason: "must not be empty when github auth is enabled".to_string(), }); } @@ -198,7 +198,7 @@ fn validate_github_webhook_strategy( && github.app_id.is_none() { errors.push(ResolveError::Invalid { - path: "server.integrations.github.app_id".to_string(), + path: "server.integrations.github.app_id".to_string(), reason: "must be set when server.integrations.github.webhooks.strategy is configured" .to_string(), }); @@ -208,7 +208,7 @@ fn validate_github_webhook_strategy( && api_layer.and_then(|api| api.url.as_ref()).is_none() { errors.push(ResolveError::Invalid { - path: "server.api.url".to_string(), + path: "server.api.url".to_string(), reason: "must be set when server.integrations.github.webhooks.strategy = \"server_url\"" .to_string(), @@ -348,20 +348,20 @@ fn resolve_integrations(layer: Option<&ServerIntegrationsLayer>) -> ServerIntegr ); warn_if_demoted_template("server.integrations.github.slug", github.slug.as_deref()); GithubIntegrationSettings { - enabled: github.enabled.unwrap_or(true), - strategy: github.strategy.unwrap_or_default(), - app_id: github.app_id.clone(), + enabled: github.enabled.unwrap_or(true), + strategy: github.strategy.unwrap_or_default(), + app_id: github.app_id.clone(), client_id: github.client_id.clone(), - slug: github.slug.clone(), - webhooks: github.webhooks.as_ref().map(resolve_github_webhooks), + slug: github.slug.clone(), + webhooks: github.webhooks.as_ref().map(resolve_github_webhooks), } }) .unwrap_or_default(), - slack: layer + slack: layer .and_then(|integrations| integrations.slack.as_ref()) .map_or( SlackIntegrationSettings { - enabled: false, + enabled: false, default_channel: None, }, |slack| { @@ -370,7 +370,7 @@ fn resolve_integrations(layer: Option<&ServerIntegrationsLayer>) -> ServerIntegr slack.default_channel.as_deref(), ); SlackIntegrationSettings { - enabled: slack.enabled.unwrap_or(true), + enabled: slack.enabled.unwrap_or(true), default_channel: slack.default_channel.clone(), } }, @@ -378,7 +378,7 @@ fn resolve_integrations(layer: Option<&ServerIntegrationsLayer>) -> ServerIntegr search: layer .and_then(|integrations| integrations.search.as_ref()) .map(|search| SearchIntegrationSettings { - provider: search.provider.unwrap_or_default(), + provider: search.provider.unwrap_or_default(), venice_engine: search.venice_engine.unwrap_or_default(), }) .unwrap_or_default(), diff --git a/lib/foundation/fabro-types/src/settings/server.rs b/lib/foundation/fabro-types/src/settings/server.rs index 044c1776b..4af44d9f1 100644 --- a/lib/foundation/fabro-types/src/settings/server.rs +++ b/lib/foundation/fabro-types/src/settings/server.rs @@ -22,16 +22,16 @@ use super::duration::Duration; /// (tests). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerNamespace { - pub listen: ServerListenSettings, - pub api: ServerApiSettings, - pub web: ServerWebSettings, - pub auth: ServerAuthSettings, - pub sandbox: ServerSandboxSettings, - pub storage: ServerStorageSettings, - pub artifacts: ServerArtifactsSettings, - pub slatedb: ServerSlateDbSettings, - pub scheduler: ServerSchedulerSettings, - pub logging: ServerLoggingSettings, + pub listen: ServerListenSettings, + pub api: ServerApiSettings, + pub web: ServerWebSettings, + pub auth: ServerAuthSettings, + pub sandbox: ServerSandboxSettings, + pub storage: ServerStorageSettings, + pub artifacts: ServerArtifactsSettings, + pub slatedb: ServerSlateDbSettings, + pub scheduler: ServerSchedulerSettings, + pub logging: ServerLoggingSettings, pub integrations: ServerIntegrationsSettings, } @@ -43,16 +43,16 @@ impl ServerNamespace { #[must_use] pub fn test_default() -> Self { Self { - listen: ServerListenSettings::default(), - api: ServerApiSettings::default(), - web: ServerWebSettings::default(), - auth: ServerAuthSettings::default(), - sandbox: ServerSandboxSettings::default(), - storage: ServerStorageSettings::default(), - artifacts: ServerArtifactsSettings::default(), - slatedb: ServerSlateDbSettings::default(), - scheduler: ServerSchedulerSettings::default(), - logging: ServerLoggingSettings::default(), + listen: ServerListenSettings::default(), + api: ServerApiSettings::default(), + web: ServerWebSettings::default(), + auth: ServerAuthSettings::default(), + sandbox: ServerSandboxSettings::default(), + storage: ServerStorageSettings::default(), + artifacts: ServerArtifactsSettings::default(), + slatedb: ServerSlateDbSettings::default(), + scheduler: ServerSchedulerSettings::default(), + logging: ServerLoggingSettings::default(), integrations: ServerIntegrationsSettings::default(), } } @@ -89,13 +89,13 @@ pub struct ServerApiSettings { #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerWebSettings { pub enabled: bool, - pub url: String, + pub url: String, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerAuthSettings { pub methods: Vec, - pub github: ServerAuthGithubSettings, + pub github: ServerAuthGithubSettings, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -117,8 +117,8 @@ pub struct ServerSandboxSettings { #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerSandboxProvidersSettings { - pub local: ServerSandboxProviderSettings, - pub docker: ServerSandboxProviderSettings, + pub local: ServerSandboxProviderSettings, + pub docker: ServerSandboxProviderSettings, pub daytona: ServerSandboxProviderSettings, } @@ -158,28 +158,28 @@ pub struct ServerStorageSettings { #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerArtifactsSettings { pub prefix: String, - pub store: ObjectStoreSettings, + pub store: ObjectStoreSettings, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerSlateDbSettings { - pub prefix: String, - pub store: ObjectStoreSettings, + pub prefix: String, + pub store: ObjectStoreSettings, #[serde( serialize_with = "serialize_std_duration", deserialize_with = "deserialize_std_duration" )] pub flush_interval: StdDuration, - pub disk_cache: bool, + pub disk_cache: bool, } impl Default for ServerSlateDbSettings { fn default() -> Self { Self { - prefix: String::new(), - store: ObjectStoreSettings::default(), + prefix: String::new(), + store: ObjectStoreSettings::default(), flush_interval: StdDuration::ZERO, - disk_cache: false, + disk_cache: false, } } } @@ -191,9 +191,9 @@ pub enum ObjectStoreSettings { root: String, }, S3 { - bucket: String, - region: String, - endpoint: Option, + bucket: String, + region: String, + endpoint: Option, path_style: bool, }, } @@ -233,7 +233,7 @@ pub enum LogDestination { #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerLoggingSettings { - pub level: Option, + pub level: Option, #[serde(default)] pub destination: LogDestination, } @@ -241,30 +241,30 @@ pub struct ServerLoggingSettings { #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct ServerIntegrationsSettings { pub github: GithubIntegrationSettings, - pub slack: SlackIntegrationSettings, + pub slack: SlackIntegrationSettings, pub search: SearchIntegrationSettings, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct GithubIntegrationSettings { - pub enabled: bool, - pub strategy: GithubIntegrationStrategy, - pub app_id: Option, + pub enabled: bool, + pub strategy: GithubIntegrationStrategy, + pub app_id: Option, pub client_id: Option, - pub slug: Option, - pub webhooks: Option, + pub slug: Option, + pub webhooks: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SlackIntegrationSettings { - pub enabled: bool, + pub enabled: bool, pub default_channel: Option, } impl Default for SlackIntegrationSettings { fn default() -> Self { Self { - enabled: true, + enabled: true, default_channel: None, } } @@ -335,7 +335,7 @@ impl VeniceSearchEngine { #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct SearchIntegrationSettings { - pub provider: SearchProvider, + pub provider: SearchProvider, pub venice_engine: VeniceSearchEngine, } From 88ed2ac9a39ba27bb34aa79f62e840aa1eaefcb2 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 21 Aug 2026 20:09:52 -0400 Subject: [PATCH 3/3] refactor(search): select backend from available credentials --- docs/internal/server-secrets-strategy.md | 1 + docs/public/administration/deploy-railway.mdx | 2 +- docs/public/administration/security.mdx | 2 +- .../administration/self-host-docker.mdx | 2 +- .../administration/server-configuration.mdx | 22 +-- docs/public/agents/prompts.mdx | 4 +- docs/public/agents/tools.mdx | 18 +-- docs/public/api-reference/fabro-api.yaml | 21 +-- docs/public/changelog/2026-08-21.mdx | 2 +- docs/public/core-concepts/models.mdx | 2 +- docs/public/integrations/brave-search.mdx | 8 +- docs/public/integrations/venice-search.mdx | 25 ++- lib/apps/fabro-server/src/demo/mod.rs | 2 +- lib/apps/fabro-server/src/diagnostics.rs | 134 ++++++++------- lib/components/fabro-agent/src/cli.rs | 3 +- lib/components/fabro-agent/src/config.rs | 7 +- lib/components/fabro-agent/src/lib.rs | 1 - .../fabro-agent/src/profiles/claude5_tools.rs | 29 +--- .../src/profiles/prompts/openai.md.j2 | 2 +- ..._patch_and_web_search_prompt_snapshot.snap | 2 +- ...t_file_and_web_search_prompt_snapshot.snap | 2 +- lib/components/fabro-agent/src/tools.rs | 10 +- lib/components/fabro-agent/src/web_search.rs | 153 +++++------------- .../fabro-workflow/src/pipeline/initialize.rs | 1 - lib/foundation/fabro-api/build.rs | 15 -- lib/foundation/fabro-api/src/lib.rs | 11 +- .../fabro-config/src/layers/combine.rs | 6 +- lib/foundation/fabro-config/src/layers/mod.rs | 4 +- .../fabro-config/src/layers/server.rs | 16 +- lib/foundation/fabro-config/src/lib.rs | 11 +- .../fabro-config/src/resolve/server.rs | 19 +-- .../fabro-config/src/tests/resolve_server.rs | 45 +----- .../fabro-types/src/settings/mod.rs | 9 +- .../fabro-types/src/settings/server.rs | 70 -------- .../src/.openapi-generator/FILES | 3 - .../fabro-api-client/src/models/index.ts | 3 - .../src/models/search-integration-settings.ts | 26 --- .../src/models/search-provider.ts | 23 --- .../models/server-integrations-settings.ts | 4 - .../src/models/venice-search-engine.ts | 23 --- 40 files changed, 188 insertions(+), 555 deletions(-) delete mode 100644 lib/packages/fabro-api-client/src/models/search-integration-settings.ts delete mode 100644 lib/packages/fabro-api-client/src/models/search-provider.ts delete mode 100644 lib/packages/fabro-api-client/src/models/venice-search-engine.ts diff --git a/docs/internal/server-secrets-strategy.md b/docs/internal/server-secrets-strategy.md index 839f2bcec..df4160886 100644 --- a/docs/internal/server-secrets-strategy.md +++ b/docs/internal/server-secrets-strategy.md @@ -39,6 +39,7 @@ the vault: - `FABRO_SLACK_BOT_TOKEN` - `DAYTONA_API_KEY` - `BRAVE_SEARCH_API_KEY` +- `VENICE_API_KEY` `FABRO_JWT_PRIVATE_KEY` and `FABRO_JWT_PUBLIC_KEY` are removed. `SESSION_SECRET` is the single auth root. diff --git a/docs/public/administration/deploy-railway.mdx b/docs/public/administration/deploy-railway.mdx index 9a52facc9..c26c845d0 100644 --- a/docs/public/administration/deploy-railway.mdx +++ b/docs/public/administration/deploy-railway.mdx @@ -41,7 +41,7 @@ Add variables in **Service → Variables** as needed. The [Server Configuration] | `SESSION_SECRET` | 64-character hex string; required when the web UI is enabled | | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN` | Optional static S3 object-store credentials | -Do not put optional integration secrets in Railway variables for server runtime. After the server is running, add LLM provider keys, Slack, Daytona, Brave Search, `GITHUB_TOKEN`, and GitHub App secrets to the server vault with `fabro secret set`, `fabro provider login`, or `fabro install`. +Do not put optional integration secrets in Railway variables for server runtime. After the server is running, add LLM provider keys, Slack, Daytona, Brave Search, Venice Search, `GITHUB_TOKEN`, and GitHub App secrets to the server vault with `fabro secret set`, `fabro provider login`, or `fabro install`. No `.env` file is auto-loaded inside the container; bootstrap variables come from Railway's environment. diff --git a/docs/public/administration/security.mdx b/docs/public/administration/security.mdx index 27e1aeea9..b27fada61 100644 --- a/docs/public/administration/security.mdx +++ b/docs/public/administration/security.mdx @@ -38,7 +38,7 @@ Fabro is single-tenant software designed for small, trusted teams. The following ### Secrets - **Keep API keys out of sandboxes.** The local sandbox strips environment variables ending in `_API_KEY`, `_SECRET`, `_TOKEN`, `_PASSWORD`, or `_CREDENTIAL`, but Docker and Daytona sandboxes provide stronger isolation — only explicitly configured variables are passed through. -- **Use the server vault for optional integration credentials.** For server-backed workflows, persist LLM provider keys, Slack, Daytona, Brave Search, GitHub token, and GitHub App secrets with `fabro provider login`, `fabro secret set`, or `fabro install`. Process env and `server.env` are reserved for bootstrap secrets such as `SESSION_SECRET`, `FABRO_DEV_TOKEN`, and object-store credentials. Do not commit secrets to version control. +- **Use the server vault for optional integration credentials.** For server-backed workflows, persist LLM provider keys, Slack, Daytona, Brave Search, Venice Search, GitHub token, and GitHub App secrets with `fabro provider login`, `fabro secret set`, or `fabro install`. Process env and `server.env` are reserved for bootstrap secrets such as `SESSION_SECRET`, `FABRO_DEV_TOKEN`, and object-store credentials. Do not commit secrets to version control. - **Rotate the session secret.** The `SESSION_SECRET` environment variable encrypts web app sessions. Rotate it periodically and use a strong random value. ### Execution diff --git a/docs/public/administration/self-host-docker.mdx b/docs/public/administration/self-host-docker.mdx index c896e29e6..ab0228149 100644 --- a/docs/public/administration/self-host-docker.mdx +++ b/docs/public/administration/self-host-docker.mdx @@ -108,7 +108,7 @@ Generate one with `openssl rand -hex 32`. | `FABRO_DEV_TOKEN` | Optional — pre-set the dev token instead of reading the one written to `/storage` on first boot | | `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN` | Optional static S3 object-store credentials | -Do not put optional integration secrets in `.env` for server runtime. Configure LLM provider keys, Slack, Daytona, Brave Search, `GITHUB_TOKEN`, and GitHub App secrets in the vault with `fabro secret set`, `fabro provider login`, or `fabro install`. +Do not put optional integration secrets in `.env` for server runtime. Configure LLM provider keys, Slack, Daytona, Brave Search, Venice Search, `GITHUB_TOKEN`, and GitHub App secrets in the vault with `fabro secret set`, `fabro provider login`, or `fabro install`. Optional: diff --git a/docs/public/administration/server-configuration.mdx b/docs/public/administration/server-configuration.mdx index 5ed65df8c..6dd8f1e67 100644 --- a/docs/public/administration/server-configuration.mdx +++ b/docs/public/administration/server-configuration.mdx @@ -334,22 +334,6 @@ Tailscale Services and Tailscale Funnel are different ingress features. Services Incoming webhooks are authenticated only by GitHub's `X-Hub-Signature-256` HMAC signature, not by Fabro's bearer/session auth. -### `[server.integrations.search]` section - -Select the HTTP backend for the built-in [`web_search`](/agents/tools#web_search) tool. Brave remains the default when this table is absent. - -```toml title="settings.toml" -[server.integrations.search] -provider = "brave" # "brave" (default) | "venice" -venice_engine = "brave" # venice-only: "brave" | "google" -``` - -- `provider = "brave"`: direct Brave Search. Requires vault `BRAVE_SEARCH_API_KEY`. See [Brave Search](/integrations/brave-search). -- `provider = "venice"`: Venice `POST /api/v1/augment/search`. Requires vault `VENICE_API_KEY` (the same key as the Venice LLM provider). See [Venice Search](/integrations/venice-search). -- `venice_engine = "brave"` is Brave **through Venice** (Firecrawl ZDR, billed as Venice credits). Direct Brave remains `provider = "brave"`. - -The tool is registered only when the selected provider is configured. Failed calls do not fall back between backends. - ### `[run.checkpoint]` section Configure checkpoint behavior for all runs. @@ -421,11 +405,13 @@ fabro secret set BRAVE_SEARCH_API_KEY BSA... fabro secret set VENICE_API_KEY venice-... ``` +The built-in [`web_search`](/agents/tools#web_search) tool selects its backend from these credentials. It uses direct Brave Search when `BRAVE_SEARCH_API_KEY` exists. Otherwise it uses Venice Search when `VENICE_API_KEY` exists. When neither exists, the tool is not registered. + | Variable | Description | |---|---| | `DAYTONA_API_KEY` | Daytona cloud sandbox API key | -| `BRAVE_SEARCH_API_KEY` | Brave Search API key (`web_search` when `provider = "brave"`) | -| `VENICE_API_KEY` | Venice API key (LLM provider, and `web_search` when `provider = "venice"`) | +| `BRAVE_SEARCH_API_KEY` | Brave Search API key; the preferred `web_search` backend when present | +| `VENICE_API_KEY` | Venice API key; used by the Venice LLM provider and by `web_search` when no Brave key exists | ### Server authentication diff --git a/docs/public/agents/prompts.mdx b/docs/public/agents/prompts.mdx index 50abbe3d1..ff2c2fd4c 100644 --- a/docs/public/agents/prompts.mdx +++ b/docs/public/agents/prompts.mdx @@ -145,7 +145,7 @@ The system prompt varies by LLM provider. Each provider has its own identity tex This is the full system prompt sent to Claude as the LLM system message. The `` block is filled in at runtime. -Tool guidance tracks the tools actually registered for the session. The `web_search` section shown below is present only when a [Brave Search API key](/integrations/brave-search) is configured; without one, both the tool and its guidance are omitted. +Tool guidance tracks the tools actually registered for the session. The `web_search` section shown below is present when a [Brave Search API key](/integrations/brave-search) or [Venice API key](/integrations/venice-search) is configured. Without either key, both the tool and its guidance are omitted. ``` You are Claude, an AI coding assistant made by Anthropic. You help users with @@ -232,7 +232,7 @@ first). Use this for finding files rather than using shell find or ls commands. ## web_search -Search the web using Brave Search. Returns titles, URLs, and descriptions. +Search the web. Returns titles, URLs, and descriptions. ## web_fetch Fetch content from a URL and optionally summarize it. Pass a prompt to diff --git a/docs/public/agents/tools.mdx b/docs/public/agents/tools.mdx index 9b17b8957..d6df674ed 100644 --- a/docs/public/agents/tools.mdx +++ b/docs/public/agents/tools.mdx @@ -123,28 +123,18 @@ Patterns are case-sensitive and relative to `path`: `*` and `?` stay within one ### web_search -Searches the web using Brave Search (default) or Venice Search. +Searches the web using Brave Search or Venice Search. Fabro selects the backend automatically from the available credentials. | Parameter | Type | Required | Description | |---|---|---|---| | `query` | string | yes | Search query. Venice rejects queries longer than 400 characters before the HTTP call. | | `max_results` | integer | no | Maximum results (default: 5, max: 20) | -| `engine` | `"brave"` \| `"google"` | no | Venice backend only. Overrides `[server.integrations.search].venice_engine`. Brave ignores this parameter. | -Select the backend in server settings. Brave remains the default when the table is absent: +Fabro uses direct [Brave Search](/integrations/brave-search) when `BRAVE_SEARCH_API_KEY` is present. Otherwise it uses [Venice Search](/integrations/venice-search) when `VENICE_API_KEY` is present. If both credentials are present, Brave wins. Venice always uses its Brave search engine. -```toml -[server.integrations.search] -provider = "venice" # "brave" (default) | "venice" -venice_engine = "brave" # venice-only: "brave" (ZDR) | "google" (anon proxy) -``` +Runs read both keys from the server vault. Workers start from a cleared environment, so exporting a key in the server's shell has no effect. The standalone agent CLI reads the keys from the invoking shell instead. -Runs read the selected provider's key from the server vault — workers start from a cleared environment, so exporting the key in the server's shell has no effect. The standalone agent CLI reads keys from the invoking shell instead. - -- Brave (`provider = "brave"`): `fabro secret set BRAVE_SEARCH_API_KEY `. See [Brave Search](/integrations/brave-search). -- Venice (`provider = "venice"`): reuse `VENICE_API_KEY` (`fabro provider login --provider venice` or `fabro secret set VENICE_API_KEY `). See [Venice Search](/integrations/venice-search). - -The tool is registered only when the **selected** provider is configured. Fabro does not fall back Brave ↔ Venice on a failed call. Returns numbered results with title, URL, and description; Venice includes `date` on a fourth line when present. +The tool is registered when either credential is available. Once Fabro selects a backend, a failed call returns an error; it does not retry through the other backend. Results contain numbered titles, URLs, and descriptions. Venice includes `date` on a fourth line when present. ### web_fetch diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index e9b019329..546b9d106 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -14231,14 +14231,12 @@ components: ServerIntegrationsSettings: type: object - required: [github, slack, search] + required: [github, slack] properties: github: $ref: "#/components/schemas/GithubIntegrationSettings" slack: $ref: "#/components/schemas/SlackIntegrationSettings" - search: - $ref: "#/components/schemas/SearchIntegrationSettings" GithubIntegrationSettings: type: object @@ -14278,23 +14276,6 @@ components: default_channel: type: ["string", "null"] - SearchIntegrationSettings: - type: object - required: [provider, venice_engine] - properties: - provider: - $ref: "#/components/schemas/SearchProvider" - venice_engine: - $ref: "#/components/schemas/VeniceSearchEngine" - - SearchProvider: - type: string - enum: [brave, venice] - - VeniceSearchEngine: - type: string - enum: [brave, google] - IntegrationWebhooksSettings: type: object required: [strategy] diff --git a/docs/public/changelog/2026-08-21.mdx b/docs/public/changelog/2026-08-21.mdx index d264b5aad..71308d8f3 100644 --- a/docs/public/changelog/2026-08-21.mdx +++ b/docs/public/changelog/2026-08-21.mdx @@ -5,6 +5,6 @@ date: "2026-08-21" ## Venice search backend for `web_search` -The built-in `web_search` tool now has a second HTTP backend. Brave Search remains the default. Set `[server.integrations.search].provider = "venice"` to send queries to Venice `POST /api/v1/augment/search`, reusing vault `VENICE_API_KEY`. Failed calls do not fall back between providers. +The built-in `web_search` tool now supports Venice as an automatic alternative to direct Brave Search. Fabro uses `BRAVE_SEARCH_API_KEY` when present. Otherwise it uses `VENICE_API_KEY` with Venice's Brave search engine. If neither key is present, the tool is not registered. Failed calls do not fall back between providers. See [Venice Search](/integrations/venice-search) and [Brave Search](/integrations/brave-search). diff --git a/docs/public/core-concepts/models.mdx b/docs/public/core-concepts/models.mdx index df78987d7..9bdeadce1 100644 --- a/docs/public/core-concepts/models.mdx +++ b/docs/public/core-concepts/models.mdx @@ -161,7 +161,7 @@ Workflow runs also add `x-session-id: ` to every LLM request so compatib Provider `agent_profile` defaults from `adapter` and controls profile-specific behavior such as which tools the agent registers, project-memory filenames, CLI/ACP command selection, and native session routing. Valid values are `anthropic`, `openai`, `gemini`, `kimi`, and `gpt56`; model-level values override provider-level values. -Two profiles are selected per model rather than per provider, because they follow the model wherever it is served: `kimi` for Kimi models, and `gpt56` for the GPT-5.6 models (Sol, Terra, Luna). The `gpt56` profile uses Codex's narrow core surface — `shell_command`, `apply_patch`, and `update_plan`, plus optional Brave-backed `web_search` — instead of fabro's dedicated file-read, discovery, and `web_fetch` tools. On OpenAI-compatible routes that cannot carry the freeform `apply_patch` grammar, it substitutes the JSON-schema `edit_file` tool. Session features may add their own question, skill, or subagent tools separately. +Two profiles are selected per model rather than per provider, because they follow the model wherever it is served: `kimi` for Kimi models, and `gpt56` for the GPT-5.6 models (Sol, Terra, Luna). The `gpt56` profile uses Codex's narrow core surface — `shell_command`, `apply_patch`, and `update_plan`, plus optional credential-backed `web_search` — instead of fabro's dedicated file-read, discovery, and `web_fetch` tools. On OpenAI-compatible routes that cannot carry the freeform `apply_patch` grammar, it substitutes the JSON-schema `edit_file` tool. Session features may add their own question, skill, or subagent tools separately. Provider `billing_policy` defaults from `adapter` and controls usage-cost estimation. Use `openai`, `anthropic`, `gemini`, or `none`. Model rows may override it for models whose billing family differs from their provider's — for example, Claude models served through OpenRouter set `billing_policy = "anthropic"` so cache reads and writes price correctly. diff --git a/docs/public/integrations/brave-search.mdx b/docs/public/integrations/brave-search.mdx index 2c46f25dc..5b6ee2475 100644 --- a/docs/public/integrations/brave-search.mdx +++ b/docs/public/integrations/brave-search.mdx @@ -3,9 +3,9 @@ title: "Brave Search" description: "Give Fabro agents web search capabilities via the Brave Search API" --- -Fabro's [`web_search`](/agents/tools#web_search) tool lets agents search the web during workflow execution. It uses the [Brave Search API](https://brave.com/search/api/) to return titles, URLs, and descriptions for any query. Setting the API key is the only configuration needed — the tool is then registered for all provider profiles (Anthropic, OpenAI, Gemini). Without a key the tool is not registered at all, so agents are never offered a search tool they cannot use. +Fabro's [`web_search`](/agents/tools#web_search) tool lets agents search the web during workflow execution. It uses the [Brave Search API](https://brave.com/search/api/) to return titles, URLs, and descriptions for any query. -Brave is the default `web_search` backend. To use Venice instead, see [Venice Search](/integrations/venice-search). +Fabro selects the backend from the credentials in its vault. Direct Brave Search is preferred whenever `BRAVE_SEARCH_API_KEY` is present. When that key is absent, Fabro can use [Venice Search](/integrations/venice-search) with `VENICE_API_KEY` instead. ## Setup @@ -23,7 +23,7 @@ fabro secret set BRAVE_SEARCH_API_KEY BSA... fabro doctor ``` -The doctor output should show **Web Search** as `brave: configured and reachable`. If the key is missing, web search is reported as a warning — workflows still run, but the `web_search` tool is omitted from the agent's tool set and its system prompt, so agents fall back to other tools. +The doctor output should show **Web Search** as `brave: configured and reachable`. If the Brave key is missing but a Venice key exists, Fabro checks Venice instead. If neither key exists, web search is reported as a warning. Workflows still run, but the `web_search` tool is omitted from the agent's tool set and its system prompt. The Fabro server reads this key from the vault only. It does not read `BRAVE_SEARCH_API_KEY` from process env or `server.env`. @@ -41,7 +41,7 @@ Agents call the `web_search` tool with a query string. Fabro sends the query to The Rust book ``` -If `BRAVE_SEARCH_API_KEY` is not configured in the vault, the tool returns an error explaining that the key is required. The agent can then fall back to other approaches. +If `BRAVE_SEARCH_API_KEY` is not configured, Fabro uses Venice when `VENICE_API_KEY` is available. If neither key is configured, the tool is not registered. See the [`web_search` tool reference](/agents/tools#web_search) for parameters and details. diff --git a/docs/public/integrations/venice-search.mdx b/docs/public/integrations/venice-search.mdx index 2e23cbad0..ac37cafb6 100644 --- a/docs/public/integrations/venice-search.mdx +++ b/docs/public/integrations/venice-search.mdx @@ -3,9 +3,9 @@ title: "Venice Search" description: "Give Fabro agents web search capabilities via Venice's augment/search API" --- -Fabro's [`web_search`](/agents/tools#web_search) tool lets agents search the web during workflow execution. The default backend is [Brave Search](/integrations/brave-search). Set `[server.integrations.search].provider = "venice"` to use [Venice Search](https://docs.venice.ai/api-reference/endpoint/augment/search) instead, reusing the same `VENICE_API_KEY` already used for the Venice LLM provider. +Fabro's [`web_search`](/agents/tools#web_search) tool lets agents search the web during workflow execution. Fabro uses [Venice Search](https://docs.venice.ai/api-reference/endpoint/augment/search) automatically when `VENICE_API_KEY` is available and a direct [Brave Search](/integrations/brave-search) key is not. -Agents keep calling `web_search`. Only the HTTP backend changes. +Venice Search reuses the same `VENICE_API_KEY` as the Venice LLM provider. Agents keep calling `web_search`; only the HTTP backend changes. ## Setup @@ -17,30 +17,25 @@ fabro provider login --provider venice fabro secret set VENICE_API_KEY venice-... ``` -2. Select Venice as the search backend: +Fabro prefers direct Brave Search whenever `BRAVE_SEARCH_API_KEY` is also present. To select Venice, leave that key unset or remove it: -```toml title="settings.toml" -[server.integrations.search] -provider = "venice" -# venice_engine = "brave" # default: Firecrawl ZDR, billed as Venice credits -# venice_engine = "google" # anonymized proxy +```bash +fabro secret rm BRAVE_SEARCH_API_KEY ``` -`provider = "brave"` (the default) talks to Brave Search directly and still needs `BRAVE_SEARCH_API_KEY`. `venice_engine = "brave"` is Brave **through Venice**, not the direct Brave backend. - -3. Verify the key is working: +2. Verify the key is working: ```bash fabro doctor ``` -The doctor output should show **Web Search** as `venice: configured and reachable`. If `VENICE_API_KEY` is missing, web search is reported as a warning — workflows still run, but the `web_search` tool is omitted from the agent's tool set. +The doctor output should show **Web Search** as `venice: configured and reachable`. If neither Venice nor Brave is configured, web search is reported as a warning. Workflows still run, but the `web_search` tool is omitted from the agent's tool set. The Fabro server reads this key from the vault only. It does not read `VENICE_API_KEY` from process env or `server.env`. ## How it works -Agents call the `web_search` tool with a query string. Fabro `POST`s to Venice `https://api.venice.ai/api/v1/augment/search` and returns numbered results with title, URL, description, and date when Venice includes one: +Agents call the `web_search` tool with a query string. Fabro `POST`s to Venice `https://api.venice.ai/api/v1/augment/search` with the Brave search engine and returns numbered results with title, URL, description, and date when Venice includes one: ``` 1. Rust Lang @@ -51,7 +46,7 @@ Agents call the `web_search` tool with a query string. Fabro `POST`s to Venice ` Venice Search is billed by Venice at $0.01 per request and is rate-limited to 20 requests per minute on the Venice side. Queries longer than 400 characters are rejected before the HTTP call. -If `VENICE_API_KEY` is not configured in the vault, the tool is not registered. Failed calls return an error; Fabro does not fall back to Brave. +If `VENICE_API_KEY` is absent but `BRAVE_SEARCH_API_KEY` exists, Fabro uses direct Brave Search. If neither key exists, the tool is not registered. After selecting Venice, a failed call returns an error; Fabro does not retry through direct Brave Search. See the [`web_search` tool reference](/agents/tools#web_search) for parameters and details. @@ -79,6 +74,6 @@ See the [`web_search` tool reference](/agents/tools#web_search) for parameters a Full `web_search` tool reference — parameters, output format, and error handling. - Direct Brave Search backend (the default when `provider` is unset). + Direct Brave Search backend, preferred whenever its key is configured. diff --git a/lib/apps/fabro-server/src/demo/mod.rs b/lib/apps/fabro-server/src/demo/mod.rs index 97c29e039..18c51e8a4 100644 --- a/lib/apps/fabro-server/src/demo/mod.rs +++ b/lib/apps/fabro-server/src/demo/mod.rs @@ -672,7 +672,7 @@ pub(crate) async fn run_diagnostics( { "name": "GitHub App", "status": "pass", "summary": "demo configured", "details": [], "remediation": null }, { "name": "Docker Sandbox", "status": "pass", "summary": "disabled", "details": [{ "text": "server.sandbox.providers.docker.enabled = false", "warn": false }], "remediation": null }, { "name": "Cloud Sandbox", "status": "warning", "summary": "not configured", "details": [], "remediation": "Set DAYTONA_API_KEY to enable cloud sandbox execution" }, - { "name": "Brave Search", "status": "warning", "summary": "not configured", "details": [], "remediation": "Set BRAVE_SEARCH_API_KEY to enable web search" } + { "name": "Web Search", "status": "warning", "summary": "optional, not configured", "details": [], "remediation": "Set BRAVE_SEARCH_API_KEY or VENICE_API_KEY to enable web search" } ] }, { diff --git a/lib/apps/fabro-server/src/diagnostics.rs b/lib/apps/fabro-server/src/diagnostics.rs index ab1a87bd0..297ee3347 100644 --- a/lib/apps/fabro-server/src/diagnostics.rs +++ b/lib/apps/fabro-server/src/diagnostics.rs @@ -12,8 +12,8 @@ use fabro_model::{Catalog, ProviderId}; use fabro_redact::redact_string; use fabro_sandbox::{DockerSandboxProvider, daytona}; use fabro_static::EnvVars; +use fabro_types::settings::ServerAuthMethod; use fabro_types::settings::server::GithubIntegrationStrategy; -use fabro_types::settings::{SearchProvider, ServerAuthMethod}; use fabro_util::check_report::{CheckDetail, CheckResult, CheckSection, CheckStatus}; use fabro_util::dev_token::validate_dev_token_format; use fabro_util::session_secret; @@ -758,17 +758,7 @@ fn check_storage_dir_path(path: &std::path::Path) -> CheckResult { } async fn check_web_search(state: &AppState) -> CheckResult { - let search = state.server_settings().server.integrations.search; - match search.provider { - SearchProvider::Brave => check_brave_search(state).await, - SearchProvider::Venice => check_venice_search(state).await, - } -} - -const WEB_SEARCH_CHECK_NAME: &str = "Web Search"; - -async fn check_brave_search(state: &AppState) -> CheckResult { - let api_key = match diagnostic_secret( + let brave_api_key = match diagnostic_secret( state, WEB_SEARCH_CHECK_NAME, EnvVars::BRAVE_SEARCH_API_KEY, @@ -778,18 +768,33 @@ async fn check_brave_search(state: &AppState) -> CheckResult { Ok(value) => value, Err(result) => return result, }; - let Some(api_key) = api_key else { - return CheckResult { - name: WEB_SEARCH_CHECK_NAME.to_string(), - status: CheckStatus::Warning, - summary: "brave: optional, not configured".to_string(), - details: Vec::new(), - remediation: Some( - "Run `fabro secret set BRAVE_SEARCH_API_KEY` to enable web search".to_string(), - ), - }; - }; + if let Some(api_key) = brave_api_key { + return check_brave_search(api_key).await; + } + let venice_api_key = + match diagnostic_secret(state, WEB_SEARCH_CHECK_NAME, EnvVars::VENICE_API_KEY).await { + Ok(value) => value, + Err(result) => return result, + }; + if let Some(api_key) = venice_api_key { + return check_venice_search(api_key).await; + } + + CheckResult { + name: WEB_SEARCH_CHECK_NAME.to_string(), + status: CheckStatus::Warning, + summary: "optional, not configured".to_string(), + details: Vec::new(), + remediation: Some( + "Run `fabro secret set BRAVE_SEARCH_API_KEY` or `fabro secret set VENICE_API_KEY` to enable web search".to_string(), + ), + } +} + +const WEB_SEARCH_CHECK_NAME: &str = "Web Search"; + +async fn check_brave_search(api_key: String) -> CheckResult { let http = match http_client_or_check(WEB_SEARCH_CHECK_NAME, CheckStatus::Warning) { Ok(http) => http, Err(result) => return result, @@ -807,24 +812,7 @@ async fn check_brave_search(state: &AppState) -> CheckResult { match_web_search_probe(probe, "brave", "BRAVE_SEARCH_API_KEY") } -async fn check_venice_search(state: &AppState) -> CheckResult { - let api_key = - match diagnostic_secret(state, WEB_SEARCH_CHECK_NAME, EnvVars::VENICE_API_KEY).await { - Ok(value) => value, - Err(result) => return result, - }; - let Some(api_key) = api_key else { - return CheckResult { - name: WEB_SEARCH_CHECK_NAME.to_string(), - status: CheckStatus::Warning, - summary: "venice: optional, not configured".to_string(), - details: Vec::new(), - remediation: Some( - "Run `fabro secret set VENICE_API_KEY` to enable web search".to_string(), - ), - }; - }; - +async fn check_venice_search(api_key: String) -> CheckResult { let http = match http_client_or_check(WEB_SEARCH_CHECK_NAME, CheckStatus::Warning) { Ok(http) => http, Err(result) => return result, @@ -833,7 +821,11 @@ async fn check_venice_search(state: &AppState) -> CheckResult { let probe = timeout(EXTERNAL_SERVICE_PROBE_TIMEOUT, async move { http.post("https://api.venice.ai/api/v1/augment/search") .bearer_auth(api_key) - .json(&serde_json::json!({ "query": "test", "limit": 1 })) + .json(&serde_json::json!({ + "query": "test", + "limit": 1, + "search_provider": "brave", + })) .send() .await .map_err(anyhow::Error::new) @@ -1256,7 +1248,7 @@ enabled = false } #[tokio::test] - async fn check_web_search_ignores_env_backed_api_key() { + async fn check_web_search_ignores_env_backed_brave_api_key() { let state = TestAppStateBuilder::new() .env_lookup(|name| { (name == EnvVars::BRAVE_SEARCH_API_KEY).then(|| "brave-from-env".to_string()) @@ -1267,29 +1259,18 @@ enabled = false assert_eq!(result.name, "Web Search"); assert_eq!(result.status, CheckStatus::Warning); - assert_eq!(result.summary, "brave: optional, not configured"); + assert_eq!(result.summary, "optional, not configured"); assert_eq!( result.remediation.as_deref(), - Some("Run `fabro secret set BRAVE_SEARCH_API_KEY` to enable web search") + Some( + "Run `fabro secret set BRAVE_SEARCH_API_KEY` or `fabro secret set VENICE_API_KEY` to enable web search" + ) ); } #[tokio::test] - async fn check_web_search_venice_ignores_env_backed_api_key() { - let settings = fabro_config::ServerSettingsBuilder::from_toml( - r#" -_version = 1 - -[server.auth] -methods = ["dev-token"] - -[server.integrations.search] -provider = "venice" -"#, - ) - .expect("venice search settings should parse"); + async fn check_web_search_ignores_env_backed_venice_api_key() { let state = TestAppStateBuilder::new() - .runtime_settings(settings, RunLayer::default()) .env_lookup(|name| { (name == EnvVars::VENICE_API_KEY).then(|| "venice-from-env".to_string()) }) @@ -1299,13 +1280,44 @@ provider = "venice" assert_eq!(result.name, "Web Search"); assert_eq!(result.status, CheckStatus::Warning); - assert_eq!(result.summary, "venice: optional, not configured"); + assert_eq!(result.summary, "optional, not configured"); assert_eq!( result.remediation.as_deref(), - Some("Run `fabro secret set VENICE_API_KEY` to enable web search") + Some( + "Run `fabro secret set BRAVE_SEARCH_API_KEY` or `fabro secret set VENICE_API_KEY` to enable web search" + ) ); } + #[tokio::test] + async fn check_web_search_prefers_brave_when_both_vault_keys_exist() { + let state = TestAppStateBuilder::new() + .vault_entries([ + (EnvVars::BRAVE_SEARCH_API_KEY, "invalid\n"), + (EnvVars::VENICE_API_KEY, "invalid\n"), + ]) + .build(); + + let result = check_web_search(&state).await; + + assert_eq!(result.name, "Web Search"); + assert_eq!(result.status, CheckStatus::Warning); + assert_eq!(result.summary, "brave: connectivity error"); + } + + #[tokio::test] + async fn check_web_search_uses_venice_when_brave_vault_key_is_absent() { + let state = TestAppStateBuilder::new() + .vault_entries([(EnvVars::VENICE_API_KEY, "invalid\n")]) + .build(); + + let result = check_web_search(&state).await; + + assert_eq!(result.name, "Web Search"); + assert_eq!(result.status, CheckStatus::Warning); + assert_eq!(result.summary, "venice: connectivity error"); + } + #[tokio::test] async fn check_crypto_requires_github_client_secret_from_vault() { let settings = fabro_config::ServerSettingsBuilder::from_toml( diff --git a/lib/components/fabro-agent/src/cli.rs b/lib/components/fabro-agent/src/cli.rs index 63c485f20..f7cfdd7f9 100644 --- a/lib/components/fabro-agent/src/cli.rs +++ b/lib/components/fabro-agent/src/cli.rs @@ -34,7 +34,7 @@ use crate::tool_permissions::{is_auto_approved, tool_category}; use crate::tools::WebFetchSummarizer; use crate::{ AgentEvent, AgentProfile, AgentProfileBuilder, LocalSandbox, Message, Sandbox, Session, - SessionOptions, SessionShutdownReason, search_settings_from_disk, + SessionOptions, SessionShutdownReason, }; #[expect( @@ -45,7 +45,6 @@ fn cli_tool_secrets() -> ToolSecrets { ToolSecrets { brave_search_api_key: std::env::var(EnvVars::BRAVE_SEARCH_API_KEY).ok(), venice_api_key: std::env::var(EnvVars::VENICE_API_KEY).ok(), - search: search_settings_from_disk(), } } diff --git a/lib/components/fabro-agent/src/config.rs b/lib/components/fabro-agent/src/config.rs index 44a9db968..cc207c3f4 100644 --- a/lib/components/fabro-agent/src/config.rs +++ b/lib/components/fabro-agent/src/config.rs @@ -6,7 +6,6 @@ use fabro_llm::types::{ReasoningEffort, Speed}; use fabro_mcp::config::McpServerSettings; use fabro_model::AgentProfileKind; use fabro_types::PermissionLevel; -use fabro_types::settings::SearchIntegrationSettings; /// Callback invoked before each tool execution. Return `Ok(())` to allow, /// `Err(message)` to deny with the given message. @@ -105,7 +104,6 @@ impl ToolHookCallback for ToolApprovalAdapter { pub struct ToolSecrets { pub brave_search_api_key: Option, pub venice_api_key: Option, - pub search: SearchIntegrationSettings, } impl std::fmt::Debug for ToolSecrets { @@ -116,7 +114,6 @@ impl std::fmt::Debug for ToolSecrets { &self.brave_search_api_key.is_some(), ) .field("venice_search_configured", &self.venice_api_key.is_some()) - .field("search_provider", &self.search.provider.as_str()) .finish() } } @@ -355,15 +352,13 @@ mod tests { fn tool_secrets_debug_redacts_values() { let secrets = ToolSecrets { brave_search_api_key: Some("brave-secret-value".to_string()), - venice_api_key: Some("venice-secret-value".to_string()), - ..ToolSecrets::default() + venice_api_key: Some("venice-secret-value".to_string()), }; let debug = format!("{secrets:?}"); assert!(debug.contains("brave_search_configured: true")); assert!(debug.contains("venice_search_configured: true")); - assert!(debug.contains("search_provider: \"brave\"")); assert!(!debug.contains("brave-secret-value")); assert!(!debug.contains("venice-secret-value")); } diff --git a/lib/components/fabro-agent/src/lib.rs b/lib/components/fabro-agent/src/lib.rs index f7600a7f6..9f982f47a 100644 --- a/lib/components/fabro-agent/src/lib.rs +++ b/lib/components/fabro-agent/src/lib.rs @@ -86,7 +86,6 @@ pub use types::{ AgentEvent, McpToolSummary, MemoryFileSummary, Message, SessionEvent, SessionState, SkillActivationSource, SkillSummary, }; -pub use web_search::search_settings_from_disk; #[cfg(test)] #[allow( diff --git a/lib/components/fabro-agent/src/profiles/claude5_tools.rs b/lib/components/fabro-agent/src/profiles/claude5_tools.rs index 140e8502a..6f53622b0 100644 --- a/lib/components/fabro-agent/src/profiles/claude5_tools.rs +++ b/lib/components/fabro-agent/src/profiles/claude5_tools.rs @@ -116,28 +116,19 @@ pub(crate) fn make_bash_tool(options: &NativeToolOptions) -> RegisteredTool { #[must_use] pub(crate) fn make_web_search_tool(backend: SearchBackend) -> RegisteredTool { - let includes_engine = backend.includes_engine_param(); let mut tool = web_search::make_web_search_tool(backend); - let mut properties = serde_json::json!({ - "query": { - "type": "string", - "description": "The web search query." - } - }); - if includes_engine { - properties["engine"] = serde_json::json!({ - "type": "string", - "enum": ["brave", "google"], - "description": "Venice search engine. brave is ZDR (default); google is an anonymized proxy." - }); - } tool.definition = definition( NativeTool::WebSearch, "Search the web when current external information is needed. Returns result titles, URLs, \ and descriptions; use WebFetch to inspect a specific URL.", serde_json::json!({ "type": "object", - "properties": properties, + "properties": { + "query": { + "type": "string", + "description": "The web search query." + } + }, "required": ["query"], "additionalProperties": false }), @@ -468,7 +459,6 @@ mod tests { use std::collections::BTreeSet; use std::sync::Mutex; - use fabro_types::settings::VeniceSearchEngine; use serde_json::json; use tokio_util::sync::CancellationToken; @@ -553,11 +543,8 @@ mod tests { &["query"], ); assert_schema( - &make_web_search_tool(SearchBackend::venice( - "key".to_string(), - VeniceSearchEngine::Brave, - )), - &["engine", "query"], + &make_web_search_tool(SearchBackend::venice("key".to_string())), + &["query"], &["query"], ); diff --git a/lib/components/fabro-agent/src/profiles/prompts/openai.md.j2 b/lib/components/fabro-agent/src/profiles/prompts/openai.md.j2 index db180ee8c..2bf337ea9 100644 --- a/lib/components/fabro-agent/src/profiles/prompts/openai.md.j2 +++ b/lib/components/fabro-agent/src/profiles/prompts/openai.md.j2 @@ -70,7 +70,7 @@ Search file contents with regex. Use glob_filter to narrow results. Find files by name pattern. {% if inputs.has_web_search %}## web_search -Search the web using Brave Search. Returns titles, URLs, and descriptions. +Search the web. Returns titles, URLs, and descriptions. {% endif %}## web_fetch Fetch content from a URL and optionally summarize it. Pass a prompt to extract specific information instead of returning the full page. URLs must start with http:// or https://. diff --git a/lib/components/fabro-agent/src/profiles/snapshots/fabro_agent__profiles__tests__openai_apply_patch_and_web_search_prompt_snapshot.snap b/lib/components/fabro-agent/src/profiles/snapshots/fabro_agent__profiles__tests__openai_apply_patch_and_web_search_prompt_snapshot.snap index 14b7c7cd0..bd48ea598 100644 --- a/lib/components/fabro-agent/src/profiles/snapshots/fabro_agent__profiles__tests__openai_apply_patch_and_web_search_prompt_snapshot.snap +++ b/lib/components/fabro-agent/src/profiles/snapshots/fabro_agent__profiles__tests__openai_apply_patch_and_web_search_prompt_snapshot.snap @@ -78,7 +78,7 @@ Search file contents with regex. Use glob_filter to narrow results. Find files by name pattern. ## web_search -Search the web using Brave Search. Returns titles, URLs, and descriptions. +Search the web. Returns titles, URLs, and descriptions. ## web_fetch Fetch content from a URL and optionally summarize it. Pass a prompt to extract specific information instead of returning the full page. URLs must start with http:// or https://. diff --git a/lib/components/fabro-agent/src/profiles/snapshots/fabro_agent__profiles__tests__openai_edit_file_and_web_search_prompt_snapshot.snap b/lib/components/fabro-agent/src/profiles/snapshots/fabro_agent__profiles__tests__openai_edit_file_and_web_search_prompt_snapshot.snap index 43e52f68b..0cd769362 100644 --- a/lib/components/fabro-agent/src/profiles/snapshots/fabro_agent__profiles__tests__openai_edit_file_and_web_search_prompt_snapshot.snap +++ b/lib/components/fabro-agent/src/profiles/snapshots/fabro_agent__profiles__tests__openai_edit_file_and_web_search_prompt_snapshot.snap @@ -68,7 +68,7 @@ Search file contents with regex. Use glob_filter to narrow results. Find files by name pattern. ## web_search -Search the web using Brave Search. Returns titles, URLs, and descriptions. +Search the web. Returns titles, URLs, and descriptions. ## web_fetch Fetch content from a URL and optionally summarize it. Pass a prompt to extract specific information instead of returning the full page. URLs must start with http:// or https://. diff --git a/lib/components/fabro-agent/src/tools.rs b/lib/components/fabro-agent/src/tools.rs index 9cbeb571b..6d9f673e1 100644 --- a/lib/components/fabro-agent/src/tools.rs +++ b/lib/components/fabro-agent/src/tools.rs @@ -50,14 +50,14 @@ fn html_to_markdown(text: &str) -> String { converter.convert(text).unwrap_or_else(|_| text.to_string()) } -/// Name of the Brave-backed web search tool. Profiles look this up in their own -/// registry to decide whether to advertise web search in the system prompt, so -/// availability and prompt guidance cannot drift apart. +/// Name of the credential-backed web search tool. Profiles look this up in +/// their own registry to decide whether to advertise web search in the system +/// prompt, so availability and prompt guidance cannot drift apart. pub const WEB_SEARCH_TOOL_NAME: &str = "web_search"; /// Registers the core tools shared by all provider profiles: `read_file`, /// `write_file`, `shell`, `grep`, `glob`, and `web_fetch`. `web_search` is -/// included when a Brave Search API key is configured. +/// included when a Brave or Venice Search API key is configured. /// /// The shell tool captures its default and max timeouts from `options`. pub fn register_core_tools( @@ -83,7 +83,7 @@ pub(crate) fn register_discovery_and_web_tools( registry.register(make_web_fetch_tool(summarizer)); } -/// Register `web_search` when the selected search provider is configured. +/// Register `web_search` when a search provider credential is configured. /// /// Separate from [`register_discovery_and_web_tools`] for profiles that offer /// search without fabro's discovery tools. diff --git a/lib/components/fabro-agent/src/web_search.rs b/lib/components/fabro-agent/src/web_search.rs index 11fe4c7bf..08c23863a 100644 --- a/lib/components/fabro-agent/src/web_search.rs +++ b/lib/components/fabro-agent/src/web_search.rs @@ -1,14 +1,13 @@ //! Built-in `web_search` backends. //! -//! Agents always call the same tool. The HTTP backend is selected by -//! `[server.integrations.search].provider`. +//! Agents always call the same tool. Brave is preferred when its credential +//! is present; otherwise Venice is used when its credential is present. use std::fmt::Write; use std::sync::OnceLock; use std::time::Duration; use fabro_llm::types::ToolDefinition; -use fabro_types::settings::{SearchIntegrationSettings, SearchProvider, VeniceSearchEngine}; use crate::config::ToolSecrets; use crate::tool_registry::{RegisteredTool, ToolSource}; @@ -29,7 +28,6 @@ pub(crate) enum SearchBackend { }, Venice { api_key: String, - engine: VeniceSearchEngine, search_url: String, }, } @@ -37,15 +35,13 @@ pub(crate) enum SearchBackend { impl SearchBackend { #[must_use] pub(crate) fn from_secrets(secrets: &ToolSecrets) -> Option { - match secrets.search.provider { - SearchProvider::Brave => secrets - .brave_search_api_key - .as_ref() - .map(|api_key| Self::brave(api_key.clone())), - SearchProvider::Venice => secrets - .venice_api_key - .as_ref() - .map(|api_key| Self::venice(api_key.clone(), secrets.search.venice_engine)), + match ( + secrets.brave_search_api_key.as_ref(), + secrets.venice_api_key.as_ref(), + ) { + (Some(api_key), _) => Some(Self::brave(api_key.clone())), + (None, Some(api_key)) => Some(Self::venice(api_key.clone())), + (None, None) => None, } } @@ -58,25 +54,14 @@ impl SearchBackend { } #[must_use] - pub(crate) fn venice(api_key: String, engine: VeniceSearchEngine) -> Self { + pub(crate) fn venice(api_key: String) -> Self { Self::Venice { api_key, - engine, search_url: VENICE_SEARCH_URL.to_string(), } } - #[must_use] - pub(crate) fn includes_engine_param(&self) -> bool { - matches!(self, Self::Venice { .. }) - } - - async fn search( - &self, - query: &str, - max_results: u64, - engine_override: Option, - ) -> Result { + async fn search(&self, query: &str, max_results: u64) -> Result { match self { Self::Brave { api_key, @@ -84,7 +69,6 @@ impl SearchBackend { } => search_brave(api_key, search_url, query, max_results).await, Self::Venice { api_key, - engine, search_url, } => { if query.chars().count() > VENICE_QUERY_MAX_CHARS { @@ -92,8 +76,7 @@ impl SearchBackend { "query exceeds Venice Search maximum of {VENICE_QUERY_MAX_CHARS} characters" )); } - let engine = engine_override.unwrap_or(*engine); - search_venice(api_key, search_url, query, max_results, engine).await + search_venice(api_key, search_url, query, max_results).await } } } @@ -150,7 +133,6 @@ async fn search_venice( search_url: &str, query: &str, max_results: u64, - engine: VeniceSearchEngine, ) -> Result { let limit = max_results.clamp(1, MAX_RESULTS); let resp = search_http_client() @@ -161,7 +143,7 @@ async fn search_venice( .json(&serde_json::json!({ "query": query, "limit": limit, - "search_provider": engine.as_str(), + "search_provider": "brave", })) .send() .await @@ -277,16 +259,6 @@ fn optional_json_str(value: &serde_json::Value, key: &str) -> Option { .map(str::to_owned) } -fn parse_engine_arg(args: &serde_json::Value) -> Result, String> { - let Some(value) = args.get("engine").and_then(serde_json::Value::as_str) else { - return Ok(None); - }; - value - .parse() - .map(Some) - .map_err(|_| format!("Invalid engine `{value}`; expected `brave` or `google`")) -} - fn max_results_arg(args: &serde_json::Value) -> u64 { args.get("max_results") .and_then(serde_json::Value::as_u64) @@ -296,25 +268,16 @@ fn max_results_arg(args: &serde_json::Value) -> u64 { #[must_use] pub(crate) fn make_web_search_tool(backend: SearchBackend) -> RegisteredTool { - let mut properties = serde_json::json!({ - "query": {"type": "string", "description": "Search query"}, - "max_results": {"type": "integer", "description": "Maximum number of results (default 5, max 20)"} - }); - if backend.includes_engine_param() { - properties["engine"] = serde_json::json!({ - "type": "string", - "enum": ["brave", "google"], - "description": "Venice search engine. `brave` is ZDR (default); `google` is an anonymized proxy." - }); - } - RegisteredTool { definition: ToolDefinition { name: WEB_SEARCH_TOOL_NAME.into(), description: "Search the web when current external information is needed. Returns result titles, URLs, and descriptions; use web_fetch for a specific URL.".into(), parameters: serde_json::json!({ "type": "object", - "properties": properties, + "properties": { + "query": {"type": "string", "description": "Search query"}, + "max_results": {"type": "integer", "description": "Maximum number of results (default 5, max 20)"} + }, "required": ["query"] }), }, @@ -322,10 +285,7 @@ pub(crate) fn make_web_search_tool(backend: SearchBackend) -> RegisteredTool { let backend = backend.clone(); Box::pin(async move { let query = required_str(&args, "query")?; - let engine = parse_engine_arg(&args)?; - backend - .search(query, max_results_arg(&args), engine) - .await + backend.search(query, max_results_arg(&args)).await }) }), source: ToolSource::Native, @@ -338,19 +298,10 @@ pub(crate) fn make_web_search_tool_with_api_key(api_key: String) -> RegisteredTo make_web_search_tool(SearchBackend::brave(api_key)) } -#[must_use] -pub fn search_settings_from_disk() -> SearchIntegrationSettings { - fabro_config::ServerSettingsBuilder::load_default() - .ok() - .map(|settings| settings.server.integrations.search) - .unwrap_or_default() -} - #[cfg(test)] mod tests { use std::sync::Arc; - use fabro_types::settings::{SearchIntegrationSettings, SearchProvider, VeniceSearchEngine}; use httpmock::Method::{GET, POST}; use httpmock::MockServer; use tokio_util::sync::CancellationToken; @@ -361,14 +312,10 @@ mod tests { use crate::test_support::MockSandbox; use crate::tool_registry::ToolContext; - fn secrets(brave: Option<&str>, venice: Option<&str>, provider: SearchProvider) -> ToolSecrets { + fn secrets(brave: Option<&str>, venice: Option<&str>) -> ToolSecrets { ToolSecrets { brave_search_api_key: brave.map(str::to_string), venice_api_key: venice.map(str::to_string), - search: SearchIntegrationSettings { - provider, - venice_engine: VeniceSearchEngine::Brave, - }, } } @@ -387,39 +334,26 @@ mod tests { } #[test] - fn from_secrets_registers_brave_by_default_when_brave_key_is_present() { - let backend = SearchBackend::from_secrets(&secrets( - Some("brave-key"), - Some("venice-key"), - SearchProvider::Brave, - )); + fn from_secrets_prefers_brave_when_both_keys_are_present() { + let backend = SearchBackend::from_secrets(&secrets(Some("brave-key"), Some("venice-key"))); assert!(matches!(backend, Some(SearchBackend::Brave { .. }))); } #[test] - fn from_secrets_omits_brave_when_key_missing() { - assert!( - SearchBackend::from_secrets(&secrets(None, Some("venice-key"), SearchProvider::Brave)) - .is_none() - ); + fn from_secrets_registers_brave_when_only_brave_key_is_present() { + let backend = SearchBackend::from_secrets(&secrets(Some("brave-key"), None)); + assert!(matches!(backend, Some(SearchBackend::Brave { .. }))); } #[test] - fn from_secrets_registers_venice_when_selected_and_key_present() { - let backend = SearchBackend::from_secrets(&secrets( - Some("brave-key"), - Some("venice-key"), - SearchProvider::Venice, - )); + fn from_secrets_registers_venice_when_only_venice_key_is_present() { + let backend = SearchBackend::from_secrets(&secrets(None, Some("venice-key"))); assert!(matches!(backend, Some(SearchBackend::Venice { .. }))); } #[test] - fn from_secrets_omits_venice_when_key_missing() { - assert!( - SearchBackend::from_secrets(&secrets(Some("brave-key"), None, SearchProvider::Venice)) - .is_none() - ); + fn from_secrets_omits_search_when_both_keys_are_missing() { + assert!(SearchBackend::from_secrets(&secrets(None, None)).is_none()); } #[test] @@ -466,26 +400,14 @@ mod tests { } #[test] - fn venice_schema_includes_engine_and_brave_schema_does_not() { + fn brave_and_venice_use_the_same_tool_schema() { let brave = make_web_search_tool(SearchBackend::brave("key".into())); - let venice = make_web_search_tool(SearchBackend::venice( - "key".into(), - VeniceSearchEngine::Brave, - )); - assert!( - brave.definition.parameters["properties"] - .get("engine") - .is_none() - ); - assert!( - venice.definition.parameters["properties"] - .get("engine") - .is_some() - ); + let venice = make_web_search_tool(SearchBackend::venice("key".into())); + assert_eq!(brave.definition.parameters, venice.definition.parameters); } #[tokio::test] - async fn venice_search_posts_augment_search_and_maps_engine() { + async fn venice_search_posts_augment_search_with_brave_engine() { let server = MockServer::start(); let mock = server.mock(|when, then| { when.method(POST) @@ -494,7 +416,7 @@ mod tests { .json_body(serde_json::json!({ "query": "fabro", "limit": 3, - "search_provider": "google" + "search_provider": "brave" })); then.status(200).json_body(serde_json::json!({ "query": "fabro", @@ -507,7 +429,7 @@ mod tests { })); }); - let mut backend = SearchBackend::venice("venice-key".into(), VeniceSearchEngine::Brave); + let mut backend = SearchBackend::venice("venice-key".into()); if let SearchBackend::Venice { search_url, .. } = &mut backend { *search_url = format!("{}/api/v1/augment/search", server.base_url()); } @@ -516,8 +438,7 @@ mod tests { &tool, serde_json::json!({ "query": "fabro", - "max_results": 3, - "engine": "google" + "max_results": 3 }), ) .await @@ -539,7 +460,7 @@ mod tests { .json_body(serde_json::json!({"results": []})); }); - let mut backend = SearchBackend::venice("venice-key".into(), VeniceSearchEngine::Brave); + let mut backend = SearchBackend::venice("venice-key".into()); if let SearchBackend::Venice { search_url, .. } = &mut backend { *search_url = format!("{}/api/v1/augment/search", server.base_url()); } @@ -567,7 +488,7 @@ mod tests { then.status(status).body("error"); }), }; - let mut backend = SearchBackend::venice("venice-key".into(), VeniceSearchEngine::Brave); + let mut backend = SearchBackend::venice("venice-key".into()); if let SearchBackend::Venice { search_url, .. } = &mut backend { *search_url = format!("{}/api/v1/augment/search", server.base_url()); } diff --git a/lib/components/fabro-workflow/src/pipeline/initialize.rs b/lib/components/fabro-workflow/src/pipeline/initialize.rs index 275e20527..c692b9627 100644 --- a/lib/components/fabro-workflow/src/pipeline/initialize.rs +++ b/lib/components/fabro-workflow/src/pipeline/initialize.rs @@ -239,7 +239,6 @@ async fn tool_secrets_from_configured_sources(vault: &Arc>) - ToolSecrets { brave_search_api_key: vault.get(EnvVars::BRAVE_SEARCH_API_KEY).map(str::to_string), venice_api_key: vault.get(EnvVars::VENICE_API_KEY).map(str::to_string), - search: fabro_agent::search_settings_from_disk(), } } diff --git a/lib/foundation/fabro-api/build.rs b/lib/foundation/fabro-api/build.rs index 577746fa4..0fb3cccd0 100644 --- a/lib/foundation/fabro-api/build.rs +++ b/lib/foundation/fabro-api/build.rs @@ -324,21 +324,6 @@ fn main() { "fabro_types::settings::server::SlackIntegrationSettings", &[], ), - ( - "SearchIntegrationSettings", - "fabro_types::settings::server::SearchIntegrationSettings", - &[], - ), - ( - "SearchProvider", - "fabro_types::settings::server::SearchProvider", - &[], - ), - ( - "VeniceSearchEngine", - "fabro_types::settings::server::VeniceSearchEngine", - &[], - ), ( "IntegrationWebhooksSettings", "fabro_types::settings::server::IntegrationWebhooksSettings", diff --git a/lib/foundation/fabro-api/src/lib.rs b/lib/foundation/fabro-api/src/lib.rs index 97eb3c35b..c53e682c7 100644 --- a/lib/foundation/fabro-api/src/lib.rs +++ b/lib/foundation/fabro-api/src/lib.rs @@ -28,12 +28,11 @@ pub mod types { pub use fabro_types::settings::run::{McpHttpProtocol, RunModelControls, RunModelSettings}; pub use fabro_types::settings::server::{ GithubIntegrationSettings, GithubIntegrationStrategy, IntegrationWebhooksSettings, - LogDestination, ObjectStoreSettings, SearchIntegrationSettings, SearchProvider, - ServerApiSettings, ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod, - ServerAuthSettings, ServerIntegrationsSettings, ServerListenSettings, - ServerLoggingSettings, ServerSandboxProviderSettings, ServerSandboxProvidersSettings, - ServerSandboxSettings, ServerSchedulerSettings, ServerSlateDbSettings, - ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings, VeniceSearchEngine, + LogDestination, ObjectStoreSettings, ServerApiSettings, ServerArtifactsSettings, + ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings, ServerIntegrationsSettings, + ServerListenSettings, ServerLoggingSettings, ServerSandboxProviderSettings, + ServerSandboxProvidersSettings, ServerSandboxSettings, ServerSchedulerSettings, + ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings, WebhookStrategy, }; pub use fabro_types::settings::{McpTransport, ServerNamespace}; diff --git a/lib/foundation/fabro-config/src/layers/combine.rs b/lib/foundation/fabro-config/src/layers/combine.rs index f3ef27232..aec393777 100644 --- a/lib/foundation/fabro-config/src/layers/combine.rs +++ b/lib/foundation/fabro-config/src/layers/combine.rs @@ -7,8 +7,8 @@ use fabro_types::settings::run::{ ApprovalMode, EnvironmentNetworkMode, EnvironmentProvider, MergeStrategy, RunMode, }; use fabro_types::settings::server::{ - GithubIntegrationStrategy, LogDestination, ObjectStoreProvider, SearchProvider, - ServerAuthMethod, VeniceSearchEngine, WebhookStrategy, + GithubIntegrationStrategy, LogDestination, ObjectStoreProvider, ServerAuthMethod, + WebhookStrategy, }; use fabro_types::settings::{Duration, InterpString, Size}; @@ -82,9 +82,7 @@ impl_combine_or_option!( GithubIntegrationStrategy, LogDestination, ObjectStoreProvider, - SearchProvider, ServerAuthMethod, - VeniceSearchEngine, WebhookStrategy, LogFilter, AgentProfileKind, diff --git a/lib/foundation/fabro-config/src/layers/mod.rs b/lib/foundation/fabro-config/src/layers/mod.rs index 253345227..c3fa1632c 100644 --- a/lib/foundation/fabro-config/src/layers/mod.rs +++ b/lib/foundation/fabro-config/src/layers/mod.rs @@ -39,8 +39,8 @@ pub use run::{ }; pub use server::{ GithubIntegrationLayer, IntegrationWebhooksLayer, ObjectStoreLocalLayer, ObjectStoreS3Layer, - SearchIntegrationLayer, ServerApiLayer, ServerArtifactsLayer, ServerAuthGithubLayer, - ServerAuthLayer, ServerIntegrationsLayer, ServerLayer, ServerListenLayer, ServerLoggingLayer, + ServerApiLayer, ServerArtifactsLayer, ServerAuthGithubLayer, ServerAuthLayer, + ServerIntegrationsLayer, ServerLayer, ServerListenLayer, ServerLoggingLayer, ServerSandboxLayer, ServerSandboxProviderLayer, ServerSandboxProvidersLayer, ServerSchedulerLayer, ServerSlateDbLayer, ServerStorageLayer, ServerWebLayer, SlackIntegrationLayer, diff --git a/lib/foundation/fabro-config/src/layers/server.rs b/lib/foundation/fabro-config/src/layers/server.rs index 867a8da22..351d4da68 100644 --- a/lib/foundation/fabro-config/src/layers/server.rs +++ b/lib/foundation/fabro-config/src/layers/server.rs @@ -1,8 +1,8 @@ //! Sparse `[server]` settings layer definitions. use fabro_types::settings::server::{ - GithubIntegrationStrategy, LogDestination, ObjectStoreProvider, SearchProvider, - ServerAuthMethod, VeniceSearchEngine, WebhookStrategy, + GithubIntegrationStrategy, LogDestination, ObjectStoreProvider, ServerAuthMethod, + WebhookStrategy, }; use fabro_types::settings::{Duration, InterpString}; use serde::{Deserialize, Serialize}; @@ -206,8 +206,6 @@ pub struct ServerIntegrationsLayer { pub github: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub slack: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub search: Option, } /// `[server.integrations.github]` — GitHub App, credentials, and inbound @@ -239,16 +237,6 @@ pub struct SlackIntegrationLayer { pub default_channel: Option, } -/// `[server.integrations.search]` — backend for the built-in `web_search` tool. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)] -#[serde(deny_unknown_fields)] -pub struct SearchIntegrationLayer { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub provider: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub venice_engine: Option, -} - #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)] #[serde(deny_unknown_fields)] pub struct IntegrationWebhooksLayer { diff --git a/lib/foundation/fabro-config/src/lib.rs b/lib/foundation/fabro-config/src/lib.rs index 43bc1c5ab..f6097b9be 100644 --- a/lib/foundation/fabro-config/src/lib.rs +++ b/lib/foundation/fabro-config/src/lib.rs @@ -53,12 +53,11 @@ pub use layers::{ RunArtifactsLayer, RunCheckpointLayer, RunCloneLayer, RunEnvironmentLayer, RunExecutionLayer, RunGitLayer, RunGoalLayer, RunIntegrationsGithubLayer, RunIntegrationsLayer, RunLayer, RunMetaBranchLayer, RunModelControlsLayer, RunModelLayer, RunPrepareLayer, RunPullRequestLayer, - RunRunBranchLayer, RunScmLayer, ScmGitHubLayer, SearchIntegrationLayer, ServerApiLayer, - ServerArtifactsLayer, ServerAuthGithubLayer, ServerAuthLayer, ServerIntegrationsLayer, - ServerLayer, ServerListenLayer, ServerLoggingLayer, ServerSandboxLayer, - ServerSandboxProviderLayer, ServerSandboxProvidersLayer, ServerSchedulerLayer, - ServerSlateDbLayer, ServerStorageLayer, ServerWebLayer, SettingsLayer, SlackIntegrationLayer, - StickyMap, StringOrSplice, WorkflowLayer, + RunRunBranchLayer, RunScmLayer, ScmGitHubLayer, ServerApiLayer, ServerArtifactsLayer, + ServerAuthGithubLayer, ServerAuthLayer, ServerIntegrationsLayer, ServerLayer, + ServerListenLayer, ServerLoggingLayer, ServerSandboxLayer, ServerSandboxProviderLayer, + ServerSandboxProvidersLayer, ServerSchedulerLayer, ServerSlateDbLayer, ServerStorageLayer, + ServerWebLayer, SettingsLayer, SlackIntegrationLayer, StickyMap, StringOrSplice, WorkflowLayer, }; pub use logging::{resolve_log_destination, resolve_log_destination_with_env}; pub use parse::ParseError; diff --git a/lib/foundation/fabro-config/src/resolve/server.rs b/lib/foundation/fabro-config/src/resolve/server.rs index 08a5900db..39b41eaf9 100644 --- a/lib/foundation/fabro-config/src/resolve/server.rs +++ b/lib/foundation/fabro-config/src/resolve/server.rs @@ -2,12 +2,12 @@ use std::path::Path; use fabro_types::settings::server::{ GithubIntegrationSettings, GithubIntegrationStrategy, IntegrationWebhooksSettings, - ObjectStoreProvider, ObjectStoreSettings, SearchIntegrationSettings, ServerApiSettings, - ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings, - ServerIntegrationsSettings, ServerListenSettings, ServerLoggingSettings, ServerNamespace, - ServerSandboxProviderSettings, ServerSandboxProvidersSettings, ServerSandboxSettings, - ServerSchedulerSettings, ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings, - SlackIntegrationSettings, WebhookStrategy, + ObjectStoreProvider, ObjectStoreSettings, ServerApiSettings, ServerArtifactsSettings, + ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings, ServerIntegrationsSettings, + ServerListenSettings, ServerLoggingSettings, ServerNamespace, ServerSandboxProviderSettings, + ServerSandboxProvidersSettings, ServerSandboxSettings, ServerSchedulerSettings, + ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings, + WebhookStrategy, }; use fabro_util::Home; @@ -375,13 +375,6 @@ fn resolve_integrations(layer: Option<&ServerIntegrationsLayer>) -> ServerIntegr } }, ), - search: layer - .and_then(|integrations| integrations.search.as_ref()) - .map(|search| SearchIntegrationSettings { - provider: search.provider.unwrap_or_default(), - venice_engine: search.venice_engine.unwrap_or_default(), - }) - .unwrap_or_default(), } } diff --git a/lib/foundation/fabro-config/src/tests/resolve_server.rs b/lib/foundation/fabro-config/src/tests/resolve_server.rs index 0e058dd12..e4eecb353 100644 --- a/lib/foundation/fabro-config/src/tests/resolve_server.rs +++ b/lib/foundation/fabro-config/src/tests/resolve_server.rs @@ -4,8 +4,8 @@ )] use fabro_types::settings::server::{ - GithubIntegrationStrategy, LogDestination, ObjectStoreSettings, SearchProvider, - ServerAuthMethod, ServerListenSettings, ServerNamespace, VeniceSearchEngine, + GithubIntegrationStrategy, LogDestination, ObjectStoreSettings, ServerAuthMethod, + ServerListenSettings, ServerNamespace, }; use fabro_util::Home; use temp_env::with_var; @@ -127,10 +127,6 @@ fn resolved_server_integrations_disable_slack_when_config_is_absent() { "enabled": false, "default_channel": null, }, - "search": { - "provider": "brave", - "venice_engine": "brave", - }, }) ); } @@ -177,43 +173,6 @@ default_channel = "#releases" ); } -#[test] -fn resolve_search_defaults_to_brave_when_config_is_absent() { - let settings = resolve_server(&parse( - r" -_version = 1 -", - )); - - assert_eq!(settings.integrations.search.provider, SearchProvider::Brave); - assert_eq!( - settings.integrations.search.venice_engine, - VeniceSearchEngine::Brave - ); -} - -#[test] -fn resolve_search_provider_venice_and_google_engine() { - let settings = resolve_server(&parse( - r#" -_version = 1 - -[server.integrations.search] -provider = "venice" -venice_engine = "google" -"#, - )); - - assert_eq!( - settings.integrations.search.provider, - SearchProvider::Venice - ); - assert_eq!( - settings.integrations.search.venice_engine, - VeniceSearchEngine::Google - ); -} - #[test] fn resolve_slack_default_channel_keeps_template_token_literal() { // `server.integrations.slack.default_channel` is a plain literal now: a diff --git a/lib/foundation/fabro-types/src/settings/mod.rs b/lib/foundation/fabro-types/src/settings/mod.rs index d7694c6fa..5bf91e4d9 100644 --- a/lib/foundation/fabro-types/src/settings/mod.rs +++ b/lib/foundation/fabro-types/src/settings/mod.rs @@ -46,11 +46,10 @@ pub use run::{ }; pub use server::{ GithubIntegrationSettings, IntegrationWebhooksSettings, LogDestination, ObjectStoreSettings, - SearchIntegrationSettings, SearchProvider, ServerApiSettings, ServerArtifactsSettings, - ServerAuthGithubSettings, ServerAuthMethod, ServerAuthSettings, ServerIntegrationsSettings, - ServerListenSettings, ServerLoggingSettings, ServerNamespace, ServerSchedulerSettings, - ServerSlateDbSettings, ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings, - VeniceSearchEngine, + ServerApiSettings, ServerArtifactsSettings, ServerAuthGithubSettings, ServerAuthMethod, + ServerAuthSettings, ServerIntegrationsSettings, ServerListenSettings, ServerLoggingSettings, + ServerNamespace, ServerSchedulerSettings, ServerSlateDbSettings, ServerStorageSettings, + ServerWebSettings, SlackIntegrationSettings, }; pub use size::{ParseSizeError, Size}; pub use workflow::WorkflowNamespace; diff --git a/lib/foundation/fabro-types/src/settings/server.rs b/lib/foundation/fabro-types/src/settings/server.rs index 4af44d9f1..ef8da68b8 100644 --- a/lib/foundation/fabro-types/src/settings/server.rs +++ b/lib/foundation/fabro-types/src/settings/server.rs @@ -242,7 +242,6 @@ pub struct ServerLoggingSettings { pub struct ServerIntegrationsSettings { pub github: GithubIntegrationSettings, pub slack: SlackIntegrationSettings, - pub search: SearchIntegrationSettings, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] @@ -270,75 +269,6 @@ impl Default for SlackIntegrationSettings { } } -/// Backend used by the built-in `web_search` tool. -#[derive( - Debug, - Clone, - Copy, - Default, - PartialEq, - Eq, - Serialize, - Deserialize, - strum::EnumString, - strum::IntoStaticStr, -)] -#[serde(rename_all = "lowercase")] -#[strum(serialize_all = "lowercase")] -pub enum SearchProvider { - #[default] - Brave, - Venice, -} - -impl SearchProvider { - #[must_use] - pub const fn as_str(self) -> &'static str { - match self { - Self::Brave => "brave", - Self::Venice => "venice", - } - } -} - -/// Venice `/augment/search` engine. `brave` is Firecrawl ZDR; `google` is an -/// anonymized proxy. Direct Brave Search remains [`SearchProvider::Brave`]. -#[derive( - Debug, - Clone, - Copy, - Default, - PartialEq, - Eq, - Serialize, - Deserialize, - strum::EnumString, - strum::IntoStaticStr, -)] -#[serde(rename_all = "lowercase")] -#[strum(serialize_all = "lowercase")] -pub enum VeniceSearchEngine { - #[default] - Brave, - Google, -} - -impl VeniceSearchEngine { - #[must_use] - pub const fn as_str(self) -> &'static str { - match self { - Self::Brave => "brave", - Self::Google => "google", - } - } -} - -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct SearchIntegrationSettings { - pub provider: SearchProvider, - pub venice_engine: VeniceSearchEngine, -} - #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct IntegrationWebhooksSettings { pub strategy: Option, diff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES index 42ca0bf8f..4f8d5f871 100644 --- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES +++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES @@ -441,8 +441,6 @@ models/saved-query.ts models/secret-list-response.ts models/secret-metadata.ts models/secret-type.ts -models/search-integration-settings.ts -models/search-provider.ts models/server-api-settings.ts models/server-artifacts-settings.ts models/server-auth-github-settings.ts @@ -534,7 +532,6 @@ models/user-response.ts models/validate-response.ts models/variable-list-response.ts models/variable.ts -models/venice-search-engine.ts models/vnc-preview-response.ts models/webhook-strategy.ts models/workflow-detail-response.ts diff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts index 12d3fa017..04df237be 100644 --- a/lib/packages/fabro-api-client/src/models/index.ts +++ b/lib/packages/fabro-api-client/src/models/index.ts @@ -410,8 +410,6 @@ export * from './saved-query'; export * from './secret-list-response'; export * from './secret-metadata'; export * from './secret-type'; -export * from './search-integration-settings'; -export * from './search-provider'; export * from './server-api-settings'; export * from './server-artifacts-settings'; export * from './server-auth-github-settings'; @@ -503,7 +501,6 @@ export * from './user-response'; export * from './validate-response'; export * from './variable'; export * from './variable-list-response'; -export * from './venice-search-engine'; export * from './vnc-preview-response'; export * from './webhook-strategy'; export * from './workflow-detail-response'; diff --git a/lib/packages/fabro-api-client/src/models/search-integration-settings.ts b/lib/packages/fabro-api-client/src/models/search-integration-settings.ts deleted file mode 100644 index 65a6902fe..000000000 --- a/lib/packages/fabro-api-client/src/models/search-integration-settings.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * Fabro Run API - * HTTP API for managing Fabro workflow run executions. - * - * The version of the OpenAPI document: 0.2.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - - -// May contain unused imports in some cases -// @ts-ignore -import type { SearchProvider } from './search-provider'; -// May contain unused imports in some cases -// @ts-ignore -import type { VeniceSearchEngine } from './venice-search-engine'; - -export interface SearchIntegrationSettings { - 'provider': SearchProvider; - 'venice_engine': VeniceSearchEngine; -} diff --git a/lib/packages/fabro-api-client/src/models/search-provider.ts b/lib/packages/fabro-api-client/src/models/search-provider.ts deleted file mode 100644 index 552ccbe54..000000000 --- a/lib/packages/fabro-api-client/src/models/search-provider.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * Fabro Run API - * HTTP API for managing Fabro workflow run executions. - * - * The version of the OpenAPI document: 0.2.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - - - - -export const SearchProvider = { - BRAVE: 'brave', - VENICE: 'venice' -} as const; - -export type SearchProvider = typeof SearchProvider[keyof typeof SearchProvider]; diff --git a/lib/packages/fabro-api-client/src/models/server-integrations-settings.ts b/lib/packages/fabro-api-client/src/models/server-integrations-settings.ts index 3525f9c5a..88b8fdd8d 100644 --- a/lib/packages/fabro-api-client/src/models/server-integrations-settings.ts +++ b/lib/packages/fabro-api-client/src/models/server-integrations-settings.ts @@ -18,13 +18,9 @@ import type { GithubIntegrationSettings } from './github-integration-settings'; // May contain unused imports in some cases // @ts-ignore -import type { SearchIntegrationSettings } from './search-integration-settings'; -// May contain unused imports in some cases -// @ts-ignore import type { SlackIntegrationSettings } from './slack-integration-settings'; export interface ServerIntegrationsSettings { 'github': GithubIntegrationSettings; 'slack': SlackIntegrationSettings; - 'search': SearchIntegrationSettings; } diff --git a/lib/packages/fabro-api-client/src/models/venice-search-engine.ts b/lib/packages/fabro-api-client/src/models/venice-search-engine.ts deleted file mode 100644 index 29e7494cf..000000000 --- a/lib/packages/fabro-api-client/src/models/venice-search-engine.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * Fabro Run API - * HTTP API for managing Fabro workflow run executions. - * - * The version of the OpenAPI document: 0.2.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - - - - -export const VeniceSearchEngine = { - BRAVE: 'brave', - GOOGLE: 'google' -} as const; - -export type VeniceSearchEngine = typeof VeniceSearchEngine[keyof typeof VeniceSearchEngine];