mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-10 03:30:59 +00:00
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 <cursoragent@cursor.com>
This commit is contained in:
parent
346e81f400
commit
53efde3930
40 changed files with 1761 additions and 684 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -2298,6 +2298,7 @@ dependencies = [
|
|||
"futures",
|
||||
"glob",
|
||||
"htmd",
|
||||
"httpmock",
|
||||
"insta",
|
||||
"jsonschema",
|
||||
"libc",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 <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 <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 <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
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
10
docs/public/changelog/2026-08-21.mdx
Normal file
10
docs/public/changelog/2026-08-21.mdx
Normal file
|
|
@ -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).
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
||||
|
|
|
|||
84
docs/public/integrations/venice-search.mdx
Normal file
84
docs/public/integrations/venice-search.mdx
Normal file
|
|
@ -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 <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
|
||||
|
||||
<Columns cols={2}>
|
||||
<Card title="Tools" icon="wrench" href="/agents/tools#web_search">
|
||||
Full `web_search` tool reference — parameters, output format, and error handling.
|
||||
</Card>
|
||||
<Card title="Brave Search" icon="globe" href="/integrations/brave-search">
|
||||
Direct Brave Search backend (the default when `provider` is unset).
|
||||
</Card>
|
||||
</Columns>
|
||||
|
|
@ -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<CheckSection>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub(crate) struct ProviderProbeReport {
|
||||
pub data: Vec<ProviderProbeResult>,
|
||||
pub data: Vec<ProviderProbeResult>,
|
||||
pub summary: ProviderProbeSummary,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub(crate) struct ProviderProbeResult {
|
||||
pub provider: ProviderId,
|
||||
pub model_id: Option<String>,
|
||||
pub status: ProviderProbeStatus,
|
||||
pub provider: ProviderId,
|
||||
pub model_id: Option<String>,
|
||||
pub status: ProviderProbeStatus,
|
||||
pub error_message: Option<String>,
|
||||
#[serde(skip)]
|
||||
diagnostic_detail: Option<String>,
|
||||
|
|
@ -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<daytona::DaytonaKeyCheck>) -> 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<daytona::DaytonaKeyCheck>) ->
|
|||
Err(err) => {
|
||||
if let Some(timeout) = err.downcast_ref::<daytona::DaytonaCredentialProbeTimeout>() {
|
||||
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<daytona::DaytonaKeyCheck>) ->
|
|||
}
|
||||
|
||||
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<anyhow::Result<Response>, 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<Option<String>, 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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
pub venice_api_key: Option<String>,
|
||||
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]
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<dyn Sandbox>,
|
||||
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<dyn Sandbox>,
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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<String> = 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<fabro_http::HttpClient> = 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<dyn Sandbox> = 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<dyn Sandbox> = 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<dyn Sandbox> = 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<dyn Sandbox> = 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<dyn Sandbox> = 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<dyn Sandbox> = 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<dyn Sandbox> = 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<dyn Sandbox> = 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<dyn Sandbox> = 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: "<html><body><h1>hello</h1></body></html>".into(),
|
||||
stderr: String::new(),
|
||||
exit_code: Some(0),
|
||||
stdout: "<html><body><h1>hello</h1></body></html>".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<dyn Sandbox> = 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<dyn Sandbox> = 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<dyn Sandbox> = Arc::new(MockSandbox {
|
||||
exec_result: ExecResult {
|
||||
stdout: "<html><body><p>Lots of content about Rust...</p></body></html>"
|
||||
.into(),
|
||||
stderr: String::new(),
|
||||
exit_code: Some(0),
|
||||
stdout: "<html><body><p>Lots of content about Rust...</p></body></html>".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<dyn Sandbox> = Arc::new(MockSandbox {
|
||||
exec_result: ExecResult {
|
||||
stdout:
|
||||
"<html><body><p>Rust is a systems programming language.</p></body></html>"
|
||||
.into(),
|
||||
stderr: String::new(),
|
||||
exit_code: Some(0),
|
||||
stdout: "<html><body><p>Rust is a systems programming language.</p></body></html>"
|
||||
.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<dyn ProviderAdapter> = 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<dyn Sandbox> = Arc::new(MockSandbox {
|
||||
exec_result: ExecResult {
|
||||
stdout: "<html><body><p>Page content</p></body></html>".into(),
|
||||
stderr: String::new(),
|
||||
exit_code: Some(0),
|
||||
stdout: "<html><body><p>Page content</p></body></html>".into(),
|
||||
stderr: String::new(),
|
||||
exit_code: Some(0),
|
||||
termination: CommandTermination::Exited,
|
||||
duration_ms: 100,
|
||||
},
|
||||
|
|
|
|||
627
lib/components/fabro-agent/src/web_search.rs
Normal file
627
lib/components/fabro-agent/src/web_search.rs
Normal file
|
|
@ -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<Self> {
|
||||
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<VeniceSearchEngine>,
|
||||
) -> Result<String, String> {
|
||||
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<fabro_http::HttpClient> = 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<String, String> {
|
||||
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<String, String> {
|
||||
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<String> {
|
||||
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<String>,
|
||||
}
|
||||
|
||||
fn format_hits(hits: Option<Vec<SearchHit>>) -> 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<String> {
|
||||
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<Option<VeniceSearchEngine>, 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<String, String> {
|
||||
let env: Arc<dyn Sandbox> = 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"));
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -235,13 +235,11 @@ async fn build_registry(
|
|||
}
|
||||
|
||||
async fn tool_secrets_from_configured_sources(vault: &Arc<AsyncRwLock<Vault>>) -> 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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<ServerListenLayer>,
|
||||
pub listen: Option<ServerListenLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub api: Option<ServerApiLayer>,
|
||||
pub api: Option<ServerApiLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub web: Option<ServerWebLayer>,
|
||||
pub web: Option<ServerWebLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub auth: Option<ServerAuthLayer>,
|
||||
pub auth: Option<ServerAuthLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub sandbox: Option<ServerSandboxLayer>,
|
||||
pub sandbox: Option<ServerSandboxLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub storage: Option<ServerStorageLayer>,
|
||||
pub storage: Option<ServerStorageLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub artifacts: Option<ServerArtifactsLayer>,
|
||||
pub artifacts: Option<ServerArtifactsLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub slatedb: Option<ServerSlateDbLayer>,
|
||||
pub slatedb: Option<ServerSlateDbLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub scheduler: Option<ServerSchedulerLayer>,
|
||||
pub scheduler: Option<ServerSchedulerLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub logging: Option<ServerLoggingLayer>,
|
||||
pub logging: Option<ServerLoggingLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub integrations: Option<ServerIntegrationsLayer>,
|
||||
}
|
||||
|
|
@ -67,7 +67,7 @@ pub struct ServerWebLayer {
|
|||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub url: Option<String>,
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
/// `[server.auth]` — cohesive server auth surface.
|
||||
|
|
@ -81,7 +81,7 @@ pub struct ServerAuthLayer {
|
|||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub methods: Option<Vec<ServerAuthMethod>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub github: Option<ServerAuthGithubLayer>,
|
||||
pub github: Option<ServerAuthGithubLayer>,
|
||||
}
|
||||
|
||||
#[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<ServerSandboxProviderLayer>,
|
||||
pub local: Option<ServerSandboxProviderLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub docker: Option<ServerSandboxProviderLayer>,
|
||||
pub docker: Option<ServerSandboxProviderLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub daytona: Option<ServerSandboxProviderLayer>,
|
||||
}
|
||||
|
|
@ -132,11 +132,11 @@ pub struct ServerArtifactsLayer {
|
|||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub provider: Option<ObjectStoreProvider>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub prefix: Option<String>,
|
||||
pub prefix: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub local: Option<ObjectStoreLocalLayer>,
|
||||
pub local: Option<ObjectStoreLocalLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub s3: Option<ObjectStoreS3Layer>,
|
||||
pub s3: Option<ObjectStoreS3Layer>,
|
||||
}
|
||||
|
||||
/// `[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<ObjectStoreProvider>,
|
||||
pub provider: Option<ObjectStoreProvider>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub prefix: Option<String>,
|
||||
pub prefix: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub flush_interval: Option<Duration>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub local: Option<ObjectStoreLocalLayer>,
|
||||
pub local: Option<ObjectStoreLocalLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub s3: Option<ObjectStoreS3Layer>,
|
||||
pub s3: Option<ObjectStoreS3Layer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub disk_cache: Option<bool>,
|
||||
pub disk_cache: Option<bool>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
pub bucket: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub region: Option<String>,
|
||||
pub region: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub endpoint: Option<String>,
|
||||
pub endpoint: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub path_style: Option<bool>,
|
||||
}
|
||||
|
|
@ -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<LogFilter>,
|
||||
pub level: Option<LogFilter>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub destination: Option<LogDestination>,
|
||||
}
|
||||
|
|
@ -205,7 +205,9 @@ pub struct ServerIntegrationsLayer {
|
|||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub github: Option<GithubIntegrationLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub slack: Option<SlackIntegrationLayer>,
|
||||
pub slack: Option<SlackIntegrationLayer>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub search: Option<SearchIntegrationLayer>,
|
||||
}
|
||||
|
||||
/// `[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<bool>,
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub strategy: Option<GithubIntegrationStrategy>,
|
||||
pub strategy: Option<GithubIntegrationStrategy>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub app_id: Option<String>,
|
||||
pub app_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub client_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub slug: Option<String>,
|
||||
pub slug: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub webhooks: Option<IntegrationWebhooksLayer>,
|
||||
pub webhooks: Option<IntegrationWebhooksLayer>,
|
||||
}
|
||||
|
||||
/// `[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<bool>,
|
||||
pub enabled: Option<bool>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub default_channel: Option<String>,
|
||||
}
|
||||
|
||||
/// `[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<SearchProvider>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub venice_engine: Option<VeniceSearchEngine>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, fabro_macros::Combine)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct IntegrationWebhooksLayer {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<ResolveError>) -> 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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<ServerAuthMethod>,
|
||||
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<String>,
|
||||
bucket: String,
|
||||
region: String,
|
||||
endpoint: Option<String>,
|
||||
path_style: bool,
|
||||
},
|
||||
}
|
||||
|
|
@ -233,7 +233,7 @@ pub enum LogDestination {
|
|||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ServerLoggingSettings {
|
||||
pub level: Option<String>,
|
||||
pub level: Option<String>,
|
||||
#[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<String>,
|
||||
pub enabled: bool,
|
||||
pub strategy: GithubIntegrationStrategy,
|
||||
pub app_id: Option<String>,
|
||||
pub client_id: Option<String>,
|
||||
pub slug: Option<String>,
|
||||
pub webhooks: Option<IntegrationWebhooksSettings>,
|
||||
pub slug: Option<String>,
|
||||
pub webhooks: Option<IntegrationWebhooksSettings>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SlackIntegrationSettings {
|
||||
pub enabled: bool,
|
||||
pub enabled: bool,
|
||||
pub default_channel: Option<String>,
|
||||
}
|
||||
|
||||
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<WebhookStrategy>,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
26
lib/packages/fabro-api-client/src/models/search-integration-settings.ts
generated
Normal file
26
lib/packages/fabro-api-client/src/models/search-integration-settings.ts
generated
Normal file
|
|
@ -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;
|
||||
}
|
||||
23
lib/packages/fabro-api-client/src/models/search-provider.ts
generated
Normal file
23
lib/packages/fabro-api-client/src/models/search-provider.ts
generated
Normal file
|
|
@ -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];
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
23
lib/packages/fabro-api-client/src/models/venice-search-engine.ts
generated
Normal file
23
lib/packages/fabro-api-client/src/models/venice-search-engine.ts
generated
Normal file
|
|
@ -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];
|
||||
Loading…
Add table
Reference in a new issue