hanzo_client/apis/websearch_api.rs
1/*
2 * Hanzo Cloud API
3 *
4 * The Hanzo Cloud API as a customer calls it: every operation under /v1/ except the operator's admin product, relay routes, legacy spellings and capabilities still reached by flag. Tagged by product: the first path segment after /v1/.
5 *
6 * The version of the OpenAPI document: v1
7 *
8 * Generated by: https://openapi-generator.tech
9 */
10
11
12use reqwest;
13use serde::{Deserialize, Serialize, de::Error as _};
14use crate::{apis::ResponseContent, models};
15use super::{Error, configuration, ContentType};
16
17
18/// struct for typed errors of method [`delete_websearch_search`]
19#[derive(Debug, Clone, Serialize, Deserialize)]
20#[serde(untagged)]
21pub enum DeleteWebsearchSearchError {
22 UnknownValue(serde_json::Value),
23}
24
25/// struct for typed errors of method [`get_websearch_search`]
26#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(untagged)]
28pub enum GetWebsearchSearchError {
29 UnknownValue(serde_json::Value),
30}
31
32/// struct for typed errors of method [`patch_websearch_search`]
33#[derive(Debug, Clone, Serialize, Deserialize)]
34#[serde(untagged)]
35pub enum PatchWebsearchSearchError {
36 UnknownValue(serde_json::Value),
37}
38
39/// struct for typed errors of method [`post_websearch_scrape`]
40#[derive(Debug, Clone, Serialize, Deserialize)]
41#[serde(untagged)]
42pub enum PostWebsearchScrapeError {
43 UnknownValue(serde_json::Value),
44}
45
46/// struct for typed errors of method [`post_websearch_search`]
47#[derive(Debug, Clone, Serialize, Deserialize)]
48#[serde(untagged)]
49pub enum PostWebsearchSearchError {
50 UnknownValue(serde_json::Value),
51}
52
53/// struct for typed errors of method [`put_websearch_search`]
54#[derive(Debug, Clone, Serialize, Deserialize)]
55#[serde(untagged)]
56pub enum PutWebsearchSearchError {
57 UnknownValue(serde_json::Value),
58}
59
60/// struct for typed errors of method [`search_web`]
61#[derive(Debug, Clone, Serialize, Deserialize)]
62#[serde(untagged)]
63pub enum SearchWebError {
64 UnknownValue(serde_json::Value),
65}
66
67
68/// Answers {query, number_of_results, results:[{url, title, content, engine}]} — the exact /search?format=json contract a SearXNG client decodes, so an agent tool configured against SearXNG reaches this with no change. `q` is the query and `language` narrows it; both are read from the QUERY STRING. Served in-process by a Go meta-search over keyless public engines, never a third-party search API and never a search key. The enabled engines run concurrently and their hits are merged, deduplicated by normalised URL (host and path, trailing slash and fragment dropped, query kept — distinct queries are distinct results) and capped at 30. Ranking is deterministic rather than scored: the first configured engine's hits lead. TWO WAYS IN, one-way equivalent, and no third: a validated principal — the same gate the whole data plane uses — passes straight through, and a caller without one must present the shared service key as X-API-Key, compared in constant time. A deployment with no key configured answers 503 rather than opening the surface to everyone, and a missing or wrong key is 401. It is never an open proxy. There is no tenant scoping beyond that gate, and there is nothing to scope: the results are public web pages, identical for every caller. It fails SOFT on the engines and closed only on the gate. An engine that errors or is served a bot-challenge page contributes zero results and never fails the request, so an empty `results` is a real answer — nothing was found — and not an outage. The array is always present, never null. The one thing to get right: every method answers identically. This is one handler registered for all of them, and it reads only the query string, so a body sent on the write verbs is ignored rather than refused.
69pub async fn delete_websearch_search(configuration: &configuration::Configuration, ) -> Result<(), Error<DeleteWebsearchSearchError>> {
70
71 let uri_str = format!("{}/v1/websearch/search", configuration.base_path);
72 let mut req_builder = configuration.client.request(reqwest::Method::DELETE, &uri_str);
73
74 if let Some(ref user_agent) = configuration.user_agent {
75 req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
76 }
77 if let Some(ref token) = configuration.bearer_access_token {
78 req_builder = req_builder.bearer_auth(token.to_owned());
79 };
80
81 let req = req_builder.build()?;
82 let resp = configuration.client.execute(req).await?;
83
84 let status = resp.status();
85
86 if !status.is_client_error() && !status.is_server_error() {
87 Ok(())
88 } else {
89 let content = resp.text().await?;
90 let entity: Option<DeleteWebsearchSearchError> = serde_json::from_str(&content).ok();
91 Err(Error::ResponseError(ResponseContent { status, content, entity }))
92 }
93}
94
95/// Answers {query, number_of_results, results:[{url, title, content, engine}]} — the exact /search?format=json contract a SearXNG client decodes, so an agent tool configured against SearXNG reaches this with no change. `q` is the query and `language` narrows it; both are read from the QUERY STRING. Served in-process by a Go meta-search over keyless public engines, never a third-party search API and never a search key. The enabled engines run concurrently and their hits are merged, deduplicated by normalised URL (host and path, trailing slash and fragment dropped, query kept — distinct queries are distinct results) and capped at 30. Ranking is deterministic rather than scored: the first configured engine's hits lead. TWO WAYS IN, one-way equivalent, and no third: a validated principal — the same gate the whole data plane uses — passes straight through, and a caller without one must present the shared service key as X-API-Key, compared in constant time. A deployment with no key configured answers 503 rather than opening the surface to everyone, and a missing or wrong key is 401. It is never an open proxy. There is no tenant scoping beyond that gate, and there is nothing to scope: the results are public web pages, identical for every caller. It fails SOFT on the engines and closed only on the gate. An engine that errors or is served a bot-challenge page contributes zero results and never fails the request, so an empty `results` is a real answer — nothing was found — and not an outage. The array is always present, never null. The one thing to get right: every method answers identically. This is one handler registered for all of them, and it reads only the query string, so a body sent on the write verbs is ignored rather than refused.
96pub async fn get_websearch_search(configuration: &configuration::Configuration, ) -> Result<(), Error<GetWebsearchSearchError>> {
97
98 let uri_str = format!("{}/v1/websearch/search", configuration.base_path);
99 let mut req_builder = configuration.client.request(reqwest::Method::GET, &uri_str);
100
101 if let Some(ref user_agent) = configuration.user_agent {
102 req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
103 }
104 if let Some(ref token) = configuration.bearer_access_token {
105 req_builder = req_builder.bearer_auth(token.to_owned());
106 };
107
108 let req = req_builder.build()?;
109 let resp = configuration.client.execute(req).await?;
110
111 let status = resp.status();
112
113 if !status.is_client_error() && !status.is_server_error() {
114 Ok(())
115 } else {
116 let content = resp.text().await?;
117 let entity: Option<GetWebsearchSearchError> = serde_json::from_str(&content).ok();
118 Err(Error::ResponseError(ResponseContent { status, content, entity }))
119 }
120}
121
122/// Answers {query, number_of_results, results:[{url, title, content, engine}]} — the exact /search?format=json contract a SearXNG client decodes, so an agent tool configured against SearXNG reaches this with no change. `q` is the query and `language` narrows it; both are read from the QUERY STRING. Served in-process by a Go meta-search over keyless public engines, never a third-party search API and never a search key. The enabled engines run concurrently and their hits are merged, deduplicated by normalised URL (host and path, trailing slash and fragment dropped, query kept — distinct queries are distinct results) and capped at 30. Ranking is deterministic rather than scored: the first configured engine's hits lead. TWO WAYS IN, one-way equivalent, and no third: a validated principal — the same gate the whole data plane uses — passes straight through, and a caller without one must present the shared service key as X-API-Key, compared in constant time. A deployment with no key configured answers 503 rather than opening the surface to everyone, and a missing or wrong key is 401. It is never an open proxy. There is no tenant scoping beyond that gate, and there is nothing to scope: the results are public web pages, identical for every caller. It fails SOFT on the engines and closed only on the gate. An engine that errors or is served a bot-challenge page contributes zero results and never fails the request, so an empty `results` is a real answer — nothing was found — and not an outage. The array is always present, never null. The one thing to get right: every method answers identically. This is one handler registered for all of them, and it reads only the query string, so a body sent on the write verbs is ignored rather than refused.
123pub async fn patch_websearch_search(configuration: &configuration::Configuration, ) -> Result<(), Error<PatchWebsearchSearchError>> {
124
125 let uri_str = format!("{}/v1/websearch/search", configuration.base_path);
126 let mut req_builder = configuration.client.request(reqwest::Method::PATCH, &uri_str);
127
128 if let Some(ref user_agent) = configuration.user_agent {
129 req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
130 }
131 if let Some(ref token) = configuration.bearer_access_token {
132 req_builder = req_builder.bearer_auth(token.to_owned());
133 };
134
135 let req = req_builder.build()?;
136 let resp = configuration.client.execute(req).await?;
137
138 let status = resp.status();
139
140 if !status.is_client_error() && !status.is_server_error() {
141 Ok(())
142 } else {
143 let content = resp.text().await?;
144 let entity: Option<PatchWebsearchSearchError> = serde_json::from_str(&content).ok();
145 Err(Error::ResponseError(ResponseContent { status, content, entity }))
146 }
147}
148
149/// Takes {url} and answers {success, data:{markdown, metadata}} — the exact contract a firecrawl client decodes. The fetch, extraction and optional browser render run in-process; there is no crawler pod to be down. The shared service key is required as an Authorization Bearer, compared in constant time: unset on the deployment is 503, missing or wrong is 401. Unlike search, a validated principal does NOT substitute for it — this is the service-to-service endpoint. A page is archived under the caller's own org and project, taken from the verified principal when there is one, so a scrape lands in the same corpus /v1/crawl fills and a URL already read under that scope is answered from the archive without touching the network. A service caller carrying no principal shares the unscoped prefix. The URL is caller-supplied and fetched from INSIDE the cluster, which makes this a request-forgery primitive by construction: in-namespace service DNS and a cloud metadata endpoint that hands credentials to anyone who asks are both a resolution away. Only http and https are accepted, and every address actually dialled must be public unicast — loopback, link-local, private and multicast are refused. The check lives in the DIALER rather than on the hostname, because resolving a name to validate it and then letting the transport resolve it again is a gap DNS rebinding walks straight through; redirects re-enter the same dialer, so a public URL that bounces to the metadata address is refused at the hop that matters. The one thing to get right: FAILURE IS 200. A missing or unparseable url, a body over the 1 MiB read cap, and a fetch that could not be completed all answer HTTP 200 with success:false and a reason — a firecrawl client reads data.success, not the status line. Only the two auth refusals use a status code, so a caller that branches on HTTP status alone will read every failed scrape as a success.
150pub async fn post_websearch_scrape(configuration: &configuration::Configuration, ) -> Result<(), Error<PostWebsearchScrapeError>> {
151
152 let uri_str = format!("{}/v1/websearch/scrape", configuration.base_path);
153 let mut req_builder = configuration.client.request(reqwest::Method::POST, &uri_str);
154
155 if let Some(ref user_agent) = configuration.user_agent {
156 req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
157 }
158 if let Some(ref token) = configuration.bearer_access_token {
159 req_builder = req_builder.bearer_auth(token.to_owned());
160 };
161
162 let req = req_builder.build()?;
163 let resp = configuration.client.execute(req).await?;
164
165 let status = resp.status();
166
167 if !status.is_client_error() && !status.is_server_error() {
168 Ok(())
169 } else {
170 let content = resp.text().await?;
171 let entity: Option<PostWebsearchScrapeError> = serde_json::from_str(&content).ok();
172 Err(Error::ResponseError(ResponseContent { status, content, entity }))
173 }
174}
175
176/// Answers {query, number_of_results, results:[{url, title, content, engine}]} — the exact /search?format=json contract a SearXNG client decodes, so an agent tool configured against SearXNG reaches this with no change. `q` is the query and `language` narrows it; both are read from the QUERY STRING. Served in-process by a Go meta-search over keyless public engines, never a third-party search API and never a search key. The enabled engines run concurrently and their hits are merged, deduplicated by normalised URL (host and path, trailing slash and fragment dropped, query kept — distinct queries are distinct results) and capped at 30. Ranking is deterministic rather than scored: the first configured engine's hits lead. TWO WAYS IN, one-way equivalent, and no third: a validated principal — the same gate the whole data plane uses — passes straight through, and a caller without one must present the shared service key as X-API-Key, compared in constant time. A deployment with no key configured answers 503 rather than opening the surface to everyone, and a missing or wrong key is 401. It is never an open proxy. There is no tenant scoping beyond that gate, and there is nothing to scope: the results are public web pages, identical for every caller. It fails SOFT on the engines and closed only on the gate. An engine that errors or is served a bot-challenge page contributes zero results and never fails the request, so an empty `results` is a real answer — nothing was found — and not an outage. The array is always present, never null. The one thing to get right: every method answers identically. This is one handler registered for all of them, and it reads only the query string, so a body sent on the write verbs is ignored rather than refused.
177pub async fn post_websearch_search(configuration: &configuration::Configuration, ) -> Result<(), Error<PostWebsearchSearchError>> {
178
179 let uri_str = format!("{}/v1/websearch/search", configuration.base_path);
180 let mut req_builder = configuration.client.request(reqwest::Method::POST, &uri_str);
181
182 if let Some(ref user_agent) = configuration.user_agent {
183 req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
184 }
185 if let Some(ref token) = configuration.bearer_access_token {
186 req_builder = req_builder.bearer_auth(token.to_owned());
187 };
188
189 let req = req_builder.build()?;
190 let resp = configuration.client.execute(req).await?;
191
192 let status = resp.status();
193
194 if !status.is_client_error() && !status.is_server_error() {
195 Ok(())
196 } else {
197 let content = resp.text().await?;
198 let entity: Option<PostWebsearchSearchError> = serde_json::from_str(&content).ok();
199 Err(Error::ResponseError(ResponseContent { status, content, entity }))
200 }
201}
202
203/// Answers {query, number_of_results, results:[{url, title, content, engine}]} — the exact /search?format=json contract a SearXNG client decodes, so an agent tool configured against SearXNG reaches this with no change. `q` is the query and `language` narrows it; both are read from the QUERY STRING. Served in-process by a Go meta-search over keyless public engines, never a third-party search API and never a search key. The enabled engines run concurrently and their hits are merged, deduplicated by normalised URL (host and path, trailing slash and fragment dropped, query kept — distinct queries are distinct results) and capped at 30. Ranking is deterministic rather than scored: the first configured engine's hits lead. TWO WAYS IN, one-way equivalent, and no third: a validated principal — the same gate the whole data plane uses — passes straight through, and a caller without one must present the shared service key as X-API-Key, compared in constant time. A deployment with no key configured answers 503 rather than opening the surface to everyone, and a missing or wrong key is 401. It is never an open proxy. There is no tenant scoping beyond that gate, and there is nothing to scope: the results are public web pages, identical for every caller. It fails SOFT on the engines and closed only on the gate. An engine that errors or is served a bot-challenge page contributes zero results and never fails the request, so an empty `results` is a real answer — nothing was found — and not an outage. The array is always present, never null. The one thing to get right: every method answers identically. This is one handler registered for all of them, and it reads only the query string, so a body sent on the write verbs is ignored rather than refused.
204pub async fn put_websearch_search(configuration: &configuration::Configuration, ) -> Result<(), Error<PutWebsearchSearchError>> {
205
206 let uri_str = format!("{}/v1/websearch/search", configuration.base_path);
207 let mut req_builder = configuration.client.request(reqwest::Method::PUT, &uri_str);
208
209 if let Some(ref user_agent) = configuration.user_agent {
210 req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
211 }
212 if let Some(ref token) = configuration.bearer_access_token {
213 req_builder = req_builder.bearer_auth(token.to_owned());
214 };
215
216 let req = req_builder.build()?;
217 let resp = configuration.client.execute(req).await?;
218
219 let status = resp.status();
220
221 if !status.is_client_error() && !status.is_server_error() {
222 Ok(())
223 } else {
224 let content = resp.text().await?;
225 let entity: Option<PutWebsearchSearchError> = serde_json::from_str(&content).ok();
226 Err(Error::ResponseError(ResponseContent { status, content, entity }))
227 }
228}
229
230/// Searches the live web and answers with ranked results. This is the fleet's path to what is happening RIGHT NOW — today's weather, an outage, a release that postdates any model's training. `q` is the query and `language` narrows it to a locale. The answer is `{query, number_of_results, results:[{url, title, content, engine}]}`, where `content` is the ENGINE's snippet and not the page: read a page with POST /v1/crawl. It is served in-process by a Go meta-search over keyless public engines — never a third-party search API and never a search key. The enabled engines run concurrently and their hits are merged, deduplicated by normalised URL (host and path, trailing slash and fragment dropped, query kept, so distinct queries stay distinct results) and capped at 30. Ranking is deterministic rather than scored: the first configured engine's hits lead. It fails SOFT on the engines. One that errors, times out or is served a bot-challenge page contributes zero results and never fails the call, so an empty `results` is a real answer — nothing was found — and not an outage. The array is always present, never null. Two refusals in the order they have to be asked, both in the PREAMBLE. A typed op is also an MCP tool, a call-plane operation, a graph field and a CLI command, and every one of those invokes it with no route and therefore no middleware — so what admits a caller here is asked where every caller reaches it rather than in a middleware only one of them passes through. A VALIDATED PRINCIPAL IS REQUIRED, and there is no tenant beyond that: the results are public web pages, identical for every caller, so nothing here is scoped and nothing here can leak across orgs. THEN THE ANTI-FORGERY TOKEN, immediately before the money, because that is what it is about. This search is the SAME bought meta-search the compat endpoint runs — the engines cost, and account.Shared/meter.go bills the caller's ledger for the answer — so a page the caller never visited must not be able to spend for them by sending their browser here with a cookie they already hold. Nothing leaks; the answer is unreadable cross-origin. What moves is money. It is account's control, the one every operation in this estate asks, and it is a no-op the moment a caller PRESENTS a credential (Bearer, gateway, API key) — which is every service and console caller here — so it costs a CLI, an agent and an API client nothing. Only the ambient-cookie path is asked for the echoed token. The raw /v1/websearch/search route asks the same control on its group (see Mount), so the two addresses of one search are admitted alike.
231pub async fn search_web(configuration: &configuration::Configuration, web_search_query: models::WebSearchQuery) -> Result<models::WebSearchResults, Error<SearchWebError>> {
232 // add a prefix to parameters to efficiently prevent name collisions
233 let p_web_search_query = web_search_query;
234
235 let uri_str = format!("{}/v1/websearch", configuration.base_path);
236 let mut req_builder = configuration.client.request(reqwest::Method::POST, &uri_str);
237
238 if let Some(ref user_agent) = configuration.user_agent {
239 req_builder = req_builder.header(reqwest::header::USER_AGENT, user_agent.clone());
240 }
241 if let Some(ref token) = configuration.bearer_access_token {
242 req_builder = req_builder.bearer_auth(token.to_owned());
243 };
244 req_builder = req_builder.json(&p_web_search_query);
245
246 let req = req_builder.build()?;
247 let resp = configuration.client.execute(req).await?;
248
249 let status = resp.status();
250 let content_type = resp
251 .headers()
252 .get("content-type")
253 .and_then(|v| v.to_str().ok())
254 .unwrap_or("application/octet-stream");
255 let content_type = super::ContentType::from(content_type);
256
257 if !status.is_client_error() && !status.is_server_error() {
258 let content = resp.text().await?;
259 match content_type {
260 ContentType::Json => serde_json::from_str(&content).map_err(Error::from),
261 ContentType::Text => return Err(Error::from(serde_json::Error::custom("Received `text/plain` content type response that cannot be converted to `models::WebSearchResults`"))),
262 ContentType::Unsupported(unknown_type) => return Err(Error::from(serde_json::Error::custom(format!("Received `{unknown_type}` content type response that cannot be converted to `models::WebSearchResults`")))),
263 }
264 } else {
265 let content = resp.text().await?;
266 let entity: Option<SearchWebError> = serde_json::from_str(&content).ok();
267 Err(Error::ResponseError(ResponseContent { status, content, entity }))
268 }
269}
270